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

module.metadata = {
  "stability": "experimental"
};

const { Cu } = require("chrome");
const { Class } = require("../sdk/core/heritage");
const { curry } = require("../sdk/lang/functional");
const { EventTarget } = require("../sdk/event/target");
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
const { emit, off, setListeners } = require("../sdk/event/core");
const { when } = require("../sdk/event/utils");
const { getFrameElement } = require("../sdk/window/utils");
const { contract, validate } = require("../sdk/util/contract");
const { data: { url: resolve }} = require("../sdk/self");
const { identify } = require("../sdk/ui/id");
const { isLocalURL, URL } = require("../sdk/url");
const { encode } = require("../sdk/base64");
const { marshal, demarshal } = require("./ports");
const { fromTarget } = require("./debuggee");
const { removed } = require("../sdk/dom/events");
const { id: addonID } = require("../sdk/self");
const { viewFor } = require("../sdk/view/core");
const { createView } = require("./panel/view");

const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
const FRAME_SCRIPT = module.uri.replace("/panel.js", "/frame-script.js");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const HTML_NS = "http://www.w3.org/1999/xhtml";

const makeID = name =>
  ("dev-panel-" + addonID + "-" + name).
  split("/").join("-").
  split(".").join("-").
  split(" ").join("-").
  replace(/[^A-Za-z0-9_\-]/g, "");


// Weak mapping between `Panel` instances and their frame's
// `nsIMessageManager`.
const managers = new WeakMap();
// Return `nsIMessageManager` for the given `Panel` instance.
const managerFor = x => managers.get(x);

// Weak mappinging between iframe's and their owner
// `Panel` instances.
const panels = new WeakMap();
const panelFor = frame => panels.get(frame);

// Weak mapping between panels and debugees they're targeting.
const debuggees = new WeakMap();
const debuggeeFor = panel => debuggees.get(panel);

const frames = new WeakMap();
const frameFor = panel => frames.get(panel);

const setAttributes = (node, attributes) => {
  for (var key in attributes)
    node.setAttribute(key, attributes[key]);
};

const onStateChange = ({target, data}) => {
  const panel = panelFor(target);
  panel.readyState = data.readyState;
  emit(panel, data.type, { target: panel, type: data.type });
};

// port event listener on the message manager that demarshalls
// and forwards to the actual receiver. This is a workaround
// until Bug 914974 is fixed.
const onPortMessage = ({data, target}) => {
  const port = demarshal(target, data.port);
  if (port)
    port.postMessage(data.message);
};

// When frame is removed from the toolbox destroy panel
// associated with it to release all the resources.
const onFrameRemove = frame => {
  panelFor(frame).destroy();
};

const onFrameInited = frame => {
  frame.style.visibility = "visible";
}

const inited = frame => new Promise(resolve => {
  const { messageManager } = frame.frameLoader;
  const listener = message => {
    messageManager.removeMessageListener("sdk/event/ready", listener);
    resolve(frame);
  };
  messageManager.addMessageListener("sdk/event/ready", listener);
});

const getTarget = ({target}) => target;

const Panel = Class({
  extends: Disposable,
  implements: [EventTarget],
  get id() {
    return makeID(this.name || this.label);
  },
  readyState: "uninitialized",
  ready: function() {
    const { readyState } = this;
    const isReady = readyState === "complete" ||
                    readyState === "interactive";
    return isReady ? Promise.resolve(this) :
           when(this, "ready").then(getTarget);
  },
  loaded: function() {
    const { readyState } = this;
    const isLoaded = readyState === "complete";
    return isLoaded ? Promise.resolve(this) :
           when(this, "load").then(getTarget);
  },
  unloaded: function() {
    const { readyState } = this;
    const isUninitialized = readyState === "uninitialized";
    return isUninitialized ? Promise.resolve(this) :
           when(this, "unload").then(getTarget);
  },
  postMessage: function(data, ports=[]) {
    const manager = managerFor(this);
    manager.sendAsyncMessage("sdk/event/message", {
      type: "message",
      bubbles: false,
      cancelable: false,
      data: data,
      origin: this.url,
      ports: ports.map(marshal(manager))
    });
  }
});
exports.Panel = Panel;

validate.define(Panel, contract({
  label: {
    is: ["string"],
    msg: "The `option.label` must be a provided"
  },
  tooltip: {
    is: ["string", "undefined"],
    msg: "The `option.tooltip` must be a string"
  },
  icon: {
    is: ["string"],
    map: x => x && resolve(x),
    ok: x => isLocalURL(x),
    msg: "The `options.icon` must be a valid local URI."
  },
  url: {
    map: x => resolve(x.toString()),
    is: ["string"],
    ok: x => isLocalURL(x),
    msg: "The `options.url` must be a valid local URI."
  },
  invertIconForLightTheme: {
    is: ["boolean", "undefined"],
    msg: "The `options.invertIconForLightTheme` must be a boolean."
  },
  invertIconForDarkTheme: {
    is: ["boolean", "undefined"],
    msg: "The `options.invertIconForDarkTheme` must be a boolean."
  }
}));

setup.define(Panel, (panel, {window, toolbox, url}) => {
  // Hack: Given that iframe created by devtools API is no good for us,
  // we obtain original iframe and replace it with the one that has
  // desired configuration.
  const original = getFrameElement(window);
  const container = original.parentNode;
  original.remove();
  const frame = createView(panel, container.ownerDocument);

  // Following modifications are a temporary workaround until Bug 1049188
  // is fixed.
  // Enforce certain iframe customizations regardless of users request.
  setAttributes(frame, {
    "id": original.id,
    "src": url,
    "flex": 1,
    "forceOwnRefreshDriver": "",
    "tooltip": "aHTMLTooltip"
  });
  frame.style.visibility = "hidden";
  frame.classList.add("toolbox-panel-iframe");
  // Inject iframe into designated node until add-on author decides
  // to inject it elsewhere instead.
  if (!frame.parentNode)
    container.appendChild(frame);

  // associate view with a panel
  frames.set(panel, frame);

  // associate panel model with a frame view.
  panels.set(frame, panel);

  const debuggee = fromTarget(toolbox.target);
  // associate debuggee with a panel.
  debuggees.set(panel, debuggee);


  // Setup listeners for the frame message manager.
  const { messageManager } = frame.frameLoader;
  messageManager.addMessageListener("sdk/event/ready", onStateChange);
  messageManager.addMessageListener("sdk/event/load", onStateChange);
  messageManager.addMessageListener("sdk/event/unload", onStateChange);
  messageManager.addMessageListener("sdk/port/message", onPortMessage);
  messageManager.loadFrameScript(FRAME_SCRIPT, false);

  managers.set(panel, messageManager);

  // destroy panel if frame is removed.
  removed(frame).then(onFrameRemove);
  // show frame when it is initialized.
  inited(frame).then(onFrameInited);


  // set listeners if there are ones defined on the prototype.
  setListeners(panel, Object.getPrototypeOf(panel));


  panel.setup({ debuggee: debuggee });
});

createView.define(Panel, (panel, document) => {
  const frame = document.createElement("iframe");
  setAttributes(frame, {
    "sandbox": "allow-scripts",
    // We end up using chrome iframe with forced message manager
    // as fixing a swapFrameLoader seemed like a giant task (see
    // Bug 1075490).
    "type": "chrome",
    "forcemessagemanager": true,
    "transparent": true,
    "seamless": "seamless",
  });
  return frame;
});

dispose.define(Panel, function(panel) {
  debuggeeFor(panel).close();

  debuggees.delete(panel);
  managers.delete(panel);
  frames.delete(panel);
  panel.readyState = "destroyed";
  panel.dispose();
});

viewFor.define(Panel, frameFor);