/* 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/. */

/**
 * Implementations of nsIControllerCommand for composer commands. These commands
 * are related to editing. You can fire these commands with following functions:
 * goDoCommand and goDoCommandParams(If command requires any parameters).
 *
 * Sometimes, we want to reflect the changes in the UI also. We have two functions
 * for that: pokeStyleUI and pokeMultiStateUI. The pokeStyleUI function is for those
 * commands which are boolean in nature for example "cmd_bold" command, text can
 * be bold or not. The pokeMultiStateUI function is for the commands which can have
 * multiple values for example "cmd_fontFace" can have different values like
 * arial, variable width etc.
 *
 * Here, some of the commands are getting executed by document.execCommand.
 * Those are listed in the gCommandMap Map object. In that also, some commands
 * are of type boolean and some are of multiple state. We have two functions to
 * execute them: doStatefulCommand and doStyleUICommand.
 *
 * All commands are not executable through document.execCommand.
 * In all those cases, we will use goDoCommand or goDoCommandParams.
 * The goDoCommandParams function is implemented in this file.
 * The goDoCOmmand function is from globalOverlay.js. For the Commands
 * which can be executed by document.execCommand, we will use doStatefulCommand
 * and doStyleUICommand.
 */

/* import-globals-from ../../../../../toolkit/components/printing/content/printUtils.js */
/* import-globals-from ../../../base/content/globalOverlay.js */
/* import-globals-from ../../../base/content/utilityOverlay.js */
/* import-globals-from editor.js */
/* import-globals-from editorUtilities.js */
/* import-globals-from MsgComposeCommands.js */

var gComposerJSCommandControllerID = 0;

/**
 * Used to register commands we have created manually.
 */
function SetupHTMLEditorCommands() {
  var commandTable = GetComposerCommandTable();
  if (!commandTable) {
    return;
  }

  // Include everything a text editor does
  SetupTextEditorCommands();

  // dump("Registering HTML editor commands\n");

  commandTable.registerCommand("cmd_renderedHTMLEnabler", nsDummyHTMLCommand);

  commandTable.registerCommand("cmd_listProperties", nsListPropertiesCommand);
  commandTable.registerCommand("cmd_colorProperties", nsColorPropertiesCommand);
  commandTable.registerCommand("cmd_increaseFontStep", nsIncreaseFontCommand);
  commandTable.registerCommand("cmd_decreaseFontStep", nsDecreaseFontCommand);
  commandTable.registerCommand(
    "cmd_objectProperties",
    nsObjectPropertiesCommand
  );
  commandTable.registerCommand(
    "cmd_removeNamedAnchors",
    nsRemoveNamedAnchorsCommand
  );

  commandTable.registerCommand("cmd_image", nsImageCommand);
  commandTable.registerCommand("cmd_hline", nsHLineCommand);
  commandTable.registerCommand("cmd_link", nsLinkCommand);
  commandTable.registerCommand("cmd_anchor", nsAnchorCommand);
  commandTable.registerCommand(
    "cmd_insertHTMLWithDialog",
    nsInsertHTMLWithDialogCommand
  );
  commandTable.registerCommand(
    "cmd_insertMathWithDialog",
    nsInsertMathWithDialogCommand
  );
  commandTable.registerCommand("cmd_insertBreakAll", nsInsertBreakAllCommand);

  commandTable.registerCommand("cmd_table", nsInsertOrEditTableCommand);
  commandTable.registerCommand("cmd_editTable", nsEditTableCommand);
  commandTable.registerCommand("cmd_SelectTable", nsSelectTableCommand);
  commandTable.registerCommand("cmd_SelectRow", nsSelectTableRowCommand);
  commandTable.registerCommand("cmd_SelectColumn", nsSelectTableColumnCommand);
  commandTable.registerCommand("cmd_SelectCell", nsSelectTableCellCommand);
  commandTable.registerCommand(
    "cmd_SelectAllCells",
    nsSelectAllTableCellsCommand
  );
  commandTable.registerCommand("cmd_InsertTable", nsInsertTableCommand);
  commandTable.registerCommand(
    "cmd_InsertRowAbove",
    nsInsertTableRowAboveCommand
  );
  commandTable.registerCommand(
    "cmd_InsertRowBelow",
    nsInsertTableRowBelowCommand
  );
  commandTable.registerCommand(
    "cmd_InsertColumnBefore",
    nsInsertTableColumnBeforeCommand
  );
  commandTable.registerCommand(
    "cmd_InsertColumnAfter",
    nsInsertTableColumnAfterCommand
  );
  commandTable.registerCommand(
    "cmd_InsertCellBefore",
    nsInsertTableCellBeforeCommand
  );
  commandTable.registerCommand(
    "cmd_InsertCellAfter",
    nsInsertTableCellAfterCommand
  );
  commandTable.registerCommand("cmd_DeleteTable", nsDeleteTableCommand);
  commandTable.registerCommand("cmd_DeleteRow", nsDeleteTableRowCommand);
  commandTable.registerCommand("cmd_DeleteColumn", nsDeleteTableColumnCommand);
  commandTable.registerCommand("cmd_DeleteCell", nsDeleteTableCellCommand);
  commandTable.registerCommand(
    "cmd_DeleteCellContents",
    nsDeleteTableCellContentsCommand
  );
  commandTable.registerCommand("cmd_JoinTableCells", nsJoinTableCellsCommand);
  commandTable.registerCommand("cmd_SplitTableCell", nsSplitTableCellCommand);
  commandTable.registerCommand(
    "cmd_TableOrCellColor",
    nsTableOrCellColorCommand
  );
  commandTable.registerCommand("cmd_smiley", nsSetSmiley);
  commandTable.registerCommand("cmd_ConvertToTable", nsConvertToTable);
}

function SetupTextEditorCommands() {
  var commandTable = GetComposerCommandTable();
  if (!commandTable) {
    return;
  }
  // dump("Registering plain text editor commands\n");

  commandTable.registerCommand("cmd_findReplace", nsFindReplaceCommand);
  commandTable.registerCommand("cmd_find", nsFindCommand);
  commandTable.registerCommand("cmd_findNext", nsFindAgainCommand);
  commandTable.registerCommand("cmd_findPrev", nsFindAgainCommand);
  commandTable.registerCommand("cmd_rewrap", nsRewrapCommand);
  commandTable.registerCommand("cmd_spelling", nsSpellingCommand);
  commandTable.registerCommand("cmd_insertChars", nsInsertCharsCommand);
}

/**
 * Used to register the command controller in the editor document.
 *
 * @returns {nsIControllerCommandTable|null} - A controller used to
 *   register the manually created commands.
 */
function GetComposerCommandTable() {
  var controller;
  if (gComposerJSCommandControllerID) {
    try {
      controller = window.content.controllers.getControllerById(
        gComposerJSCommandControllerID
      );
    } catch (e) {}
  }
  if (!controller) {
    // create it
    controller =
      Cc["@mozilla.org/embedcomp/base-command-controller;1"].createInstance();

    var editorController = controller.QueryInterface(Ci.nsIControllerContext);
    editorController.setCommandContext(GetCurrentEditorElement());
    window.content.controllers.insertControllerAt(0, controller);

    // Store the controller ID so we can be sure to get the right one later
    gComposerJSCommandControllerID =
      window.content.controllers.getControllerId(controller);
  }

  if (controller) {
    var interfaceRequestor = controller.QueryInterface(
      Ci.nsIInterfaceRequestor
    );
    return interfaceRequestor.getInterface(Ci.nsIControllerCommandTable);
  }
  return null;
}

/* eslint-disable complexity */

/**
 * Get the state of the given command and call the pokeStyleUI or pokeMultiStateUI
 * according to the type of the command to reflect the UI changes in the editor.
 *
 * @param {string} command - The id of the command.
 */
function goUpdateCommandState(command) {
  try {
    var controller =
      document.commandDispatcher.getControllerForCommand(command);
    if (!(controller instanceof Ci.nsICommandController)) {
      return;
    }

    var params = newCommandParams();
    if (!params) {
      return;
    }

    controller.getCommandStateWithParams(command, params);

    switch (command) {
      case "cmd_bold":
      case "cmd_italic":
      case "cmd_underline":
      case "cmd_var":
      case "cmd_samp":
      case "cmd_code":
      case "cmd_acronym":
      case "cmd_abbr":
      case "cmd_cite":
      case "cmd_strong":
      case "cmd_em":
      case "cmd_superscript":
      case "cmd_subscript":
      case "cmd_strikethrough":
      case "cmd_tt":
      case "cmd_nobreak":
      case "cmd_ul":
      case "cmd_ol":
        pokeStyleUI(command, params.getBooleanValue("state_all"));
        break;

      case "cmd_paragraphState":
      case "cmd_align":
      case "cmd_highlight":
      case "cmd_backgroundColor":
      case "cmd_fontColor":
      case "cmd_fontFace":
        pokeMultiStateUI(command, params);
        break;

      case "cmd_indent":
      case "cmd_outdent":
      case "cmd_increaseFont":
      case "cmd_decreaseFont":
      case "cmd_increaseFontStep":
      case "cmd_decreaseFontStep":
      case "cmd_removeStyles":
      case "cmd_smiley":
        break;

      default:
        dump("no update for command: " + command + "\n");
    }
  } catch (e) {
    console.error(e);
  }
}
/* eslint-enable complexity */

/**
 * Used in the oncommandupdate attribute of the goUpdateComposerMenuItems.
 * For any commandset events fired, this function will be called.
 * Used to update the UI state of the editor buttons and menulist.
 * Whenever you change your selection in the editor part, i.e. if you move
 * your cursor, you will find this functions getting called and
 * updating the editor UI of toolbarbuttons and menulists. This is mainly
 * to update the UI according to your selection in the editor part.
 *
 * @param {XULElement} commandset - The <xul:commandset> element to update for.
 */
function goUpdateComposerMenuItems(commandset) {
  // dump("Updating commands for " + commandset.id + "\n");
  for (var i = 0; i < commandset.children.length; i++) {
    var commandNode = commandset.children[i];
    var commandID = commandNode.id;
    if (commandID) {
      goUpdateCommand(commandID); // enable or disable
      if (commandNode.hasAttribute("state")) {
        goUpdateCommandState(commandID);
      }
    }
  }
}

/**
 * Execute the command with the provided parameters.
 * This is directly calling commands with multiple state attributes, which
 * are not supported by document.execCommand()
 *
 * @param {string} command - The command ID.
 * @param {string} paramValue - The parameter value.
 */
function goDoCommandParams(command, paramValue) {
  try {
    let params = newCommandParams();
    params.setStringValue("state_attribute", paramValue);
    let controller =
      document.commandDispatcher.getControllerForCommand(command);
    if (controller && controller.isCommandEnabled(command)) {
      if (controller instanceof Ci.nsICommandController) {
        controller.doCommandWithParams(command, params);
      } else {
        controller.doCommand(command);
      }
    }
  } catch (e) {
    console.error(e);
  }
}

/**
 * Update the UI to reflect setting a given state for a command. This
 * is used for boolean type of commands.
 *
 * @param {string} uiID - The id of the command.
 * @param {boolean} desiredState - State to set for the command.
 */
function pokeStyleUI(uiID, desiredState) {
  let commandNode = document.getElementById(uiID);
  let uiState = commandNode.getAttribute("state") == "true";
  if (desiredState != uiState) {
    commandNode.setAttribute("state", desiredState ? "true" : "false");
    let buttonId;
    switch (uiID) {
      case "cmd_bold":
        buttonId = "boldButton";
        break;
      case "cmd_italic":
        buttonId = "italicButton";
        break;
      case "cmd_underline":
        buttonId = "underlineButton";
        break;
      case "cmd_ul":
        buttonId = "ulButton";
        break;
      case "cmd_ol":
        buttonId = "olButton";
        break;
    }
    if (buttonId) {
      document.getElementById(buttonId).checked = desiredState;
    }
  }
}

/**
 * Maps internal command names to their document.execCommand() command string.
 */
let gCommandMap = new Map([
  ["cmd_bold", "bold"],
  ["cmd_italic", "italic"],
  ["cmd_underline", "underline"],
  ["cmd_strikethrough", "strikethrough"],
  ["cmd_superscript", "superscript"],
  ["cmd_subscript", "subscript"],
  ["cmd_ul", "InsertUnorderedList"],
  ["cmd_ol", "InsertOrderedList"],
  ["cmd_fontFace", "fontName"],

  // This are currently implemented with the help of
  // color selection dialog box in the editor.js.
  // ["cmd_highlight", "backColor"],
  // ["cmd_fontColor", "foreColor"],
]);

/**
 * Used for the boolean type commands available through
 * document.execCommand(). We will also call pokeStyleUI to update
 * the UI.
 *
 * @param {string} cmdStr - The id of the command.
 */
function doStyleUICommand(cmdStr) {
  GetCurrentEditorElement().contentDocument.execCommand(
    gCommandMap.get(cmdStr),
    false,
    null
  );
  let commandNode = document.getElementById(cmdStr);
  let newState = commandNode.getAttribute("state") != "true";
  pokeStyleUI(cmdStr, newState);
}

// Copied from jsmime.js.
function stringToTypedArray(buffer) {
  var typedarray = new Uint8Array(buffer.length);
  for (var i = 0; i < buffer.length; i++) {
    typedarray[i] = buffer.charCodeAt(i);
  }
  return typedarray;
}

/**
 * Update the UI to reflect setting a given state for a command. This is used
 * when the command state has a string value i.e. multiple state type commands.
 *
 * @param {string} uiID - The id of the command.
 * @param {nsICommandParams} cmdParams - Command parameters object.
 */
function pokeMultiStateUI(uiID, cmdParams) {
  let desiredAttrib;
  if (cmdParams.getBooleanValue("state_mixed")) {
    desiredAttrib = "mixed";
  } else if (
    cmdParams.getValueType("state_attribute") == Ci.nsICommandParams.eStringType
  ) {
    desiredAttrib = cmdParams.getCStringValue("state_attribute");
    // Decode UTF-8, for example for font names in Japanese.
    desiredAttrib = new TextDecoder("UTF-8").decode(
      stringToTypedArray(desiredAttrib)
    );
  } else {
    desiredAttrib = cmdParams.getStringValue("state_attribute");
  }

  let commandNode = document.getElementById(uiID);
  let uiState = commandNode.getAttribute("state");
  if (desiredAttrib != uiState) {
    commandNode.setAttribute("state", desiredAttrib);
    switch (uiID) {
      case "cmd_paragraphState": {
        onParagraphFormatChange();
        break;
      }
      case "cmd_fontFace": {
        onFontFaceChange();
        break;
      }
      case "cmd_fontColor": {
        onFontColorChange();
        break;
      }
      case "cmd_backgroundColor": {
        onBackgroundColorChange();
        break;
      }
    }
  }
}

/**
 * Perform the action of the multiple states type commands available through
 * document.execCommand().
 *
 * @param {string} commandID - The id of the command.
 * @param {string} newState - The parameter value.
 * @param {boolean} updateUI - updates the UI if true. Used when
 *   function is called in another JavaScript function.
 */
function doStatefulCommand(commandID, newState, updateUI) {
  if (commandID == "cmd_align") {
    let command;
    switch (newState) {
      case "left":
        command = "justifyLeft";
        break;
      case "center":
        command = "justifyCenter";
        break;
      case "right":
        command = "justifyRight";
        break;
      case "justify":
        command = "justifyFull";
        break;
    }
    GetCurrentEditorElement().contentDocument.execCommand(command, false, null);
  } else if (commandID == "cmd_fontFace" && newState == "") {
    goDoCommandParams(commandID, newState);
  } else {
    GetCurrentEditorElement().contentDocument.execCommand(
      gCommandMap.get(commandID),
      false,
      newState
    );
  }

  if (updateUI) {
    let commandNode = document.getElementById(commandID);
    commandNode.setAttribute("state", newState);
    switch (commandID) {
      case "cmd_fontFace": {
        onFontFaceChange();
        break;
      }
    }
  } else {
    let commandNode = document.getElementById(commandID);
    if (commandNode) {
      commandNode.setAttribute("state", newState);
    }
  }
}

var nsDummyHTMLCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    // do nothing
    dump("Hey, who's calling the dummy command?\n");
  },
};

// ------- output utilities   ----- //

// returns a fileExtension string
function GetExtensionBasedOnMimeType(aMIMEType) {
  try {
    var mimeService = null;
    mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);

    var fileExtension = mimeService.getPrimaryExtension(aMIMEType, null);

    // the MIME service likes to give back ".htm" for text/html files,
    // so do a special-case fix here.
    if (fileExtension == "htm") {
      fileExtension = "html";
    }

    return fileExtension;
  } catch (e) {}
  return "";
}

function GetSuggestedFileName(aDocumentURLString, aMIMEType) {
  var extension = GetExtensionBasedOnMimeType(aMIMEType);
  if (extension) {
    extension = "." + extension;
  }

  // check for existing file name we can use
  if (aDocumentURLString && !IsUrlAboutBlank(aDocumentURLString)) {
    try {
      let docURI = Services.io.newURI(
        aDocumentURLString,
        GetCurrentEditor().documentCharacterSet
      );
      docURI = docURI.QueryInterface(Ci.nsIURL);

      // grab the file name
      let url = validateFileName(decodeURIComponent(docURI.fileBaseName));
      if (url) {
        return url + extension;
      }
    } catch (e) {}
  }

  // Check if there is a title we can use to generate a valid filename,
  // if we can't, use the default filename.
  var title =
    validateFileName(GetDocumentTitle()) ||
    GetString("untitledDefaultFilename");
  return title + extension;
}

/**
 * @returns {Promise} dialogResult
 */
function PromptForSaveLocation(
  aDoSaveAsText,
  aEditorType,
  aMIMEType,
  aDocumentURLString
) {
  var dialogResult = {};
  dialogResult.filepickerClick = Ci.nsIFilePicker.returnCancel;
  dialogResult.resultingURI = "";
  dialogResult.resultingLocalFile = null;

  var fp = null;
  try {
    fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  } catch (e) {}
  if (!fp) {
    return dialogResult;
  }

  // determine prompt string based on type of saving we'll do
  var promptString;
  if (aDoSaveAsText || aEditorType == "text") {
    promptString = GetString("SaveTextAs");
  } else {
    promptString = GetString("SaveDocumentAs");
  }

  fp.init(window, promptString, Ci.nsIFilePicker.modeSave);

  // Set filters according to the type of output
  if (aDoSaveAsText) {
    fp.appendFilters(Ci.nsIFilePicker.filterText);
  } else {
    fp.appendFilters(Ci.nsIFilePicker.filterHTML);
  }
  fp.appendFilters(Ci.nsIFilePicker.filterAll);

  // now let's actually set the filepicker's suggested filename
  var suggestedFileName = GetSuggestedFileName(aDocumentURLString, aMIMEType);
  if (suggestedFileName) {
    fp.defaultString = suggestedFileName;
  }

  // set the file picker's current directory
  // assuming we have information needed (like prior saved location)
  try {
    var fileHandler = GetFileProtocolHandler();

    var isLocalFile = true;
    try {
      let docURI = Services.io.newURI(
        aDocumentURLString,
        GetCurrentEditor().documentCharacterSet
      );
      isLocalFile = docURI.schemeIs("file");
    } catch (e) {}

    var parentLocation = null;
    if (isLocalFile) {
      var fileLocation = fileHandler.getFileFromURLSpec(aDocumentURLString); // this asserts if url is not local
      parentLocation = fileLocation.parent;
    }
    if (parentLocation) {
      // Save current filepicker's default location
      if ("gFilePickerDirectory" in window) {
        gFilePickerDirectory = fp.displayDirectory;
      }

      fp.displayDirectory = parentLocation;
    } else {
      // Initialize to the last-used directory for the particular type (saved in prefs)
      SetFilePickerDirectory(fp, aEditorType);
    }
  } catch (e) {}

  return new Promise(resolve => {
    fp.open(rv => {
      dialogResult.filepickerClick = rv;
      if (rv != Ci.nsIFilePicker.returnCancel && fp.file) {
        // Allow OK and replace.
        // reset urlstring to new save location
        dialogResult.resultingURIString = fileHandler.getURLSpecFromActualFile(
          fp.file
        );
        dialogResult.resultingLocalFile = fp.file;
        SaveFilePickerDirectory(fp, aEditorType);
        resolve(dialogResult);
      } else if ("gFilePickerDirectory" in window && gFilePickerDirectory) {
        fp.displayDirectory = gFilePickerDirectory;
        resolve(null);
      }
    });
  });
}

/**
 * If needed, prompt for document title and set the document title to the
 * preferred value.
 *
 * @returns true if the title was set up successfully;
 *         false if the user cancelled the title prompt
 */
function PromptAndSetTitleIfNone() {
  if (GetDocumentTitle()) {
    // we have a title; no need to prompt!
    return true;
  }

  let result = { value: null };
  let captionStr = GetString("DocumentTitle");
  let msgStr = GetString("NeedDocTitle") + "\n" + GetString("DocTitleHelp");
  let confirmed = Services.prompt.prompt(
    window,
    captionStr,
    msgStr,
    result,
    null,
    { value: 0 }
  );
  if (confirmed) {
    SetDocumentTitle(TrimString(result.value));
  }

  return confirmed;
}

var gPersistObj;

// Don't forget to do these things after calling OutputFileWithPersistAPI:
// we need to update the uri before notifying listeners
//    UpdateWindowTitle();
//    if (!aSaveCopy)
//      editor.resetModificationCount();
// this should cause notification to listeners that document has changed

function OutputFileWithPersistAPI(
  editorDoc,
  aDestinationLocation,
  aRelatedFilesParentDir,
  aMimeType
) {
  gPersistObj = null;
  var editor = GetCurrentEditor();
  try {
    editor.forceCompositionEnd();
  } catch (e) {}

  var isLocalFile = false;
  try {
    aDestinationLocation.QueryInterface(Ci.nsIFile);
    isLocalFile = true;
  } catch (e) {
    try {
      var tmp = aDestinationLocation.QueryInterface(Ci.nsIURI);
      isLocalFile = tmp.schemeIs("file");
    } catch (e) {}
  }

  try {
    // we should supply a parent directory if/when we turn on functionality to save related documents
    var persistObj = Cc[
      "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"
    ].createInstance(Ci.nsIWebBrowserPersist);
    persistObj.progressListener = gEditorOutputProgressListener;

    var wrapColumn = GetWrapColumn();
    var outputFlags = GetOutputFlags(aMimeType, wrapColumn);

    // for 4.x parity as well as improving readability of file locally on server
    // this will always send crlf for upload (http/ftp)
    if (!isLocalFile) {
      // if we aren't saving locally then send both cr and lf
      outputFlags |=
        Ci.nsIWebBrowserPersist.ENCODE_FLAGS_CR_LINEBREAKS |
        Ci.nsIWebBrowserPersist.ENCODE_FLAGS_LF_LINEBREAKS;

      // we want to serialize the output for all remote publishing
      // some servers can handle only one connection at a time
      // some day perhaps we can make this user-configurable per site?
      persistObj.persistFlags =
        persistObj.persistFlags |
        Ci.nsIWebBrowserPersist.PERSIST_FLAGS_SERIALIZE_OUTPUT;
    }

    // note: we always want to set the replace existing files flag since we have
    // already given user the chance to not replace an existing file (file picker)
    // or the user picked an option where the file is implicitly being replaced (save)
    persistObj.persistFlags =
      persistObj.persistFlags |
      Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_BASE_TAG_MODIFICATIONS |
      Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
      Ci.nsIWebBrowserPersist.PERSIST_FLAGS_DONT_FIXUP_LINKS |
      Ci.nsIWebBrowserPersist.PERSIST_FLAGS_DONT_CHANGE_FILENAMES |
      Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FIXUP_ORIGINAL_DOM;
    persistObj.saveDocument(
      editorDoc,
      aDestinationLocation,
      aRelatedFilesParentDir,
      aMimeType,
      outputFlags,
      wrapColumn
    );
    gPersistObj = persistObj;
  } catch (e) {
    dump("caught an error, bail\n");
    return false;
  }

  return true;
}

// returns output flags based on mimetype, wrapCol and prefs
function GetOutputFlags(aMimeType, aWrapColumn) {
  var outputFlags = 0;
  var editor = GetCurrentEditor();
  var outputEntity =
    editor && editor.documentCharacterSet == "ISO-8859-1"
      ? Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_LATIN1_ENTITIES
      : Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
  if (aMimeType == "text/plain") {
    // When saving in "text/plain" format, always do formatting
    outputFlags |= Ci.nsIWebBrowserPersist.ENCODE_FLAGS_FORMATTED;
  } else {
    // Should we prettyprint? Check the pref
    if (Services.prefs.getBoolPref("editor.prettyprint")) {
      outputFlags |= Ci.nsIWebBrowserPersist.ENCODE_FLAGS_FORMATTED;
    }

    try {
      // How much entity names should we output? Check the pref
      switch (Services.prefs.getCharPref("editor.encode_entity")) {
        case "basic":
          outputEntity =
            Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
          break;
        case "latin1":
          outputEntity =
            Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_LATIN1_ENTITIES;
          break;
        case "html":
          outputEntity =
            Ci.nsIWebBrowserPersist.ENCODE_FLAGS_ENCODE_HTML_ENTITIES;
          break;
        case "none":
          outputEntity = 0;
          break;
      }
    } catch (e) {}
  }
  outputFlags |= outputEntity;

  if (aWrapColumn > 0) {
    outputFlags |= Ci.nsIWebBrowserPersist.ENCODE_FLAGS_WRAP;
  }

  return outputFlags;
}

// returns number of column where to wrap
function GetWrapColumn() {
  try {
    return GetCurrentEditor().wrapWidth;
  } catch (e) {}
  return 0;
}

const gShowDebugOutputStateChange = false;
const gShowDebugOutputProgress = false;
const gShowDebugOutputStatusChange = false;

const gShowDebugOutputLocationChange = false;
const gShowDebugOutputSecurityChange = false;

const kErrorBindingAborted = 2152398850;
const kErrorBindingRedirected = 2152398851;
const kFileNotFound = 2152857618;

var gEditorOutputProgressListener = {
  onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
    // Use this to access onStateChange flags
    var requestSpec;
    try {
      var channel = aRequest.QueryInterface(Ci.nsIChannel);
      requestSpec = StripUsernamePasswordFromURI(channel.URI);
    } catch (e) {
      if (gShowDebugOutputStateChange) {
        dump("***** onStateChange; NO REQUEST CHANNEL\n");
      }
    }

    if (gShowDebugOutputStateChange) {
      dump("\n***** onStateChange request: " + requestSpec + "\n");
      dump("      state flags: ");

      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
        dump(" STATE_START, ");
      }
      if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
        dump(" STATE_STOP, ");
      }
      if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
        dump(" STATE_IS_NETWORK ");
      }

      dump(`\n * requestSpec=${requestSpec}, aStatus=${aStatus}\n`);

      DumpDebugStatus(aStatus);
    }
  },

  onProgressChange(
    aWebProgress,
    aRequest,
    aCurSelfProgress,
    aMaxSelfProgress,
    aCurTotalProgress,
    aMaxTotalProgress
  ) {
    if (!gPersistObj) {
      return;
    }

    if (gShowDebugOutputProgress) {
      dump(
        "\n onProgressChange: gPersistObj.result=" + gPersistObj.result + "\n"
      );
      try {
        var channel = aRequest.QueryInterface(Ci.nsIChannel);
        dump("***** onProgressChange request: " + channel.URI.spec + "\n");
      } catch (e) {}
      dump(
        "*****       self:  " +
          aCurSelfProgress +
          " / " +
          aMaxSelfProgress +
          "\n"
      );
      dump(
        "*****       total: " +
          aCurTotalProgress +
          " / " +
          aMaxTotalProgress +
          "\n\n"
      );

      if (gPersistObj.currentState == gPersistObj.PERSIST_STATE_READY) {
        dump(" Persister is ready to save data\n\n");
      } else if (gPersistObj.currentState == gPersistObj.PERSIST_STATE_SAVING) {
        dump(" Persister is saving data.\n\n");
      } else if (
        gPersistObj.currentState == gPersistObj.PERSIST_STATE_FINISHED
      ) {
        dump(" PERSISTER HAS FINISHED SAVING DATA\n\n\n");
      }
    }
  },

  onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
    if (gShowDebugOutputLocationChange) {
      dump("***** onLocationChange: " + aLocation.spec + "\n");
      try {
        var channel = aRequest.QueryInterface(Ci.nsIChannel);
        dump("*****          request: " + channel.URI.spec + "\n");
      } catch (e) {}
    }
  },

  onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
    if (gShowDebugOutputStatusChange) {
      dump("***** onStatusChange: " + aMessage + "\n");
      try {
        var channel = aRequest.QueryInterface(Ci.nsIChannel);
        dump("*****        request: " + channel.URI.spec + "\n");
      } catch (e) {
        dump("          couldn't get request\n");
      }

      DumpDebugStatus(aStatus);

      if (gPersistObj) {
        if (gPersistObj.currentState == gPersistObj.PERSIST_STATE_READY) {
          dump(" Persister is ready to save data\n\n");
        } else if (
          gPersistObj.currentState == gPersistObj.PERSIST_STATE_SAVING
        ) {
          dump(" Persister is saving data.\n\n");
        } else if (
          gPersistObj.currentState == gPersistObj.PERSIST_STATE_FINISHED
        ) {
          dump(" PERSISTER HAS FINISHED SAVING DATA\n\n\n");
        }
      }
    }
  },

  onSecurityChange(aWebProgress, aRequest, state) {
    if (gShowDebugOutputSecurityChange) {
      try {
        var channel = aRequest.QueryInterface(Ci.nsIChannel);
        dump("***** onSecurityChange request: " + channel.URI.spec + "\n");
      } catch (e) {}
    }
  },

  onContentBlockingEvent(aWebProgress, aRequest, aEvent) {},

  QueryInterface: ChromeUtils.generateQI([
    "nsIWebProgressListener",
    "nsISupportsWeakReference",
  ]),
};

/* eslint-disable complexity */
function DumpDebugStatus(aStatus) {
  // see nsError.h and netCore.h and ftpCore.h

  if (aStatus == kErrorBindingAborted) {
    dump("***** status is NS_BINDING_ABORTED\n");
  } else if (aStatus == kErrorBindingRedirected) {
    dump("***** status is NS_BINDING_REDIRECTED\n");
  } else if (aStatus == 2152398859) {
    // in netCore.h 11
    dump("***** status is ALREADY_CONNECTED\n");
  } else if (aStatus == 2152398860) {
    // in netCore.h 12
    dump("***** status is NOT_CONNECTED\n");
  } else if (aStatus == 2152398861) {
    //  in nsISocketTransportService.idl 13
    dump("***** status is CONNECTION_REFUSED\n");
  } else if (aStatus == 2152398862) {
    // in nsISocketTransportService.idl 14
    dump("***** status is NET_TIMEOUT\n");
  } else if (aStatus == 2152398863) {
    // in netCore.h 15
    dump("***** status is IN_PROGRESS\n");
  } else if (aStatus == 2152398864) {
    // 0x804b0010 in netCore.h 16
    dump("***** status is OFFLINE\n");
  } else if (aStatus == 2152398865) {
    // in netCore.h 17
    dump("***** status is NO_CONTENT\n");
  } else if (aStatus == 2152398866) {
    // in netCore.h 18
    dump("***** status is UNKNOWN_PROTOCOL\n");
  } else if (aStatus == 2152398867) {
    // in netCore.h 19
    dump("***** status is PORT_ACCESS_NOT_ALLOWED\n");
  } else if (aStatus == 2152398868) {
    // in nsISocketTransportService.idl 20
    dump("***** status is NET_RESET\n");
  } else if (aStatus == 2152398869) {
    // in ftpCore.h 21
    dump("***** status is FTP_LOGIN\n");
  } else if (aStatus == 2152398870) {
    // in ftpCore.h 22
    dump("***** status is FTP_CWD\n");
  } else if (aStatus == 2152398871) {
    // in ftpCore.h 23
    dump("***** status is FTP_PASV\n");
  } else if (aStatus == 2152398872) {
    // in ftpCore.h 24
    dump("***** status is FTP_PWD\n");
  } else if (aStatus == 2152857601) {
    dump("***** status is UNRECOGNIZED_PATH\n");
  } else if (aStatus == 2152857602) {
    dump("***** status is UNRESOLABLE SYMLINK\n");
  } else if (aStatus == 2152857604) {
    dump("***** status is UNKNOWN_TYPE\n");
  } else if (aStatus == 2152857605) {
    dump("***** status is DESTINATION_NOT_DIR\n");
  } else if (aStatus == 2152857606) {
    dump("***** status is TARGET_DOES_NOT_EXIST\n");
  } else if (aStatus == 2152857608) {
    dump("***** status is ALREADY_EXISTS\n");
  } else if (aStatus == 2152857609) {
    dump("***** status is INVALID_PATH\n");
  } else if (aStatus == 2152857610) {
    dump("***** status is DISK_FULL\n");
  } else if (aStatus == 2152857612) {
    dump("***** status is NOT_DIRECTORY\n");
  } else if (aStatus == 2152857613) {
    dump("***** status is IS_DIRECTORY\n");
  } else if (aStatus == 2152857614) {
    dump("***** status is IS_LOCKED\n");
  } else if (aStatus == 2152857615) {
    dump("***** status is TOO_BIG\n");
  } else if (aStatus == 2152857616) {
    dump("***** status is NO_DEVICE_SPACE\n");
  } else if (aStatus == 2152857617) {
    dump("***** status is NAME_TOO_LONG\n");
  } else if (aStatus == 2152857618) {
    // 80520012
    dump("***** status is FILE_NOT_FOUND\n");
  } else if (aStatus == 2152857619) {
    dump("***** status is READ_ONLY\n");
  } else if (aStatus == 2152857620) {
    dump("***** status is DIR_NOT_EMPTY\n");
  } else if (aStatus == 2152857621) {
    dump("***** status is ACCESS_DENIED\n");
  } else if (aStatus == 2152398878) {
    dump("***** status is ? (No connection or time out?)\n");
  } else {
    dump("***** status is " + aStatus + "\n");
  }
}
/* eslint-enable complexity */

const kSupportedTextMimeTypes = [
  "text/plain",
  "text/css",
  "text/rdf",
  "text/xsl",
  "text/javascript", // obsolete type
  "text/ecmascript", // obsolete type
  "application/javascript",
  "application/ecmascript",
  "application/x-javascript", // obsolete type
  "application/xhtml+xml",
];

function IsSupportedTextMimeType(aMimeType) {
  for (var i = 0; i < kSupportedTextMimeTypes.length; i++) {
    if (kSupportedTextMimeTypes[i] == aMimeType) {
      return true;
    }
  }
  return false;
}

/* eslint-disable complexity */
// throws an error or returns true if user attempted save; false if user canceled save
async function SaveDocument(aSaveAs, aSaveCopy, aMimeType) {
  var editor = GetCurrentEditor();
  if (!aMimeType || !editor) {
    throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
  }

  var editorDoc = editor.document;
  if (!editorDoc) {
    throw Components.Exception("", Cr.NS_ERROR_NOT_INITIALIZED);
  }

  // if we don't have the right editor type bail (we handle text and html)
  var editorType = GetCurrentEditorType();
  if (!["text", "html", "htmlmail", "textmail"].includes(editorType)) {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  var saveAsTextFile = IsSupportedTextMimeType(aMimeType);

  // check if the file is to be saved is a format we don't understand; if so, bail
  if (
    aMimeType != kHTMLMimeType &&
    aMimeType != kXHTMLMimeType &&
    !saveAsTextFile
  ) {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  if (saveAsTextFile) {
    aMimeType = "text/plain";
  }

  var urlstring = GetDocumentUrl();
  var mustShowFileDialog =
    aSaveAs || IsUrlAboutBlank(urlstring) || urlstring == "";

  // If editing a remote URL, force SaveAs dialog
  if (!mustShowFileDialog && GetScheme(urlstring) != "file") {
    mustShowFileDialog = true;
  }

  var doUpdateURI = false;
  var tempLocalFile = null;

  if (mustShowFileDialog) {
    try {
      // Prompt for title if we are saving to HTML
      if (!saveAsTextFile && editorType == "html") {
        var userContinuing = PromptAndSetTitleIfNone(); // not cancel
        if (!userContinuing) {
          return false;
        }
      }

      var dialogResult = await PromptForSaveLocation(
        saveAsTextFile,
        editorType,
        aMimeType,
        urlstring
      );
      if (!dialogResult) {
        return false;
      }

      // What is this unused 'replacing' var supposed to be doing?
      /* eslint-disable-next-line no-unused-vars */
      var replacing =
        dialogResult.filepickerClick == Ci.nsIFilePicker.returnReplace;

      urlstring = dialogResult.resultingURIString;
      tempLocalFile = dialogResult.resultingLocalFile;

      // update the new URL for the webshell unless we are saving a copy
      if (!aSaveCopy) {
        doUpdateURI = true;
      }
    } catch (e) {
      console.error(e);
      return false;
    }
  } // mustShowFileDialog

  var success = true;
  try {
    // if somehow we didn't get a local file but we did get a uri,
    // attempt to create the localfile if it's a "file" url
    var docURI;
    if (!tempLocalFile) {
      docURI = Services.io.newURI(urlstring, editor.documentCharacterSet);

      if (docURI.schemeIs("file")) {
        var fileHandler = GetFileProtocolHandler();
        tempLocalFile = fileHandler
          .getFileFromURLSpec(urlstring)
          .QueryInterface(Ci.nsIFile);
      }
    }

    // this is the location where the related files will go
    var relatedFilesDir = null;

    // Only change links or move files if pref is set
    // and we are saving to a new location
    if (Services.prefs.getBoolPref("editor.save_associated_files") && aSaveAs) {
      try {
        if (tempLocalFile) {
          // if we are saving to the same parent directory, don't set relatedFilesDir
          // grab old location, chop off file
          // grab new location, chop off file, compare
          var oldLocation = GetDocumentUrl();
          var oldLocationLastSlash = oldLocation.lastIndexOf("/");
          if (oldLocationLastSlash != -1) {
            oldLocation = oldLocation.slice(0, oldLocationLastSlash);
          }

          var relatedFilesDirStr = urlstring;
          var newLocationLastSlash = relatedFilesDirStr.lastIndexOf("/");
          if (newLocationLastSlash != -1) {
            relatedFilesDirStr = relatedFilesDirStr.slice(
              0,
              newLocationLastSlash
            );
          }
          if (
            oldLocation == relatedFilesDirStr ||
            IsUrlAboutBlank(oldLocation)
          ) {
            relatedFilesDir = null;
          } else {
            relatedFilesDir = tempLocalFile.parent;
          }
        } else {
          var lastSlash = urlstring.lastIndexOf("/");
          if (lastSlash != -1) {
            var relatedFilesDirString = urlstring.slice(0, lastSlash + 1); // include last slash
            relatedFilesDir = Services.io.newURI(
              relatedFilesDirString,
              editor.documentCharacterSet
            );
          }
        }
      } catch (e) {
        relatedFilesDir = null;
      }
    }

    let destinationLocation = tempLocalFile ? tempLocalFile : docURI;

    success = OutputFileWithPersistAPI(
      editorDoc,
      destinationLocation,
      relatedFilesDir,
      aMimeType
    );
  } catch (e) {
    success = false;
  }

  if (success) {
    try {
      if (doUpdateURI) {
        // If a local file, we must create a new uri from nsIFile
        if (tempLocalFile) {
          docURI = GetFileProtocolHandler().newFileURI(tempLocalFile);
        }
      }

      // Update window title to show possibly different filename
      // This also covers problem that after undoing a title change,
      //   window title loses the extra [filename] part that this adds
      UpdateWindowTitle();

      if (!aSaveCopy) {
        editor.resetModificationCount();
      }
      // this should cause notification to listeners that document has changed

      // Set UI based on whether we're editing a remote or local url
      goUpdateCommand("cmd_save");
    } catch (e) {}
  } else {
    Services.prompt.alert(
      window,
      GetString("SaveDocument"),
      GetString("SaveFileFailed")
    );
  }
  return success;
}
/* eslint-enable complexity */

var nsFindReplaceCommand = {
  isCommandEnabled(aCommand, editorElement) {
    return editorElement.getEditor(editorElement.contentWindow) != null;
  },

  getCommandStateParams(aCommand, aParams, editorElement) {},
  doCommandParams(aCommand, aParams, editorElement) {},

  doCommand(aCommand, editorElement) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdReplace.xhtml",
      "_blank",
      "chrome,modal,titlebar",
      editorElement
    );
  },
};

var nsFindCommand = {
  isCommandEnabled(aCommand, editorElement) {
    return editorElement.getEditor(editorElement.contentWindow) != null;
  },

  getCommandStateParams(aCommand, aParams, editorElement) {},
  doCommandParams(aCommand, aParams, editorElement) {},

  doCommand(aCommand, editorElement) {
    document.getElementById("FindToolbar").onFindCommand();
  },
};

var nsFindAgainCommand = {
  isCommandEnabled(aCommand, editorElement) {
    // we can only do this if the search pattern is non-empty. Not sure how
    // to get that from here
    return editorElement.getEditor(editorElement.contentWindow) != null;
  },

  getCommandStateParams(aCommand, aParams, editorElement) {},
  doCommandParams(aCommand, aParams, editorElement) {},

  doCommand(aCommand, editorElement) {
    let findPrev = aCommand == "cmd_findPrev";
    document.getElementById("FindToolbar").onFindAgainCommand(findPrev);
  },
};

var nsRewrapCommand = {
  isCommandEnabled(aCommand, dummy) {
    return (
      IsDocumentEditable() &&
      !IsInHTMLSourceMode() &&
      GetCurrentEditor() instanceof Ci.nsIEditorMailSupport
    );
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    GetCurrentEditor().QueryInterface(Ci.nsIEditorMailSupport).rewrap(false);
  },
};

var nsSpellingCommand = {
  isCommandEnabled(aCommand, dummy) {
    return (
      IsDocumentEditable() && !IsInHTMLSourceMode() && IsSpellCheckerInstalled()
    );
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    window.cancelSendMessage = false;
    try {
      var skipBlockQuotes =
        window.document.documentElement.getAttribute("windowtype") ==
        "msgcompose";
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdSpellCheck.xhtml",
        "_blank",
        "dialog,close,titlebar,modal,resizable",
        false,
        skipBlockQuotes,
        true
      );
    } catch (ex) {}
  },
};

var nsImageCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdImageProps.xhtml",
      "_blank",
      "chrome,close,titlebar,modal"
    );
  },
};

var nsHLineCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    // Inserting an HLine is different in that we don't use properties dialog
    //  unless we are editing an existing line's attributes
    //  We get the last-used attributes from the prefs and insert immediately

    var tagName = "hr";
    var editor = GetCurrentEditor();

    var hLine;
    try {
      hLine = editor.getSelectedElement(tagName);
    } catch (e) {
      return;
    }

    if (hLine) {
      // We only open the dialog for an existing HRule
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdHLineProps.xhtml",
        "_blank",
        "chrome,close,titlebar,modal"
      );
    } else {
      try {
        hLine = editor.createElementWithDefaults(tagName);

        // We change the default attributes to those saved in the user prefs
        let align = Services.prefs.getIntPref("editor.hrule.align");
        if (align == 0) {
          editor.setAttributeOrEquivalent(hLine, "align", "left", true);
        } else if (align == 2) {
          editor.setAttributeOrEquivalent(hLine, "align", "right", true);
        }

        // Note: Default is center (don't write attribute)

        let width = Services.prefs.getIntPref("editor.hrule.width");
        if (Services.prefs.getBoolPref("editor.hrule.width_percent")) {
          width = width + "%";
        }

        editor.setAttributeOrEquivalent(hLine, "width", width, true);

        let height = Services.prefs.getIntPref("editor.hrule.height");
        editor.setAttributeOrEquivalent(hLine, "size", String(height), true);

        if (Services.prefs.getBoolPref("editor.hrule.shading")) {
          hLine.removeAttribute("noshade");
        } else {
          hLine.setAttribute("noshade", "noshade");
        }

        editor.insertElementAtSelection(hLine, true);
      } catch (e) {}
    }
  },
};

var nsLinkCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    // If selected element is an image, launch that dialog instead
    // since last tab panel handles link around an image
    var element = GetObjectForProperties();
    if (element && element.nodeName.toLowerCase() == "img") {
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdImageProps.xhtml",
        "_blank",
        "chrome,close,titlebar,modal",
        null,
        true
      );
    } else {
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdLinkProps.xhtml",
        "_blank",
        "chrome,close,titlebar,modal"
      );
    }
  },
};

var nsAnchorCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdNamedAnchorProps.xhtml",
      "_blank",
      "chrome,close,titlebar,modal",
      ""
    );
  },
};

var nsInsertHTMLWithDialogCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    gMsgCompose.allowRemoteContent = true;
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdInsSrc.xhtml",
      "_blank",
      "chrome,close,titlebar,modal,resizable",
      ""
    );
  },
};

var nsInsertMathWithDialogCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdInsertMath.xhtml",
      "_blank",
      "chrome,close,titlebar,modal,resizable",
      ""
    );
  },
};

var nsInsertCharsCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    EditorFindOrCreateInsertCharWindow();
  },
};

var nsInsertBreakAllCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentEditor().insertHTML("<br clear='all'>");
    } catch (e) {}
  },
};

var nsListPropertiesCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdListProps.xhtml",
      "_blank",
      "chrome,close,titlebar,modal"
    );
  },
};

var nsObjectPropertiesCommand = {
  isCommandEnabled(aCommand, dummy) {
    var isEnabled = false;
    if (IsDocumentEditable() && IsEditingRenderedHTML()) {
      isEnabled =
        GetObjectForProperties() != null ||
        GetCurrentEditor().getSelectedElement("href") != null;
    }
    return isEnabled;
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    // Launch Object properties for appropriate selected element
    var element = GetObjectForProperties();
    if (element) {
      var name = element.nodeName.toLowerCase();
      switch (name) {
        case "img":
          gMsgCompose.allowRemoteContent = true;
          goDoCommand("cmd_image");
          break;
        case "hr":
          goDoCommand("cmd_hline");
          break;
        case "table":
          EditorInsertOrEditTable(false);
          break;
        case "td":
        case "th":
          EditorTableCellProperties();
          break;
        case "ol":
        case "ul":
        case "dl":
        case "li":
          goDoCommand("cmd_listProperties");
          break;
        case "a":
          if (element.name) {
            goDoCommand("cmd_anchor");
          } else if (element.href) {
            goDoCommand("cmd_link");
          }
          break;
        case "math":
          goDoCommand("cmd_insertMathWithDialog");
          break;
        default:
          doAdvancedProperties(element);
          break;
      }
    } else {
      // We get a partially-selected link if asked for specifically
      try {
        element = GetCurrentEditor().getSelectedElement("href");
      } catch (e) {}
      if (element) {
        goDoCommand("cmd_link");
      }
    }
  },
};

var nsSetSmiley = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {
    try {
      let editor = GetCurrentEditor();
      let smileyCode = aParams.getStringValue("state_attribute");
      editor.insertHTML(smileyCode);
      window.content.focus();
    } catch (e) {
      dump("Exception occurred in smiley InsertElementAtSelection\n");
    }
  },
  // This is now deprecated in favor of "doCommandParams"
  doCommand(aCommand) {},
};

function doAdvancedProperties(element) {
  if (element) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdAdvancedEdit.xhtml",
      "_blank",
      "chrome,close,titlebar,modal,resizable=yes",
      "",
      element
    );
  }
}

var nsColorPropertiesCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdColorProps.xhtml",
      "_blank",
      "chrome,close,titlebar,modal",
      ""
    );
    UpdateDefaultColors();
  },
};

var nsIncreaseFontCommand = {
  isCommandEnabled(aCommand, dummy) {
    if (!(IsDocumentEditable() && IsEditingRenderedHTML())) {
      return false;
    }
    let setIndex = parseInt(getLegacyFontSize());
    return setIndex < 6;
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    let setIndex = parseInt(getLegacyFontSize());
    EditorSetFontSize((setIndex + 1).toString());
  },
};

var nsDecreaseFontCommand = {
  isCommandEnabled(aCommand, dummy) {
    if (!(IsDocumentEditable() && IsEditingRenderedHTML())) {
      return false;
    }
    let setIndex = parseInt(getLegacyFontSize());
    return setIndex > 1;
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    let setIndex = parseInt(getLegacyFontSize());
    EditorSetFontSize((setIndex - 1).toString());
  },
};

var nsRemoveNamedAnchorsCommand = {
  isCommandEnabled(aCommand, dummy) {
    // We could see if there's any link in selection, but it doesn't seem worth the work!
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    EditorRemoveTextProperty("name", "");
    window.content.focus();
  },
};

var nsInsertOrEditTableCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    if (IsInTableCell()) {
      EditorTableCellProperties();
    } else {
      EditorInsertOrEditTable(true);
    }
  },
};

var nsEditTableCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTable();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    EditorInsertOrEditTable(false);
  },
};

var nsSelectTableCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTable();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().selectTable();
    } catch (e) {}
    window.content.focus();
  },
};

var nsSelectTableRowCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().selectTableRow();
    } catch (e) {}
    window.content.focus();
  },
};

var nsSelectTableColumnCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().selectTableColumn();
    } catch (e) {}
    window.content.focus();
  },
};

var nsSelectTableCellCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().selectTableCell();
    } catch (e) {}
    window.content.focus();
  },
};

var nsSelectAllTableCellsCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTable();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().selectAllTableCells();
    } catch (e) {}
    window.content.focus();
  },
};

var nsInsertTableCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsDocumentEditable() && IsEditingRenderedHTML();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    EditorInsertTable();
  },
};

var nsInsertTableRowAboveCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().insertTableRow(1, false);
    } catch (e) {}
    window.content.focus();
  },
};

var nsInsertTableRowBelowCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().insertTableRow(1, true);
    } catch (e) {}
    window.content.focus();
  },
};

var nsInsertTableColumnBeforeCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().insertTableColumn(1, false);
    } catch (e) {}
    window.content.focus();
  },
};

var nsInsertTableColumnAfterCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().insertTableColumn(1, true);
    } catch (e) {}
    window.content.focus();
  },
};

var nsInsertTableCellBeforeCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().insertTableCell(1, false);
    } catch (e) {}
    window.content.focus();
  },
};

var nsInsertTableCellAfterCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().insertTableCell(1, true);
    } catch (e) {}
    window.content.focus();
  },
};

var nsDeleteTableCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTable();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().deleteTable();
    } catch (e) {}
    window.content.focus();
  },
};

var nsDeleteTableRowCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    var rows = GetNumberOfContiguousSelectedRows();
    // Delete at least one row
    if (rows == 0) {
      rows = 1;
    }

    try {
      var editor = GetCurrentTableEditor();
      editor.beginTransaction();

      // Loop to delete all blocks of contiguous, selected rows
      while (rows) {
        editor.deleteTableRow(rows);
        rows = GetNumberOfContiguousSelectedRows();
      }
    } finally {
      editor.endTransaction();
    }
    window.content.focus();
  },
};

var nsDeleteTableColumnCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    var columns = GetNumberOfContiguousSelectedColumns();
    // Delete at least one column
    if (columns == 0) {
      columns = 1;
    }

    try {
      var editor = GetCurrentTableEditor();
      editor.beginTransaction();

      // Loop to delete all blocks of contiguous, selected columns
      while (columns) {
        editor.deleteTableColumn(columns);
        columns = GetNumberOfContiguousSelectedColumns();
      }
    } finally {
      editor.endTransaction();
    }
    window.content.focus();
  },
};

var nsDeleteTableCellCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().deleteTableCell(1);
    } catch (e) {}
    window.content.focus();
  },
};

var nsDeleteTableCellContentsCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTableCell();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().deleteTableCellContents();
    } catch (e) {}
    window.content.focus();
  },
};

var nsJoinTableCellsCommand = {
  isCommandEnabled(aCommand, dummy) {
    if (IsDocumentEditable() && IsEditingRenderedHTML()) {
      try {
        var editor = GetCurrentTableEditor();
        var tagNameObj = { value: "" };
        var countObj = { value: 0 };
        var cell = editor.getSelectedOrParentTableElement(tagNameObj, countObj);

        // We need a cell and either > 1 selected cell or a cell to the right
        //  (this cell may originate in a row spanned from above current row)
        // Note that editor returns "td" for "th" also.
        // (this is a pain! Editor and gecko use lowercase tagNames, JS uses uppercase!)
        if (cell && tagNameObj.value == "td") {
          // Selected cells
          if (countObj.value > 1) {
            return true;
          }

          var colSpan = cell.getAttribute("colspan");

          // getAttribute returns string, we need number
          // no attribute means colspan = 1
          if (!colSpan) {
            colSpan = Number(1);
          } else {
            colSpan = Number(colSpan);
          }

          var rowObj = { value: 0 };
          var colObj = { value: 0 };
          editor.getCellIndexes(cell, rowObj, colObj);

          // Test if cell exists to the right of current cell
          // (cells with 0 span should never have cells to the right
          //  if there is, user can select the 2 cells to join them)
          return (
            colSpan &&
            editor.getCellAt(null, rowObj.value, colObj.value + colSpan)
          );
        }
      } catch (e) {}
    }
    return false;
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    // Param: Don't merge non-contiguous cells
    try {
      GetCurrentTableEditor().joinTableCells(false);
    } catch (e) {}
    window.content.focus();
  },
};

var nsSplitTableCellCommand = {
  isCommandEnabled(aCommand, dummy) {
    if (IsDocumentEditable() && IsEditingRenderedHTML()) {
      var tagNameObj = { value: "" };
      var countObj = { value: 0 };
      var cell;
      try {
        cell = GetCurrentTableEditor().getSelectedOrParentTableElement(
          tagNameObj,
          countObj
        );
      } catch (e) {}

      // We need a cell parent and there's just 1 selected cell
      // or selection is entirely inside 1 cell
      if (
        cell &&
        tagNameObj.value == "td" &&
        countObj.value <= 1 &&
        IsSelectionInOneCell()
      ) {
        var colSpan = cell.getAttribute("colspan");
        var rowSpan = cell.getAttribute("rowspan");
        if (!colSpan) {
          colSpan = 1;
        }
        if (!rowSpan) {
          rowSpan = 1;
        }
        return colSpan > 1 || rowSpan > 1 || colSpan == 0 || rowSpan == 0;
      }
    }
    return false;
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    try {
      GetCurrentTableEditor().splitTableCell();
    } catch (e) {}
    window.content.focus();
  },
};

var nsTableOrCellColorCommand = {
  isCommandEnabled(aCommand, dummy) {
    return IsInTable();
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    EditorSelectColor("TableOrCell");
  },
};

var nsConvertToTable = {
  isCommandEnabled(aCommand, dummy) {
    if (IsDocumentEditable() && IsEditingRenderedHTML()) {
      var selection;
      try {
        selection = GetCurrentEditor().selection;
      } catch (e) {}

      if (selection && !selection.isCollapsed) {
        // Don't allow if table or cell is the selection
        var element;
        try {
          element = GetCurrentEditor().getSelectedElement("");
        } catch (e) {}
        if (element) {
          var name = element.nodeName.toLowerCase();
          if (
            name == "td" ||
            name == "th" ||
            name == "caption" ||
            name == "table"
          ) {
            return false;
          }
        }

        // Selection start and end must be in the same cell
        //   in same cell or both are NOT in a cell
        if (
          GetParentTableCell(selection.focusNode) !=
          GetParentTableCell(selection.anchorNode)
        ) {
          return false;
        }

        return true;
      }
    }
    return false;
  },

  getCommandStateParams(aCommand, aParams, aRefCon) {},
  doCommandParams(aCommand, aParams, aRefCon) {},

  doCommand(aCommand) {
    if (this.isCommandEnabled()) {
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdConvertToTable.xhtml",
        "_blank",
        "chrome,close,titlebar,modal"
      );
    }
  },
};
