/* 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 {
  windowGlobalTargetSpec,
} = require("resource://devtools/shared/specs/targets/window-global.js");
const {
  FrontClassWithSpec,
  registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
  TargetMixin,
} = require("resource://devtools/client/fronts/targets/target-mixin.js");

class WindowGlobalTargetFront extends TargetMixin(
  FrontClassWithSpec(windowGlobalTargetSpec)
) {
  constructor(client, targetFront, parentFront) {
    super(client, targetFront, parentFront);

    // For targets which support the Watcher and configuration actor, the status
    // for the `javascriptEnabled` setting will be available on the configuration
    // front, and the target will only be used to read the initial value from older
    // servers.
    // Note: this property is marked as private but is accessed by the
    // TargetCommand to provide the "isJavascriptEnabled" wrapper. It should NOT be
    // used anywhere else.
    this._javascriptEnabled = null;

    // If this target was retrieved via NodeFront connectToFrame, keep a
    // reference to the parent NodeFront.
    this._parentNodeFront = null;

    this._onTabNavigated = this._onTabNavigated.bind(this);
    this._onFrameUpdate = this._onFrameUpdate.bind(this);

    this.on("tabNavigated", this._onTabNavigated);
    this.on("frameUpdate", this._onFrameUpdate);
  }

  form(json) {
    this.actorID = json.actor;
    this.browsingContextID = json.browsingContextID;
    this.innerWindowId = json.innerWindowId;
    this.processID = json.processID;
    this.isFallbackExtensionDocument = json.isFallbackExtensionDocument;

    // Save the full form for Target class usage.
    // Do not use `form` name to avoid colliding with protocol.js's `form` method
    this.targetForm = json;

    this.outerWindowID = json.outerWindowID;
    this.favicon = json.favicon;

    // Initial value for the page title and url. Since the WindowGlobalTargetActor can
    // be created very early, those might not represent the actual value we'd want to
    // display for the user (e.g. the <title> might not have been parsed yet, and the
    // url could still be about:blank, which is what the platform uses at the very start
    // of a navigation to a new location).
    // Those values are set again  from the targetCommand when receiving DOCUMENT_EVENT
    // resource, at which point both values should be in their expected form.
    this.setTitle(json.title);
    this.setUrl(json.url);
  }

  /**
   * Event listener for `frameUpdate` event.
   */
  _onFrameUpdate(packet) {
    this.emit("frame-update", packet);
  }

  /**
   * Event listener for `tabNavigated` event.
   */
  _onTabNavigated(packet) {
    const event = Object.create(null);
    event.url = packet.url;
    event.title = packet.title;
    event.isFrameSwitching = packet.isFrameSwitching;

    // Keep the title unmodified when a developer toolbox switches frame
    // for a tab (Bug 1261687).
    if (!packet.isFrameSwitching) {
      this.setTitle(packet.title);
      this.setUrl(packet.url);
    }

    // Send any stored event payload (DOMWindow or nsIRequest) for backwards
    // compatibility with non-remotable tools.
    if (packet.state == "start") {
      this.emit("will-navigate", event);
    } else {
      this.emit("navigate", event);
    }
  }

  getParentNodeFront() {
    return this._parentNodeFront;
  }

  setParentNodeFront(nodeFront) {
    this._parentNodeFront = nodeFront;
  }

  /**
   * Set the targetFront url.
   *
   * @param {string} url
   */
  setUrl(url) {
    this._url = url;
  }

  /**
   * Set the targetFront title.
   *
   * @param {string} title
   */
  setTitle(title) {
    this._title = title;
  }

  async detach() {
    // When calling this.destroy() at the end of this method,
    // we will end up calling detach again from TargetMixin.destroy.
    // Avoid invalid loops and do not try to resolve only once the previous call to detach
    // is done as it would do async infinite loop that never resolves.
    if (this._isDetaching) {
      return;
    }
    this._isDetaching = true;

    // Remove listeners set in constructor
    this.off("tabNavigated", this._onTabNavigated);
    this.off("frameUpdate", this._onFrameUpdate);

    try {
      await super.detach();
    } catch (e) {
      this.logDetachError(e, "browsing context");
    }

    // Detach will destroy the target actor, but the server won't emit any
    // target-destroyed-form in such manual, client side destruction.
    // So that we have to manually destroy the associated front on the client
    //
    // If detach was called by TargetFrontMixin.destroy, avoid recalling it from it
    // as it would do an async infinite loop which would never resolve.
    if (!this.isDestroyedOrBeingDestroyed()) {
      this.destroy();
    }
  }

  destroy() {
    const promise = super.destroy();
    this._parentNodeFront = null;

    // As detach isn't necessarily called on target's destroy
    // (it isn't for local tabs), ensure removing listeners set in constructor.
    this.off("tabNavigated", this._onTabNavigated);
    this.off("frameUpdate", this._onFrameUpdate);

    return promise;
  }
}

exports.WindowGlobalTargetFront = WindowGlobalTargetFront;
registerFront(exports.WindowGlobalTargetFront);
