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

/* import-globals-from ../../../../../toolkit/content/viewZoomOverlay.js */
/* import-globals-from ../../../base/content/globalOverlay.js */
/* import-globals-from ComposerCommands.js */
/* import-globals-from editorUtilities.js */

var { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

/* Main Composer window UI control */

var gComposerWindowControllerID = 0;
var prefAuthorString = "";

var kDisplayModeNormal = 0;
var kDisplayModeAllTags = 1;
var kDisplayModeSource = 2;
var kDisplayModePreview = 3;

const kDisplayModeMenuIDs = [
  "viewNormalMode",
  "viewAllTagsMode",
  "viewSourceMode",
  "viewPreviewMode",
];
const kDisplayModeTabIDS = [
  "NormalModeButton",
  "TagModeButton",
  "SourceModeButton",
  "PreviewModeButton",
];
const kNormalStyleSheet = "chrome://messenger/skin/shared/editorContent.css";
const kContentEditableStyleSheet = "resource://gre/res/contenteditable.css";

var kTextMimeType = "text/plain";
var kHTMLMimeType = "text/html";
var kXHTMLMimeType = "application/xhtml+xml";

var gPreviousNonSourceDisplayMode = 1;
var gEditorDisplayMode = -1;
var gDocWasModified = false; // Check if clean document, if clean then unload when user "Opens"
var gContentWindow = 0;
var gSourceContentWindow = 0;
var gSourceTextEditor = null;
var gContentWindowDeck;
var gFormatToolbar;
var gFormatToolbarHidden = false;
var gViewFormatToolbar;
var gChromeState;
var gColorObj = {
  LastTextColor: "",
  LastBackgroundColor: "",
  LastHighlightColor: "",
  Type: "",
  SelectedType: "",
  NoDefault: false,
  Cancel: false,
  HighlightColor: "",
  BackgroundColor: "",
  PageColor: "",
  TextColor: "",
  TableColor: "",
  CellColor: "",
};
var gDefaultTextColor = "";
var gDefaultBackgroundColor = "";
var gCSSPrefListener;
var gEditorToolbarPrefListener;
var gReturnInParagraphPrefListener;
var gLocalFonts = null;

var gLastFocusNode = null;
var gLastFocusNodeWasSelected = false;

// These must be kept in synch with the XUL <options> lists
var gFontSizeNames = [
  "xx-small",
  "x-small",
  "small",
  "medium",
  "large",
  "x-large",
  "xx-large",
];

var kUseCssPref = "editor.use_css";
var kCRInParagraphsPref = "editor.CR_creates_new_p";

// This should be called by all editor users when they close their window.
function EditorCleanup() {
  SwitchInsertCharToAnotherEditorOrClose();
}

/** @implements {nsIDocumentStateListener} */
var DocumentReloadListener = {
  NotifyDocumentWillBeDestroyed() {},

  NotifyDocumentStateChanged(isNowDirty) {
    var editor = GetCurrentEditor();
    try {
      // unregister the listener to prevent multiple callbacks
      editor.removeDocumentStateListener(DocumentReloadListener);

      var charset = editor.documentCharacterSet;

      // update the META charset with the current presentation charset
      editor.documentCharacterSet = charset;
    } catch (e) {}
  },
};

// implements nsIObserver
var gEditorDocumentObserver = {
  observe(aSubject, aTopic, aData) {
    // Should we allow this even if NOT the focused editor?
    var commandManager = GetCurrentCommandManager();
    if (commandManager != aSubject) {
      return;
    }

    var editor = GetCurrentEditor();
    switch (aTopic) {
      case "obs_documentCreated":
        // Just for convenience
        gContentWindow = window.content;

        // Get state to see if document creation succeeded
        var params = newCommandParams();
        if (!params) {
          return;
        }

        try {
          commandManager.getCommandState(aTopic, gContentWindow, params);
          var errorStringId = 0;
          var editorStatus = params.getLongValue("state_data");
          if (!editor && editorStatus == Ci.nsIEditingSession.eEditorOK) {
            dump(
              "\n ****** NO EDITOR BUT NO EDITOR ERROR REPORTED ******* \n\n"
            );
            editorStatus = Ci.nsIEditingSession.eEditorErrorUnknown;
          }

          switch (editorStatus) {
            case Ci.nsIEditingSession.eEditorErrorCantEditFramesets:
              errorStringId = "CantEditFramesetMsg";
              break;
            case Ci.nsIEditingSession.eEditorErrorCantEditMimeType:
              errorStringId = "CantEditMimeTypeMsg";
              break;
            case Ci.nsIEditingSession.eEditorErrorUnknown:
              errorStringId = "CantEditDocumentMsg";
              break;
            // Note that for "eEditorErrorFileNotFound,
            // network code popped up an alert dialog, so we don't need to
          }
          if (errorStringId) {
            Services.prompt.alert(window, "", GetString(errorStringId));
          }
        } catch (e) {
          dump("EXCEPTION GETTING obs_documentCreated state " + e + "\n");
        }

        // We have a bad editor -- nsIEditingSession will rebuild an editor
        //   with a blank page, so simply abort here
        if (editorStatus) {
          return;
        }

        if (!("InsertCharWindow" in window)) {
          window.InsertCharWindow = null;
        }

        let domWindowUtils =
          GetCurrentEditorElement().contentWindow.windowUtils;
        // And extra styles for showing anchors, table borders, smileys, etc.
        domWindowUtils.loadSheetUsingURIString(
          kNormalStyleSheet,
          domWindowUtils.AGENT_SHEET
        );

        // Remove contenteditable stylesheets if they were applied by the
        // editingSession.
        domWindowUtils.removeSheetUsingURIString(
          kContentEditableStyleSheet,
          domWindowUtils.AGENT_SHEET
        );

        // Add mouse click watcher if right type of editor
        if (IsHTMLEditor()) {
          // Force color widgets to update
          onFontColorChange();
          onBackgroundColorChange();
        }
        break;

      case "cmd_setDocumentModified":
        window.updateCommands("save");
        break;

      case "obs_documentWillBeDestroyed":
        dump("obs_documentWillBeDestroyed notification\n");
        break;

      case "obs_documentLocationChanged":
        // Ignore this when editor doesn't exist,
        //   which happens once when page load starts
        if (editor) {
          try {
            editor.updateBaseURL();
          } catch (e) {
            dump(e);
          }
        }
        break;

      case "cmd_bold":
        // Update all style items
        // cmd_bold is a proxy; see EditorSharedStartup (above) for details
        window.updateCommands("style");
        window.updateCommands("undo");
        break;
    }
  },
};

function SetFocusOnStartup() {
  gContentWindow.focus();
}

function EditorLoadUrl(url) {
  try {
    if (url) {
      let loadURIOptions = {
        loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE,
        triggeringPrincipal:
          Services.scriptSecurityManager.getSystemPrincipal(),
      };
      GetCurrentEditorElement().webNavigation.fixupAndLoadURIString(
        url,
        loadURIOptions
      );
    }
  } catch (e) {
    dump(" EditorLoadUrl failed: " + e + "\n");
  }
}

// This should be called by all Composer types
function EditorSharedStartup() {
  // Just for convenience
  gContentWindow = window.content;

  // Disable DNS Prefetching on the docshell - we don't need it for composer
  // type windows.
  GetCurrentEditorElement().docShell.allowDNSPrefetch = false;

  let messageEditorBrowser = GetCurrentEditorElement();
  messageEditorBrowser.addEventListener(
    "DoZoomEnlargeBy10",
    () => {
      ZoomManager.scrollZoomEnlarge(messageEditorBrowser);
    },
    true
  );
  messageEditorBrowser.addEventListener(
    "DoZoomReduceBy10",
    () => {
      ZoomManager.scrollReduceEnlarge(messageEditorBrowser);
    },
    true
  );

  // Set up the mime type and register the commands.
  if (IsHTMLEditor()) {
    SetupHTMLEditorCommands();
  } else {
    SetupTextEditorCommands();
  }

  // add observer to be called when document is really done loading
  // and is modified
  // Note: We're really screwed if we fail to install this observer!
  try {
    var commandManager = GetCurrentCommandManager();
    commandManager.addCommandObserver(
      gEditorDocumentObserver,
      "obs_documentCreated"
    );
    commandManager.addCommandObserver(
      gEditorDocumentObserver,
      "cmd_setDocumentModified"
    );
    commandManager.addCommandObserver(
      gEditorDocumentObserver,
      "obs_documentWillBeDestroyed"
    );
    commandManager.addCommandObserver(
      gEditorDocumentObserver,
      "obs_documentLocationChanged"
    );

    // Until nsIControllerCommandGroup-based code is implemented,
    //  we will observe just the bold command to trigger update of
    //  all toolbar style items
    commandManager.addCommandObserver(gEditorDocumentObserver, "cmd_bold");
  } catch (e) {
    dump(e);
  }

  var isMac = AppConstants.platform == "macosx";

  // Set platform-specific hints for how to select cells
  // Mac uses "Cmd", all others use "Ctrl"
  var tableKey = GetString(isMac ? "XulKeyMac" : "TableSelectKey");
  var dragStr = tableKey + GetString("Drag");
  var clickStr = tableKey + GetString("Click");

  var delStr = GetString(isMac ? "Clear" : "Del");

  SafeSetAttribute("menu_SelectCell", "acceltext", clickStr);
  SafeSetAttribute("menu_SelectRow", "acceltext", dragStr);
  SafeSetAttribute("menu_SelectColumn", "acceltext", dragStr);
  SafeSetAttribute("menu_SelectAllCells", "acceltext", dragStr);
  // And add "Del" or "Clear"
  SafeSetAttribute("menu_DeleteCellContents", "acceltext", delStr);

  // Set text for indent, outdent keybinding

  // hide UI that we don't have components for
  RemoveInapplicableUIElements();

  // Use browser colors as initial values for editor's default colors
  var BrowserColors = GetDefaultBrowserColors();
  if (BrowserColors) {
    gDefaultTextColor = BrowserColors.TextColor;
    gDefaultBackgroundColor = BrowserColors.BackgroundColor;
  }

  // For new window, no default last-picked colors
  gColorObj.LastTextColor = "";
  gColorObj.LastBackgroundColor = "";
  gColorObj.LastHighlightColor = "";
}

function SafeSetAttribute(nodeID, attributeName, attributeValue) {
  var theNode = document.getElementById(nodeID);
  if (theNode) {
    theNode.setAttribute(attributeName, attributeValue);
  }
}

async function CheckAndSaveDocument(command, allowDontSave) {
  var document;
  try {
    // if we don't have an editor or an document, bail
    var editor = GetCurrentEditor();
    document = editor.document;
    if (!document) {
      return true;
    }
  } catch (e) {
    return true;
  }

  if (!IsDocumentModified() && !IsHTMLSourceChanged()) {
    return true;
  }

  // call window.focus, since we need to pop up a dialog
  // and therefore need to be visible (to prevent user confusion)
  top.document.commandDispatcher.focusedWindow.focus();

  var strID;
  switch (command) {
    case "cmd_close":
      strID = "BeforeClosing";
      break;
  }

  var reasonToSave = strID ? GetString(strID) : "";

  var title = document.title || GetString("untitledDefaultFilename");

  var dialogTitle = GetString("SaveDocument");
  var dialogMsg = GetString("SaveFilePrompt");
  dialogMsg = dialogMsg
    .replace(/%title%/, title)
    .replace(/%reason%/, reasonToSave);

  let result = { value: 0 };
  let promptFlags =
    Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
  let button1Title = null;
  let button3Title = null;

  promptFlags +=
    Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0;

  // If allowing "Don't..." button, add that
  if (allowDontSave) {
    promptFlags +=
      Services.prompt.BUTTON_TITLE_DONT_SAVE * Services.prompt.BUTTON_POS_2;
  }

  result = Services.prompt.confirmEx(
    window,
    dialogTitle,
    dialogMsg,
    promptFlags,
    button1Title,
    null,
    button3Title,
    null,
    { value: 0 }
  );

  if (result == 0) {
    // Save to local disk
    return SaveDocument(false, false, editor.contentsMIMEType);
  }

  if (result == 2) {
    // "Don't Save"
    return true;
  }

  // Default or result == 1 (Cancel)
  return false;
}

// --------------------------- Text style ---------------------------

function editorSetParagraphState(state) {
  if (state === "") {
    // Corresponds to body text. Has no corresponding formatBlock value.
    goDoCommandParams("cmd_paragraphState", "");
  } else {
    GetCurrentEditor().document.execCommand("formatBlock", false, state);
  }
  document.getElementById("cmd_paragraphState").setAttribute("state", state);
  onParagraphFormatChange();
}

function onParagraphFormatChange() {
  let paraMenuList = document.getElementById("ParagraphSelect");
  if (!paraMenuList) {
    return;
  }

  var commandNode = document.getElementById("cmd_paragraphState");
  var state = commandNode.getAttribute("state");

  // force match with "normal"
  if (state == "body") {
    state = "";
  }

  if (state == "mixed") {
    // Selection is the "mixed" ( > 1 style) state
    paraMenuList.selectedItem = null;
    paraMenuList.setAttribute("label", GetString("Mixed"));
  } else {
    var menuPopup = document.getElementById("ParagraphPopup");
    for (let menuItem of menuPopup.children) {
      if (menuItem.value === state) {
        paraMenuList.selectedItem = menuItem;
        break;
      }
    }
  }
}

function editorRemoveTextStyling() {
  GetCurrentEditor().document.execCommand("removeFormat", false, null);
  // After removing the formatting, update the full styling command set.
  window.updateCommands("style");
}

/**
 * Selects the current font face in the menulist.
 */
function onFontFaceChange() {
  let fontFaceMenuList = document.getElementById("FontFaceSelect");
  var commandNode = document.getElementById("cmd_fontFace");
  var editorFont = commandNode.getAttribute("state");

  // Strip quotes in font names. Experiments have shown that we only
  // ever get double quotes around the font name, never single quotes,
  // even if they were in the HTML source. Also single or double
  // quotes within the font name are never returned.
  editorFont = editorFont.replace(/"/g, "");

  switch (editorFont) {
    case "mixed":
      // Selection is the "mixed" ( > 1 style) state.
      fontFaceMenuList.selectedItem = null;
      fontFaceMenuList.setAttribute("label", GetString("Mixed"));
      return;
    case "":
    case "serif":
    case "sans-serif":
      // Generic variable width.
      fontFaceMenuList.selectedIndex = 0;
      return;
    case "tt":
    case "monospace":
      // Generic fixed width.
      fontFaceMenuList.selectedIndex = 1;
      return;
    default:
  }

  let menuPopup = fontFaceMenuList.menupopup;
  let menuItems = menuPopup.children;

  const genericFamilies = [
    "serif",
    "sans-serif",
    "monospace",
    "fantasy",
    "cursive",
  ];
  // Bug 1139524: Normalise before we compare: Make it lower case
  // and replace ", " with "," so that entries like
  // "Helvetica, Arial, sans-serif" are always recognised correctly
  let editorFontToLower = editorFont.toLowerCase().replace(/, /g, ",");
  let foundFont = null;
  let exactMatch = false;
  let usedFontsSep = menuPopup.querySelector(
    "menuseparator.fontFaceMenuAfterUsedFonts"
  );
  let editorFontOptions = editorFontToLower.split(",");
  let editorOptionsCount = editorFontOptions.length;
  let matchedFontIndex = editorOptionsCount; // initialise to high invalid value

  // The font menu has this structure:
  // 0: Variable Width
  // 1: Fixed Width
  // 2: Separator
  // 3: Helvetica, Arial (stored as Helvetica, Arial, sans-serif)
  // 4: Times (stored as Times New Roman, Times, serif)
  // 5: Courier (stored as Courier New, Courier, monospace)
  // 6: Separator, "menuseparator.fontFaceMenuAfterDefaultFonts"
  // from 7: Used Font Section (for quick selection)
  // followed by separator, "menuseparator.fontFaceMenuAfterUsedFonts"
  // followed by all other available fonts.
  // The following variable keeps track of where we are when we loop over the menu.
  let afterUsedFontSection = false;

  // The menu items not only have "label" and "value", but also some other attributes:
  // "value_parsed": Is the toLowerCase() and space-stripped value.
  // "value_cache":  Is a concatenation of all editor fonts that were ever mapped
  //                 onto this menu item. This is done for optimization.
  // "used":         This item is in the used font section.

  for (let i = 0; i < menuItems.length; i++) {
    let menuItem = menuItems.item(i);
    if (
      menuItem.hasAttribute("label") &&
      menuItem.hasAttribute("value_parsed")
    ) {
      // The element seems to represent a font <menuitem>.
      let fontMenuValue = menuItem.getAttribute("value_parsed");
      if (
        fontMenuValue == editorFontToLower ||
        (menuItem.hasAttribute("value_cache") &&
          menuItem
            .getAttribute("value_cache")
            .split("|")
            .includes(editorFontToLower))
      ) {
        // This menuitem contains the font we are looking for.
        foundFont = menuItem;
        exactMatch = true;
        break;
      } else if (editorOptionsCount > 1 && afterUsedFontSection) {
        // Once we are in the list of all other available fonts,
        // we will find the one that best matches one of the options.
        let matchPos = editorFontOptions.indexOf(fontMenuValue);
        if (matchPos >= 0 && matchPos < matchedFontIndex) {
          // This menu font comes earlier in the list of options,
          // so prefer it.
          matchedFontIndex = matchPos;
          foundFont = menuItem;
          // If we matched the first option, we don't need to look for
          // a better match.
          if (matchPos == 0) {
            break;
          }
        }
      }
    } else if (menuItem == usedFontsSep) {
      // Some other element type.
      // We have now passed the section of used fonts and are now in the list of all.
      afterUsedFontSection = true;
    }
  }

  if (foundFont) {
    let defaultFontsSep = menuPopup.querySelector(
      "menuseparator.fontFaceMenuAfterDefaultFonts"
    );
    if (exactMatch) {
      if (afterUsedFontSection) {
        // Copy the matched font into the section of used fonts.
        // We insert after the separator following the default fonts,
        // so right at the beginning of the used fonts section.
        let copyItem = foundFont.cloneNode(true);
        menuPopup.insertBefore(copyItem, defaultFontsSep.nextElementSibling);
        usedFontsSep.hidden = false;
        foundFont = copyItem;
        foundFont.setAttribute("used", "true");
      }
    } else {
      // Keep only the found font and generic families in the font string.
      editorFont = editorFont
        .replace(/, /g, ",")
        .split(",")
        .filter(
          font =>
            font.toLowerCase() == foundFont.getAttribute("value_parsed") ||
            genericFamilies.includes(font)
        )
        .join(",");

      // Check if such an item is already in the used font section.
      if (afterUsedFontSection) {
        foundFont = menuPopup.querySelector(
          'menuitem[used="true"][value_parsed="' +
            editorFont.toLowerCase() +
            '"]'
        );
      }
      // If not, create a new entry which will be inserted into that section.
      if (!foundFont) {
        foundFont = createFontFaceMenuitem(editorFont, editorFont, menuPopup);
      }

      // Add the editor font string into the 'cache' attribute in the element
      // so we can later find it quickly without building the reduced string again.
      let fontCache = "";
      if (foundFont.hasAttribute("value_cache")) {
        fontCache = foundFont.getAttribute("value_cache");
      }
      foundFont.setAttribute(
        "value_cache",
        fontCache + "|" + editorFontToLower
      );

      // If we created a new item, set it up and insert.
      if (!foundFont.hasAttribute("used")) {
        foundFont.setAttribute("used", "true");
        usedFontsSep.hidden = false;
        menuPopup.insertBefore(foundFont, defaultFontsSep.nextElementSibling);
      }
    }
  } else {
    // The editor encountered a font that is not installed on this system.
    // Add it to the font menu now, in the used-fonts section right at the
    // bottom before the separator of the section.
    let fontLabel = GetFormattedString("NotInstalled", editorFont);
    foundFont = createFontFaceMenuitem(fontLabel, editorFont, menuPopup);
    foundFont.setAttribute("used", "true");
    usedFontsSep.hidden = false;
    menuPopup.insertBefore(foundFont, usedFontsSep);
  }
  fontFaceMenuList.selectedItem = foundFont;
}

/**
 * Changes the font size for the selection or at the insertion point. This
 * requires an integer from 1-7 as a value argument (x-small - xxx-large)
 *
 * @param {"1"|"2"|"3"|"4"|"5"|"6"|"7"} size - The font size.
 */
function EditorSetFontSize(size) {
  // For normal/medium size (that is 3), we clear size.
  if (size == "3") {
    EditorRemoveTextProperty("font", "size");
    // Also remove big and small,
    //  else it will seem like size isn't changing correctly
    EditorRemoveTextProperty("small", "");
    EditorRemoveTextProperty("big", "");
  } else {
    GetCurrentEditor().document.execCommand("fontSize", false, size);
  }
  // Enable or Disable the toolbar buttons according to the font size.
  goUpdateCommand("cmd_decreaseFontStep");
  goUpdateCommand("cmd_increaseFontStep");
  gContentWindow.focus();
}

function initFontFaceMenu(menuPopup) {
  initLocalFontFaceMenu(menuPopup);

  if (menuPopup) {
    var children = menuPopup.children;
    if (!children) {
      return;
    }

    var mixed = { value: false };
    var editorFont = GetCurrentEditor().getFontFaceState(mixed);

    // Strip quotes in font names. Experiments have shown that we only
    // ever get double quotes around the font name, never single quotes,
    // even if they were in the HTML source. Also single or double
    // quotes within the font name are never returned.
    editorFont = editorFont.replace(/"/g, "");

    if (!mixed.value) {
      switch (editorFont) {
        case "":
        case "serif":
        case "sans-serif":
          // Generic variable width.
          editorFont = "";
          break;
        case "tt":
        case "monospace":
          // Generic fixed width.
          editorFont = "monospace";
          break;
        default:
          editorFont = editorFont.toLowerCase().replace(/, /g, ","); // bug 1139524
      }
    }

    var editorFontOptions = editorFont.split(",");
    var matchedOption = editorFontOptions.length; // initialise to high invalid value
    for (var i = 0; i < children.length; i++) {
      var menuItem = children[i];
      if (menuItem.localName == "menuitem") {
        var matchFound = false;
        if (!mixed.value) {
          var menuFont = menuItem
            .getAttribute("value")
            .toLowerCase()
            .replace(/, /g, ",");

          // First compare the entire font string to match items that contain commas.
          if (menuFont == editorFont) {
            menuItem.setAttribute("checked", "true");
            break;
          } else if (editorFontOptions.length > 1) {
            // Next compare the individual options.
            var matchPos = editorFontOptions.indexOf(menuFont);
            if (matchPos >= 0 && matchPos < matchedOption) {
              // This menu font comes earlier in the list of options,
              // so prefer it.
              menuItem.setAttribute("checked", "true");

              // If we matched the first option, we don't need to look for
              // a better match.
              if (matchPos == 0) {
                break;
              }

              matchedOption = matchPos;
              matchFound = true;
            }
          }
        }

        // In case this item doesn't match, make sure we've cleared the checkmark.
        if (!matchFound) {
          menuItem.removeAttribute("checked");
        }
      }
    }
  }
}

// Number of fixed font face menuitems, these are:
// Variable Width
// Fixed Width
// ==separator
// Helvetica, Arial
// Times
// Courier
// ==separator
// ==separator
const kFixedFontFaceMenuItems = 8;

function initLocalFontFaceMenu(menuPopup) {
  if (!gLocalFonts) {
    // Build list of all local fonts once per editor
    try {
      var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"].getService(
        Ci.nsIFontEnumerator
      );
      gLocalFonts = enumerator.EnumerateAllFonts();
    } catch (e) {}
  }

  // Don't use radios for menulists.
  let useRadioMenuitems = menuPopup.parentNode.localName == "menu";
  menuPopup.setAttribute("useRadios", useRadioMenuitems);
  if (menuPopup.children.length == kFixedFontFaceMenuItems) {
    if (gLocalFonts.length == 0) {
      menuPopup.querySelector(".fontFaceMenuAfterDefaultFonts").hidden = true;
    }
    for (let i = 0; i < gLocalFonts.length; ++i) {
      // Remove Linux system generic fonts that collide with CSS generic fonts.
      if (
        gLocalFonts[i] != "" &&
        gLocalFonts[i] != "serif" &&
        gLocalFonts[i] != "sans-serif" &&
        gLocalFonts[i] != "monospace"
      ) {
        let itemNode = createFontFaceMenuitem(
          gLocalFonts[i],
          gLocalFonts[i],
          menuPopup
        );
        menuPopup.appendChild(itemNode);
      }
    }
  }
}

/**
 * Creates a menuitem element for the font faces menulist. Returns the menuitem
 * but does not add it automatically to the menupopup.
 *
 * @param aFontLabel  Label to be displayed for the item.
 * @param aFontName   The font face value to be used for the item.
 *                    Will be used in <font face="value"> in the edited document.
 * @param aMenuPopup  The menupopup for which this menuitem is created.
 */
function createFontFaceMenuitem(aFontLabel, aFontName, aMenuPopup) {
  let itemNode = document.createXULElement("menuitem");
  itemNode.setAttribute("label", aFontLabel);
  itemNode.setAttribute("value", aFontName);
  itemNode.setAttribute(
    "value_parsed",
    aFontName.toLowerCase().replace(/, /g, ",")
  );
  itemNode.setAttribute("tooltiptext", aFontLabel);
  if (aMenuPopup.getAttribute("useRadios") == "true") {
    itemNode.setAttribute("type", "radio");
    itemNode.setAttribute("observes", "cmd_renderedHTMLEnabler");
  }
  return itemNode;
}

/**
 * Helper function
 *
 * @see https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#legacy-font-size-for
 */
function getLegacyFontSize() {
  let fontSize = GetCurrentEditor().document.queryCommandValue("fontSize");
  // If one selects all the texts in the editor and deletes it, the editor
  // will return null fontSize. We will set it to default value then.
  if (!fontSize) {
    fontSize = Services.prefs.getCharPref("msgcompose.font_size", "3");
  }
  return fontSize;
}

function initFontSizeMenu(menuPopup) {
  if (menuPopup) {
    let fontSize = getLegacyFontSize();
    for (let menuitem of menuPopup.children) {
      if (menuitem.getAttribute("value") == fontSize) {
        menuitem.setAttribute("checked", true);
      }
    }
  }
}

function onFontColorChange() {
  ChangeButtonColor("cmd_fontColor", "TextColorButton", gDefaultTextColor);
}

function onBackgroundColorChange() {
  ChangeButtonColor(
    "cmd_backgroundColor",
    "BackgroundColorButton",
    gDefaultBackgroundColor
  );
}

/* Helper function that changes the button color.
 *   commandID - The ID of the command element.
 *   id - The ID of the button needing to be changed.
 *   defaultColor - The default color the button gets set to.
 */
function ChangeButtonColor(commandID, id, defaultColor) {
  var commandNode = document.getElementById(commandID);
  if (commandNode) {
    var color = commandNode.getAttribute("state");
    var button = document.getElementById(id);
    if (button) {
      button.setAttribute("color", color);

      // No color or a mixed color - get color set on page or other defaults.
      if (!color || color == "mixed") {
        color = defaultColor;
      }

      button.style.backgroundColor = color;
    }
  }
}

// Call this when user changes text and/or background colors of the page
function UpdateDefaultColors() {
  var BrowserColors = GetDefaultBrowserColors();
  var bodyelement = GetBodyElement();
  var defTextColor = gDefaultTextColor;
  var defBackColor = gDefaultBackgroundColor;

  if (bodyelement) {
    var color = bodyelement.getAttribute("text");
    if (color) {
      gDefaultTextColor = color;
    } else if (BrowserColors) {
      gDefaultTextColor = BrowserColors.TextColor;
    }

    color = bodyelement.getAttribute("bgcolor");
    if (color) {
      gDefaultBackgroundColor = color;
    } else if (BrowserColors) {
      gDefaultBackgroundColor = BrowserColors.BackgroundColor;
    }
  }

  // Trigger update on toolbar
  if (defTextColor != gDefaultTextColor) {
    goUpdateCommandState("cmd_fontColor");
    onFontColorChange();
  }
  if (defBackColor != gDefaultBackgroundColor) {
    goUpdateCommandState("cmd_backgroundColor");
    onBackgroundColorChange();
  }
}

function GetBackgroundElementWithColor() {
  var editor = GetCurrentTableEditor();
  if (!editor) {
    return null;
  }

  gColorObj.Type = "";
  gColorObj.PageColor = "";
  gColorObj.TableColor = "";
  gColorObj.CellColor = "";
  gColorObj.BackgroundColor = "";
  gColorObj.SelectedType = "";

  var tagNameObj = { value: "" };
  var element;
  try {
    element = editor.getSelectedOrParentTableElement(tagNameObj, { value: 0 });
  } catch (e) {}

  if (element && tagNameObj && tagNameObj.value) {
    gColorObj.BackgroundColor = GetHTMLOrCSSStyleValue(
      element,
      "bgcolor",
      "background-color"
    );
    gColorObj.BackgroundColor = ConvertRGBColorIntoHEXColor(
      gColorObj.BackgroundColor
    );
    if (tagNameObj.value.toLowerCase() == "td") {
      gColorObj.Type = "Cell";
      gColorObj.CellColor = gColorObj.BackgroundColor;

      // Get any color that might be on parent table
      var table = GetParentTable(element);
      gColorObj.TableColor = GetHTMLOrCSSStyleValue(
        table,
        "bgcolor",
        "background-color"
      );
      gColorObj.TableColor = ConvertRGBColorIntoHEXColor(gColorObj.TableColor);
    } else {
      gColorObj.Type = "Table";
      gColorObj.TableColor = gColorObj.BackgroundColor;
    }
    gColorObj.SelectedType = gColorObj.Type;
  } else {
    let IsCSSPrefChecked = Services.prefs.getBoolPref(kUseCssPref);
    if (IsCSSPrefChecked && IsHTMLEditor()) {
      let selection = editor.selection;
      if (selection) {
        element = selection.focusNode;
        while (!editor.nodeIsBlock(element)) {
          element = element.parentNode;
        }
      } else {
        element = GetBodyElement();
      }
    } else {
      element = GetBodyElement();
    }
    if (element) {
      gColorObj.Type = "Page";
      gColorObj.BackgroundColor = GetHTMLOrCSSStyleValue(
        element,
        "bgcolor",
        "background-color"
      );
      if (gColorObj.BackgroundColor == "") {
        gColorObj.BackgroundColor = "transparent";
      } else {
        gColorObj.BackgroundColor = ConvertRGBColorIntoHEXColor(
          gColorObj.BackgroundColor
        );
      }
      gColorObj.PageColor = gColorObj.BackgroundColor;
    }
  }
  return element;
}

/* eslint-disable complexity */
function EditorSelectColor(colorType, mouseEvent) {
  var editor = GetCurrentEditor();
  if (!editor || !gColorObj) {
    return;
  }

  // Shift + mouse click automatically applies last color, if available
  var useLastColor = mouseEvent
    ? mouseEvent.button == 0 && mouseEvent.shiftKey
    : false;
  var element;
  var table;
  var currentColor = "";
  var commandNode;

  if (!colorType) {
    colorType = "";
  }

  if (colorType == "Text") {
    gColorObj.Type = colorType;

    // Get color from command node state
    commandNode = document.getElementById("cmd_fontColor");
    currentColor = commandNode.getAttribute("state");
    currentColor = ConvertRGBColorIntoHEXColor(currentColor);
    gColorObj.TextColor = currentColor;

    if (useLastColor && gColorObj.LastTextColor) {
      gColorObj.TextColor = gColorObj.LastTextColor;
    } else {
      useLastColor = false;
    }
  } else if (colorType == "Highlight") {
    gColorObj.Type = colorType;

    // Get color from command node state
    commandNode = document.getElementById("cmd_highlight");
    currentColor = commandNode.getAttribute("state");
    currentColor = ConvertRGBColorIntoHEXColor(currentColor);
    gColorObj.HighlightColor = currentColor;

    if (useLastColor && gColorObj.LastHighlightColor) {
      gColorObj.HighlightColor = gColorObj.LastHighlightColor;
    } else {
      useLastColor = false;
    }
  } else {
    element = GetBackgroundElementWithColor();
    if (!element) {
      return;
    }

    // Get the table if we found a cell
    if (gColorObj.Type == "Table") {
      table = element;
    } else if (gColorObj.Type == "Cell") {
      table = GetParentTable(element);
    }

    // Save to avoid resetting if not necessary
    currentColor = gColorObj.BackgroundColor;

    if (colorType == "TableOrCell" || colorType == "Cell") {
      if (gColorObj.Type == "Cell") {
        gColorObj.Type = colorType;
      } else if (gColorObj.Type != "Table") {
        return;
      }
    } else if (colorType == "Table" && gColorObj.Type == "Page") {
      return;
    }

    if (colorType == "" && gColorObj.Type == "Cell") {
      // Using empty string for requested type means
      //  we can let user select cell or table
      gColorObj.Type = "TableOrCell";
    }

    if (useLastColor && gColorObj.LastBackgroundColor) {
      gColorObj.BackgroundColor = gColorObj.LastBackgroundColor;
    } else {
      useLastColor = false;
    }
  }
  // Save the type we are really requesting
  colorType = gColorObj.Type;

  if (!useLastColor) {
    // Avoid the JS warning
    gColorObj.NoDefault = false;

    // Launch the ColorPicker dialog
    // TODO: Figure out how to position this under the color buttons on the toolbar
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdColorPicker.xhtml",
      "_blank",
      "chrome,close,titlebar,modal",
      "",
      gColorObj
    );

    // User canceled the dialog
    if (gColorObj.Cancel) {
      return;
    }
  }

  if (gColorObj.Type == "Text") {
    if (currentColor != gColorObj.TextColor) {
      if (gColorObj.TextColor) {
        GetCurrentEditor().document.execCommand(
          "foreColor",
          false,
          gColorObj.TextColor
        );
      } else {
        EditorRemoveTextProperty("font", "color");
      }
    }
    // Update the command state (this will trigger color button update)
    goUpdateCommandState("cmd_fontColor");
  } else if (gColorObj.Type == "Highlight") {
    if (currentColor != gColorObj.HighlightColor) {
      if (gColorObj.HighlightColor) {
        GetCurrentEditor().document.execCommand(
          "backColor",
          false,
          gColorObj.HighlightColor
        );
      } else {
        EditorRemoveTextProperty("font", "bgcolor");
      }
    }
    // Update the command state (this will trigger color button update)
    goUpdateCommandState("cmd_highlight");
  } else if (element) {
    if (gColorObj.Type == "Table") {
      // Set background on a table
      // Note that we shouldn't trust "currentColor" because of "TableOrCell" behavior
      if (table) {
        var bgcolor = table.getAttribute("bgcolor");
        if (bgcolor != gColorObj.BackgroundColor) {
          try {
            if (gColorObj.BackgroundColor) {
              editor.setAttributeOrEquivalent(
                table,
                "bgcolor",
                gColorObj.BackgroundColor,
                false
              );
            } else {
              editor.removeAttributeOrEquivalent(table, "bgcolor", false);
            }
          } catch (e) {}
        }
      }
    } else if (currentColor != gColorObj.BackgroundColor && IsHTMLEditor()) {
      editor.beginTransaction();
      try {
        editor.setBackgroundColor(gColorObj.BackgroundColor);

        if (gColorObj.Type == "Page" && gColorObj.BackgroundColor) {
          // Set all page colors not explicitly set,
          //  else you can end up with unreadable pages
          //  because viewer's default colors may not be same as page author's
          var bodyelement = GetBodyElement();
          if (bodyelement) {
            var defColors = GetDefaultBrowserColors();
            if (defColors) {
              if (!bodyelement.getAttribute("text")) {
                editor.setAttributeOrEquivalent(
                  bodyelement,
                  "text",
                  defColors.TextColor,
                  false
                );
              }

              // The following attributes have no individual CSS declaration counterparts
              // Getting rid of them in favor of CSS implies CSS rules management
              if (!bodyelement.getAttribute("link")) {
                editor.setAttribute(bodyelement, "link", defColors.LinkColor);
              }

              if (!bodyelement.getAttribute("alink")) {
                editor.setAttribute(
                  bodyelement,
                  "alink",
                  defColors.ActiveLinkColor
                );
              }

              if (!bodyelement.getAttribute("vlink")) {
                editor.setAttribute(
                  bodyelement,
                  "vlink",
                  defColors.VisitedLinkColor
                );
              }
            }
          }
        }
      } catch (e) {}

      editor.endTransaction();
    }

    goUpdateCommandState("cmd_backgroundColor");
  }
  gContentWindow.focus();
}
/* eslint-enable complexity */

function GetParentTable(element) {
  var node = element;
  while (node) {
    if (node.nodeName.toLowerCase() == "table") {
      return node;
    }

    node = node.parentNode;
  }
  return node;
}

function GetParentTableCell(element) {
  var node = element;
  while (node) {
    if (
      node.nodeName.toLowerCase() == "td" ||
      node.nodeName.toLowerCase() == "th"
    ) {
      return node;
    }

    node = node.parentNode;
  }
  return node;
}

function EditorDblClick(event) {
  // Only bring up properties if clicked on an element or selected link
  let element = event.target;
  //  We use "href" instead of "a" to not be fooled by named anchor
  if (!element) {
    try {
      element = GetCurrentEditor().getSelectedElement("href");
    } catch (e) {}
  }

  // Don't fire for body/p and other block elements.
  // It's common that people try to double-click
  // to select a word, but the click hits an empty area.
  if (
    element &&
    ![
      "body",
      "p",
      "h1",
      "h2",
      "h3",
      "h4",
      "h5",
      "h6",
      "blockquote",
      "div",
      "pre",
    ].includes(element.nodeName.toLowerCase())
  ) {
    goDoCommand("cmd_objectProperties");
    event.preventDefault();
  }
}

/* TODO: We need an oncreate hook to do enabling/disabling for the
        Format menu. There should be code like this for the
        object-specific "Properties" item
*/
// For property dialogs, we want the selected element,
//  but will accept a parent link, list, or table cell if inside one
function GetObjectForProperties() {
  var editor = GetCurrentEditor();
  if (!editor || !IsHTMLEditor()) {
    return null;
  }

  var element;
  try {
    element = editor.getSelectedElement("");
  } catch (e) {}
  if (element) {
    if (element.namespaceURI == "http://www.w3.org/1998/Math/MathML") {
      // If the object is a MathML element, we collapse the selection on it and
      // we return its <math> ancestor. Hence the math dialog will be used.
      GetCurrentEditor().selection.collapse(element, 0);
    } else {
      return element;
    }
  }

  // Find nearest parent of selection anchor node
  //   that is a link, list, table cell, or table

  var anchorNode;
  var node;
  try {
    anchorNode = editor.selection.anchorNode;
    if (anchorNode.firstChild) {
      // Start at actual selected node
      var offset = editor.selection.anchorOffset;
      // Note: If collapsed, offset points to element AFTER caret,
      //  thus node may be null
      node = anchorNode.childNodes.item(offset);
    }
    if (!node) {
      node = anchorNode;
    }
  } catch (e) {}

  while (node) {
    if (node.nodeName) {
      var nodeName = node.nodeName.toLowerCase();

      // Done when we hit the body or #text.
      if (nodeName == "body" || nodeName == "#text") {
        break;
      }

      if (
        (nodeName == "a" && node.href) ||
        nodeName == "ol" ||
        nodeName == "ul" ||
        nodeName == "dl" ||
        nodeName == "td" ||
        nodeName == "th" ||
        nodeName == "table" ||
        nodeName == "math"
      ) {
        return node;
      }
    }
    node = node.parentNode;
  }
  return null;
}

function UpdateWindowTitle() {
  try {
    var filename = "";
    var windowTitle = "";
    var title = document.title;

    // Append just the 'leaf' filename to the Doc. Title for the window caption
    var docUrl = GetDocumentUrl();
    if (docUrl && !IsUrlAboutBlank(docUrl)) {
      var scheme = GetScheme(docUrl);
      filename = GetFilename(docUrl);
      if (filename) {
        windowTitle = " [" + scheme + ":/.../" + filename + "]";
      }

      var fileType = IsHTMLEditor() ? "html" : "text";
      // Save changed title in the recent pages data in prefs
      SaveRecentFilesPrefs(title, fileType);
    }

    document.title = (title || filename) + windowTitle;
  } catch (e) {
    dump(e);
  }
}

function SaveRecentFilesPrefs(aTitle, aFileType) {
  var curUrl = StripPassword(GetDocumentUrl());
  var historyCount = Services.prefs.getIntPref("editor.history.url_maximum");

  var titleArray = [];
  var urlArray = [];
  var typeArray = [];

  if (historyCount && !IsUrlAboutBlank(curUrl) && GetScheme(curUrl) != "data") {
    titleArray.push(aTitle);
    urlArray.push(curUrl);
    typeArray.push(aFileType);
  }

  for (let i = 0; i < historyCount && urlArray.length < historyCount; i++) {
    let url = Services.prefs.getStringPref("editor.history_url_" + i, "");

    // Continue if URL pref is missing because
    //  a URL not found during loading may have been removed

    // Skip over current an "data" URLs
    if (url && url != curUrl && GetScheme(url) != "data") {
      let title = Services.prefs.getStringPref("editor.history_title_" + i, "");
      let fileType = Services.prefs.getStringPref(
        "editor.history_type_" + i,
        ""
      );
      titleArray.push(title);
      urlArray.push(url);
      typeArray.push(fileType);
    }
  }

  // Resave the list back to prefs in the new order
  for (let i = 0; i < urlArray.length; i++) {
    SetStringPref("editor.history_title_" + i, titleArray[i]);
    SetStringPref("editor.history_url_" + i, urlArray[i]);
    SetStringPref("editor.history_type_" + i, typeArray[i]);
  }
}

function EditorInitFormatMenu() {
  try {
    InitObjectPropertiesMenuitem();
    InitRemoveStylesMenuitems(
      "removeStylesMenuitem",
      "removeLinksMenuitem",
      "removeNamedAnchorsMenuitem"
    );
  } catch (ex) {}
}

function InitObjectPropertiesMenuitem() {
  // Set strings and enable for the [Object] Properties item
  // Note that we directly do the enabling instead of
  // using goSetCommandEnabled since we already have the command.
  var cmd = document.getElementById("cmd_objectProperties");
  if (!cmd) {
    return null;
  }

  var element;
  var menuStr = GetString("AdvancedProperties");
  var name;

  if (IsEditingRenderedHTML()) {
    element = GetObjectForProperties();
  }

  if (element && element.nodeName) {
    var objStr = "";
    cmd.removeAttribute("disabled");
    name = element.nodeName.toLowerCase();
    switch (name) {
      case "img":
        // Check if img is enclosed in link
        //  (use "href" to not be fooled by named anchor)
        try {
          if (GetCurrentEditor().getElementOrParentByTagName("href", element)) {
            objStr = GetString("ImageAndLink");
            // Return "href" so it is detected as a link.
            name = "href";
          }
        } catch (e) {}

        if (objStr == "") {
          objStr = GetString("Image");
        }
        break;
      case "hr":
        objStr = GetString("HLine");
        break;
      case "table":
        objStr = GetString("Table");
        break;
      case "th":
        name = "td";
      // Falls through
      case "td":
        objStr = GetString("TableCell");
        break;
      case "ol":
      case "ul":
      case "dl":
        objStr = GetString("List");
        break;
      case "li":
        objStr = GetString("ListItem");
        break;
      case "form":
        objStr = GetString("Form");
        break;
      case "input":
        var type = element.getAttribute("type");
        if (type && type.toLowerCase() == "image") {
          objStr = GetString("InputImage");
        } else {
          objStr = GetString("InputTag");
        }
        break;
      case "textarea":
        objStr = GetString("TextArea");
        break;
      case "select":
        objStr = GetString("Select");
        break;
      case "button":
        objStr = GetString("Button");
        break;
      case "label":
        objStr = GetString("Label");
        break;
      case "fieldset":
        objStr = GetString("FieldSet");
        break;
      case "a":
        if (element.name) {
          objStr = GetString("NamedAnchor");
          name = "anchor";
        } else if (element.href) {
          objStr = GetString("Link");
          name = "href";
        }
        break;
    }
    if (objStr) {
      menuStr = GetString("ObjectProperties").replace(/%obj%/, objStr);
    }
  } else {
    // We show generic "Properties" string, but disable the command.
    cmd.setAttribute("disabled", "true");
  }
  cmd.setAttribute("label", menuStr);
  cmd.setAttribute("accesskey", GetString("ObjectPropertiesAccessKey"));
  return name;
}

function InitParagraphMenu() {
  var mixedObj = { value: null };
  var state;
  try {
    state = GetCurrentEditor().getParagraphState(mixedObj);
  } catch (e) {}
  var IDSuffix;

  // PROBLEM: When we get blockquote, it masks other styles contained by it
  // We need a separate method to get blockquote state

  // We use "x" as uninitialized paragraph state
  if (!state || state == "x") {
    // No paragraph container.
    IDSuffix = "bodyText";
  } else {
    IDSuffix = state;
  }

  // Set "radio" check on one item, but...
  var menuItem = document.getElementById("menu_" + IDSuffix);
  menuItem.setAttribute("checked", "true");

  // ..."bodyText" is returned if mixed selection, so remove checkmark
  if (mixedObj.value) {
    menuItem.setAttribute("checked", "false");
  }
}

function GetListStateString() {
  try {
    var editor = GetCurrentEditor();

    var mixedObj = { value: null };
    var hasOL = { value: false };
    var hasUL = { value: false };
    var hasDL = { value: false };
    editor.getListState(mixedObj, hasOL, hasUL, hasDL);

    if (mixedObj.value) {
      return "mixed";
    }
    if (hasOL.value) {
      return "ol";
    }
    if (hasUL.value) {
      return "ul";
    }

    if (hasDL.value) {
      var hasLI = { value: false };
      var hasDT = { value: false };
      var hasDD = { value: false };
      editor.getListItemState(mixedObj, hasLI, hasDT, hasDD);
      if (mixedObj.value) {
        return "mixed";
      }
      if (hasLI.value) {
        return "li";
      }
      if (hasDT.value) {
        return "dt";
      }
      if (hasDD.value) {
        return "dd";
      }
    }
  } catch (e) {}

  // return "noList" if we aren't in a list at all
  return "noList";
}

function InitListMenu() {
  if (!IsHTMLEditor()) {
    return;
  }

  var IDSuffix = GetListStateString();

  // Set enable state for the "None" menuitem
  goSetCommandEnabled("cmd_removeList", IDSuffix != "noList");

  // Set "radio" check on one item, but...
  // we won't find a match if it's "mixed"
  var menuItem = document.getElementById("menu_" + IDSuffix);
  if (menuItem) {
    menuItem.setAttribute("checked", "true");
  }
}

function GetAlignmentString() {
  var mixedObj = { value: null };
  var alignObj = { value: null };
  try {
    GetCurrentEditor().getAlignment(mixedObj, alignObj);
  } catch (e) {}

  if (mixedObj.value) {
    return "mixed";
  }
  if (alignObj.value == Ci.nsIHTMLEditor.eLeft) {
    return "left";
  }
  if (alignObj.value == Ci.nsIHTMLEditor.eCenter) {
    return "center";
  }
  if (alignObj.value == Ci.nsIHTMLEditor.eRight) {
    return "right";
  }
  if (alignObj.value == Ci.nsIHTMLEditor.eJustify) {
    return "justify";
  }

  // return "left" if we got here
  return "left";
}

function InitAlignMenu() {
  if (!IsHTMLEditor()) {
    return;
  }

  var IDSuffix = GetAlignmentString();

  // we won't find a match if it's "mixed"
  var menuItem = document.getElementById("menu_" + IDSuffix);
  if (menuItem) {
    menuItem.setAttribute("checked", "true");
  }
}

function EditorSetDefaultPrefsAndDoctype() {
  var editor = GetCurrentEditor();

  var domdoc;
  try {
    domdoc = editor.document;
  } catch (e) {
    dump(e + "\n");
  }
  if (!domdoc) {
    dump("EditorSetDefaultPrefsAndDoctype: EDITOR DOCUMENT NOT FOUND\n");
    return;
  }

  // Insert a doctype element
  // if it is missing from existing doc
  if (!domdoc.doctype) {
    var newdoctype = domdoc.implementation.createDocumentType(
      "HTML",
      "-//W3C//DTD HTML 4.01 Transitional//EN",
      ""
    );
    if (newdoctype) {
      domdoc.insertBefore(newdoctype, domdoc.firstChild);
    }
  }

  // search for head; we'll need this for meta tag additions
  let headelement = domdoc.querySelector("head");
  if (!headelement) {
    headelement = domdoc.createElement("head");
    domdoc.insertAfter(headelement, domdoc.firstChild);
  }

  /* only set default prefs for new documents */
  if (!IsUrlAboutBlank(GetDocumentUrl())) {
    return;
  }

  // search for author meta tag.
  // if one is found, don't do anything.
  // if not, create one and make it a child of the head tag
  //   and set its content attribute to the value of the editor.author preference.

  if (domdoc.querySelector("meta")) {
    // we should do charset first since we need to have charset before
    // hitting other 8-bit char in other meta tags
    // grab charset pref and make it the default charset
    var element;
    var prefCharsetString = Services.prefs.getCharPref(
      "intl.charset.fallback.override"
    );
    if (prefCharsetString) {
      editor.documentCharacterSet = prefCharsetString;
    }

    // let's start by assuming we have an author in case we don't have the pref

    var prefAuthorString = null;
    let authorFound = domdoc.querySelector('meta[name="author"]');
    try {
      prefAuthorString = Services.prefs.getStringPref("editor.author");
    } catch (ex) {}
    if (
      prefAuthorString &&
      prefAuthorString != 0 &&
      !authorFound &&
      headelement
    ) {
      // create meta tag with 2 attributes
      element = domdoc.createElement("meta");
      if (element) {
        element.setAttribute("name", "author");
        element.setAttribute("content", prefAuthorString);
        headelement.appendChild(element);
      }
    }
  }

  // add title tag if not present
  if (headelement && !editor.document.querySelector("title")) {
    var titleElement = domdoc.createElement("title");
    if (titleElement) {
      headelement.appendChild(titleElement);
    }
  }

  // find body node
  var bodyelement = GetBodyElement();
  if (bodyelement) {
    if (Services.prefs.getBoolPref("editor.use_custom_colors")) {
      let text_color = Services.prefs.getCharPref("editor.text_color");
      let background_color = Services.prefs.getCharPref(
        "editor.background_color"
      );

      // add the color attributes to the body tag.
      // and use them for the default text and background colors if not empty
      editor.setAttributeOrEquivalent(bodyelement, "text", text_color, true);
      gDefaultTextColor = text_color;
      editor.setAttributeOrEquivalent(
        bodyelement,
        "bgcolor",
        background_color,
        true
      );
      gDefaultBackgroundColor = background_color;
      bodyelement.setAttribute(
        "link",
        Services.prefs.getCharPref("editor.link_color")
      );
      bodyelement.setAttribute(
        "alink",
        Services.prefs.getCharPref("editor.active_link_color")
      );
      bodyelement.setAttribute(
        "vlink",
        Services.prefs.getCharPref("editor.followed_link_color")
      );
    }
    // Default image is independent of Custom colors???
    try {
      let background_image = Services.prefs.getCharPref(
        "editor.default_background_image"
      );
      if (background_image) {
        editor.setAttributeOrEquivalent(
          bodyelement,
          "background",
          background_image,
          true
        );
      }
    } catch (e) {
      dump("BACKGROUND EXCEPTION: " + e + "\n");
    }
  }
  // auto-save???
}

function GetBodyElement() {
  try {
    return GetCurrentEditor().rootElement;
  } catch (ex) {
    dump("no body tag found?!\n");
    //  better have one, how can we blow things up here?
  }
  return null;
}

// --------------------------------------------------------------------
function initFontStyleMenu(menuPopup) {
  for (var i = 0; i < menuPopup.children.length; i++) {
    var menuItem = menuPopup.children[i];
    var theStyle = menuItem.getAttribute("state");
    if (theStyle) {
      menuItem.setAttribute("checked", theStyle);
    }
  }
}

// -----------------------------------------------------------------------------------
function IsSpellCheckerInstalled() {
  return true; // Always installed.
}

// -----------------------------------------------------------------------------------
function IsFindInstalled() {
  return (
    "@mozilla.org/embedcomp/rangefind;1" in Cc &&
    "@mozilla.org/find/find_service;1" in Cc
  );
}

// -----------------------------------------------------------------------------------
function RemoveInapplicableUIElements() {
  // For items that are in their own menu block, remove associated separator
  // (we can't use "hidden" since class="hide-in-IM" CSS rule interferes)

  // if no find, remove find ui
  if (!IsFindInstalled()) {
    HideItem("menu_find");
    HideItem("menu_findnext");
    HideItem("menu_replace");
    HideItem("menu_find");
    RemoveItem("sep_find");
  }

  // if no spell checker, remove spell checker ui
  if (!IsSpellCheckerInstalled()) {
    HideItem("spellingButton");
    HideItem("menu_checkspelling");
    RemoveItem("sep_checkspelling");
  }

  // Remove menu items (from overlay shared with HTML editor) in non-HTML.
  if (!IsHTMLEditor()) {
    HideItem("insertAnchor");
    HideItem("insertImage");
    HideItem("insertHline");
    HideItem("insertTable");
    HideItem("insertHTML");
    HideItem("insertFormMenu");
    HideItem("fileExportToText");
    HideItem("viewFormatToolbar");
    HideItem("viewEditModeToolbar");
  }
}

function HideItem(id) {
  var item = document.getElementById(id);
  if (item) {
    item.hidden = true;
  }
}

function RemoveItem(id) {
  var item = document.getElementById(id);
  if (item) {
    item.remove();
  }
}

// Command Updating Strategy:
//   Don't update on on selection change, only when menu is displayed,
//   with this "oncreate" handler:
function EditorInitTableMenu() {
  try {
    InitJoinCellMenuitem("menu_JoinTableCells");
  } catch (ex) {}

  // Set enable states for all table commands
  goUpdateTableMenuItems(document.getElementById("composerTableMenuItems"));
}

function InitJoinCellMenuitem(id) {
  // Change text on the "Join..." item depending if we
  //   are joining selected cells or just cell to right
  // TODO: What to do about normal selection that crosses
  //       table border? Try to figure out all cells
  //       included in the selection?
  var menuText;
  var menuItem = document.getElementById(id);
  if (!menuItem) {
    return;
  }

  // Use "Join selected cells if there's more than 1 cell selected
  var numSelected;
  var foundElement;

  try {
    var tagNameObj = {};
    var countObj = { value: 0 };
    foundElement = GetCurrentTableEditor().getSelectedOrParentTableElement(
      tagNameObj,
      countObj
    );
    numSelected = countObj.value;
  } catch (e) {}
  if (foundElement && numSelected > 1) {
    menuText = GetString("JoinSelectedCells");
  } else {
    menuText = GetString("JoinCellToRight");
  }

  menuItem.setAttribute("label", menuText);
  menuItem.setAttribute("accesskey", GetString("JoinCellAccesskey"));
}

function InitRemoveStylesMenuitems(
  removeStylesId,
  removeLinksId,
  removeNamedAnchorsId
) {
  var editor = GetCurrentEditor();
  if (!editor) {
    return;
  }

  // Change wording of menuitems depending on selection
  var stylesItem = document.getElementById(removeStylesId);
  var linkItem = document.getElementById(removeLinksId);

  var isCollapsed = editor.selection.isCollapsed;
  if (stylesItem) {
    stylesItem.setAttribute(
      "label",
      isCollapsed ? GetString("StopTextStyles") : GetString("RemoveTextStyles")
    );
    stylesItem.setAttribute(
      "accesskey",
      GetString("RemoveTextStylesAccesskey")
    );
  }
  if (linkItem) {
    linkItem.setAttribute(
      "label",
      isCollapsed ? GetString("StopLinks") : GetString("RemoveLinks")
    );
    linkItem.setAttribute("accesskey", GetString("RemoveLinksAccesskey"));
    // Note: disabling text style is a pain since there are so many - forget it!

    // Disable if not in a link, but always allow "Remove"
    //  if selection isn't collapsed since we only look at anchor node
    try {
      SetElementEnabled(
        linkItem,
        !isCollapsed || editor.getElementOrParentByTagName("href", null)
      );
    } catch (e) {}
  }
  // Disable if selection is collapsed
  SetElementEnabledById(removeNamedAnchorsId, !isCollapsed);
}

function goUpdateTableMenuItems(commandset) {
  var editor = GetCurrentTableEditor();
  if (!editor) {
    dump("goUpdateTableMenuItems: too early, not initialized\n");
    return;
  }

  var enabled = false;
  var enabledIfTable = false;

  var flags = editor.flags;
  if (!(flags & Ci.nsIEditor.eEditorReadonlyMask) && IsEditingRenderedHTML()) {
    var tagNameObj = { value: "" };
    var element;
    try {
      element = editor.getSelectedOrParentTableElement(tagNameObj, {
        value: 0,
      });
    } catch (e) {}

    if (element) {
      // Value when we need to have a selected table or inside a table
      enabledIfTable = true;

      // All others require being inside a cell or selected cell
      enabled = tagNameObj.value == "td";
    }
  }

  // Loop through command nodes
  for (var i = 0; i < commandset.children.length; i++) {
    var commandID = commandset.children[i].getAttribute("id");
    if (commandID) {
      if (
        commandID == "cmd_InsertTable" ||
        commandID == "cmd_JoinTableCells" ||
        commandID == "cmd_SplitTableCell" ||
        commandID == "cmd_ConvertToTable"
      ) {
        // Call the update method in the command class
        goUpdateCommand(commandID);
      } else if (
        commandID == "cmd_DeleteTable" ||
        commandID == "cmd_editTable" ||
        commandID == "cmd_TableOrCellColor" ||
        commandID == "cmd_SelectTable"
      ) {
        // Directly set with the values calculated here
        goSetCommandEnabled(commandID, enabledIfTable);
      } else {
        goSetCommandEnabled(commandID, enabled);
      }
    }
  }
}

// -----------------------------------------------------------------------------------
// Helpers for inserting and editing tables:

function IsInTable() {
  var editor = GetCurrentEditor();
  try {
    var flags = editor.flags;
    return (
      IsHTMLEditor() &&
      !(flags & Ci.nsIEditor.eEditorReadonlyMask) &&
      IsEditingRenderedHTML() &&
      null != editor.getElementOrParentByTagName("table", null)
    );
  } catch (e) {}
  return false;
}

function IsInTableCell() {
  try {
    var editor = GetCurrentEditor();
    var flags = editor.flags;
    return (
      IsHTMLEditor() &&
      !(flags & Ci.nsIEditor.eEditorReadonlyMask) &&
      IsEditingRenderedHTML() &&
      null != editor.getElementOrParentByTagName("td", null)
    );
  } catch (e) {}
  return false;
}

function IsSelectionInOneCell() {
  try {
    var editor = GetCurrentEditor();
    var selection = editor.selection;

    if (selection.rangeCount == 1) {
      // We have a "normal" single-range selection
      if (
        !selection.isCollapsed &&
        selection.anchorNode != selection.focusNode
      ) {
        // Check if both nodes are within the same cell
        var anchorCell = editor.getElementOrParentByTagName(
          "td",
          selection.anchorNode
        );
        var focusCell = editor.getElementOrParentByTagName(
          "td",
          selection.focusNode
        );
        return (
          focusCell != null && anchorCell != null && focusCell == anchorCell
        );
      }
      // Collapsed selection or anchor == focus (thus must be in 1 cell)
      return true;
    }
  } catch (e) {}
  return false;
}

// Call this with insertAllowed = true to allow inserting if not in existing table,
//   else use false to do nothing if not in a table
function EditorInsertOrEditTable(insertAllowed) {
  if (IsInTable()) {
    // Edit properties of existing table
    window.openDialog(
      "chrome://messenger/content/messengercompose/EdTableProps.xhtml",
      "_blank",
      "chrome,close,titlebar,modal",
      "",
      "TablePanel"
    );
    gContentWindow.focus();
  } else if (insertAllowed) {
    try {
      if (GetCurrentEditor().selection.isCollapsed) {
        // If we have a caret, insert a blank table...
        EditorInsertTable();
      } else {
        // Else convert the selection into a table.
        goDoCommand("cmd_ConvertToTable");
      }
    } catch (e) {}
  }
}

function EditorInsertTable() {
  // Insert a new table
  window.openDialog(
    "chrome://messenger/content/messengercompose/EdInsertTable.xhtml",
    "_blank",
    "chrome,close,titlebar,modal",
    ""
  );
  gContentWindow.focus();
}

function EditorTableCellProperties() {
  if (!IsHTMLEditor()) {
    return;
  }

  try {
    var cell = GetCurrentEditor().getElementOrParentByTagName("td", null);
    if (cell) {
      // Start Table Properties dialog on the "Cell" panel
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdTableProps.xhtml",
        "_blank",
        "chrome,close,titlebar,modal",
        "",
        "CellPanel"
      );
      gContentWindow.focus();
    }
  } catch (e) {}
}

function GetNumberOfContiguousSelectedRows() {
  if (!IsHTMLEditor()) {
    return 0;
  }

  var rows = 0;
  var editor = GetCurrentTableEditor();
  var rowObj = { value: 0 };
  var colObj = { value: 0 };
  var cell = editor.getFirstSelectedCellInTable(rowObj, colObj);
  if (!cell) {
    return 0;
  }

  // We have at least one row
  rows++;

  var lastIndex = rowObj.value;
  for (let cell of editor.getSelectedCells()) {
    editor.getCellIndexes(cell, rowObj, colObj);
    var index = rowObj.value;
    if (index == lastIndex + 1) {
      lastIndex = index;
      rows++;
    }
  }

  return rows;
}

function GetNumberOfContiguousSelectedColumns() {
  if (!IsHTMLEditor()) {
    return 0;
  }

  var columns = 0;

  var editor = GetCurrentTableEditor();
  var colObj = { value: 0 };
  var rowObj = { value: 0 };
  var cell = editor.getFirstSelectedCellInTable(rowObj, colObj);
  if (!cell) {
    return 0;
  }

  // We have at least one column
  columns++;

  var lastIndex = colObj.value;
  for (let cell of editor.getSelectedCells()) {
    editor.getCellIndexes(cell, rowObj, colObj);
    var index = colObj.value;
    if (index == lastIndex + 1) {
      lastIndex = index;
      columns++;
    }
  }

  return columns;
}

function EditorOnFocus() {
  // Current window already has the InsertCharWindow
  if ("InsertCharWindow" in window && window.InsertCharWindow) {
    return;
  }

  // Find window with an InsertCharsWindow and switch association to this one
  var windowWithDialog = FindEditorWithInsertCharDialog();
  if (windowWithDialog) {
    // Switch the dialog to current window
    // this sets focus to dialog, so bring focus back to editor window
    if (SwitchInsertCharToThisWindow(windowWithDialog)) {
      top.document.commandDispatcher.focusedWindow.focus();
    }
  }
}

function SwitchInsertCharToThisWindow(windowWithDialog) {
  if (
    windowWithDialog &&
    "InsertCharWindow" in windowWithDialog &&
    windowWithDialog.InsertCharWindow
  ) {
    // Move dialog association to the current window
    window.InsertCharWindow = windowWithDialog.InsertCharWindow;
    windowWithDialog.InsertCharWindow = null;

    // Switch the dialog's opener to current window's
    window.InsertCharWindow.opener = window;

    // Bring dialog to the foreground
    window.InsertCharWindow.focus();
    return true;
  }
  return false;
}

function FindEditorWithInsertCharDialog() {
  try {
    // Find window with an InsertCharsWindow and switch association to this one

    for (let tempWindow of Services.wm.getEnumerator(null)) {
      if (
        !tempWindow.closed &&
        tempWindow != window &&
        "InsertCharWindow" in tempWindow &&
        tempWindow.InsertCharWindow
      ) {
        return tempWindow;
      }
    }
  } catch (e) {}
  return null;
}

function EditorFindOrCreateInsertCharWindow() {
  if ("InsertCharWindow" in window && window.InsertCharWindow) {
    window.InsertCharWindow.focus();
  } else {
    // Since we switch the dialog during EditorOnFocus(),
    //   this should really never be found, but it's good to be sure
    var windowWithDialog = FindEditorWithInsertCharDialog();
    if (windowWithDialog) {
      SwitchInsertCharToThisWindow(windowWithDialog);
    } else {
      // The dialog will set window.InsertCharWindow to itself
      window.openDialog(
        "chrome://messenger/content/messengercompose/EdInsertChars.xhtml",
        "_blank",
        "chrome,close,titlebar",
        ""
      );
    }
  }
}

// Find another HTML editor window to associate with the InsertChar dialog
//   or close it if none found  (May be a mail composer)
function SwitchInsertCharToAnotherEditorOrClose() {
  if ("InsertCharWindow" in window && window.InsertCharWindow) {
    var enumerator;
    try {
      enumerator = Services.wm.getEnumerator(null);
    } catch (e) {}
    if (!enumerator) {
      return;
    }

    // TODO: Fix this to search for command controllers and look for "cmd_InsertChars"
    // For now, detect just Web Composer and HTML Mail Composer
    for (let tempWindow of enumerator) {
      if (
        !tempWindow.closed &&
        tempWindow != window &&
        tempWindow != window.InsertCharWindow &&
        "GetCurrentEditor" in tempWindow &&
        tempWindow.GetCurrentEditor()
      ) {
        tempWindow.InsertCharWindow = window.InsertCharWindow;
        window.InsertCharWindow = null;
        tempWindow.InsertCharWindow.opener = tempWindow;
        return;
      }
    }
    // Didn't find another editor - close the dialog
    window.InsertCharWindow.close();
  }
}

function UpdateTOC() {
  window.openDialog(
    "chrome://messenger/content/messengercompose/EdInsertTOC.xhtml",
    "_blank",
    "chrome,close,modal,titlebar"
  );
  window.content.focus();
}

function InitTOCMenu() {
  var elt = GetCurrentEditor().document.getElementById("mozToc");
  var createMenuitem = document.getElementById("insertTOCMenuitem");
  var updateMenuitem = document.getElementById("updateTOCMenuitem");
  var removeMenuitem = document.getElementById("removeTOCMenuitem");
  if (removeMenuitem && createMenuitem && updateMenuitem) {
    if (elt) {
      createMenuitem.setAttribute("disabled", "true");
      updateMenuitem.removeAttribute("disabled");
      removeMenuitem.removeAttribute("disabled");
    } else {
      createMenuitem.removeAttribute("disabled");
      removeMenuitem.setAttribute("disabled", "true");
      updateMenuitem.setAttribute("disabled", "true");
    }
  }
}

function RemoveTOC() {
  var theDocument = GetCurrentEditor().document;
  var elt = theDocument.getElementById("mozToc");
  if (elt) {
    elt.remove();
  }

  let anchorNodes = theDocument.querySelectorAll('a[name^="mozTocId"]');
  for (let node of anchorNodes) {
    if (node.parentNode) {
      node.remove();
    }
  }
}
