/* 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 { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";

import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";

const { ExtensionError } = ExtensionUtils;

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
  mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
});

class Tab {
  constructor(window) {
    this.id = GeckoViewTabBridge.windowIdToTabId(window.docShell.outerWindowID);
    this.browser = window.browser;
    this.active = false;
  }

  get linkedBrowser() {
    return this.browser;
  }

  getActive() {
    return this.active;
  }

  get userContextId() {
    return this.browser.ownerGlobal.moduleManager.settings
      .unsafeSessionContextId;
  }
}

// Because of bug 1410749, we can't use 0, though, and just to be safe
// we choose a value that is unlikely to overlap with Fennec's tab IDs.
const TAB_ID_BASE = 10000;

export const GeckoViewTabBridge = {
  /**
   * Converts windowId to tabId as in GeckoView every browser window has exactly one tab.
   *
   * @param {number} windowId outerWindowId
   *
   * @returns {number} tabId
   */
  windowIdToTabId(windowId) {
    return TAB_ID_BASE + windowId;
  },

  /**
   * Converts tabId to windowId.
   *
   * @param {number} tabId
   *
   * @returns {number}
   *          outerWindowId of browser window to which the tab belongs.
   */
  tabIdToWindowId(tabId) {
    return tabId - TAB_ID_BASE;
  },

  /**
   * Delegates openOptionsPage handling to the app.
   *
   * @param {number} extensionId
   *        The ID of the extension requesting the options menu.
   *
   * @returns {Promise<Void>}
   *          A promise resolved after successful handling.
   */
  async openOptionsPage(extensionId) {
    debug`openOptionsPage for extensionId ${extensionId}`;

    try {
      await lazy.EventDispatcher.instance.sendRequestForResult({
        type: "GeckoView:WebExtension:OpenOptionsPage",
        extensionId,
      });
    } catch (errorMessage) {
      // The error message coming from GeckoView is about :OpenOptionsPage not
      // being registered so we need to have one that's extension friendly
      // here.
      throw new ExtensionError("runtime.openOptionsPage is not supported");
    }
  },

  /**
   * Request the GeckoView App to create a new tab (GeckoSession).
   *
   * @param {object} options
   * @param {string} options.extensionId
   *        The ID of the extension that requested a new tab.
   * @param {object} options.createProperties
   *        The properties for the new tab, see tabs.create reference for details.
   *
   * @returns {Promise<Tab>}
   *          A promise resolved to the newly created tab.
   * @throws {Error}
   *         Throws an error if the GeckoView app doesn't support tabs.create or fails to handle the request.
   */
  async createNewTab({ extensionId, createProperties } = {}) {
    debug`createNewTab`;

    const newSessionId = Services.uuid
      .generateUUID()
      .toString()
      .slice(1, -1)
      .replace(/-/g, "");

    // The window might already be open by the time we get the response, so we
    // need to start waiting before we send the message.
    const windowPromise = new Promise(resolve => {
      const handler = {
        observe(aSubject, aTopic) {
          if (
            aTopic === "geckoview-window-created" &&
            aSubject.name === newSessionId
          ) {
            Services.obs.removeObserver(handler, "geckoview-window-created");
            resolve(aSubject);
          }
        },
      };
      Services.obs.addObserver(handler, "geckoview-window-created");
    });

    let didOpenSession = false;
    try {
      didOpenSession = await lazy.EventDispatcher.instance.sendRequestForResult(
        {
          type: "GeckoView:WebExtension:NewTab",
          extensionId,
          createProperties,
          newSessionId,
        }
      );
    } catch (errorMessage) {
      // The error message coming from GeckoView is about :NewTab not being
      // registered so we need to have one that's extension friendly here.
      throw new ExtensionError("tabs.create is not supported");
    }

    if (!didOpenSession) {
      throw new ExtensionError("Cannot create new tab");
    }

    const window = await windowPromise;
    if (!window.tab) {
      window.tab = new Tab(window);
    }
    return window.tab;
  },

  /**
   * Request the GeckoView App to close a tab (GeckoSession).
   *
   * @param {object} options
   * @param {Window} options.window The window owning the tab to close
   * @param {string} options.extensionId
   *
   * @returns {Promise<Void>}
   *          A promise resolved after GeckoSession is closed.
   * @throws {Error}
   *         Throws an error if the GeckoView app doesn't allow extension to close tab.
   */
  async closeTab({ window, extensionId } = {}) {
    try {
      await window.WindowEventDispatcher.sendRequestForResult({
        type: "GeckoView:WebExtension:CloseTab",
        extensionId,
      });
    } catch (errorMessage) {
      throw new ExtensionError(errorMessage);
    }
  },

  async updateTab({ window, extensionId, updateProperties } = {}) {
    try {
      await window.WindowEventDispatcher.sendRequestForResult({
        type: "GeckoView:WebExtension:UpdateTab",
        extensionId,
        updateProperties,
      });
    } catch (errorMessage) {
      throw new ExtensionError(errorMessage);
    }
  },
};

export class GeckoViewTab extends GeckoViewModule {
  onInit() {
    const { window } = this;
    if (!window.tab) {
      window.tab = new Tab(window);
    }

    this.registerListener([
      "GeckoView:WebExtension:SetTabActive",
      "GeckoView:FlushSessionState",
    ]);
  }

  onEvent(aEvent, aData) {
    debug`onEvent: event=${aEvent}, data=${aData}`;

    switch (aEvent) {
      case "GeckoView:WebExtension:SetTabActive": {
        const { active } = aData;
        lazy.mobileWindowTracker.setTabActive(this.window, active);
        break;
      }
      case "GeckoView:FlushSessionState": {
        if (Services.appinfo.sessionHistoryInParent) {
          if (this.browser && this.browser.frameLoader) {
            this.browser.frameLoader.requestTabStateFlush();
          }
        }
        break;
      }
    }
  }
}

const { debug, warn } = GeckoViewTab.initLogging("GeckoViewTab");
