"use strict";

var {classes: Cc, interfaces: Ci, utils: Cu} = Components;

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

XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
                                  "resource://gre/modules/ExtensionManagement.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchURLFilters",
                                  "resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigation",
                                  "resource://gre/modules/WebNavigation.jsm");

Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
  SingletonEventManager,
  ignoreEvent,
} = ExtensionUtils;

const defaultTransitionTypes = {
  topFrame: "link",
  subFrame: "auto_subframe",
};

const frameTransitions = {
  anyFrame: {
    qualifiers: ["server_redirect", "client_redirect", "forward_back"],
  },
  topFrame: {
    types: ["reload", "form_submit"],
  },
};

const tabTransitions = {
  topFrame: {
    qualifiers: ["from_address_bar"],
    types: ["auto_bookmark", "typed", "keyword", "generated", "link"],
  },
  subFrame: {
    types: ["manual_subframe"],
  },
};

function isTopLevelFrame({frameId, parentFrameId}) {
  return frameId == 0 && parentFrameId == -1;
}

function fillTransitionProperties(eventName, src, dst) {
  if (eventName == "onCommitted" || eventName == "onHistoryStateUpdated") {
    let frameTransitionData = src.frameTransitionData || {};
    let tabTransitionData = src.tabTransitionData || {};

    let transitionType, transitionQualifiers = [];

    // Fill transition properties for any frame.
    for (let qualifier of frameTransitions.anyFrame.qualifiers) {
      if (frameTransitionData[qualifier]) {
        transitionQualifiers.push(qualifier);
      }
    }

    if (isTopLevelFrame(dst)) {
      for (let type of frameTransitions.topFrame.types) {
        if (frameTransitionData[type]) {
          transitionType = type;
        }
      }

      for (let qualifier of tabTransitions.topFrame.qualifiers) {
        if (tabTransitionData[qualifier]) {
          transitionQualifiers.push(qualifier);
        }
      }

      for (let type of tabTransitions.topFrame.types) {
        if (tabTransitionData[type]) {
          transitionType = type;
        }
      }

      // If transitionType is not defined, defaults it to "link".
      if (!transitionType) {
        transitionType = defaultTransitionTypes.topFrame;
      }
    } else {
      // If it is sub-frame, transitionType defaults it to "auto_subframe",
      // "manual_subframe" is set only in case of a recent user interaction.
      transitionType = tabTransitionData.link ?
        "manual_subframe" : defaultTransitionTypes.subFrame;
    }

    // Fill the transition properties in the webNavigation event object.
    dst.transitionType = transitionType;
    dst.transitionQualifiers = transitionQualifiers;
  }
}

// Similar to WebRequestEventManager but for WebNavigation.
function WebNavigationEventManager(context, eventName) {
  let name = `webNavigation.${eventName}`;
  let register = (callback, urlFilters) => {
    // Don't create a MatchURLFilters instance if the listener does not include any filter.
    let filters = urlFilters ?
          new MatchURLFilters(urlFilters.url) : null;

    let listener = data => {
      if (!data.browser) {
        return;
      }

      let data2 = {
        url: data.url,
        timeStamp: Date.now(),
        frameId: ExtensionManagement.getFrameId(data.windowId),
        parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
      };

      if (eventName == "onErrorOccurred") {
        data2.error = data.error;
      }

      // Fills in tabId typically.
      extensions.emit("fill-browser-data", data.browser, data2);
      if (data2.tabId < 0) {
        return;
      }

      fillTransitionProperties(eventName, data, data2);

      context.runSafe(callback, data2);
    };

    WebNavigation[eventName].addListener(listener, filters);
    return () => {
      WebNavigation[eventName].removeListener(listener);
    };
  };

  return SingletonEventManager.call(this, context, name, register);
}

WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype);

function convertGetFrameResult(tabId, data) {
  return {
    errorOccurred: data.errorOccurred,
    url: data.url,
    tabId,
    frameId: ExtensionManagement.getFrameId(data.windowId),
    parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
  };
}

extensions.registerSchemaAPI("webNavigation", "addon_parent", context => {
  return {
    webNavigation: {
      onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"),
      onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
      onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
      onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
      onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
      onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
      onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
      onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
      onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
      getAllFrames(details) {
        let tab = TabManager.getTab(details.tabId, context);

        let {innerWindowID, messageManager} = tab.linkedBrowser;
        let recipient = {innerWindowID};

        return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
                      .then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
      },
      getFrame(details) {
        let tab = TabManager.getTab(details.tabId, context);

        let recipient = {
          innerWindowID: tab.linkedBrowser.innerWindowID,
        };

        let mm = tab.linkedBrowser.messageManager;
        return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient})
                      .then((result) => {
                        return result ?
                          convertGetFrameResult(details.tabId, result) :
                          Promise.reject({message: `No frame found with frameId: ${details.frameId}`});
                      });
      },
    },
  };
});