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

/* global MozElements MozXULElement */

/* import-globals-from ../../base/content/dateFormat.js */
// TODO: This is completely bogus. Only one use of this file also has FilterEditor.js.
/* import-globals-from FilterEditor.js */

// Wrap in a block to prevent leaking to window scope.
{
  const { MailServices } = ChromeUtils.import(
    "resource:///modules/MailServices.jsm"
  );
  const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");

  const updateParentNode = parentNode => {
    if (parentNode.hasAttribute("initialActionIndex")) {
      let actionIndex = parentNode.getAttribute("initialActionIndex");
      let filterAction = gFilter.getActionAt(actionIndex);
      parentNode.initWithAction(filterAction);
    }
    parentNode.updateRemoveButton();
  };

  class MozRuleactiontargetTag extends MozXULElement {
    connectedCallback() {
      const menulist = document.createXULElement("menulist");
      const menuPopup = document.createXULElement("menupopup");

      menulist.classList.add("ruleactionitem");
      menulist.setAttribute("flex", "1");
      menulist.appendChild(menuPopup);

      for (let taginfo of MailServices.tags.getAllTags()) {
        const newMenuItem = document.createXULElement("menuitem");
        newMenuItem.setAttribute("label", taginfo.tag);
        newMenuItem.setAttribute("value", taginfo.key);
        if (taginfo.color) {
          newMenuItem.setAttribute("style", `color: ${taginfo.color};`);
        }
        menuPopup.appendChild(newMenuItem);
      }

      this.appendChild(menulist);

      updateParentNode(this.closest(".ruleaction"));
    }
  }

  class MozRuleactiontargetPriority extends MozXULElement {
    connectedCallback() {
      this.appendChild(
        MozXULElement.parseXULToFragment(
          `
          <menulist class="ruleactionitem" flex="1">
            <menupopup>
              <menuitem value="6" label="&highestPriorityCmd.label;"></menuitem>
              <menuitem value="5" label="&highPriorityCmd.label;"></menuitem>
              <menuitem value="4" label="&normalPriorityCmd.label;"></menuitem>
              <menuitem value="3" label="&lowPriorityCmd.label;"></menuitem>
              <menuitem value="2" label="&lowestPriorityCmd.label;"></menuitem>
            </menupopup>
          </menulist>
          `,
          ["chrome://messenger/locale/FilterEditor.dtd"]
        )
      );

      updateParentNode(this.closest(".ruleaction"));
    }
  }

  class MozRuleactiontargetJunkscore extends MozXULElement {
    connectedCallback() {
      this.appendChild(
        MozXULElement.parseXULToFragment(
          `
          <menulist class="ruleactionitem" flex="1">
            <menupopup>
              <menuitem value="100" label="&junk.label;"/>
              <menuitem value="0" label="&notJunk.label;"/>
            </menupopup>
          </menulist>
          `,
          ["chrome://messenger/locale/FilterEditor.dtd"]
        )
      );

      updateParentNode(this.closest(".ruleaction"));
    }
  }

  class MozRuleactiontargetReplyto extends MozXULElement {
    connectedCallback() {
      const menulist = document.createXULElement("menulist");
      const menuPopup = document.createXULElement("menupopup");

      menulist.classList.add("ruleactionitem");
      menulist.setAttribute("flex", "1");
      menulist.appendChild(menuPopup);

      this.appendChild(menulist);

      let ruleaction = this.closest(".ruleaction");
      let raMenulist = ruleaction.querySelector(
        '[is="ruleactiontype-menulist"]'
      );
      for (let { label, value } of raMenulist.findTemplates()) {
        menulist.appendItem(label, value);
      }
      updateParentNode(ruleaction);
    }
  }

  class MozRuleactiontargetForwardto extends MozXULElement {
    connectedCallback() {
      const input = document.createElementNS(
        "http://www.w3.org/1999/xhtml",
        "input"
      );
      input.classList.add("ruleactionitem", "input-inline");

      this.classList.add("input-container");
      this.appendChild(input);

      updateParentNode(this.closest(".ruleaction"));
    }
  }

  class MozRuleactiontargetFolder extends MozXULElement {
    connectedCallback() {
      this.appendChild(
        MozXULElement.parseXULToFragment(
          `
          <menulist class="ruleactionitem
                    folderMenuItem"
                    flex="1"
                    displayformat="verbose">
            <menupopup is="folder-menupopup"
                       mode="filing"
                       class="menulist-menupopup"
                       showRecent="true"
                       recentLabel="&recentFolders.label;"
                       showFileHereLabel="true">
            </menupopup>
          </menulist>
          `,
          ["chrome://messenger/locale/messenger.dtd"]
        )
      );

      this.menulist = this.querySelector("menulist");

      this.menulist.addEventListener("command", event => {
        this.setPicker(event);
      });

      updateParentNode(this.closest(".ruleaction"));

      let folder = this.menulist.value
        ? MailUtils.getOrCreateFolder(this.menulist.value)
        : gFilterList.folder;

      // An account folder is not a move/copy target; show "Choose Folder".
      folder = folder.isServer ? null : folder;

      this.menulist.menupopup.selectFolder(folder);
    }

    setPicker(event) {
      this.menulist.menupopup.selectFolder(event.target._folder);
    }
  }

  class MozRuleactiontargetWrapper extends MozXULElement {
    static get observedAttributes() {
      return ["type"];
    }

    get ruleactiontargetElement() {
      return this.node;
    }

    connectedCallback() {
      this._updateAttributes();
    }

    attributeChangedCallback() {
      this._updateAttributes();
    }

    _getChildNode(type) {
      const elementMapping = {
        movemessage: "ruleactiontarget-folder",
        copymessage: "ruleactiontarget-folder",
        setpriorityto: "ruleactiontarget-priority",
        setjunkscore: "ruleactiontarget-junkscore",
        forwardmessage: "ruleactiontarget-forwardto",
        replytomessage: "ruleactiontarget-replyto",
        addtagtomessage: "ruleactiontarget-tag",
      };
      const elementName = elementMapping[type];

      return elementName ? document.createXULElement(elementName) : null;
    }

    _updateAttributes() {
      if (!this.hasAttribute("type")) {
        return;
      }

      const type = this.getAttribute("type");

      while (this.lastChild) {
        this.lastChild.remove();
      }

      if (type == null) {
        return;
      }

      this.node = this._getChildNode(type);

      if (this.node) {
        this.node.setAttribute("flex", "1");
        this.appendChild(this.node);
      } else {
        updateParentNode(this.closest(".ruleaction"));
      }
    }
  }

  customElements.define("ruleactiontarget-tag", MozRuleactiontargetTag);
  customElements.define(
    "ruleactiontarget-priority",
    MozRuleactiontargetPriority
  );
  customElements.define(
    "ruleactiontarget-junkscore",
    MozRuleactiontargetJunkscore
  );
  customElements.define("ruleactiontarget-replyto", MozRuleactiontargetReplyto);
  customElements.define(
    "ruleactiontarget-forwardto",
    MozRuleactiontargetForwardto
  );
  customElements.define("ruleactiontarget-folder", MozRuleactiontargetFolder);
  customElements.define("ruleactiontarget-wrapper", MozRuleactiontargetWrapper);

  /**
   * This is an abstract class for search menulist general functionality.
   *
   * @abstract
   * @augments MozXULElement
   */
  class MozSearchMenulistAbstract extends MozXULElement {
    static get observedAttributes() {
      return ["flex", "disabled"];
    }

    constructor() {
      super();
      this.internalScope = null;
      this.internalValue = -1;
      this.validityManager = Cc[
        "@mozilla.org/mail/search/validityManager;1"
      ].getService(Ci.nsIMsgSearchValidityManager);
    }

    connectedCallback() {
      if (this.delayConnectedCallback() || this.hasChildNodes()) {
        return;
      }

      this.menulist = document.createXULElement("menulist");
      this.menulist.classList.add("search-menulist");
      this.menulist.addEventListener("command", this.onSelect.bind(this));
      this.menupopup = document.createXULElement("menupopup");
      this.menupopup.classList.add("search-menulist-popup");
      this.menulist.appendChild(this.menupopup);
      this.appendChild(this.menulist);
      this._updateAttributes();
    }

    attributeChangedCallback() {
      this._updateAttributes();
    }

    _updateAttributes() {
      if (!this.menulist) {
        return;
      }
      if (this.hasAttribute("flex")) {
        this.menulist.setAttribute("flex", this.getAttribute("flex"));
      } else {
        this.menulist.removeAttribute("flex");
      }
      if (this.hasAttribute("disabled")) {
        this.menulist.setAttribute("disabled", this.getAttribute("disabled"));
      } else {
        this.menulist.removeAttribute("disabled");
      }
    }

    set searchScope(val) {
      // if scope isn't changing this is a noop
      if (this.internalScope == val) {
        return;
      }
      this.internalScope = val;
      this.refreshList();
      if (this.targets) {
        this.targets.forEach(target => {
          customElements.upgrade(target);
          target.searchScope = val;
        });
      }
    }

    get searchScope() {
      return this.internalScope;
    }

    get validityTable() {
      return this.validityManager.getTable(this.searchScope);
    }

    get targets() {
      const forAttrs = this.getAttribute("for");
      if (!forAttrs) {
        return null;
      }
      const targetIds = forAttrs.split(",");
      if (targetIds.length == 0) {
        return null;
      }

      return targetIds
        .map(id => document.getElementById(id))
        .filter(e => e != null);
    }

    get optargets() {
      const forAttrs = this.getAttribute("opfor");
      if (!forAttrs) {
        return null;
      }
      const optargetIds = forAttrs.split(",");
      if (optargetIds.length == 0) {
        return null;
      }

      return optargetIds
        .map(id => document.getElementById(id))
        .filter(e => e != null);
    }

    set value(val) {
      if (this.internalValue == val) {
        return;
      }
      this.internalValue = val;
      this.menulist.selectedItem = this.validMenuitem;
      // now notify targets of new parent's value
      if (this.targets) {
        this.targets.forEach(target => {
          customElements.upgrade(target);
          target.parentValue = val;
        });
      }
      // now notify optargets of new op parent's value
      if (this.optargets) {
        this.optargets.forEach(optarget => {
          customElements.upgrade(optarget);
          optarget.opParentValue = val;
        });
      }
    }

    get value() {
      return this.internalValue;
    }

    /**
     * Gets the label of the menulist's selected item.
     */
    get label() {
      return this.menulist.selectedItem.getAttribute("label");
    }

    get validMenuitem() {
      if (this.value == -1) {
        // -1 means not initialized
        return null;
      }
      let isCustom = isNaN(this.value);
      let typedValue = isCustom ? this.value : parseInt(this.value);
      // custom attribute to style the unavailable menulist item
      this.menulist.setAttribute(
        "unavailable",
        !this.valueIds.includes(typedValue) ? "true" : null
      );
      // add a hidden menulist item if value is missing
      let menuitem = this.menulist.querySelector(`[value="${this.value}"]`);
      if (!menuitem) {
        // need to add a hidden menuitem
        menuitem = this.menulist.appendItem(this.valueLabel, this.value);
        menuitem.hidden = true;
      }
      return menuitem;
    }

    refreshList(dontRestore) {
      const menuItemIds = this.valueIds;
      const menuItemStrings = this.valueStrings;
      const popup = this.menupopup;
      // save our old "value" so we can restore it later
      let oldData;
      if (!dontRestore) {
        oldData = this.menulist.value;
      }
      // remove the old popup children
      while (popup.hasChildNodes()) {
        popup.lastChild.remove();
      }
      let newSelection;
      let customizePos = -1;
      for (let i = 0; i < menuItemIds.length; i++) {
        // create the menuitem
        if (Ci.nsMsgSearchAttrib.OtherHeader == menuItemIds[i].toString()) {
          customizePos = i;
        } else {
          const menuitem = document.createXULElement("menuitem");
          menuitem.setAttribute("label", menuItemStrings[i]);
          menuitem.setAttribute("value", menuItemIds[i]);
          popup.appendChild(menuitem);
          // try to restore the selection
          if (!newSelection || oldData == menuItemIds[i].toString()) {
            newSelection = menuitem;
          }
        }
      }
      if (customizePos != -1) {
        const separator = document.createXULElement("menuseparator");
        popup.appendChild(separator);
        const menuitem = document.createXULElement("menuitem");
        menuitem.setAttribute("label", menuItemStrings[customizePos]);
        menuitem.setAttribute("value", menuItemIds[customizePos]);
        popup.appendChild(menuitem);
      }

      // If we are either uninitialized, or if we are called because
      // of a change in our parent, update the value to the
      // default stored in newSelection.
      if ((this.value == -1 || dontRestore) && newSelection) {
        this.value = newSelection.getAttribute("value");
      }
      this.menulist.selectedItem = this.validMenuitem;
    }

    onSelect(event) {
      if (this.menulist.value == Ci.nsMsgSearchAttrib.OtherHeader) {
        // Customize menuitem selected.
        let args = {};
        window.openDialog(
          "chrome://messenger/content/CustomHeaders.xhtml",
          "",
          "modal,centerscreen,resizable,titlebar,chrome",
          args
        );
        // User may have removed the custom header currently selected
        // in the menulist so temporarily set the selection to a safe value.
        this.value = Ci.nsMsgSearchAttrib.OtherHeader;
        // rebuild the menulist
        UpdateAfterCustomHeaderChange();
        // Find the created or chosen custom header and select it.
        let menuitem = null;
        if (args.selectedVal) {
          menuitem = this.menulist.querySelector(
            `[label="${args.selectedVal}"]`
          );
        }
        if (menuitem) {
          this.value = menuitem.value;
        } else {
          // Nothing was picked in the custom headers editor so just pick something
          // instead of the current "Customize" menuitem.
          this.value = this.menulist.getItemAtIndex(0).value;
        }
      } else {
        this.value = this.menulist.value;
      }
    }
  }

  /**
   * The MozSearchAttribute widget is typically used in the search and filter dialogs to show a list
   * of possible message headers.
   *
   * @augments MozSearchMenulistAbstract
   */
  class MozSearchAttribute extends MozSearchMenulistAbstract {
    constructor() {
      super();

      this.stringBundle = Services.strings.createBundle(
        "chrome://messenger/locale/search-attributes.properties"
      );
    }

    connectedCallback() {
      super.connectedCallback();

      initializeTermFromId(this.id);
    }

    get valueLabel() {
      if (isNaN(this.value)) {
        // is this a custom term?
        let customTerm = MailServices.filters.getCustomTerm(this.value);
        if (customTerm) {
          return customTerm.name;
        }
        // The custom term may be missing after the extension that added it
        // was disabled or removed. We need to notify the user.
        let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
          Ci.nsIScriptError
        );
        scriptError.init(
          "Missing custom search term " + this.value,
          null,
          null,
          0,
          0,
          Ci.nsIScriptError.errorFlag,
          "component javascript"
        );
        Services.console.logMessage(scriptError);
        return this.stringBundle.GetStringFromName("MissingCustomTerm");
      }
      return this.stringBundle.GetStringFromName(
        this.validityManager.getAttributeProperty(parseInt(this.value))
      );
    }

    get valueIds() {
      let result = this.validityTable.getAvailableAttributes();
      // add any available custom search terms
      for (let customTerm of MailServices.filters.getCustomTerms()) {
        // For custom terms, the array element is a string with the custom id
        // instead of the integer attribute
        if (customTerm.getAvailable(this.searchScope, null)) {
          result.push(customTerm.id);
        }
      }
      return result;
    }

    get valueStrings() {
      let strings = [];
      let ids = this.valueIds;
      let hdrsArray = null;
      try {
        let hdrs = Services.prefs.getCharPref("mailnews.customHeaders");
        hdrs = hdrs.replace(/\s+/g, ""); // remove white spaces before splitting
        hdrsArray = hdrs.match(/[^:]+/g);
      } catch (ex) {}
      let j = 0;
      for (let i = 0; i < ids.length; i++) {
        if (isNaN(ids[i])) {
          // Is this a custom search term?
          let customTerm = MailServices.filters.getCustomTerm(ids[i]);
          if (customTerm) {
            strings[i] = customTerm.name;
          } else {
            strings[i] = "";
          }
        } else if (ids[i] > Ci.nsMsgSearchAttrib.OtherHeader && hdrsArray) {
          strings[i] = hdrsArray[j++];
        } else {
          strings[i] = this.stringBundle.GetStringFromName(
            this.validityManager.getAttributeProperty(ids[i])
          );
        }
      }
      return strings;
    }
  }
  customElements.define("search-attribute", MozSearchAttribute);

  /**
   * MozSearchOperator contains a list of operators that can be applied on search-attribute and
   * search-value value.
   *
   * @augments MozSearchMenulistAbstract
   */
  class MozSearchOperator extends MozSearchMenulistAbstract {
    constructor() {
      super();

      this.stringBundle = Services.strings.createBundle(
        "chrome://messenger/locale/search-operators.properties"
      );
    }

    connectedCallback() {
      super.connectedCallback();

      this.searchAttribute = Ci.nsMsgSearchAttrib.Default;
    }

    get valueLabel() {
      return this.stringBundle.GetStringFromName(this.value);
    }

    get valueIds() {
      let isCustom = isNaN(this.searchAttribute);
      if (isCustom) {
        let customTerm = MailServices.filters.getCustomTerm(
          this.searchAttribute
        );
        if (customTerm) {
          return customTerm.getAvailableOperators(this.searchScope);
        }
        return [Ci.nsMsgSearchOp.Contains];
      }
      return this.validityTable.getAvailableOperators(this.searchAttribute);
    }

    get valueStrings() {
      let strings = [];
      let ids = this.valueIds;
      for (let i = 0; i < ids.length; i++) {
        strings[i] = this.stringBundle.GetStringFromID(ids[i]);
      }
      return strings;
    }

    set parentValue(val) {
      if (
        this.searchAttribute == val &&
        val != Ci.nsMsgSearchAttrib.OtherHeader
      ) {
        return;
      }
      this.searchAttribute = val;
      this.refreshList(true); // don't restore the selection, since searchvalue nulls it
      if (val == Ci.nsMsgSearchAttrib.AgeInDays) {
        // We want "Age in Days" to default to "is less than".
        this.value = Ci.nsMsgSearchOp.IsLessThan;
      }
    }

    get parentValue() {
      return this.searchAttribute;
    }
  }
  customElements.define("search-operator", MozSearchOperator);

  /**
   * MozSearchValue is a widget that allows selecting the value to search or filter on. It can be a
   * text entry, priority, status, junk status, tags, hasAttachment status, and addressbook etc.
   *
   * @augments MozXULElement
   */
  class MozSearchValue extends MozXULElement {
    static get observedAttributes() {
      return ["disabled"];
    }

    constructor() {
      super();

      this.addEventListener("keypress", event => {
        if (event.keyCode != KeyEvent.DOM_VK_RETURN) {
          return;
        }
        onEnterInSearchTerm(event);
      });

      this.internalOperator = null;
      this.internalAttribute = null;
      this.internalValue = null;

      this.inputType = "none";
    }

    connectedCallback() {
      this.classList.add("input-container");
    }

    static get stringBundle() {
      if (!this._stringBundle) {
        this._stringBundle = Services.strings.createBundle(
          "chrome://messenger/locale/messenger.properties"
        );
      }
      return this._stringBundle;
    }

    /**
     * Create a menulist to be used as the input.
     *
     * @param {object[]} itemDataList - An ordered list of items to add to the
     *   menulist. Each entry must have a 'value' property to be used as the
     *   item value. If the entry has a 'label' property, it will be used
     *   directly as the item label, otherwise it must identify a bundle string
     *   using the 'stringId' property.
     *
     * @returns {MozMenuList} - The newly created menulist.
     */
    static _createMenulist(itemDataList) {
      let menulist = document.createXULElement("menulist");
      menulist.classList.add("search-value-menulist");
      let menupopup = document.createXULElement("menupopup");
      menupopup.classList.add("search-value-popup");

      let bundle = this.stringBundle;

      for (let itemData of itemDataList) {
        let item = document.createXULElement("menuitem");
        item.classList.add("search-value-menuitem");
        item.label =
          itemData.label || bundle.GetStringFromName(itemData.stringId);
        item.value = itemData.value;
        menupopup.appendChild(item);
      }
      menulist.appendChild(menupopup);
      return menulist;
    }

    /**
     * Set the child input. The input will only be changed if the type changes.
     *
     * @param {string} type - The type of input to use.
     * @param {string|number|undefined} value - A value to set on the input, or
     *   leave undefined to not change the value. See setInputValue.
     */
    setInput(type, value) {
      if (type != this.inputType) {
        this.inputType = type;
        this.input?.remove();
        let input;
        switch (type) {
          case "text":
            input = document.createElement("input");
            input.type = "text";
            input.classList.add("input-inline", "search-value-input");
            break;
          case "date":
            input = document.createElement("input");
            input.type = "text";
            input.classList.add("input-inline", "search-value-input");
            if (!value) {
              // Newly created date input shows today's date.
              // value is expected in microseconds since epoch.
              value = Date.now() * 1000;
            }
            break;
          case "size":
            input = document.createElement("input");
            input.type = "number";
            input.min = 0;
            input.max = 1000000000;
            input.classList.add("input-inline", "search-value-input");
            break;
          case "age":
            input = document.createElement("input");
            input.type = "number";
            input.min = -40000; // ~100 years.
            input.max = 40000;
            input.classList.add("input-inline", "search-value-input");
            break;
          case "percent":
            input = document.createElement("input");
            input.type = "number";
            input.min = 0;
            input.max = 100;
            input.classList.add("input-inline", "search-value-input");
            break;
          case "priority":
            input = this.constructor._createMenulist([
              { stringId: "priorityHighest", value: Ci.nsMsgPriority.highest },
              { stringId: "priorityHigh", value: Ci.nsMsgPriority.high },
              { stringId: "priorityNormal", value: Ci.nsMsgPriority.normal },
              { stringId: "priorityLow", value: Ci.nsMsgPriority.low },
              { stringId: "priorityLowest", value: Ci.nsMsgPriority.lowest },
            ]);
            break;
          case "status":
            input = this.constructor._createMenulist([
              { stringId: "replied", value: Ci.nsMsgMessageFlags.Replied },
              { stringId: "read", value: Ci.nsMsgMessageFlags.Read },
              { stringId: "new", value: Ci.nsMsgMessageFlags.New },
              { stringId: "forwarded", value: Ci.nsMsgMessageFlags.Forwarded },
              { stringId: "flagged", value: Ci.nsMsgMessageFlags.Marked },
            ]);
            break;
          case "addressbook":
            input = document.createXULElement("menulist", {
              is: "menulist-addrbooks",
            });
            input.setAttribute("localonly", "true");
            input.classList.add("search-value-menulist");
            if (!value) {
              // Select the personal addressbook by default.
              value = "jsaddrbook://abook.sqlite";
            }
            break;
          case "tags":
            input = this.constructor._createMenulist(
              MailServices.tags.getAllTags().map(taginfo => {
                return { label: taginfo.tag, value: taginfo.key };
              })
            );
            break;
          case "junk-status":
            // "Junk Status is/isn't/is empty/isn't empty 'Junk'".
            input = this.constructor._createMenulist([
              { stringId: "junk", value: Ci.nsIJunkMailPlugin.JUNK },
            ]);
            break;
          case "attachment-status":
            // "Attachment Status is/isn't 'Has Attachments'".
            input = this.constructor._createMenulist([
              { stringId: "hasAttachments", value: "0" },
            ]);
            break;
          case "junk-origin":
            input = this.constructor._createMenulist([
              { stringId: "junkScoreOriginPlugin", value: "plugin" },
              { stringId: "junkScoreOriginUser", value: "user" },
              { stringId: "junkScoreOriginFilter", value: "filter" },
              { stringId: "junkScoreOriginWhitelist", value: "whitelist" },
              { stringId: "junkScoreOriginImapFlag", value: "imapflag" },
            ]);
            break;
          case "none":
            input = null;
            break;
          case "custom":
            // Used by extensions.
            // FIXME: We need a better way for extensions to set a custom input.
            input = document.createXULElement("hbox");
            input.setAttribute("flex", "1");
            input.classList.add("search-value-custom");
            break;
          default:
            throw new Error(`Unrecognised input type "${type}"`);
        }

        this.input = input;
        if (input) {
          this.appendChild(input);
        }

        this._updateAttributes();
      }

      this.setInputValue(value);
    }

    /**
     * Set the child input to the given value.
     *
     * @param {string|number} value - The value to set on the input. For "date"
     *   inputs, this should be a number of microseconds since the epoch.
     */
    setInputValue(value) {
      if (value === undefined) {
        return;
      }
      switch (this.inputType) {
        case "text":
        case "size":
        case "age":
        case "percent":
          this.input.value = value;
          break;
        case "date":
          this.input.value = convertPRTimeToString(value);
          break;
        case "priority":
        case "status":
        case "addressbook":
        case "tags":
        case "junk-status":
        case "attachment-status":
        case "junk-origin":
          let item = this.input.querySelector(`menuitem[value="${value}"]`);
          if (item) {
            this.input.selectedItem = item;
          }
          break;
        case "none":
          // Silently ignore the value.
          break;
        case "custom":
          this.input.setAttribute("value", value);
          break;
        default:
          throw new Error(`Unhandled input type "${this.inputType}"`);
      }
    }

    /**
     * Get the child input's value.
     *
     * @returns {string|number} - The value set in the input. For "date"
     *   inputs, this is the number of microseconds since the epoch.
     */
    getInputValue() {
      switch (this.inputType) {
        case "text":
        case "size":
        case "age":
        case "percent":
          return this.input.value;
        case "date":
          return convertStringToPRTime(this.input.value);
        case "priority":
        case "status":
        case "addressbook":
        case "tags":
        case "junk-status":
        case "attachment-status":
        case "junk-origin":
          return this.input.selectedItem.value;
        case "none":
          return "";
        case "custom":
          return this.input.getAttribute("value");
        default:
          throw new Error(`Unhandled input type "${this.inputType}"`);
      }
    }

    /**
     * Get the element's displayed value.
     *
     * @returns {string} - The value seen by the user.
     */
    getReadableValue() {
      switch (this.inputType) {
        case "text":
        case "size":
        case "age":
        case "percent":
        case "date":
          return this.input.value;
        case "priority":
        case "status":
        case "addressbook":
        case "tags":
        case "junk-status":
        case "attachment-status":
        case "junk-origin":
          return this.input.selectedItem.label;
        case "none":
          return "";
        case "custom":
          return this.input.getAttribute("value");
        default:
          throw new Error(`Unhandled input type "${this.inputType}"`);
      }
    }

    attributeChangedCallback() {
      this._updateAttributes();
    }

    _updateAttributes() {
      if (!this.input) {
        return;
      }
      if (this.hasAttribute("disabled")) {
        this.input.setAttribute("disabled", this.getAttribute("disabled"));
      } else {
        this.input.removeAttribute("disabled");
      }
    }

    /**
     * Update the displayed input according to the selected sibling attributes
     * and operators.
     *
     * @param {nsIMsgSearchValue} [value] - A value to display in the input. Or
     *   leave unset to not change the value.
     */
    updateDisplay(value) {
      let operator = Number(this.internalOperator);
      switch (Number(this.internalAttribute)) {
        // Use the index to hide/show the appropriate child.
        case Ci.nsMsgSearchAttrib.Priority:
          this.setInput("priority", value?.priority);
          break;
        case Ci.nsMsgSearchAttrib.MsgStatus:
          this.setInput("status", value?.status);
          break;
        case Ci.nsMsgSearchAttrib.Date:
          this.setInput("date", value?.date);
          break;
        case Ci.nsMsgSearchAttrib.Sender:
        case Ci.nsMsgSearchAttrib.To:
        case Ci.nsMsgSearchAttrib.ToOrCC:
        case Ci.nsMsgSearchAttrib.AllAddresses:
        case Ci.nsMsgSearchAttrib.CC:
          if (
            operator == Ci.nsMsgSearchOp.IsntInAB ||
            operator == Ci.nsMsgSearchOp.IsInAB
          ) {
            this.setInput("addressbook", value?.str);
          } else {
            this.setInput("text", value?.str);
          }
          break;
        case Ci.nsMsgSearchAttrib.Keywords:
          this.setInput(
            operator == Ci.nsMsgSearchOp.IsEmpty ||
              operator == Ci.nsMsgSearchOp.IsntEmpty
              ? "none"
              : "tags",
            value?.str
          );
          break;
        case Ci.nsMsgSearchAttrib.JunkStatus:
          this.setInput(
            operator == Ci.nsMsgSearchOp.IsEmpty ||
              operator == Ci.nsMsgSearchOp.IsntEmpty
              ? "none"
              : "junk-status",
            value?.junkStatus
          );
          break;
        case Ci.nsMsgSearchAttrib.HasAttachmentStatus:
          this.setInput("attachment-status", value?.hasAttachmentStatus);
          break;
        case Ci.nsMsgSearchAttrib.JunkScoreOrigin:
          this.setInput("junk-origin", value?.str);
          break;
        case Ci.nsMsgSearchAttrib.AgeInDays:
          this.setInput("age", value?.age);
          break;
        case Ci.nsMsgSearchAttrib.Size:
          this.setInput("size", value?.size);
          break;
        case Ci.nsMsgSearchAttrib.JunkPercent:
          this.setInput("percent", value?.junkPercent);
          break;
        default:
          if (isNaN(this.internalAttribute)) {
            // Custom attribute, the internalAttribute is a string.
            // FIXME: We need a better way for extensions to set a custom input.
            this.setInput("custom", value?.str);
            this.input.setAttribute("searchAttribute", this.internalAttribute);
          } else {
            this.setInput("text", value?.str);
          }
          break;
      }
    }

    /**
     * The sibling operator type.
     *
     * @type {nsMsgSearchOpValue}
     */
    set opParentValue(val) {
      if (this.internalOperator == val) {
        return;
      }
      this.internalOperator = val;
      this.updateDisplay();
    }

    get opParentValue() {
      return this.internalOperator;
    }

    /**
     * A duplicate of the searchAttribute property.
     *
     * @type {nsMsgSearchAttribValue}
     */
    set parentValue(val) {
      this.searchAttribute = val;
    }

    get parentValue() {
      return this.searchAttribute;
    }

    /**
     * The sibling attribute type.
     *
     * @type {nsMsgSearchAttribValue}
     */
    set searchAttribute(val) {
      if (this.internalAttribute == val) {
        return;
      }
      this.internalAttribute = val;
      this.updateDisplay();
    }

    get searchAttribute() {
      return this.internalAttribute;
    }

    /**
     * The stored value for this element.
     *
     * Note that the input value is *derived* from this object when it is set.
     * But changes to the input value using the UI will not change the stored
     * value until the save method is called.
     *
     * @type {nsIMsgSearchValue}
     */
    set value(val) {
      // val is a nsIMsgSearchValue object
      this.internalValue = val;
      this.updateDisplay(val);
    }

    get value() {
      return this.internalValue;
    }

    /**
     * Updates the stored value for this element to reflect its current input
     * value.
     */
    save() {
      let searchValue = this.value;
      let searchAttribute = this.searchAttribute;

      searchValue.attrib = isNaN(searchAttribute)
        ? Ci.nsMsgSearchAttrib.Custom
        : searchAttribute;
      switch (Number(searchAttribute)) {
        case Ci.nsMsgSearchAttrib.Priority:
          searchValue.priority = this.getInputValue();
          break;
        case Ci.nsMsgSearchAttrib.MsgStatus:
          searchValue.status = this.getInputValue();
          break;
        case Ci.nsMsgSearchAttrib.AgeInDays:
          searchValue.age = this.getInputValue();
          break;
        case Ci.nsMsgSearchAttrib.Date:
          searchValue.date = this.getInputValue();
          break;
        case Ci.nsMsgSearchAttrib.JunkStatus:
          searchValue.junkStatus = this.getInputValue();
          break;
        case Ci.nsMsgSearchAttrib.HasAttachmentStatus:
          searchValue.status = Ci.nsMsgMessageFlags.Attachment;
          break;
        case Ci.nsMsgSearchAttrib.JunkPercent:
          searchValue.junkPercent = this.getInputValue();
          break;
        case Ci.nsMsgSearchAttrib.Size:
          searchValue.size = this.getInputValue();
          break;
        default:
          searchValue.str = this.getInputValue();
          break;
      }
    }

    /**
     * Stores the displayed value for this element in the given object.
     *
     * Note that after this call, the stored value will remain pointing to the
     * given searchValue object.
     *
     * @param {nsIMsgSearchValue} searchValue - The object to store the
     *   displayed value in.
     */
    saveTo(searchValue) {
      this.internalValue = searchValue;
      this.save();
    }
  }
  customElements.define("search-value", MozSearchValue);

  // The menulist CE is defined lazily. Create one now to get menulist defined,
  // allowing us to inherit from it.
  if (!customElements.get("menulist")) {
    delete document.createXULElement("menulist");
  }
  {
    /**
     * The MozRuleactiontypeMenulist is a widget that allows selecting the actions from the given menulist for
     * the selected folder. It gets displayed in the message filter dialog box.
     *
     * @augments {MozMenuList}
     */
    class MozRuleactiontypeMenulist extends customElements.get("menulist") {
      connectedCallback() {
        super.connectedCallback();
        if (this.delayConnectedCallback() || this.hasConnected) {
          return;
        }
        this.hasConnected = true;

        this.setAttribute("is", "ruleactiontype-menulist");
        this.addEventListener("command", event => {
          this.parentNode.setAttribute("value", this.value);
          checkActionsReorder();
        });

        this.addEventListener("popupshowing", event => {
          let unavailableActions = this.usedActionsList();
          for (let index = 0; index < this.menuitems.length; index++) {
            let menu = this.menuitems[index];
            menu.setAttribute("disabled", menu.value in unavailableActions);
          }
        });

        this.menuitems = this.getElementsByTagNameNS(
          this.namespaceURI,
          "menuitem"
        );

        // Force initialization of the menulist custom element first.
        customElements.upgrade(this);
        this.addCustomActions();
        this.hideInvalidActions();
        // Differentiate between creating a new, next available action,
        // and creating a row which will be initialized with an action.
        if (!this.parentNode.hasAttribute("initialActionIndex")) {
          let unavailableActions = this.usedActionsList();
          // Select the first one that's not in the list.
          for (let index = 0; index < this.menuitems.length; index++) {
            let menu = this.menuitems[index];
            if (!(menu.value in unavailableActions) && !menu.hidden) {
              this.value = menu.value;
              this.parentNode.setAttribute("value", menu.value);
              break;
            }
          }
        } else {
          this.parentNode.mActionTypeInitialized = true;
          this.parentNode.clearInitialActionIndex();
        }
      }

      hideInvalidActions() {
        let menupopup = this.menupopup;
        let scope = getScopeFromFilterList(gFilterList);

        // Walk through the list of filter actions and hide any actions which aren't valid
        // for our given scope (news, imap, pop, etc) and context.
        let elements;

        // Disable / enable all elements in the "filteractionlist"
        // based on the scope and the "enablefornews" attribute.
        elements = menupopup.getElementsByAttribute("enablefornews", "true");
        for (let i = 0; i < elements.length; i++) {
          elements[i].hidden = scope != Ci.nsMsgSearchScope.newsFilter;
        }

        elements = menupopup.getElementsByAttribute("enablefornews", "false");
        for (let i = 0; i < elements.length; i++) {
          elements[i].hidden = scope == Ci.nsMsgSearchScope.newsFilter;
        }

        elements = menupopup.getElementsByAttribute("enableforpop3", "true");
        for (let i = 0; i < elements.length; i++) {
          elements[i].hidden = !(
            gFilterList.folder.server.type == "pop3" ||
            gFilterList.folder.server.type == "none"
          );
        }

        elements = menupopup.getElementsByAttribute("isCustom", "true");
        // Note there might be an additional element here as a placeholder
        // for a missing action, so we iterate over the known actions
        // instead of the elements.
        for (let i = 0; i < gCustomActions.length; i++) {
          elements[i].hidden = !gCustomActions[i].isValidForType(
            gFilterType,
            scope
          );
        }

        // Disable "Reply with Template" if there are no templates.
        if (this.findTemplates().length == 0) {
          elements = menupopup.getElementsByAttribute(
            "value",
            "replytomessage"
          );
          if (elements.length == 1) {
            elements[0].hidden = true;
          }
        }
      }

      addCustomActions() {
        let menupopup = this.menupopup;
        for (let i = 0; i < gCustomActions.length; i++) {
          let customAction = gCustomActions[i];
          let menuitem = document.createXULElement("menuitem");
          menuitem.setAttribute("label", customAction.name);
          menuitem.setAttribute("value", customAction.id);
          menuitem.setAttribute("isCustom", "true");
          menupopup.appendChild(menuitem);
        }
      }

      /**
       * Returns a hash containing all of the filter actions which are currently
       * being used by other filteractionrows.
       *
       * @returns {object} - a hash containing all of the filter actions which are
       *                    currently being used by other filteractionrows.
       */
      usedActionsList() {
        let usedActions = {};
        let currentFilterActionRow = this.parentNode;
        let listBox = currentFilterActionRow.parentNode; // need to account for the list item.
        // Now iterate over each list item in the list box.
        for (let index = 0; index < listBox.getRowCount(); index++) {
          let filterActionRow = listBox.getItemAtIndex(index);
          if (filterActionRow != currentFilterActionRow) {
            let actionValue = filterActionRow.getAttribute("value");

            // Let custom actions decide if dups are allowed.
            let isCustom = false;
            for (let i = 0; i < gCustomActions.length; i++) {
              if (gCustomActions[i].id == actionValue) {
                isCustom = true;
                if (!gCustomActions[i].allowDuplicates) {
                  usedActions[actionValue] = true;
                }
                break;
              }
            }

            if (!isCustom) {
              // The following actions can appear more than once in a single filter
              // so do not set them as already used.
              if (
                actionValue != "addtagtomessage" &&
                actionValue != "forwardmessage" &&
                actionValue != "copymessage"
              ) {
                usedActions[actionValue] = true;
              }
              // If either Delete message or Move message exists, disable the other one.
              // It does not make sense to apply both to the same message.
              if (actionValue == "deletemessage") {
                usedActions.movemessage = true;
              } else if (actionValue == "movemessage") {
                usedActions.deletemessage = true;
              } else if (actionValue == "markasread") {
                // The same with Mark as read/Mark as Unread.
                usedActions.markasunread = true;
              } else if (actionValue == "markasunread") {
                usedActions.markasread = true;
              }
            }
          }
        }
        return usedActions;
      }

      /**
       * Check if there exist any templates in this account.
       *
       * @returns {object[]} - An array of template headers: each has a label and
       *                      a value.
       */
      findTemplates() {
        let identities = MailServices.accounts.getIdentitiesForServer(
          gFilterList.folder.server
        );
        // Typically if this is Local Folders.
        if (identities.length == 0) {
          if (MailServices.accounts.defaultAccount) {
            identities.push(
              MailServices.accounts.defaultAccount.defaultIdentity
            );
          }
        }

        let templates = [];
        let foldersScanned = [];

        for (let identity of identities) {
          let enumerator = null;
          let msgFolder = MailUtils.getExistingFolder(
            identity.stationeryFolder
          );
          // If we already processed this folder, do not set enumerator
          // so that we skip this identity.
          if (msgFolder && !foldersScanned.includes(msgFolder)) {
            foldersScanned.push(msgFolder);
            enumerator = msgFolder.msgDatabase.enumerateMessages();
          }

          if (!enumerator) {
            continue;
          }

          for (let header of enumerator) {
            let uri =
              msgFolder.URI +
              "?messageId=" +
              header.messageId +
              "&subject=" +
              header.mime2DecodedSubject;
            templates.push({ label: header.mime2DecodedSubject, value: uri });
          }
        }
        return templates;
      }
    }

    customElements.define(
      "ruleactiontype-menulist",
      MozRuleactiontypeMenulist,
      { extends: "menulist" }
    );
  }

  /**
   * The MozRuleactionRichlistitem is a widget which gives the options to filter
   * the messages with following elements: ruleactiontype-menulist, ruleactiontarget-wrapper
   * and button to add or remove the MozRuleactionRichlistitem. It gets added in the
   * filterActionList richlistbox in the Filter Editor dialog.
   *
   * @augments {MozElements.MozRichlistitem}
   */
  class MozRuleactionRichlistitem extends MozElements.MozRichlistitem {
    static get inheritedAttributes() {
      return { ".ruleactiontarget": "type=value" };
    }

    constructor() {
      super();

      this.mActionTypeInitialized = false;
      this.mRuleActionTargetInitialized = false;
    }

    connectedCallback() {
      if (this.delayConnectedCallback() || this.hasChildNodes()) {
        return;
      }
      this.setAttribute("is", "ruleaction-richlistitem");
      this.appendChild(
        MozXULElement.parseXULToFragment(
          `
          <menulist is="ruleactiontype-menulist" style="flex: &filterActionTypeFlexValue;">
            <menupopup>
              <menuitem label="&moveMessage.label;"
                        value="movemessage"
                        enablefornews="false"></menuitem>
              <menuitem label="&copyMessage.label;"
                        value="copymessage"></menuitem>
              <menuseparator enablefornews="false"></menuseparator>
              <menuitem label="&forwardTo.label;"
                        value="forwardmessage"
                        enablefornews="false"></menuitem>
              <menuitem label="&replyWithTemplate.label;"
                        value="replytomessage"
                        enablefornews="false"></menuitem>
              <menuseparator></menuseparator>
              <menuitem label="&markMessageRead.label;"
                        value="markasread"></menuitem>
              <menuitem label="&markMessageUnread.label;"
                        value="markasunread"></menuitem>
              <menuitem label="&markMessageStarred.label;"
                        value="markasflagged"></menuitem>
              <menuitem label="&setPriority.label;"
                        value="setpriorityto"></menuitem>
              <menuitem label="&addTag.label;"
                        value="addtagtomessage"></menuitem>
              <menuitem label="&setJunkScore.label;"
                        value="setjunkscore"
                        enablefornews="false"></menuitem>
              <menuseparator enableforpop3="true"></menuseparator>
              <menuitem label="&deleteMessage.label;"
                        value="deletemessage"></menuitem>
              <menuitem label="&deleteFromPOP.label;"
                        value="deletefrompopserver"
                        enableforpop3="true"></menuitem>
              <menuitem label="&fetchFromPOP.label;"
                        value="fetchfrompopserver"
                        enableforpop3="true"></menuitem>
              <menuseparator></menuseparator>
              <menuitem label="&ignoreThread.label;"
                        value="ignorethread"></menuitem>
              <menuitem label="&ignoreSubthread.label;"
                        value="ignoresubthread"></menuitem>
              <menuitem label="&watchThread.label;"
                        value="watchthread"></menuitem>
              <menuseparator></menuseparator>
              <menuitem label="&stopExecution.label;"
                        value="stopexecution"></menuitem>
            </menupopup>
          </menulist>
          <ruleactiontarget-wrapper class="ruleactiontarget"
                                    style="flex: &filterActionTargetFlexValue;">
          </ruleactiontarget-wrapper>
          <hbox>
            <button class="small-button"
                    label="+"
                    tooltiptext="&addAction.tooltip;"
                    oncommand="this.parentNode.parentNode.addRow();"></button>
            <button class="small-button remove-small-button"
                    label="−"
                    tooltiptext="&removeAction.tooltip;"
                    oncommand="this.parentNode.parentNode.removeRow();"></button>
          </hbox>
          `,
          [
            "chrome://messenger/locale/messenger.dtd",
            "chrome://messenger/locale/FilterEditor.dtd",
          ]
        )
      );

      this.mRuleActionType = this.querySelector("menulist");
      this.mRemoveButton = this.querySelector(".remove-small-button");
      this.mListBox = this.parentNode;
      this.initializeAttributeInheritance();
    }

    set selected(val) {
      // This provides a dummy selected property that the richlistbox expects to
      // be able to call. See bug 202036.
    }

    get selected() {
      return false;
    }

    _fireEvent(aName) {
      // This provides a dummy _fireEvent function that the richlistbox expects to
      // be able to call. See bug 202036.
    }

    /**
     * We should only remove the initialActionIndex after we have been told that
     * both the rule action type and the rule action target have both been built
     * since they both need this piece of information. This complication arises
     * because both of these child elements are getting bound asynchronously
     * after the search row has been constructed.
     */
    clearInitialActionIndex() {
      if (this.mActionTypeInitialized && this.mRuleActionTargetInitialized) {
        this.removeAttribute("initialActionIndex");
      }
    }

    initWithAction(aFilterAction) {
      let filterActionStr;
      let actionTarget = this.children[1];
      let actionItem = actionTarget.ruleactiontargetElement;
      let nsMsgFilterAction = Ci.nsMsgFilterAction;
      switch (aFilterAction.type) {
        case nsMsgFilterAction.Custom:
          filterActionStr = aFilterAction.customId;
          if (actionItem) {
            actionItem.children[0].value = aFilterAction.strValue;
          }

          // Make sure the custom action has been added. If not, it
          // probably was from an extension that has been removed. We'll
          // show a dummy menuitem to warn the user.
          let needCustomLabel = true;
          for (let i = 0; i < gCustomActions.length; i++) {
            if (gCustomActions[i].id == filterActionStr) {
              needCustomLabel = false;
              break;
            }
          }
          if (needCustomLabel) {
            let menuitem = document.createXULElement("menuitem");
            menuitem.setAttribute(
              "label",
              gFilterBundle.getString("filterMissingCustomAction")
            );
            menuitem.setAttribute("value", filterActionStr);
            menuitem.disabled = true;
            this.mRuleActionType.menupopup.appendChild(menuitem);
            let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
              Ci.nsIScriptError
            );
            scriptError.init(
              "Missing custom action " + filterActionStr,
              null,
              null,
              0,
              0,
              Ci.nsIScriptError.errorFlag,
              "component javascript"
            );
            Services.console.logMessage(scriptError);
          }
          break;
        case nsMsgFilterAction.MoveToFolder:
        case nsMsgFilterAction.CopyToFolder:
          actionItem.children[0].value = aFilterAction.targetFolderUri;
          break;
        case nsMsgFilterAction.Reply:
        case nsMsgFilterAction.Forward:
          actionItem.children[0].value = aFilterAction.strValue;
          break;
        case nsMsgFilterAction.ChangePriority:
          actionItem.children[0].value = aFilterAction.priority;
          break;
        case nsMsgFilterAction.JunkScore:
          actionItem.children[0].value = aFilterAction.junkScore;
          break;
        case nsMsgFilterAction.AddTag:
          actionItem.children[0].value = aFilterAction.strValue;
          break;
        default:
          break;
      }
      if (aFilterAction.type != nsMsgFilterAction.Custom) {
        filterActionStr = gFilterActionStrings[aFilterAction.type];
      }
      this.mRuleActionType.value = filterActionStr;
      this.mRuleActionTargetInitialized = true;
      this.clearInitialActionIndex();
      checkActionsReorder();
    }

    /**
     * Function is used to check if the filter is valid or not. This routine
     * also prompts the user.
     *
     * @returns {boolean} - true if this row represents a valid filter action.
     */
    validateAction() {
      let filterActionString = this.getAttribute("value");
      let actionTarget = this.children[1];
      let actionTargetLabel =
        actionTarget.ruleactiontargetElement &&
        actionTarget.ruleactiontargetElement.children[0].value;
      let errorString, customError;

      switch (filterActionString) {
        case "movemessage":
        case "copymessage":
          let msgFolder = actionTargetLabel
            ? MailUtils.getOrCreateFolder(actionTargetLabel)
            : null;
          if (!msgFolder || !msgFolder.canFileMessages) {
            errorString = "mustSelectFolder";
          }
          break;
        case "forwardmessage":
          if (
            actionTargetLabel.length < 3 ||
            actionTargetLabel.indexOf("@") < 1
          ) {
            errorString = "enterValidEmailAddress";
          }
          break;
        case "replytomessage":
          if (!actionTarget.ruleactiontargetElement.children[0].selectedItem) {
            errorString = "pickTemplateToReplyWith";
          }
          break;
        default:
          // Locate the correct custom action, and check validity.
          for (let i = 0; i < gCustomActions.length; i++) {
            if (gCustomActions[i].id == filterActionString) {
              customError = gCustomActions[i].validateActionValue(
                actionTargetLabel,
                gFilterList.folder,
                gFilterType
              );
              break;
            }
          }
          break;
      }

      errorString = errorString
        ? gFilterBundle.getString(errorString)
        : customError;
      if (errorString) {
        Services.prompt.alert(window, null, errorString);
      }

      return !errorString;
    }

    /**
     * Create a new filter action, fill it in, and then append it to the filter.
     *
     * @param {object} aFilter - filter object to save.
     */
    saveToFilter(aFilter) {
      let filterAction = aFilter.createAction();
      let filterActionString = this.getAttribute("value");
      filterAction.type = gFilterActionStrings.indexOf(filterActionString);
      let actionTarget = this.children[1];
      let actionItem = actionTarget.ruleactiontargetElement;
      let nsMsgFilterAction = Ci.nsMsgFilterAction;
      switch (filterAction.type) {
        case nsMsgFilterAction.ChangePriority:
          filterAction.priority = actionItem.children[0].getAttribute("value");
          break;
        case nsMsgFilterAction.MoveToFolder:
        case nsMsgFilterAction.CopyToFolder:
          filterAction.targetFolderUri = actionItem.children[0].value;
          break;
        case nsMsgFilterAction.JunkScore:
          filterAction.junkScore = actionItem.children[0].value;
          break;
        case nsMsgFilterAction.Custom:
          filterAction.customId = filterActionString;
        // Fall through to set the value.
        default:
          if (actionItem && actionItem.children.length > 0) {
            filterAction.strValue = actionItem.children[0].value;
          }
          break;
      }
      aFilter.appendAction(filterAction);
    }

    /**
     * If we only have one row of actions, then disable the remove button for that row.
     */
    updateRemoveButton() {
      this.mListBox.getItemAtIndex(0).mRemoveButton.disabled =
        this.mListBox.getRowCount() == 1;
    }

    addRow() {
      let listItem = document.createXULElement("richlistitem", {
        is: "ruleaction-richlistitem",
      });
      listItem.classList.add("ruleaction");
      listItem.setAttribute("onfocus", "this.storeFocus();");
      this.mListBox.insertBefore(listItem, this.nextElementSibling);
      this.mListBox.ensureElementIsVisible(listItem);

      // Make sure the first remove button is enabled.
      this.updateRemoveButton();
      checkActionsReorder();
    }

    removeRow() {
      // this.mListBox will fail after the row is removed, so save a reference.
      let listBox = this.mListBox;
      if (listBox.getRowCount() > 1) {
        this.remove();
      }
      // Can't use 'this' as it is destroyed now.
      listBox.getItemAtIndex(0).updateRemoveButton();
      checkActionsReorder();
    }

    /**
     * When this action row is focused, store its index in the parent richlistbox.
     */
    storeFocus() {
      this.mListBox.setAttribute(
        "focusedAction",
        this.mListBox.getIndexOfItem(this)
      );
    }
  }

  customElements.define("ruleaction-richlistitem", MozRuleactionRichlistitem, {
    extends: "richlistitem",
  });
}
