/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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/. */

"use strict";

var { ExtensionSupport } = ChromeUtils.import(
  "resource:///modules/ExtensionSupport.jsm"
);
var { ExtensionUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionUtils.sys.mjs"
);

var { getUniqueId } = ExtensionUtils;

let scripts = new Set();

ExtensionSupport.registerWindowListener("ext-composeScripts", {
  chromeURLs: [
    "chrome://messenger/content/messengercompose/messengercompose.xhtml",
  ],
  onLoadWindow: async win => {
    await new Promise(resolve =>
      win.addEventListener("compose-editor-ready", resolve, { once: true })
    );
    for (let script of scripts) {
      if (script.type == "compose") {
        script.executeInWindow(
          win,
          script.extension.tabManager.getWrapper(win)
        );
      }
    }
  },
});

ExtensionSupport.registerWindowListener("ext-messageDisplayScripts", {
  chromeURLs: [
    "chrome://messenger/content/messageWindow.xhtml",
    "chrome://messenger/content/messenger.xhtml",
  ],
  onLoadWindow(win) {
    win.addEventListener("MsgLoaded", event => {
      // `event.target` is an about:message window.
      let nativeTab = event.target.tabOrWindow;
      for (let script of scripts) {
        if (script.type == "messageDisplay") {
          script.executeInWindow(
            win,
            script.extension.tabManager.wrapTab(nativeTab)
          );
        }
      }
    });
  },
});

/**
 * Represents (in the main browser process) a script registered
 * programmatically (instead of being included in the addon manifest).
 *
 * @param {ProxyContextParent} context
 *        The parent proxy context related to the extension context which
 *        has registered the script.
 * @param {RegisteredScriptOptions} details
 *        The options object related to the registered script
 *        (which has the properties described in the extensionScripts.json
 *        JSON API schema file).
 */
class ExtensionScriptParent {
  constructor(type, context, details) {
    this.type = type;
    this.context = context;
    this.extension = context.extension;
    this.scriptId = getUniqueId();

    this.options = this._convertOptions(details);
    context.callOnClose(this);

    scripts.add(this);
  }

  close() {
    this.destroy();
  }

  destroy() {
    if (this.destroyed) {
      throw new ExtensionError("Unable to destroy ExtensionScriptParent twice");
    }

    scripts.delete(this);

    this.destroyed = true;
    this.context.forgetOnClose(this);
    this.context = null;
    this.options = null;
  }

  _convertOptions(details) {
    const options = {
      js: [],
      css: [],
    };

    if (details.js && details.js.length) {
      options.js = details.js.map(data => {
        return {
          code: data.code || null,
          file: data.file || null,
        };
      });
    }

    if (details.css && details.css.length) {
      options.css = details.css.map(data => {
        return {
          code: data.code || null,
          file: data.file || null,
        };
      });
    }

    return options;
  }

  async executeInWindow(window, tab) {
    for (let css of this.options.css) {
      await tab.insertCSS(this.context, { ...css, frameId: null });
    }
    for (let js of this.options.js) {
      await tab.executeScript(this.context, { ...js, frameId: null });
    }
    window.dispatchEvent(new window.CustomEvent("extension-scripts-added"));
  }
}

this.extensionScripts = class extends ExtensionAPI {
  getAPI(context) {
    // Map of the script registered from the extension context.
    //
    // Map<scriptId -> ExtensionScriptParent>
    const parentScriptsMap = new Map();

    // Unregister all the scriptId related to a context when it is closed.
    context.callOnClose({
      close() {
        for (let script of parentScriptsMap.values()) {
          script.destroy();
        }
        parentScriptsMap.clear();
      },
    });

    return {
      extensionScripts: {
        async register(type, details) {
          const script = new ExtensionScriptParent(type, context, details);
          const { scriptId } = script;

          parentScriptsMap.set(scriptId, script);
          return scriptId;
        },

        // This method is not available to the extension code, the extension code
        // doesn't have access to the internally used scriptId, on the contrary
        // the extension code will call script.unregister on the script API object
        // that is resolved from the register API method returned promise.
        async unregister(scriptId) {
          const script = parentScriptsMap.get(scriptId);
          if (!script) {
            console.error(new ExtensionError(`No such script ID: ${scriptId}`));

            return;
          }

          parentScriptsMap.delete(scriptId);
          script.destroy();
        },
      },
    };
  }
};
