/* 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': 'unstable'
};

const { Cc, Ci } = require('chrome');
const array = require('../util/array');
const { defer } = require('sdk/core/promise');

const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
                       getService(Ci.nsIWindowWatcher);
const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
                        getService(Ci.nsIAppShellService);
const WM = Cc['@mozilla.org/appshell/window-mediator;1'].
           getService(Ci.nsIWindowMediator);
const io = Cc['@mozilla.org/network/io-service;1'].
           getService(Ci.nsIIOService);
const FM = Cc["@mozilla.org/focus-manager;1"].
              getService(Ci.nsIFocusManager);

const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';

const BROWSER = 'navigator:browser',
      URI_BROWSER = 'chrome://browser/content/browser.xul',
      NAME = '_blank',
      FEATURES = 'chrome,all,dialog=no,non-private';

function isWindowPrivate(win) {
  if (!win)
    return false;

  // if the pbService is undefined, the PrivateBrowsingUtils.jsm is available,
  // and the app is Firefox, then assume per-window private browsing is
  // enabled.
  try {
    return win.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIWebNavigation)
                  .QueryInterface(Ci.nsILoadContext)
                  .usePrivateBrowsing;
  }
  catch(e) {}

  // Sometimes the input is not a nsIDOMWindow.. but it is still a winodw.
  try {
    return !!win.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing;
  }
  catch (e) {}

  return false;
}
exports.isWindowPrivate = isWindowPrivate;

function getMostRecentBrowserWindow() {
  return getMostRecentWindow(BROWSER);
}
exports.getMostRecentBrowserWindow = getMostRecentBrowserWindow;

function getHiddenWindow() {
  return appShellService.hiddenDOMWindow;
}
exports.getHiddenWindow = getHiddenWindow;

function getMostRecentWindow(type) {
  return WM.getMostRecentWindow(type);
}
exports.getMostRecentWindow = getMostRecentWindow;

/**
 * Returns the ID of the window's current inner window.
 */
function getInnerId(window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor).
                getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
};
exports.getInnerId = getInnerId;

/**
 * Returns the ID of the window's outer window.
 */
function getOuterId(window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor).
                getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
};
exports.getOuterId = getOuterId;

/**
 * Returns window by the outer window id.
 */
const getByOuterId = WM.getOuterWindowWithId;
exports.getByOuterId = getByOuterId;

const getByInnerId = WM.getCurrentInnerWindowWithId;
exports.getByInnerId = getByInnerId;

/**
 * Returns `nsIXULWindow` for the given `nsIDOMWindow`.
 */
function getXULWindow(window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIWebNavigation).
    QueryInterface(Ci.nsIDocShellTreeItem).
    treeOwner.QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIXULWindow);
};
exports.getXULWindow = getXULWindow;

function getDOMWindow(xulWindow) {
  return xulWindow.QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIDOMWindow);
}
exports.getDOMWindow = getDOMWindow;

/**
 * Returns `nsIBaseWindow` for the given `nsIDOMWindow`.
 */
function getBaseWindow(window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor).
    getInterface(Ci.nsIWebNavigation).
    QueryInterface(Ci.nsIDocShell).
    QueryInterface(Ci.nsIDocShellTreeItem).
    treeOwner.
    QueryInterface(Ci.nsIBaseWindow);
}
exports.getBaseWindow = getBaseWindow;

/**
 * Returns the `nsIDOMWindow` toplevel window for any child/inner window
 */
function getToplevelWindow(window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIWebNavigation)
               .QueryInterface(Ci.nsIDocShellTreeItem)
               .rootTreeItem
               .QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIDOMWindow);
}
exports.getToplevelWindow = getToplevelWindow;

function getWindowDocShell(window) window.gBrowser.docShell;
exports.getWindowDocShell = getWindowDocShell;

function getWindowLoadingContext(window) {
  return getWindowDocShell(window).
         QueryInterface(Ci.nsILoadContext);
}
exports.getWindowLoadingContext = getWindowLoadingContext;

const isTopLevel = window => window && getToplevelWindow(window) === window;
exports.isTopLevel = isTopLevel;

/**
 * Takes hash of options and serializes it to a features string that
 * can be used passed to `window.open`. For more details on features string see:
 * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features
 */
function serializeFeatures(options) {
  return Object.keys(options).reduce(function(result, name) {
    let value = options[name];

    // the chrome and private features are special
    if ((name == 'private' || name == 'chrome'))
      return result + ((value === true) ? ',' + name : '');

    return result + ',' + name + '=' +
           (value === true ? 'yes' : value === false ? 'no' : value);
  }, '').substr(1);
}

/**
 * Opens a top level window and returns it's `nsIDOMWindow` representation.
 * @params {String} uri
 *    URI of the document to be loaded into window.
 * @params {nsIDOMWindow} options.parent
 *    Used as parent for the created window.
 * @params {String} options.name
 *    Optional name that is assigned to the window.
 * @params {Object} options.features
 *    Map of key, values like: `{ width: 10, height: 15, chrome: true, private: true }`.
 */
function open(uri, options) {
  uri = uri || URI_BROWSER;
  options = options || {};

  if (['chrome', 'resource', 'data'].indexOf(io.newURI(uri, null, null).scheme) < 0)
    throw new Error('only chrome, resource and data uris are allowed');

  let newWindow = windowWatcher.
    openWindow(options.parent || null,
               uri,
               options.name || null,
               options.features ? serializeFeatures(options.features) : null,
               options.args || null);

  return newWindow;
}
exports.open = open;

function onFocus(window) {
  let { resolve, promise } = defer();

  if (isFocused(window)) {
    resolve(window);
  }
  else {
    window.addEventListener("focus", function focusListener() {
      window.removeEventListener("focus", focusListener, true);
      resolve(window);
    }, true);
  }

  return promise;
}
exports.onFocus = onFocus;

function isFocused(window) {
  const FM = Cc["@mozilla.org/focus-manager;1"].
                getService(Ci.nsIFocusManager);

  let childTargetWindow = {};
  FM.getFocusedElementForWindow(window, true, childTargetWindow);
  childTargetWindow = childTargetWindow.value;

  let focusedChildWindow = {};
  if (FM.activeWindow) {
    FM.getFocusedElementForWindow(FM.activeWindow, true, focusedChildWindow);
    focusedChildWindow = focusedChildWindow.value;
  }

  return (focusedChildWindow === childTargetWindow);
}
exports.isFocused = isFocused;

/**
 * Opens a top level window and returns it's `nsIDOMWindow` representation.
 * Same as `open` but with more features
 * @param {Object} options
 *
 */
function openDialog(options) {
  options = options || {};

  let features = options.features || FEATURES;
  let featureAry = features.toLowerCase().split(',');

  if (!!options.private) {
    // add private flag if private window is desired
    if (!array.has(featureAry, 'private')) {
      featureAry.push('private');
    }

    // remove the non-private flag ig a private window is desired
    let nonPrivateIndex = featureAry.indexOf('non-private');
    if (nonPrivateIndex >= 0) {
      featureAry.splice(nonPrivateIndex, 1);
    }

    features = featureAry.join(',');
  }

  let browser = getMostRecentBrowserWindow();

  // if there is no browser then do nothing
  if (!browser)
    return undefined;

  let newWindow = browser.openDialog.apply(
      browser,
      array.flatten([
        options.url || URI_BROWSER,
        options.name || NAME,
        features,
        options.args || null
      ])
  );

  return newWindow;
}
exports.openDialog = openDialog;

/**
 * Returns an array of all currently opened windows.
 * Note that these windows may still be loading.
 */
function windows(type, options) {
  options = options || {};
  let list = [];
  let winEnum = WM.getEnumerator(type);
  while (winEnum.hasMoreElements()) {
    let window = winEnum.getNext().QueryInterface(Ci.nsIDOMWindow);
    // Only add non-private windows when pb permission isn't set,
    // unless an option forces the addition of them.
    if (!window.closed && (options.includePrivate || !isWindowPrivate(window))) {
      list.push(window);
    }
  }
  return list;
}
exports.windows = windows;

/**
 * Check if the given window is interactive.
 * i.e. if its "DOMContentLoaded" event has already been fired.
 * @params {nsIDOMWindow} window
 */
const isInteractive = window =>
  window.document.readyState === "interactive" ||
  isDocumentLoaded(window) ||
  // XUL documents stays '"uninitialized"' until it's `readyState` becomes
  // `"complete"`.
  isXULDocumentWindow(window) && window.document.readyState === "interactive";
exports.isInteractive = isInteractive;

const isXULDocumentWindow = ({document}) =>
  document.documentElement &&
  document.documentElement.namespaceURI === XUL_NS;

/**
 * Check if the given window is completely loaded.
 * i.e. if its "load" event has already been fired and all possible DOM content
 * is done loading (the whole DOM document, images content, ...)
 * @params {nsIDOMWindow} window
 */
function isDocumentLoaded(window) {
  return window.document.readyState == "complete";
}
exports.isDocumentLoaded = isDocumentLoaded;

function isBrowser(window) {
  try {
    return window.document.documentElement.getAttribute("windowtype") === BROWSER;
  }
  catch (e) {}
  return false;
};
exports.isBrowser = isBrowser;

function getWindowTitle(window) {
  return window && window.document ? window.document.title : null;
}
exports.getWindowTitle = getWindowTitle;

function isXULBrowser(window) {
  return !!(isBrowser(window) && window.XULBrowserWindow);
}
exports.isXULBrowser = isXULBrowser;

/**
 * Returns the most recent focused window
 */
function getFocusedWindow() {
  let window = WM.getMostRecentWindow(BROWSER);

  return window ? window.document.commandDispatcher.focusedWindow : null;
}
exports.getFocusedWindow = getFocusedWindow;

/**
 * Returns the focused browser window if any, or the most recent one.
 * Opening new window, updates most recent window, but focus window
 * changes later; so most recent window and focused window are not always
 * the same.
 */
function getFocusedBrowser() {
  let window = FM.activeWindow;
  return isBrowser(window) ? window : getMostRecentBrowserWindow()
}
exports.getFocusedBrowser = getFocusedBrowser;

/**
 * Returns the focused element in the most recent focused window
 */
function getFocusedElement() {
  let window = WM.getMostRecentWindow(BROWSER);

  return window ? window.document.commandDispatcher.focusedElement : null;
}
exports.getFocusedElement = getFocusedElement;

function getFrames(window) {
  return Array.slice(window.frames).reduce(function(frames, frame) {
    return frames.concat(frame, getFrames(frame));
  }, []);
}
exports.getFrames = getFrames;

function getScreenPixelsPerCSSPixel(window) {
  return window.QueryInterface(Ci.nsIInterfaceRequestor).
                getInterface(Ci.nsIDOMWindowUtils).screenPixelsPerCSSPixel;
}
exports.getScreenPixelsPerCSSPixel = getScreenPixelsPerCSSPixel;

function getOwnerBrowserWindow(node) {
  /**
  Takes DOM node and returns browser window that contains it.
  **/
  let window = getToplevelWindow(node.ownerDocument.defaultView);
  // If anchored window is browser then it's target browser window.
  return isBrowser(window) ? window : null;
}
exports.getOwnerBrowserWindow = getOwnerBrowserWindow;

function getParentWindow(window) {
  try {
    return window.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebNavigation)
      .QueryInterface(Ci.nsIDocShellTreeItem).parent
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIDOMWindow);
  }
  catch (e) {}
  return null;
}
exports.getParentWindow = getParentWindow;


function getParentFrame(window) {
  try {
    return window.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebNavigation)
      .QueryInterface(Ci.nsIDocShellTreeItem).parent
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIDOMWindow);
  }
  catch (e) {}
  return null;
}
exports.getParentWindow = getParentWindow;

// The element in which the window is embedded, or `null`
// if the window is top-level. Similar to `window.frameElement`
// but can cross chrome-content boundries.
const getFrameElement = target =>
  (target instanceof Ci.nsIDOMDocument ? target.defaultView : target).
  QueryInterface(Ci.nsIInterfaceRequestor).
  getInterface(Ci.nsIDOMWindowUtils).
  containerElement;
exports.getFrameElement = getFrameElement;
