/* 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 { Class } = require('../core/heritage');
const { observer } = require('./observer');
const { isBrowser, getMostRecentBrowserWindow, windows, open, getInnerId,
        getWindowTitle, isFocused, isWindowPrivate } = require('../window/utils');
const { List, addListItem, removeListItem } = require('../util/list');
const { viewFor } = require('../view/core');
const { modelFor } = require('../model/core');
const { emit, emitOnObject, setListeners } = require('../event/core');
const { once } = require('../dom/events');
const { EventTarget } = require('../event/target');
const { getSelectedTab } = require('../tabs/utils');
const { Cc, Ci } = require('chrome');
const { Options } = require('../tabs/common');
const system = require('../system/events');
const { ignoreWindow, isPrivate, isWindowPBSupported } = require('../private-browsing/utils');
const { data, isPrivateBrowsingSupported } = require('../self');
const { setImmediate } = require('../timers');

const supportPrivateWindows = isPrivateBrowsingSupported && isWindowPBSupported;

const modelsFor = new WeakMap();
const viewsFor = new WeakMap();

const Window = Class({
  implements: [EventTarget],
  initialize: function(domWindow) {
    modelsFor.set(domWindow, this);
    viewsFor.set(this, domWindow);
  },

  get title() {
    return getWindowTitle(viewsFor.get(this));
  },

  activate: function() {
    viewsFor.get(this).focus();
  },

  close: function(callback) {
    let domWindow = viewsFor.get(this);

    if (callback) {
      // We want to catch the close event immediately after the close events are
      // emitted everywhere but without letting the event loop spin. Registering
      // for the same events as windowEventListener but afterwards does this
      let listener = (event, closedWin) => {
        if (event != "close" || closedWin != domWindow)
          return;

        observer.off("*", listener);
        callback();
      }

      observer.on("*", listener);
    }

    domWindow.close();
  }
});

const windowTabs = new WeakMap();

const BrowserWindow = Class({
  extends: Window,

  get tabs() {
    let tabs = windowTabs.get(this);
    if (tabs)
      return tabs;

    return new WindowTabs(this);
  }
});

const WindowTabs = Class({
  implements: [EventTarget],
  extends: List,
  initialize: function(window) {
    List.prototype.initialize.call(this);
    windowTabs.set(window, this);
    viewsFor.set(this, viewsFor.get(window));

    // Make sure the tabs module has loaded and found all existing tabs
    const tabs = require('../tabs');

    for (let tab of tabs) {
      if (tab.window == window)
        addListItem(this, tab);
    }
  },

  get activeTab() {
    return modelFor(getSelectedTab(viewsFor.get(this)));
  },

  open: function(options) {
    options = Options(options);

    let domWindow = viewsFor.get(this);
    let { Tab } = require('../tabs/tab-firefox');

    // The capturing listener will see the TabOpen event before
    // sdk/tabs/observer giving us time to set up the tab and listeners before
    // the real open event is fired
    let listener = event => {
      new Tab(event.target, options);
    };

    once(domWindow, "TabOpen", listener, true);
    domWindow.gBrowser.addTab(options.url);
  }
});

const BrowserWindows = Class({
  implements: [EventTarget],
  extends: List,
  initialize: function() {
    List.prototype.initialize.call(this);
  },

  get activeWindow() {
    let domWindow = getMostRecentBrowserWindow();
    if (ignoreWindow(domWindow))
      return null;
    return modelsFor.get(domWindow);
  },

  open: function(options) {
    if (typeof options == "string")
      options = { url: options };

    let { url, isPrivate } = options;
    if (url)
      url = data.url(url);

    let args = Cc["@mozilla.org/supports-string;1"].
               createInstance(Ci.nsISupportsString);
    args.data = url;

    let features = {
      chrome: true,
      all: true,
      dialog: false
    };
    features.private = supportPrivateWindows && isPrivate;

    let domWindow = open(null, {
      parent: null,
      name: "_blank",
      features,
      args
    })

    let window = makeNewWindow(domWindow, true);
    setListeners(window, options);
    return window;
  }
});

const browserWindows = new BrowserWindows();
exports.browserWindows = browserWindows;

function windowEmit(window, event, ...args) {
  if (window instanceof BrowserWindow && (event == "open" || event == "close"))
    emitOnObject(window, event, browserWindows, window, ...args);
  else
    emit(window, event, window, ...args);

  if (window instanceof BrowserWindow)
    emit(browserWindows, event, window, ...args);
}

function makeNewWindow(domWindow, browserHint = false) {
  if (browserHint || isBrowser(domWindow))
    return new BrowserWindow(domWindow);
  else
    return new Window(domWindow);
}

for (let domWindow of windows()) {
  let window = makeNewWindow(domWindow);
  if (window instanceof BrowserWindow)
    addListItem(browserWindows, window);
}

var windowEventListener = (event, domWindow, ...args) => {
  if (ignoreWindow(domWindow))
    return;

  let window = modelsFor.get(domWindow);
  if (!window)
    window = makeNewWindow(domWindow);

  if (isBrowser(domWindow)) {
    if (event == "open")
      addListItem(browserWindows, window);
    else if (event == "close")
      removeListItem(browserWindows, window);
  }

  windowEmit(window, event, ...args);

  // The window object shouldn't be reachable after closed
  if (event == "close") {
    viewsFor.delete(window);
    modelsFor.delete(domWindow);
  }
};
observer.on("*", windowEventListener);

viewFor.define(BrowserWindow, window => {
  return viewsFor.get(window);
})

const isBrowserWindow = (x) => x instanceof BrowserWindow;
isPrivate.when(isBrowserWindow, (w) => isWindowPrivate(viewsFor.get(w)));
isFocused.when(isBrowserWindow, (w) => isFocused(viewsFor.get(w)));
