/* 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 EXPORTED_SYMBOLS = ["WebNavigation"];

const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");

// TODO:
// Transition types and qualifiers
// onReferenceFragmentUpdated also triggers for pushState
// getFrames, getAllFrames
// onCreatedNavigationTarget, onHistoryStateUpdated

var Manager = {
  listeners: new Map(),

  init() {
    Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
    Services.mm.addMessageListener("Extension:StateChange", this);
    Services.mm.addMessageListener("Extension:LocationChange", this);
    Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
  },

  uninit() {
    Services.mm.removeMessageListener("Extension:StateChange", this);
    Services.mm.removeMessageListener("Extension:LocationChange", this);
    Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
    Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
    Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
  },

  addListener(type, listener) {
    if (this.listeners.size == 0) {
      this.init();
    }

    if (!this.listeners.has(type)) {
      this.listeners.set(type, new Set());
    }
    let listeners = this.listeners.get(type);
    listeners.add(listener);
  },

  removeListener(type, listener) {
    let listeners = this.listeners.get(type);
    if (!listeners) {
      return;
    }
    listeners.delete(listener);
    if (listeners.size == 0) {
      this.listeners.delete(type);
    }

    if (this.listeners.size == 0) {
      this.uninit();
    }
  },

  receiveMessage({name, data, target}) {
    switch (name) {
      case "Extension:StateChange":
        this.onStateChange(target, data);
        break;

      case "Extension:LocationChange":
        this.onLocationChange(target, data);
        break;

      case "Extension:DOMContentLoaded":
        this.onLoad(target, data);
        break;
    }
  },

  onStateChange(browser, data) {
    let stateFlags = data.stateFlags;
    if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
      let url = data.requestURL;
      if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
        this.fire("onBeforeNavigate", browser, data, {url});
      } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
        if (Components.isSuccessCode(data.status)) {
          this.fire("onCompleted", browser, data, {url});
        } else {
          let error = `Error code ${data.status}`;
          this.fire("onErrorOccurred", browser, data, {error, url});
        }
      }
    }
  },

  onLocationChange(browser, data) {
    let url = data.location;

    if (data.isReferenceFragmentUpdated) {
      this.fire("onReferenceFragmentUpdated", browser, data, {url});
    } else if (data.isHistoryStateUpdated) {
      this.fire("onHistoryStateUpdated", browser, data, {url});
    } else {
      this.fire("onCommitted", browser, data, {url});
    }
  },

  onLoad(browser, data) {
    this.fire("onDOMContentLoaded", browser, data, {url: data.url});
  },

  fire(type, browser, data, extra) {
    let listeners = this.listeners.get(type);
    if (!listeners) {
      return;
    }

    let details = {
      browser,
      windowId: data.windowId,
    };

    if (data.parentWindowId) {
      details.parentWindowId = data.parentWindowId;
    }

    for (let prop in extra) {
      details[prop] = extra[prop];
    }

    for (let listener of listeners) {
      listener(details);
    }
  },
};

const EVENTS = [
  "onBeforeNavigate",
  "onCommitted",
  "onDOMContentLoaded",
  "onCompleted",
  "onErrorOccurred",
  "onReferenceFragmentUpdated",
  "onHistoryStateUpdated",
  // "onCreatedNavigationTarget",
];

var WebNavigation = {};

for (let event of EVENTS) {
  WebNavigation[event] = {
    addListener: Manager.addListener.bind(Manager, event),
    removeListener: Manager.removeListener.bind(Manager, event),
  };
}
