/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/ExtensionParent.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                  "resource:///modules/E10SUtils.jsm");

const {
  watchExtensionProxyContextLoad,
} = ExtensionParent;

const {
  IconDetails,
  promiseEvent,
} = ExtensionUtils;

const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

/**
 * Represents an addon devtools panel in the main process.
 *
 * @param {ExtensionChildProxyContext} context
 *        A devtools extension proxy context running in a main process.
 * @param {object} options
 * @param {string} options.id
 *        The id of the addon devtools panel.
 * @param {string} options.icon
 *        The icon of the addon devtools panel.
 * @param {string} options.title
 *        The title of the addon devtools panel.
 * @param {string} options.url
 *        The url of the addon devtools panel, relative to the extension base URL.
 */
class ParentDevToolsPanel {
  constructor(context, panelOptions) {
    const toolbox = context.devToolsToolbox;
    if (!toolbox) {
      // This should never happen when this constructor is called with a valid
      // devtools extension context.
      throw Error("Missing mandatory toolbox");
    }

    this.extension = context.extension;
    this.viewType = "devtools_panel";

    this.visible = false;
    this.toolbox = toolbox;

    this.context = context;

    this.panelOptions = panelOptions;

    this.context.callOnClose(this);

    this.id = this.panelOptions.id;

    this.onToolboxPanelSelect = this.onToolboxPanelSelect.bind(this);
    this.onToolboxReady = this.onToolboxReady.bind(this);

    this.panelAdded = false;

    if (this.toolbox.isReady) {
      this.onToolboxReady();
    } else {
      this.toolbox.once("ready", this.onToolboxReady);
    }

    this.waitTopLevelContext = new Promise(resolve => {
      this._resolveTopLevelContext = resolve;
    });
  }

  addPanel() {
    const {icon, title} = this.panelOptions;
    const extensionName = this.context.extension.name;

    this.toolbox.addAdditionalTool({
      id: this.id,
      url: "about:blank",
      icon: icon,
      label: title,
      tooltip: `DevTools Panel added by "${extensionName}" add-on.`,
      invertIconForLightTheme: true,
      visibilityswitch:  `devtools.webext-${this.id}.enabled`,
      isTargetSupported: target => target.isLocalTab,
      build: (window, toolbox) => {
        if (toolbox !== this.toolbox) {
          throw new Error("Unexpected toolbox received on addAdditionalTool build property");
        }

        const destroy = this.buildPanel(window, toolbox);

        return {toolbox, destroy};
      },
    });
  }

  buildPanel(window, toolbox) {
    const {url} = this.panelOptions;
    const {document} = window;

    const browser = document.createElementNS(XUL_NS, "browser");
    browser.setAttribute("type", "content");
    browser.setAttribute("disableglobalhistory", "true");
    browser.setAttribute("style", "width: 100%; height: 100%;");
    browser.setAttribute("transparent", "true");
    browser.setAttribute("class", "webextension-devtoolsPanel-browser");
    browser.setAttribute("webextension-view-type", "devtools_panel");
    browser.setAttribute("flex", "1");

    this.browser = browser;

    const {extension} = this.context;

    let awaitFrameLoader = Promise.resolve();
    if (extension.remote) {
      browser.setAttribute("remote", "true");
      browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
      awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
    } else if (!AppConstants.RELEASE_OR_BETA) {
      // NOTE: Using a content iframe here breaks the devtools panel
      // switching between docked and undocked mode,
      // because of a swapFrameLoader exception (see bug 1075490).
      browser.setAttribute("type", "chrome");
      browser.setAttribute("forcemessagemanager", true);
    }

    let hasTopLevelContext = false;

    // Listening to new proxy contexts.
    const unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(this, context => {
      // Keep track of the toolbox and target associated to the context, which is
      // needed by the API methods implementation.
      context.devToolsToolbox = toolbox;

      if (!hasTopLevelContext) {
        hasTopLevelContext = true;

        // Resolve the promise when the root devtools_panel context has been created.
        awaitFrameLoader.then(() => this._resolveTopLevelContext(context));
      }
    });

    document.body.setAttribute("style", "margin: 0; padding: 0;");
    document.body.appendChild(browser);

    extensions.emit("extension-browser-inserted", browser, {
      devtoolsToolboxInfo: {
        toolboxPanelId: this.id,
        inspectedWindowTabId: getTargetTabIdForToolbox(toolbox),
      },
    });

    browser.loadURI(url);

    toolbox.on("select", this.onToolboxPanelSelect);

    // Return a cleanup method that is when the panel is destroyed, e.g.
    // - when addon devtool panel has been disabled by the user from the toolbox preferences,
    //   its ParentDevToolsPanel instance is still valid, but the built devtools panel is removed from
    //   the toolbox (and re-built again if the user re-enable it from the toolbox preferences panel)
    // - when the creator context has been destroyed, the ParentDevToolsPanel close method is called,
    //   it remove the tool definition from the toolbox, which will call this destroy method.
    return () => {
      unwatchExtensionProxyContextLoad();
      browser.remove();
      toolbox.off("select", this.onToolboxPanelSelect);

      // If the panel has been disabled from the toolbox preferences,
      // we need to re-initialize the waitTopLevelContext Promise.
      this.waitTopLevelContext = new Promise(resolve => {
        this._resolveTopLevelContext = resolve;
      });
    };
  }

  onToolboxReady() {
    if (!this.panelAdded) {
      this.panelAdded = true;
      this.addPanel();
    }
  }

  onToolboxPanelSelect(what, id) {
    if (!this.waitTopLevelContext || !this.panelAdded) {
      return;
    }
    if (!this.visible && id === this.id) {
      // Wait that the panel is fully loaded and emit show.
      this.waitTopLevelContext.then(() => {
        this.visible = true;
        this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelShown", {
          toolboxPanelId: this.id,
        });
      });
    } else if (this.visible && id !== this.id) {
      this.visible = false;
      this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelHidden", {
        toolboxPanelId: this.id,
      });
    }
  }

  close() {
    const {toolbox} = this;

    if (!toolbox) {
      throw new Error("Unable to destroy a closed devtools panel");
    }

    toolbox.off("ready", this.onToolboxReady);

    // Explicitly remove the panel if it is registered and the toolbox is not
    // closing itself.
    if (toolbox.isToolRegistered(this.id) && !toolbox._destroyer) {
      toolbox.removeAdditionalTool(this.id);
    }

    this.context = null;
    this.toolbox = null;
    this.waitTopLevelContext = null;
    this._resolveTopLevelContext = null;
  }
}

extensions.registerSchemaAPI("devtools.panels", "devtools_parent", context => {
  // An incremental "per context" id used in the generated devtools panel id.
  let nextPanelId = 0;

  return {
    devtools: {
      panels: {
        create(title, icon, url) {
          // Get a fallback icon from the manifest data.
          if (icon === "" && context.extension.manifest.icons) {
            const iconInfo = IconDetails.getPreferredIcon(context.extension.manifest.icons,
                                                          context.extension, 128);
            icon = iconInfo ? iconInfo.icon : "";
          }

          icon = context.extension.baseURI.resolve(icon);
          url = context.extension.baseURI.resolve(url);

          const baseId = `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
          const id = `${makeWidgetId(baseId)}-devtools-panel`;

          new ParentDevToolsPanel(context, {title, icon, url, id});

          // Resolved to the devtools panel id into the child addon process,
          // where it will be used to identify the messages related
          // to the panel API onShown/onHidden events.
          return Promise.resolve(id);
        },
      },
    },
  };
});
