/* 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 { observer: windowObserver } = require('../windows/observer');
const { addListItem, removeListItem } = require('../util/list');
const { viewFor } = require('../view/core');
const { modelFor } = require('../model/core');
const { emit, setListeners } = require('../event/core');
const { EventTarget } = require('../event/target');
const { getBrowserForTab, setTabURL, getTabId, getTabURL, getTabForBrowser,
        getTabs, getTabTitle, setTabTitle, getIndex, closeTab, reload, move,
        activateTab, pin, unpin, isTab } = require('./utils');
const { isBrowser, getInnerId, isWindowPrivate } = require('../window/utils');
const { getThumbnailURIForWindow, BLANK } = require("../content/thumbnail");
const { when } = require('../system/unload');
const { ignoreWindow, isPrivate } = require('../private-browsing/utils')
const { defer } = require('../lang/functional');
const { getURL } = require('../url/utils');
const { frames, remoteRequire } = require('../remote/parent');
remoteRequire('sdk/content/tab-events');

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

const tabEvents = {};
exports.tabEvents = tabEvents;

function browser(tab) {
  return getBrowserForTab(viewsFor.get(tab));
}

function isDestroyed(tab) {
  return destroyed.has(tab);
}

function isClosed(tab) {
  if (!viewsFor.has(tab))
    return true;
  return viewsFor.get(tab).closing;
}

// private tab attribute where the remote cached value is stored
const remoteReadyStateCached = Symbol("remoteReadyStateCached");

const Tab = Class({
  implements: [EventTarget],
  initialize: function(tabElement, options = null) {
    modelsFor.set(tabElement, this);
    viewsFor.set(this, tabElement);

    if (options) {
      EventTarget.prototype.initialize.call(this, options);

      if (options.isPinned)
        this.pin();

      // Note that activate is defered and so will run after any open event
      // is sent out
      if (!options.inBackground)
        this.activate();
    }

    getURL.implement(this, tab => tab.url);
    isPrivate.implement(this, tab => {
      return isWindowPrivate(viewsFor.get(tab).ownerDocument.defaultView);
    });
  },

  get id() {
    return isDestroyed(this) ? undefined : getTabId(viewsFor.get(this));
  },

  get title() {
    return isDestroyed(this) ? undefined : getTabTitle(viewsFor.get(this));
  },

  set title(val) {
    if (isDestroyed(this))
      return;

    setTabTitle(viewsFor.get(this), val);
  },

  get url() {
    return isDestroyed(this) ? undefined : getTabURL(viewsFor.get(this));
  },

  set url(val) {
    if (isDestroyed(this))
      return;

    setTabURL(viewsFor.get(this), val);
  },

  get contentType() {
    return isDestroyed(this) ? undefined : browser(this).documentContentType;
  },

  get index() {
    return isDestroyed(this) ? undefined : getIndex(viewsFor.get(this));
  },

  set index(val) {
    if (isDestroyed(this))
      return;

    move(viewsFor.get(this), val);
  },

  get isPinned() {
    return isDestroyed(this) ? undefined : viewsFor.get(this).pinned;
  },

  get window() {
    if (isClosed(this))
      return undefined;

    // TODO: Remove the dependency on the windows module, see bug 792670
    require('../windows');
    let tabElement = viewsFor.get(this);
    let domWindow = tabElement.ownerDocument.defaultView;
    return modelFor(domWindow);
  },

  get readyState() {
    return isDestroyed(this) ? undefined : this[remoteReadyStateCached] || "uninitialized";
  },

  pin: function() {
    if (isDestroyed(this))
      return;

    pin(viewsFor.get(this));
  },

  unpin: function() {
    if (isDestroyed(this))
      return;

    unpin(viewsFor.get(this));
  },

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

    if (isDestroyed(this) || !tabElement || !tabElement.parentNode) {
      if (callback)
        callback();
      return;
    }

    this.once('close', () => {
      this.destroy();
      if (callback)
        callback();
    });

    closeTab(tabElement);
  },

  reload: function() {
    if (isDestroyed(this))
      return;

    reload(viewsFor.get(this));
  },

  activate: defer(function() {
    if (isDestroyed(this))
      return;

    activateTab(viewsFor.get(this));
  }),

  getThumbnail: function() {
    if (isDestroyed(this))
      return BLANK;

    // TODO: This is unimplemented in e10s: bug 1148601
    if (browser(this).isRemoteBrowser) {
      console.error('This method is not supported with E10S');
      return BLANK;
    }
    return getThumbnailURIForWindow(browser(this).contentWindow);
  },

  attach: function(options) {
    if (isDestroyed(this))
      return;

    let { Worker } = require('../content/worker');
    let { connect, makeChildOptions } = require('../content/utils');

    let worker = Worker(options);
    worker.once("detach", () => {
      worker.destroy();
    });

    let attach = frame => {
      let childOptions = makeChildOptions(options);
      frame.port.emit("sdk/tab/attach", childOptions);
      connect(worker, frame, { id: childOptions.id, url: this.url });
    };

    // Do this synchronously if possible
    let frame = frames.getFrameForBrowser(browser(this));
    if (frame) {
      attach(frame);
    }
    else {
      let listener = (frame) => {
        if (frame.frameElement != browser(this))
          return;

        frames.off("attach", listener);
        attach(frame);
      };
      frames.on("attach", listener);
    }

    return worker;
  },

  destroy: function() {
    if (isDestroyed(this))
      return;

    destroyed.set(this, true);
  }
});
exports.Tab = Tab;

viewFor.define(Tab, tab => viewsFor.get(tab));

// Returns the high-level window for this DOM window if the windows module has
// ever been loaded otherwise returns null
function maybeWindowFor(domWindow) {
  try {
    return modelFor(domWindow);
  }
  catch (e) {
    return null;
  }
}

function tabEmit(tab, event, ...args) {
  // Don't emit events for destroyed tabs
  if (isDestroyed(tab))
    return;

  // If the windows module was never loaded this will return null. We don't need
  // to emit to the window.tabs object in this case as nothing can be listening.
  let tabElement = viewsFor.get(tab);
  let window = maybeWindowFor(tabElement.ownerDocument.defaultView);
  if (window)
    emit(window.tabs, event, tab, ...args);

  emit(tabEvents, event, tab, ...args);
  emit(tab, event, tab, ...args);
}

function windowClosed(domWindow) {
  if (!isBrowser(domWindow))
    return;

  for (let tabElement of getTabs(domWindow)) {
    tabEventListener("close", tabElement);
  }
}
windowObserver.on('close', windowClosed);

// Don't want to send close events after unloaded
when(_ => {
  windowObserver.off('close', windowClosed);
});

// Listen for tabbrowser events
function tabEventListener(event, tabElement, ...args) {
  let domWindow = tabElement.ownerDocument.defaultView;

  if (ignoreWindow(domWindow))
    return;

  // Don't send events for tabs that are already closing
  if (event != "close" && (tabElement.closing || !tabElement.parentNode))
    return;

  let tab = modelsFor.get(tabElement);
  if (!tab)
    tab = new Tab(tabElement);

  let window = maybeWindowFor(domWindow);

  if (event == "open") {
    // Note, add to the window tabs first because if this is the first access to
    // window.tabs it will be prefilling itself with everything from tabs
    if (window)
      addListItem(window.tabs, tab);
    // The tabs module will take care of adding to its internal list
  }
  else if (event == "close") {
    if (window)
      removeListItem(window.tabs, tab);
    // The tabs module will take care of removing from its internal list
  }
  else if (event == "init" || event == "create" || event == "ready" || event == "load") {
    // Ignore load events from before browser windows have fully loaded, these
    // are for about:blank in the initial tab
    if (isBrowser(domWindow) && !domWindow.gBrowserInit.delayedStartupFinished)
      return;

    // update the cached remote readyState value
    let { readyState } = args[0] || {};
    tab[remoteReadyStateCached] = readyState;
  }

  if (event == "init") {
    // Do not emit events for the detected existent tabs, we only need to cache
    // their current document.readyState value.
    return;
  }

  tabEmit(tab, event, ...args);

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

// Listen for tab events from content
frames.port.on('sdk/tab/event', (frame, event, ...args) => {
  if (!frame.isTab)
    return;

  let tabElement = getTabForBrowser(frame.frameElement);
  if (!tabElement)
    return;

  tabEventListener(event, tabElement, ...args);
});

// Implement `modelFor` function for the Tab instances..
modelFor.when(isTab, view => {
  return modelsFor.get(view);
});
