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

// The ext-* files are imported into the same scopes.
/* import-globals-from ../../../toolkit/components/extensions/ext-c-toolkit.js */

ChromeUtils.defineModuleGetter(this, "ExtensionChildDevToolsUtils",
                               "resource://gre/modules/ExtensionChildDevToolsUtils.jsm");

var {
  promiseDocumentLoaded,
} = ExtensionUtils;

/**
 * Represents an addon devtools panel in the child process.
 *
 * @param {DevtoolsExtensionContext}
 *   A devtools extension context running in a child process.
 * @param {object} panelOptions
 * @param {string} panelOptions.id
 *   The id of the addon devtools panel registered in the main process.
 */
class ChildDevToolsPanel extends ExtensionUtils.EventEmitter {
  constructor(context, {id}) {
    super();

    this.context = context;
    this.context.callOnClose(this);

    this.id = id;
    this._panelContext = null;

    this.mm = context.messageManager;
    this.mm.addMessageListener("Extension:DevToolsPanelShown", this);
    this.mm.addMessageListener("Extension:DevToolsPanelHidden", this);
  }

  get panelContext() {
    if (this._panelContext) {
      return this._panelContext;
    }

    for (let view of this.context.extension.devtoolsViews) {
      if (view.viewType === "devtools_panel" &&
          view.devtoolsToolboxInfo.toolboxPanelId === this.id) {
        this._panelContext = view;

        // Reset the cached _panelContext property when the view is closed.
        view.callOnClose({
          close: () => {
            this._panelContext = null;
          },
        });
        return view;
      }
    }

    return null;
  }

  receiveMessage({name, data}) {
    // Filter out any message that is not related to the id of this
    // toolbox panel.
    if (!data || data.toolboxPanelId !== this.id) {
      return;
    }

    switch (name) {
      case "Extension:DevToolsPanelShown":
        // Filter out *Shown message received while the panel context do not yet
        // exist.
        if (!this.panelContext || !this.panelContext.contentWindow) {
          return;
        }
        this.onParentPanelShown();
        break;
      case "Extension:DevToolsPanelHidden":
        this.onParentPanelHidden();
        break;
    }
  }

  onParentPanelShown() {
    const {document} = this.panelContext.contentWindow;

    // Ensure that the onShown event is fired when the panel document has
    // been fully loaded.
    promiseDocumentLoaded(document).then(() => {
      this.emit("shown", this.panelContext.contentWindow);
    });
  }

  onParentPanelHidden() {
    this.emit("hidden");
  }

  api() {
    return {
      onShown: new EventManager(
        this.context, "devtoolsPanel.onShown", fire => {
          const listener = (eventName, panelContentWindow) => {
            fire.asyncWithoutClone(panelContentWindow);
          };
          this.on("shown", listener);
          return () => {
            this.off("shown", listener);
          };
        }).api(),

      onHidden: new EventManager(
        this.context, "devtoolsPanel.onHidden", fire => {
          const listener = () => {
            fire.async();
          };
          this.on("hidden", listener);
          return () => {
            this.off("hidden", listener);
          };
        }).api(),

      // TODO(rpl): onSearch event and createStatusBarButton method
    };
  }

  close() {
    this.mm.removeMessageListener("Extension:DevToolsPanelShown", this);
    this.mm.removeMessageListener("Extension:DevToolsPanelHidden", this);

    this._panelContext = null;
    this.context = null;
  }
}

/**
 * Represents an addon devtools inspector sidebar in the child process.
 *
 * @param {DevtoolsExtensionContext}
 *   A devtools extension context running in a child process.
 * @param {object} sidebarOptions
 * @param {string} sidebarOptions.id
 *   The id of the addon devtools sidebar registered in the main process.
 */
class ChildDevToolsInspectorSidebar extends ExtensionUtils.EventEmitter {
  constructor(context, {id}) {
    super();

    this.context = context;
    this.context.callOnClose(this);

    this.id = id;

    this.mm = context.messageManager;
    this.mm.addMessageListener("Extension:DevToolsInspectorSidebarShown", this);
    this.mm.addMessageListener("Extension:DevToolsInspectorSidebarHidden", this);
  }

  close() {
    this.mm.removeMessageListener("Extension:DevToolsInspectorSidebarShown", this);
    this.mm.removeMessageListener("Extension:DevToolsInspectorSidebarHidden", this);

    this.content = null;
  }

  receiveMessage({name, data}) {
    // Filter out any message that is not related to the id of this
    // toolbox panel.
    if (!data || data.inspectorSidebarId !== this.id) {
      return;
    }

    switch (name) {
      case "Extension:DevToolsInspectorSidebarShown":
        this.onParentSidebarShown();
        break;
      case "Extension:DevToolsInspectorSidebarHidden":
        this.onParentSidebarHidden();
        break;
    }
  }

  onParentSidebarShown() {
    // TODO: wait and emit sidebar contentWindow once sidebar.setPage is supported.
    this.emit("shown");
  }

  onParentSidebarHidden() {
    this.emit("hidden");
  }

  api() {
    const {context, id} = this;

    return {
      onShown: new EventManager(
        context, "devtoolsInspectorSidebar.onShown", fire => {
          const listener = (eventName, panelContentWindow) => {
            fire.asyncWithoutClone(panelContentWindow);
          };
          this.on("shown", listener);
          return () => {
            this.off("shown", listener);
          };
        }).api(),

      onHidden: new EventManager(
        context, "devtoolsInspectorSidebar.onHidden", fire => {
          const listener = () => {
            fire.async();
          };
          this.on("hidden", listener);
          return () => {
            this.off("hidden", listener);
          };
        }).api(),

      setObject(jsonObject, rootTitle) {
        return context.cloneScope.Promise.resolve().then(() => {
          return context.childManager.callParentAsyncFunction(
            "devtools.panels.elements.Sidebar.setObject",
            [id, jsonObject, rootTitle]
          );
        });
      },

      setExpression(evalExpression, rootTitle) {
        return context.cloneScope.Promise.resolve().then(() => {
          return context.childManager.callParentAsyncFunction(
            "devtools.panels.elements.Sidebar.setExpression",
            [id, evalExpression, rootTitle]
          );
        });
      },
    };
  }
}

this.devtools_panels = class extends ExtensionAPI {
  getAPI(context) {
    const themeChangeObserver = ExtensionChildDevToolsUtils.getThemeChangeObserver();

    return {
      devtools: {
        panels: {
          elements: {
            createSidebarPane(title) {
              // NOTE: this is needed to be able to return to the caller (the extension)
              // a promise object that it had the privileges to use (e.g. by marking this
              // method async we will return a promise object which can only be used by
              // chrome privileged code).
              return context.cloneScope.Promise.resolve().then(async () => {
                const sidebarId = await context.childManager.callParentAsyncFunction(
                  "devtools.panels.elements.createSidebarPane", [title]);

                const sidebar = new ChildDevToolsInspectorSidebar(context, {id: sidebarId});

                const sidebarAPI = Cu.cloneInto(sidebar.api(),
                                                context.cloneScope,
                                                {cloneFunctions: true});

                return sidebarAPI;
              });
            },
          },
          create(title, icon, url) {
            // NOTE: this is needed to be able to return to the caller (the extension)
            // a promise object that it had the privileges to use (e.g. by marking this
            // method async we will return a promise object which can only be used by
            // chrome privileged code).
            return context.cloneScope.Promise.resolve().then(async () => {
              const panelId = await context.childManager.callParentAsyncFunction(
                "devtools.panels.create", [title, icon, url]);

              const devtoolsPanel = new ChildDevToolsPanel(context, {id: panelId});

              const devtoolsPanelAPI = Cu.cloneInto(devtoolsPanel.api(),
                                                    context.cloneScope,
                                                    {cloneFunctions: true});
              return devtoolsPanelAPI;
            });
          },
          get themeName() {
            return themeChangeObserver.themeName;
          },
          onThemeChanged: new EventManager(
            context, "devtools.panels.onThemeChanged", fire => {
              const listener = (eventName, themeName) => {
                fire.async(themeName);
              };
              themeChangeObserver.on("themeChanged", listener);
              return () => {
                themeChangeObserver.off("themeChanged", listener);
              };
            }).api(),
        },
      },
    };
  }
};
