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

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

XPCOMUtils.defineLazyModuleGetters(this, {
  QuickFilterManager: "resource:///modules/QuickFilterManager.jsm",
  MailServices: "resource:///modules/MailServices.jsm",
});

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "gDynamicPaneConfig",
  "mail.pane_config.dynamic",
  0
);

const LAYOUTS = ["standard", "wide", "vertical"];
// From nsIMsgDBView.idl
const SORT_TYPE_MAP = new Map(
  Object.keys(Ci.nsMsgViewSortType).map(key => {
    // Change "byFoo" to "foo".
    let shortKey = key[2].toLowerCase() + key.substring(3);
    return [Ci.nsMsgViewSortType[key], shortKey];
  })
);
const SORT_ORDER_MAP = new Map(
  Object.keys(Ci.nsMsgViewSortOrder).map(key => [
    Ci.nsMsgViewSortOrder[key],
    key,
  ])
);

/**
 * Converts a mail tab to a simple object for use in messages.
 *
 * @returns {object}
 */
function convertMailTab(tab, context) {
  let mailTabObject = {
    id: tab.id,
    windowId: tab.windowId,
    active: tab.active,
    sortType: null,
    sortOrder: null,
    viewType: null,
    layout: LAYOUTS[gDynamicPaneConfig],
    folderPaneVisible: null,
    messagePaneVisible: null,
  };

  let about3Pane = tab.nativeTab.chromeBrowser.contentWindow;
  let { gViewWrapper, paneLayout } = about3Pane;
  mailTabObject.folderPaneVisible = paneLayout.folderPaneVisible;
  mailTabObject.messagePaneVisible = paneLayout.messagePaneVisible;
  mailTabObject.sortType = SORT_TYPE_MAP.get(gViewWrapper?.primarySortType);
  mailTabObject.sortOrder = SORT_ORDER_MAP.get(gViewWrapper?.primarySortOrder);
  if (gViewWrapper?.showGroupedBySort) {
    mailTabObject.viewType = "groupedBySortType";
  } else if (gViewWrapper?.showThreaded) {
    mailTabObject.viewType = "groupedByThread";
  } else {
    mailTabObject.viewType = "ungrouped";
  }
  if (context.extension.hasPermission("accountsRead")) {
    mailTabObject.displayedFolder = convertFolder(about3Pane.gFolder);
  }
  return mailTabObject;
}

/**
 * Listens for changes in the UI to fire events.
 */
var uiListener = new (class extends EventEmitter {
  constructor() {
    super();
    this.listenerCount = 0;
    this.handleEvent = this.handleEvent.bind(this);
    this.lastSelected = new WeakMap();
  }

  handleEvent(event) {
    let browser = event.target.browsingContext.embedderElement;
    let tabmail = browser.ownerGlobal.top.document.getElementById("tabmail");
    let nativeTab = tabmail.tabInfo.find(
      t =>
        t.chromeBrowser == browser ||
        t.chromeBrowser == browser.browsingContext.parent.embedderElement
    );

    if (nativeTab.mode.name != "mail3PaneTab") {
      return;
    }

    let tabId = tabTracker.getId(nativeTab);
    let tab = tabTracker.getTab(tabId);

    if (event.type == "folderURIChanged") {
      let folderURI = event.detail;
      let folder = MailServices.folderLookup.getFolderForURL(folderURI);
      if (this.lastSelected.get(tab) == folder) {
        return;
      }
      this.lastSelected.set(tab, folder);
      this.emit("folder-changed", tab, folder);
    } else if (event.type == "messageURIChanged") {
      let messages =
        nativeTab.chromeBrowser.contentWindow.gDBView?.getSelectedMsgHdrs();
      if (messages) {
        this.emit("messages-changed", tab, messages);
      }
    }
  }

  incrementListeners() {
    this.listenerCount++;
    if (this.listenerCount == 1) {
      windowTracker.addListener("folderURIChanged", this);
      windowTracker.addListener("messageURIChanged", this);
    }
  }
  decrementListeners() {
    this.listenerCount--;
    if (this.listenerCount == 0) {
      windowTracker.removeListener("folderURIChanged", this);
      windowTracker.removeListener("messageURIChanged", this);
      this.lastSelected = new WeakMap();
    }
  }
})();

this.mailTabs = class extends ExtensionAPIPersistent {
  PERSISTENT_EVENTS = {
    // For primed persistent events (deactivated background), the context is only
    // available after fire.wakeup() has fulfilled (ensuring the convert() function
    // has been called).

    onDisplayedFolderChanged({ context, fire }) {
      const { extension } = this;
      const { tabManager } = extension;
      async function listener(event, tab, folder) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        fire.sync(tabManager.convert(tab), convertFolder(folder));
      }
      uiListener.on("folder-changed", listener);
      uiListener.incrementListeners();
      return {
        unregister: () => {
          uiListener.off("folder-changed", listener);
          uiListener.decrementListeners();
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
    onSelectedMessagesChanged({ context, fire }) {
      const { extension } = this;
      const { tabManager } = extension;
      async function listener(event, tab, messages) {
        if (fire.wakeup) {
          await fire.wakeup();
        }
        let page = await messageListTracker.startList(messages, extension);
        fire.sync(tabManager.convert(tab), page);
      }
      uiListener.on("messages-changed", listener);
      uiListener.incrementListeners();
      return {
        unregister: () => {
          uiListener.off("messages-changed", listener);
          uiListener.decrementListeners();
        },
        convert(newFire, extContext) {
          fire = newFire;
          context = extContext;
        },
      };
    },
  };

  getAPI(context) {
    let { extension } = context;
    let { tabManager } = extension;

    /**
     * Gets the tab for the given tab id, or the active tab if the id is null.
     *
     * @param {?Integer} tabId - The tab id to get
     * @returns {Tab} The matching tab, or the active tab
     */
    async function getTabOrActive(tabId) {
      let tab;
      if (tabId) {
        tab = tabManager.get(tabId);
      } else {
        tab = tabManager.wrapTab(tabTracker.activeTab);
        tabId = tab.id;
      }

      if (tab && tab.type == "mail") {
        let windowId = windowTracker.getId(getTabWindow(tab.nativeTab));
        // Before doing anything with the mail tab, ensure its outer window is
        // fully loaded.
        await getNormalWindowReady(context, windowId);
        return tab;
      }
      throw new ExtensionError(`Invalid mail tab ID: ${tabId}`);
    }

    /**
     * Set the currently displayed folder in the given tab.
     *
     * @param {NativeTabInfo} nativeTabInfo
     * @param {nsIMsgFolder} folder
     * @param {boolean} restorePreviousSelection - Select the previously selected
     *   messages of the folder, after it has been set.
     */
    async function setFolder(nativeTabInfo, folder, restorePreviousSelection) {
      let about3Pane = nativeTabInfo.chromeBrowser.contentWindow;
      if (!nativeTabInfo.folder || nativeTabInfo.folder.URI != folder.URI) {
        await new Promise(resolve => {
          let listener = event => {
            if (event.detail == folder.URI) {
              about3Pane.removeEventListener("folderURIChanged", listener);
              resolve();
            }
          };
          about3Pane.addEventListener("folderURIChanged", listener);
          if (restorePreviousSelection) {
            about3Pane.restoreState({
              folderURI: folder.URI,
            });
          } else {
            about3Pane.threadPane.forgetSelection(folder.URI);
            nativeTabInfo.folder = folder;
          }
        });
      }
    }

    return {
      mailTabs: {
        async query({ active, currentWindow, lastFocusedWindow, windowId }) {
          await getNormalWindowReady();
          return Array.from(
            tabManager.query(
              {
                active,
                currentWindow,
                lastFocusedWindow,
                mailTab: true,
                windowId,

                // All of these are needed for tabManager to return every tab we want.
                cookieStoreId: null,
                index: null,
                screen: null,
                title: null,
                url: null,
                windowType: null,
              },
              context
            ),
            tab => convertMailTab(tab, context)
          );
        },

        async get(tabId) {
          let tab = await getTabOrActive(tabId);
          return convertMailTab(tab, context);
        },
        async getCurrent() {
          try {
            let tab = await getTabOrActive();
            return convertMailTab(tab, context);
          } catch (e) {
            // Do not throw, if the active tab is not a mail tab, but return undefined.
            return undefined;
          }
        },

        async update(tabId, args) {
          let tab = await getTabOrActive(tabId);
          let { nativeTab } = tab;
          let about3Pane = nativeTab.chromeBrowser.contentWindow;

          let {
            displayedFolder,
            layout,
            folderPaneVisible,
            messagePaneVisible,
            sortOrder,
            sortType,
            viewType,
          } = args;

          if (displayedFolder) {
            if (!extension.hasPermission("accountsRead")) {
              throw new ExtensionError(
                'Updating the displayed folder requires the "accountsRead" permission'
              );
            }

            let folderUri = folderPathToURI(
              displayedFolder.accountId,
              displayedFolder.path
            );
            let folder = MailServices.folderLookup.getFolderForURL(folderUri);
            if (!folder) {
              throw new ExtensionError(
                `Folder "${displayedFolder.path}" for account ` +
                  `"${displayedFolder.accountId}" not found.`
              );
            }
            await setFolder(nativeTab, folder, true);
          }

          if (sortType) {
            // Change "foo" to "byFoo".
            sortType = "by" + sortType[0].toUpperCase() + sortType.substring(1);
            if (
              sortType in Ci.nsMsgViewSortType &&
              sortOrder &&
              sortOrder in Ci.nsMsgViewSortOrder
            ) {
              about3Pane.gViewWrapper.sort(
                Ci.nsMsgViewSortType[sortType],
                Ci.nsMsgViewSortOrder[sortOrder]
              );
            }
          }

          switch (viewType) {
            case "groupedBySortType":
              about3Pane.gViewWrapper.showGroupedBySort = true;
              break;
            case "groupedByThread":
              about3Pane.gViewWrapper.showThreaded = true;
              break;
            case "ungrouped":
              about3Pane.gViewWrapper.showUnthreaded = true;
              break;
          }

          // Layout applies to all folder tabs.
          if (layout) {
            Services.prefs.setIntPref(
              "mail.pane_config.dynamic",
              LAYOUTS.indexOf(layout)
            );
          }

          if (typeof folderPaneVisible == "boolean") {
            about3Pane.paneLayout.folderPaneVisible = folderPaneVisible;
          }
          if (typeof messagePaneVisible == "boolean") {
            about3Pane.paneLayout.messagePaneVisible = messagePaneVisible;
          }
        },

        async getSelectedMessages(tabId) {
          let tab = await getTabOrActive(tabId);
          let dbView = tab.nativeTab.chromeBrowser.contentWindow?.gDBView;
          let messageList = dbView ? dbView.getSelectedMsgHdrs() : [];
          return messageListTracker.startList(messageList, extension);
        },

        async setSelectedMessages(tabId, messageIds) {
          if (
            !extension.hasPermission("messagesRead") ||
            !extension.hasPermission("accountsRead")
          ) {
            throw new ExtensionError(
              'Using mailTabs.setSelectedMessages() requires the "accountsRead" and the "messagesRead" permission'
            );
          }

          let tab = await getTabOrActive(tabId);
          let refFolder, refMsgId;
          let msgHdrs = [];
          for (let messageId of messageIds) {
            let msgHdr = messageTracker.getMessage(messageId);
            if (!refFolder) {
              refFolder = msgHdr.folder;
              refMsgId = messageId;
            }
            if (msgHdr.folder == refFolder) {
              msgHdrs.push(msgHdr);
            } else {
              throw new ExtensionError(
                `Message ${refMsgId} and message ${messageId} are not in the same folder, cannot select them both.`
              );
            }
          }

          if (refFolder) {
            await setFolder(tab.nativeTab, refFolder, false);
          }
          let about3Pane = tab.nativeTab.chromeBrowser.contentWindow;
          const selectedIndices = msgHdrs.map(
            about3Pane.gViewWrapper.getViewIndexForMsgHdr,
            about3Pane.gViewWrapper
          );
          about3Pane.threadTree.selectedIndices = selectedIndices;
          if (selectedIndices.length) {
            about3Pane.threadTree.scrollToIndex(selectedIndices[0], true);
          }
        },

        async setQuickFilter(tabId, state) {
          let tab = await getTabOrActive(tabId);
          let nativeTab = tab.nativeTab;
          let about3Pane = nativeTab.chromeBrowser.contentWindow;

          let filterer = about3Pane.quickFilterBar.filterer;
          filterer.clear();

          // Map of QuickFilter state names to possible WebExtensions state names.
          let stateMap = {
            unread: "unread",
            starred: "flagged",
            addrBook: "contact",
            attachment: "attachment",
          };

          filterer.visible = state.show !== false;
          for (let [key, name] of Object.entries(stateMap)) {
            filterer.setFilterValue(key, state[name]);
          }

          if (state.tags) {
            filterer.filterValues.tags = {
              mode: "OR",
              tags: {},
            };
            for (let tag of MailServices.tags.getAllTags()) {
              filterer.filterValues.tags[tag.key] = null;
            }
            if (typeof state.tags == "object") {
              filterer.filterValues.tags.mode =
                state.tags.mode == "any" ? "OR" : "AND";
              for (let [key, value] of Object.entries(state.tags.tags)) {
                filterer.filterValues.tags.tags[key] = value;
              }
            }
          }
          if (state.text) {
            filterer.filterValues.text = {
              states: {
                recipients: state.text.recipients || false,
                sender: state.text.author || false,
                subject: state.text.subject || false,
                body: state.text.body || false,
              },
              text: state.text.text,
            };
          }

          about3Pane.quickFilterBar.updateSearch();
        },

        onDisplayedFolderChanged: new EventManager({
          context,
          module: "mailTabs",
          event: "onDisplayedFolderChanged",
          extensionApi: this,
        }).api(),

        onSelectedMessagesChanged: new EventManager({
          context,
          module: "mailTabs",
          event: "onSelectedMessagesChanged",
          extensionApi: this,
        }).api(),
      },
    };
  }
};
