/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "EditorCommands.h"

#include "mozilla/Assertions.h"
#include "mozilla/EditorBase.h"
#include "mozilla/FlushType.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/Maybe.h"
#include "mozilla/MozPromise.h"  // for mozilla::detail::Any
#include "mozilla/dom/DataTransfer.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Selection.h"
#include "nsCommandParams.h"
#include "nsIClipboard.h"
#include "nsIEditingSession.h"
#include "nsIPrincipal.h"
#include "nsISelectionController.h"
#include "nsITransferable.h"
#include "nsString.h"
#include "nsAString.h"

class nsISupports;

#define STATE_ENABLED "state_enabled"
#define STATE_ATTRIBUTE "state_attribute"
#define STATE_DATA "state_data"

namespace mozilla {

using detail::Any;

/******************************************************************************
 * mozilla::EditorCommand
 ******************************************************************************/

bool EditorCommand::IsCommandEnabled(const nsACString& aCommandName,
                                     nsISupports* aCommandRefCon) {
  nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
  EditorBase* editorBase = editor ? editor->AsEditorBase() : nullptr;
  return IsCommandEnabled(GetInternalCommand(aCommandName),
                          MOZ_KnownLive(editorBase));
}

nsresult EditorCommand::DoCommand(const nsACString& aCommandName,
                                  nsICommandParams* aParams,
                                  nsISupports* aCommandRefCon) {
  if (NS_WARN_IF(!aCommandRefCon)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
  if (NS_WARN_IF(!editor)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsCommandParams* params = aParams ? aParams->AsCommandParams() : nullptr;
  Command command = GetInternalCommand(aCommandName, params);
  EditorCommandParamType paramType = EditorCommand::GetParamType(command);
  if (paramType == EditorCommandParamType::None) {
    nsresult rv = DoCommandParam(
        command, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "Failed to do command from nsIControllerCommand::DoCommandParams()");
    return rv;
  }

  if (Any(paramType & EditorCommandParamType::Bool)) {
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      Maybe<bool> boolParam = Nothing();
      if (params) {
        ErrorResult error;
        boolParam = Some(params->GetBool(STATE_ATTRIBUTE, error));
        if (NS_WARN_IF(error.Failed())) {
          return error.StealNSResult();
        }
      }
      nsresult rv = DoCommandParam(
          command, boolParam, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    MOZ_ASSERT_UNREACHABLE("Unexpected state for bool");
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  // Special case for MultiStateCommandBase.  It allows both CString and String
  // in STATE_ATTRIBUTE and CString is preferred.
  if (Any(paramType & EditorCommandParamType::CString) &&
      Any(paramType & EditorCommandParamType::String)) {
    if (!params) {
      nsresult rv =
          DoCommandParam(command, VoidString(),
                         MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "Failed to do command from "
                           "nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      nsCString cStringParam;
      nsresult rv = params->GetCString(STATE_ATTRIBUTE, cStringParam);
      if (NS_SUCCEEDED(rv)) {
        NS_ConvertUTF8toUTF16 stringParam(cStringParam);
        nsresult rv =
            DoCommandParam(command, stringParam,
                           MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
        NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                             "Failed to do command from "
                             "nsIControllerCommand::DoCommandParams()");
        return rv;
      }
      nsString stringParam;
      DebugOnly<nsresult> rvIgnored =
          params->GetString(STATE_ATTRIBUTE, stringParam);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                           "Failed to get string from STATE_ATTRIBUTE");
      rv = DoCommandParam(command, stringParam,
                          MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "Failed to do command from "
                           "nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    MOZ_ASSERT_UNREACHABLE("Unexpected state for CString/String");
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  if (Any(paramType & EditorCommandParamType::CString)) {
    if (!params) {
      nsresult rv =
          DoCommandParam(command, VoidCString(),
                         MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      nsCString cStringParam;
      nsresult rv = params->GetCString(STATE_ATTRIBUTE, cStringParam);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      rv = DoCommandParam(command, cStringParam,
                          MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    MOZ_ASSERT_UNREACHABLE("Unexpected state for CString");
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  if (Any(paramType & EditorCommandParamType::String)) {
    if (!params) {
      nsresult rv =
          DoCommandParam(command, VoidString(),
                         MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    nsString stringParam;
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      nsresult rv = params->GetString(STATE_ATTRIBUTE, stringParam);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else if (Any(paramType & EditorCommandParamType::StateData)) {
      nsresult rv = params->GetString(STATE_DATA, stringParam);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else {
      MOZ_ASSERT_UNREACHABLE("Unexpected state for String");
      return NS_ERROR_NOT_IMPLEMENTED;
    }
    nsresult rv = DoCommandParam(
        command, stringParam, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "Failed to do command from nsIControllerCommand::DoCommandParams()");
    return rv;
  }

  if (Any(paramType & EditorCommandParamType::Transferable)) {
    nsCOMPtr<nsITransferable> transferable;
    if (params) {
      nsCOMPtr<nsISupports> supports = params->GetISupports("transferable");
      transferable = do_QueryInterface(supports);
    }
    nsresult rv = DoCommandParam(
        command, transferable, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "Failed to do command from nsIControllerCommand::DoCommandParams()");
    return rv;
  }

  MOZ_ASSERT_UNREACHABLE("Unexpected param type");
  return NS_ERROR_NOT_IMPLEMENTED;
}

void EditorCommand::GetCommandStateParams(const nsACString& aCommandName,
                                          nsICommandParams* aParams,
                                          nsISupports* aCommandRefCon) {
  MOZ_ASSERT(aParams);
  if (nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon)) {
    GetCommandStateParams(GetInternalCommand(aCommandName),
                          MOZ_KnownLive(*aParams->AsCommandParams()),
                          MOZ_KnownLive(editor->AsEditorBase()), nullptr);
    return;
  }
  nsCOMPtr<nsIEditingSession> editingSession =
      do_QueryInterface(aCommandRefCon);
  GetCommandStateParams(GetInternalCommand(aCommandName),
                        MOZ_KnownLive(*aParams->AsCommandParams()), nullptr,
                        editingSession);
}

/******************************************************************************
 * mozilla::UndoCommand
 ******************************************************************************/

StaticRefPtr<UndoCommand> UndoCommand::sInstance;

bool UndoCommand::IsCommandEnabled(Command aCommand,
                                   EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanUndo();
}

nsresult UndoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.UndoAsAction(1, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
  return rv;
}

nsresult UndoCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::RedoCommand
 ******************************************************************************/

StaticRefPtr<RedoCommand> RedoCommand::sInstance;

bool RedoCommand::IsCommandEnabled(Command aCommand,
                                   EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanRedo();
}

nsresult RedoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.RedoAsAction(1, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RedoAsAction() failed");
  return rv;
}

nsresult RedoCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CutCommand
 ******************************************************************************/

StaticRefPtr<CutCommand> CutCommand::sInstance;

bool CutCommand::IsCommandEnabled(Command aCommand,
                                  EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() &&
         aEditorBase->IsCutCommandEnabled();
}

nsresult CutCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                               nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.CutAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
  return rv;
}

nsresult CutCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CutOrDeleteCommand
 ******************************************************************************/

StaticRefPtr<CutOrDeleteCommand> CutOrDeleteCommand::sInstance;

bool CutOrDeleteCommand::IsCommandEnabled(Command aCommand,
                                          EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult CutOrDeleteCommand::DoCommand(Command aCommand,
                                       EditorBase& aEditorBase,
                                       nsIPrincipal* aPrincipal) const {
  dom::Selection* selection = aEditorBase.GetSelection();
  if (selection && selection->IsCollapsed()) {
    nsresult rv = aEditorBase.DeleteSelectionAsAction(
        nsIEditor::eNext, nsIEditor::eStrip, aPrincipal);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::DeleteSelectionAsAction() failed");
    return rv;
  }
  nsresult rv = aEditorBase.CutAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
  return rv;
}

nsresult CutOrDeleteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CopyCommand
 ******************************************************************************/

StaticRefPtr<CopyCommand> CopyCommand::sInstance;

bool CopyCommand::IsCommandEnabled(Command aCommand,
                                   EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsCopyCommandEnabled();
}

nsresult CopyCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                nsIPrincipal* aPrincipal) const {
  // Shouldn't cause "beforeinput" event so that we don't need to specify
  // the given principal.
  return aEditorBase.Copy();
}

nsresult CopyCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CopyOrDeleteCommand
 ******************************************************************************/

StaticRefPtr<CopyOrDeleteCommand> CopyOrDeleteCommand::sInstance;

bool CopyOrDeleteCommand::IsCommandEnabled(Command aCommand,
                                           EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult CopyOrDeleteCommand::DoCommand(Command aCommand,
                                        EditorBase& aEditorBase,
                                        nsIPrincipal* aPrincipal) const {
  dom::Selection* selection = aEditorBase.GetSelection();
  if (selection && selection->IsCollapsed()) {
    nsresult rv = aEditorBase.DeleteSelectionAsAction(
        nsIEditor::eNextWord, nsIEditor::eStrip, aPrincipal);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::DeleteSelectionAsAction() failed");
    return rv;
  }
  // Shouldn't cause "beforeinput" event so that we don't need to specify
  // the given principal.
  nsresult rv = aEditorBase.Copy();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::Copy() failed");
  return rv;
}

nsresult CopyOrDeleteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::PasteCommand
 ******************************************************************************/

StaticRefPtr<PasteCommand> PasteCommand::sInstance;

bool PasteCommand::IsCommandEnabled(Command aCommand,
                                    EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
}

nsresult PasteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                 nsIPrincipal* aPrincipal) const {
  RefPtr<dom::DataTransfer> dataTransfer;
  nsCOMPtr<nsIPrincipal> subjectPrincipal =
      aPrincipal ? aPrincipal
                 : nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller();
  MOZ_ASSERT(subjectPrincipal);

  // If we don't need to get user confirmation for clipboard access, we could
  // just let EditorBase::PasteAsAction() to create DataTransfer instance
  // synchronously for paste event. Otherwise, we need to spin the event loop to
  // wait for the clipboard paste contextmenu to be shown and get user
  // confirmation which are all handled in parent process before sending the
  // paste event.
  if (!nsContentUtils::PrincipalHasPermission(*subjectPrincipal,
                                              nsGkAtoms::clipboardRead)) {
    MOZ_DIAGNOSTIC_ASSERT(StaticPrefs::dom_execCommand_paste_enabled(),
                          "How did we get here?");
    // This will spin the event loop.
    nsCOMPtr<nsPIDOMWindowOuter> window = aEditorBase.GetWindow();
    dataTransfer = dom::DataTransfer::WaitForClipboardDataSnapshotAndCreate(
        window, subjectPrincipal);
    if (!dataTransfer) {
      return NS_SUCCESS_DOM_NO_OPERATION;
    }
  }

  nsresult rv = aEditorBase.PasteAsAction(nsIClipboard::kGlobalClipboard,
                                          EditorBase::DispatchPasteEvent::Yes,
                                          dataTransfer, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::PasteAsAction(nsIClipboard::"
                       "kGlobalClipboard, DispatchPasteEvent::Yes) failed");
  return rv;
}

nsresult PasteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/*****************************************************************************
 * mozilla::PasteNoFormattingCommand
 *****************************************************************************/

StaticRefPtr<PasteNoFormattingCommand> PasteNoFormattingCommand::sInstance;

bool PasteNoFormattingCommand::IsCommandEnabled(Command aCommand,
                                                EditorBase* aEditorBase) const {
  return aEditorBase && aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
}

nsresult PasteNoFormattingCommand::DoCommand(Command aCommand,
                                             EditorBase& aEditorBase,
                                             nsIPrincipal* aPrincipal) const {
#ifdef DEBUG
  // cmd_pasteNoFormatting is not available through document.execCommand.
  // So it should always have clipboardRead permission.
  nsCOMPtr<nsIPrincipal> subjectPrincipal =
      aPrincipal ? aPrincipal
                 : nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller();
  MOZ_ASSERT(nsContentUtils::PrincipalHasPermission(*subjectPrincipal,
                                                    nsGkAtoms::clipboardRead));
#endif
  nsresult rv;
  if (HTMLEditor* htmlEditor = aEditorBase.GetAsHTMLEditor()) {
    // Known live because we hold a ref above in "editor"
    rv = MOZ_KnownLive(htmlEditor)
             ->PasteNoFormattingAsAction(nsIClipboard::kGlobalClipboard,
                                         EditorBase::DispatchPasteEvent::Yes,
                                         nullptr, aPrincipal);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::PasteNoFormattingAsAction("
                         "DispatchPasteEvent::Yes) failed");
  } else {
    rv = aEditorBase.PasteAsAction(nsIClipboard::kGlobalClipboard,
                                   EditorBase::DispatchPasteEvent::Yes, nullptr,
                                   aPrincipal);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::PasteAsAction(nsIClipboard::"
                         "kGlobalClipboard, DispatchPasteEvent::Yes) failed");
  }
  return rv;
}

nsresult PasteNoFormattingCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::PasteTransferableCommand
 ******************************************************************************/

StaticRefPtr<PasteTransferableCommand> PasteTransferableCommand::sInstance;

bool PasteTransferableCommand::IsCommandEnabled(Command aCommand,
                                                EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanPasteTransferable(nullptr);
}

nsresult PasteTransferableCommand::DoCommand(Command aCommand,
                                             EditorBase& aEditorBase,
                                             nsIPrincipal* aPrincipal) const {
  return NS_ERROR_FAILURE;
}

nsresult PasteTransferableCommand::DoCommandParam(
    Command aCommand, nsITransferable* aTransferableParam,
    EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const {
  if (NS_WARN_IF(!aTransferableParam)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsresult rv = aEditorBase.PasteTransferableAsAction(
      aTransferableParam, EditorBase::DispatchPasteEvent::Yes, aPrincipal);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "EditorBase::PasteTransferableAsAction(DispatchPasteEvent::Yes) failed");
  return rv;
}

nsresult PasteTransferableCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  if (NS_WARN_IF(!aEditorBase)) {
    return NS_ERROR_INVALID_ARG;
  }

  nsCOMPtr<nsISupports> supports = aParams.GetISupports("transferable");
  if (NS_WARN_IF(!supports)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsITransferable> trans;
  trans = do_QueryInterface(supports);
  if (NS_WARN_IF(!trans)) {
    return NS_ERROR_FAILURE;
  }

  return aParams.SetBool(STATE_ENABLED,
                         aEditorBase->CanPasteTransferable(trans));
}

/******************************************************************************
 * mozilla::SwitchTextDirectionCommand
 ******************************************************************************/

StaticRefPtr<SwitchTextDirectionCommand> SwitchTextDirectionCommand::sInstance;

bool SwitchTextDirectionCommand::IsCommandEnabled(
    Command aCommand, EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}

nsresult SwitchTextDirectionCommand::DoCommand(Command aCommand,
                                               EditorBase& aEditorBase,
                                               nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.ToggleTextDirectionAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::ToggleTextDirectionAsAction() failed");
  return rv;
}

nsresult SwitchTextDirectionCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::DeleteCommand
 ******************************************************************************/

StaticRefPtr<DeleteCommand> DeleteCommand::sInstance;

bool DeleteCommand::IsCommandEnabled(Command aCommand,
                                     EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  // We can generally delete whenever the selection is editable.  However,
  // cmd_delete doesn't make sense if the selection is collapsed because it's
  // directionless.
  bool isEnabled =
      aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();

  if (aCommand == Command::Delete && isEnabled) {
    return aEditorBase->CanDeleteSelection();
  }
  return isEnabled;
}

nsresult DeleteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                  nsIPrincipal* aPrincipal) const {
  nsIEditor::EDirection deleteDir = nsIEditor::eNone;
  switch (aCommand) {
    case Command::Delete:
      // Really this should probably be eNone, but it only makes a difference
      // if the selection is collapsed, and then this command is disabled.  So
      // let's keep it as it always was to avoid breaking things.
      deleteDir = nsIEditor::ePrevious;
      break;
    case Command::DeleteCharForward:
      deleteDir = nsIEditor::eNext;
      break;
    case Command::DeleteCharBackward:
      deleteDir = nsIEditor::ePrevious;
      break;
    case Command::DeleteWordBackward:
      deleteDir = nsIEditor::ePreviousWord;
      break;
    case Command::DeleteWordForward:
      deleteDir = nsIEditor::eNextWord;
      break;
    case Command::DeleteToBeginningOfLine:
      deleteDir = nsIEditor::eToBeginningOfLine;
      break;
    case Command::DeleteToEndOfLine:
      deleteDir = nsIEditor::eToEndOfLine;
      break;
    default:
      MOZ_CRASH("Unrecognized nsDeleteCommand");
  }
  nsresult rv = aEditorBase.DeleteSelectionAsAction(
      deleteDir, nsIEditor::eStrip, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::DeleteSelectionAsAction() failed");
  return rv;
}

nsresult DeleteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::SelectAllCommand
 ******************************************************************************/

StaticRefPtr<SelectAllCommand> SelectAllCommand::sInstance;

bool SelectAllCommand::IsCommandEnabled(Command aCommand,
                                        EditorBase* aEditorBase) const {
  // You can always select all, unless the selection is editable,
  // and the editable region is empty!
  if (!aEditorBase) {
    return true;
  }

  // You can select all if there is an editor which is non-empty
  return !aEditorBase->IsEmpty();
}

nsresult SelectAllCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                     nsIPrincipal* aPrincipal) const {
  // Shouldn't cause "beforeinput" event so that we don't need to specify
  // aPrincipal.
  nsresult rv = aEditorBase.SelectAll();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::SelectAll() failed");
  return rv;
}

nsresult SelectAllCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::SelectionMoveCommands
 ******************************************************************************/

StaticRefPtr<SelectionMoveCommands> SelectionMoveCommands::sInstance;

bool SelectionMoveCommands::IsCommandEnabled(Command aCommand,
                                             EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

static const struct ScrollCommand {
  Command mReverseScroll;
  Command mForwardScroll;
  nsresult (NS_STDCALL nsISelectionController::*scroll)(bool);
} scrollCommands[] = {{Command::ScrollTop, Command::ScrollBottom,
                       &nsISelectionController::CompleteScroll},
                      {Command::ScrollPageUp, Command::ScrollPageDown,
                       &nsISelectionController::ScrollPage},
                      {Command::ScrollLineUp, Command::ScrollLineDown,
                       &nsISelectionController::ScrollLine}};

static const struct MoveCommand {
  Command mReverseMove;
  Command mForwardMove;
  Command mReverseSelect;
  Command mForwardSelect;
  nsresult (NS_STDCALL nsISelectionController::*move)(bool, bool);
} moveCommands[] = {
    {Command::CharPrevious, Command::CharNext, Command::SelectCharPrevious,
     Command::SelectCharNext, &nsISelectionController::CharacterMove},
    {Command::LinePrevious, Command::LineNext, Command::SelectLinePrevious,
     Command::SelectLineNext, &nsISelectionController::LineMove},
    {Command::WordPrevious, Command::WordNext, Command::SelectWordPrevious,
     Command::SelectWordNext, &nsISelectionController::WordMove},
    {Command::BeginLine, Command::EndLine, Command::SelectBeginLine,
     Command::SelectEndLine, &nsISelectionController::IntraLineMove},
    {Command::BeginParagraph, Command::EndParagraph,
     Command::SelectBeginParagraph, Command::SelectEndParagraph,
     &nsISelectionController::ParagraphMove},
    {Command::MovePageUp, Command::MovePageDown, Command::SelectPageUp,
     Command::SelectPageDown, &nsISelectionController::PageMove},
    {Command::MoveTop, Command::MoveBottom, Command::SelectTop,
     Command::SelectBottom, &nsISelectionController::CompleteMove}};

static const struct PhysicalCommand {
  Command mMove;
  Command mSelect;
  int16_t direction;
  int16_t amount;
} physicalCommands[] = {
    {Command::MoveLeft, Command::SelectLeft, nsISelectionController::MOVE_LEFT,
     0},
    {Command::MoveRight, Command::SelectRight,
     nsISelectionController::MOVE_RIGHT, 0},
    {Command::MoveUp, Command::SelectUp, nsISelectionController::MOVE_UP, 0},
    {Command::MoveDown, Command::SelectDown, nsISelectionController::MOVE_DOWN,
     0},
    {Command::MoveLeft2, Command::SelectLeft2,
     nsISelectionController::MOVE_LEFT, 1},
    {Command::MoveRight2, Command::SelectRight2,
     nsISelectionController::MOVE_RIGHT, 1},
    {Command::MoveUp2, Command::SelectUp2, nsISelectionController::MOVE_UP, 1},
    {Command::MoveDown2, Command::SelectDown2,
     nsISelectionController::MOVE_DOWN, 1},
    {Command::MoveLeft3, Command::SelectLeft3,
     nsISelectionController::MOVE_LEFT, 2},
    {Command::MoveRight3, Command::SelectRight3,
     nsISelectionController::MOVE_RIGHT, 2}};

nsresult SelectionMoveCommands::DoCommand(Command aCommand,
                                          EditorBase& aEditorBase,
                                          nsIPrincipal* aPrincipal) const {
  RefPtr<dom::Document> document = aEditorBase.GetDocument();
  if (document) {
    // Most of the commands below (possibly all of them) need layout to
    // be up to date.
    document->FlushPendingNotifications(FlushType::Layout);
  }

  nsCOMPtr<nsISelectionController> selectionController =
      aEditorBase.GetSelectionController();
  if (NS_WARN_IF(!selectionController)) {
    return NS_ERROR_FAILURE;
  }

  // scroll commands
  for (const auto& cmd : scrollCommands) {
    if (aCommand == cmd.mReverseScroll) {
      return (selectionController->*(cmd.scroll))(false);
    }
    if (aCommand == cmd.mForwardScroll) {
      return (selectionController->*(cmd.scroll))(true);
    }
  }

  // caret movement/selection commands
  for (const auto& cmd : moveCommands) {
    if (aCommand == cmd.mReverseMove) {
      return (selectionController->*(cmd.move))(false, false);
    }
    if (aCommand == cmd.mForwardMove) {
      return (selectionController->*(cmd.move))(true, false);
    }
    if (aCommand == cmd.mReverseSelect) {
      return (selectionController->*(cmd.move))(false, true);
    }
    if (aCommand == cmd.mForwardSelect) {
      return (selectionController->*(cmd.move))(true, true);
    }
  }

  // physical-direction movement/selection
  for (auto cmd : physicalCommands) {
    if (aCommand == cmd.mMove) {
      nsresult rv =
          selectionController->PhysicalMove(cmd.direction, cmd.amount, false);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "nsISelectionController::PhysicalMove() failed to move caret");
      return rv;
    }
    if (aCommand == cmd.mSelect) {
      nsresult rv =
          selectionController->PhysicalMove(cmd.direction, cmd.amount, true);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "nsISelectionController::PhysicalMove() failed to select");
      return rv;
    }
  }

  return NS_ERROR_FAILURE;
}

nsresult SelectionMoveCommands::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::InsertPlaintextCommand
 ******************************************************************************/

StaticRefPtr<InsertPlaintextCommand> InsertPlaintextCommand::sInstance;

bool InsertPlaintextCommand::IsCommandEnabled(Command aCommand,
                                              EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}

nsresult InsertPlaintextCommand::DoCommand(Command aCommand,
                                           EditorBase& aEditorBase,
                                           nsIPrincipal* aPrincipal) const {
  // XXX InsertTextAsAction() is not same as OnInputText().  However, other
  //     commands to insert line break or paragraph separator use OnInput*().
  //     According to the semantics of those methods, using *AsAction() is
  //     better, however, this may not cause two or more placeholder
  //     transactions to the top transaction since its name may not be
  //     nsGkAtoms::TypingTxnName.
  DebugOnly<nsresult> rvIgnored =
      aEditorBase.InsertTextAsAction(u""_ns, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                       "EditorBase::InsertTextAsAction() failed, but ignored");
  return NS_OK;
}

nsresult InsertPlaintextCommand::DoCommandParam(
    Command aCommand, const nsAString& aStringParam, EditorBase& aEditorBase,
    nsIPrincipal* aPrincipal) const {
  if (NS_WARN_IF(aStringParam.IsVoid())) {
    return NS_ERROR_INVALID_ARG;
  }

  // XXX InsertTextAsAction() is not same as OnInputText().  However, other
  //     commands to insert line break or paragraph separator use OnInput*().
  //     According to the semantics of those methods, using *AsAction() is
  //     better, however, this may not cause two or more placeholder
  //     transactions to the top transaction since its name may not be
  //     nsGkAtoms::TypingTxnName.
  DebugOnly<nsresult> rvIgnored =
      aEditorBase.InsertTextAsAction(aStringParam, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                       "EditorBase::InsertTextAsAction() failed, but ignored");
  return NS_OK;
}

nsresult InsertPlaintextCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::InsertParagraphCommand
 ******************************************************************************/

StaticRefPtr<InsertParagraphCommand> InsertParagraphCommand::sInstance;

bool InsertParagraphCommand::IsCommandEnabled(Command aCommand,
                                              EditorBase* aEditorBase) const {
  if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
    return false;
  }
  return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}

nsresult InsertParagraphCommand::DoCommand(Command aCommand,
                                           EditorBase& aEditorBase,
                                           nsIPrincipal* aPrincipal) const {
  if (aEditorBase.IsSingleLineEditor()) {
    return NS_ERROR_FAILURE;
  }
  if (aEditorBase.IsHTMLEditor()) {
    nsresult rv = MOZ_KnownLive(aEditorBase.AsHTMLEditor())
                      ->InsertParagraphSeparatorAsAction(aPrincipal);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::InsertParagraphSeparatorAsAction() failed");
    return rv;
  }
  nsresult rv = aEditorBase.InsertLineBreakAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::InsertLineBreakAsAction() failed");
  return rv;
}

nsresult InsertParagraphCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::InsertLineBreakCommand
 ******************************************************************************/

StaticRefPtr<InsertLineBreakCommand> InsertLineBreakCommand::sInstance;

bool InsertLineBreakCommand::IsCommandEnabled(Command aCommand,
                                              EditorBase* aEditorBase) const {
  if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
    return false;
  }
  return aEditorBase->IsModifiable() && aEditorBase->IsSelectionEditable();
}

nsresult InsertLineBreakCommand::DoCommand(Command aCommand,
                                           EditorBase& aEditorBase,
                                           nsIPrincipal* aPrincipal) const {
  if (aEditorBase.IsSingleLineEditor()) {
    return NS_ERROR_FAILURE;
  }
  nsresult rv = aEditorBase.InsertLineBreakAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::InsertLineBreakAsAction() failed");
  return rv;
}

nsresult InsertLineBreakCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::PasteQuotationCommand
 ******************************************************************************/

StaticRefPtr<PasteQuotationCommand> PasteQuotationCommand::sInstance;

bool PasteQuotationCommand::IsCommandEnabled(Command aCommand,
                                             EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return !aEditorBase->IsSingleLineEditor() &&
         aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
}

nsresult PasteQuotationCommand::DoCommand(Command aCommand,
                                          EditorBase& aEditorBase,
                                          nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.PasteAsQuotationAsAction(
      nsIClipboard::kGlobalClipboard, EditorBase::DispatchPasteEvent::Yes,
      nullptr, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::PasteAsQuotationAsAction(nsIClipboard::"
                       "kGlobalClipboard, DispatchPasteEvent::Yes) failed");
  return rv;
}

nsresult PasteQuotationCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  if (!aEditorBase) {
    return NS_OK;
  }
  aParams.SetBool(STATE_ENABLED,
                  aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard));
  return NS_OK;
}

}  // namespace mozilla
