/* 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";

const EventEmitter = require("resource://devtools/shared/event-emitter.js");
loader.lazyRequireGetter(
  this,
  "openContentLink",
  "resource://devtools/client/shared/link.js",
  true
);

/**
 * This object represents DOM panel. It's responsibility is to
 * render Document Object Model of the current debugger target.
 */
class DomPanel {
  constructor(iframeWindow, toolbox, commands) {
    this.panelWin = iframeWindow;
    this._toolbox = toolbox;
    this._commands = commands;

    this.onContentMessage = this.onContentMessage.bind(this);
    this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);

    this.pendingRequests = new Map();

    EventEmitter.decorate(this);
  }
  /**
   * Open is effectively an asynchronous constructor.
   *
   * @return object
   *         A promise that is resolved when the DOM panel completes opening.
   */
  async open() {
    // Wait for the retrieval of root object properties before resolving open
    const onGetProperties = new Promise(resolve => {
      this._resolveOpen = resolve;
    });

    await this.initialize();

    await onGetProperties;

    return this;
  }

  // Initialization
  async initialize() {
    this.panelWin.addEventListener(
      "devtools/content/message",
      this.onContentMessage,
      true
    );

    this._toolbox.on("select", this.onPanelVisibilityChange);

    // onTargetAvailable is mandatory when calling watchTargets
    this._onTargetAvailable = () => {};
    this._onTargetSelected = this._onTargetSelected.bind(this);
    await this._commands.targetCommand.watchTargets({
      types: [this._commands.targetCommand.TYPES.FRAME],
      onAvailable: this._onTargetAvailable,
      onSelected: this._onTargetSelected,
    });

    this.onResourceAvailable = this.onResourceAvailable.bind(this);
    await this._commands.resourceCommand.watchResources(
      [this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
      {
        onAvailable: this.onResourceAvailable,
      }
    );

    // Export provider object with useful API for DOM panel.
    const provider = {
      getToolbox: this.getToolbox.bind(this),
      getPrototypeAndProperties: this.getPrototypeAndProperties.bind(this),
      openLink: this.openLink.bind(this),
      // Resolve DomPanel.open once the object properties are fetched
      onPropertiesFetched: () => {
        if (this._resolveOpen) {
          this._resolveOpen();
          this._resolveOpen = null;
        }
      },
    };

    exportIntoContentScope(this.panelWin, provider, "DomProvider");
  }

  destroy() {
    if (this._destroyed) {
      return;
    }
    this._destroyed = true;

    this._commands.targetCommand.unwatchTargets({
      types: [this._commands.targetCommand.TYPES.FRAME],
      onAvailable: this._onTargetAvailable,
      onSelected: this._onTargetSelected,
    });
    this._commands.resourceCommand.unwatchResources(
      [this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
      { onAvailable: this.onResourceAvailable }
    );
    this._toolbox.off("select", this.onPanelVisibilityChange);

    this.emit("destroyed");
  }

  // Events
  refresh() {
    // Do not refresh if the panel isn't visible.
    if (!this.isPanelVisible()) {
      return;
    }

    // Do not refresh if it isn't necessary.
    if (!this.shouldRefresh) {
      return;
    }

    // Alright reset the flag we are about to refresh the panel.
    this.shouldRefresh = false;

    this.getRootGrip().then(rootGrip => {
      this.postContentMessage("initialize", rootGrip);
    });
  }

  /**
   * Make sure the panel is refreshed, either when navigation occurs or when a frame is
   * selected in the iframe picker.
   * The panel is refreshed immediately if it's currently selected or lazily when the user
   * actually selects it.
   */
  forceRefresh() {
    this.shouldRefresh = true;
    // This will end up calling scriptCommand execute method to retrieve the `window` grip
    // on targetCommand.selectedTargetFront.
    this.refresh();
  }

  _onTargetSelected() {
    this.forceRefresh();
  }

  onResourceAvailable(resources) {
    for (const resource of resources) {
      // Only consider top level document, and ignore remote iframes top document
      if (
        resource.resourceType ===
          this._commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
        resource.name === "dom-complete" &&
        resource.targetFront.isTopLevel
      ) {
        this.forceRefresh();
      }
    }
  }

  /**
   * Make sure the panel is refreshed (if needed) when it's selected.
   */
  onPanelVisibilityChange() {
    this.refresh();
  }

  // Helpers
  /**
   * Return true if the DOM panel is currently selected.
   */
  isPanelVisible() {
    return this._toolbox.currentToolId === "dom";
  }

  async getPrototypeAndProperties(objectFront) {
    if (!objectFront.actorID) {
      console.error("No actor!", objectFront);
      throw new Error("Failed to get object front.");
    }

    // Bail out if target doesn't exist (toolbox maybe closed already).
    if (!this.currentTarget) {
      return null;
    }

    // Check for a previously stored request for grip.
    let request = this.pendingRequests.get(objectFront.actorID);

    // If no request is in progress create a new one.
    if (!request) {
      request = objectFront.getPrototypeAndProperties();
      this.pendingRequests.set(objectFront.actorID, request);
    }

    const response = await request;
    this.pendingRequests.delete(objectFront.actorID);

    // Fire an event about not having any pending requests.
    if (!this.pendingRequests.size) {
      this.emit("no-pending-requests");
    }

    return response;
  }

  openLink(url) {
    openContentLink(url);
  }

  async getRootGrip() {
    const { result } = await this._toolbox.commands.scriptCommand.execute(
      "window",
      {
        disableBreaks: true,
      }
    );
    return result;
  }

  postContentMessage(type, args) {
    const data = {
      type,
      args,
    };

    const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
      bubbles: true,
      cancelable: true,
      data,
    });

    this.panelWin.dispatchEvent(event);
  }

  onContentMessage(event) {
    const data = event.data;
    const method = data.type;
    if (typeof this[method] == "function") {
      this[method](data.args);
    }
  }

  getToolbox() {
    return this._toolbox;
  }

  get currentTarget() {
    return this._toolbox.target;
  }
}

// Helpers

function exportIntoContentScope(win, obj, defineAs) {
  const clone = Cu.createObjectIn(win, {
    defineAs,
  });

  const props = Object.getOwnPropertyNames(obj);
  for (let i = 0; i < props.length; i++) {
    const propName = props[i];
    const propValue = obj[propName];
    if (typeof propValue == "function") {
      Cu.exportFunction(propValue, clone, {
        defineAs: propName,
      });
    }
  }
}

// Exports from this module
exports.DomPanel = DomPanel;
