/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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 EventEmitter = require("devtools/shared/event-emitter");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const Services = require("Services");
const {DOMHelpers} = require("resource://devtools/client/shared/DOMHelpers.jsm");

loader.lazyRequireGetter(this, "system", "devtools/shared/system");

/* A host should always allow this much space for the page to be displayed.
 * There is also a min-height on the browser, but we still don't want to set
 * frame.height to be larger than that, since it can cause problems with
 * resizing the toolbox and panel layout. */
const MIN_PAGE_SIZE = 25;

/**
 * A toolbox host represents an object that contains a toolbox (e.g. the
 * sidebar or a separate window). Any host object should implement the
 * following functions:
 *
 * create() - create the UI and emit a 'ready' event when the UI is ready to use
 * destroy() - destroy the host's UI
 */

exports.Hosts = {
  "bottom": BottomHost,
  "side": SidebarHost,
  "window": WindowHost,
  "custom": CustomHost
};

/**
 * Host object for the dock on the bottom of the browser
 */
function BottomHost(hostTab) {
  this.hostTab = hostTab;

  EventEmitter.decorate(this);
}

BottomHost.prototype = {
  type: "bottom",

  heightPref: "devtools.toolbox.footer.height",

  /**
   * Create a box at the bottom of the host tab.
   */
  create: function () {
    let deferred = defer();

    let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
    let ownerDocument = gBrowser.ownerDocument;
    this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser);

    this._splitter = ownerDocument.createElement("splitter");
    this._splitter.setAttribute("class", "devtools-horizontal-splitter");
    // Avoid resizing notification containers
    this._splitter.setAttribute("resizebefore", "flex");

    this.frame = ownerDocument.createElement("iframe");
    this.frame.className = "devtools-toolbox-bottom-iframe";
    this.frame.height = Math.min(
      Services.prefs.getIntPref(this.heightPref),
      this._nbox.clientHeight - MIN_PAGE_SIZE
    );

    this._nbox.appendChild(this._splitter);
    this._nbox.appendChild(this.frame);

    let frameLoad = () => {
      this.emit("ready", this.frame);
      deferred.resolve(this.frame);
    };

    this.frame.tooltip = "aHTMLTooltip";

    // we have to load something so we can switch documents if we have to
    this.frame.setAttribute("src", "about:blank");

    let domHelper = new DOMHelpers(this.frame.contentWindow);
    domHelper.onceDOMReady(frameLoad);

    focusTab(this.hostTab);

    return deferred.promise;
  },

  /**
   * Raise the host.
   */
  raise: function () {
    focusTab(this.hostTab);
  },

  /**
   * Minimize this host so that only the toolbox tabbar remains visible.
   * @param {Number} height The height to minimize to. Defaults to 0, which
   * means that the toolbox won't be visible at all once minimized.
   */
  minimize: function (height = 0) {
    if (this.isMinimized) {
      return;
    }
    this.isMinimized = true;

    let onTransitionEnd = event => {
      if (event.propertyName !== "margin-bottom") {
        // Ignore transitionend on unrelated properties.
        return;
      }

      this.frame.removeEventListener("transitionend", onTransitionEnd);
      this.emit("minimized");
    };
    this.frame.addEventListener("transitionend", onTransitionEnd);
    this.frame.style.marginBottom = -this.frame.height + height + "px";
    this._splitter.classList.add("disabled");
  },

  /**
   * If the host was minimized before, maximize it again (the host will be
   * maximized to the height it previously had).
   */
  maximize: function () {
    if (!this.isMinimized) {
      return;
    }
    this.isMinimized = false;

    let onTransitionEnd = event => {
      if (event.propertyName !== "margin-bottom") {
        // Ignore transitionend on unrelated properties.
        return;
      }

      this.frame.removeEventListener("transitionend", onTransitionEnd);
      this.emit("maximized");
    };
    this.frame.addEventListener("transitionend", onTransitionEnd);
    this.frame.style.marginBottom = "0";
    this._splitter.classList.remove("disabled");
  },

  /**
   * Toggle the minimize mode.
   * @param {Number} minHeight The height to minimize to.
   */
  toggleMinimizeMode: function (minHeight) {
    this.isMinimized ? this.maximize() : this.minimize(minHeight);
  },

  /**
   * Set the toolbox title.
   * Nothing to do for this host type.
   */
  setTitle: function () {},

  /**
   * Destroy the bottom dock.
   */
  destroy: function () {
    if (!this._destroyed) {
      this._destroyed = true;

      Services.prefs.setIntPref(this.heightPref, this.frame.height);
      this._nbox.removeChild(this._splitter);
      this._nbox.removeChild(this.frame);
      this.frame = null;
      this._nbox = null;
      this._splitter = null;
    }

    return promise.resolve(null);
  }
};

/**
 * Host object for the in-browser sidebar
 */
function SidebarHost(hostTab) {
  this.hostTab = hostTab;

  EventEmitter.decorate(this);
}

SidebarHost.prototype = {
  type: "side",

  widthPref: "devtools.toolbox.sidebar.width",

  /**
   * Create a box in the sidebar of the host tab.
   */
  create: function () {
    let deferred = defer();

    let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser;
    let ownerDocument = gBrowser.ownerDocument;
    this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser);

    this._splitter = ownerDocument.createElement("splitter");
    this._splitter.setAttribute("class", "devtools-side-splitter");

    this.frame = ownerDocument.createElement("iframe");
    this.frame.className = "devtools-toolbox-side-iframe";

    this.frame.width = Math.min(
      Services.prefs.getIntPref(this.widthPref),
      this._sidebar.clientWidth - MIN_PAGE_SIZE
    );

    this._sidebar.appendChild(this._splitter);
    this._sidebar.appendChild(this.frame);

    let frameLoad = () => {
      this.emit("ready", this.frame);
      deferred.resolve(this.frame);
    };

    this.frame.tooltip = "aHTMLTooltip";
    this.frame.setAttribute("src", "about:blank");

    let domHelper = new DOMHelpers(this.frame.contentWindow);
    domHelper.onceDOMReady(frameLoad);

    focusTab(this.hostTab);

    return deferred.promise;
  },

  /**
   * Raise the host.
   */
  raise: function () {
    focusTab(this.hostTab);
  },

  /**
   * Set the toolbox title.
   * Nothing to do for this host type.
   */
  setTitle: function () {},

  /**
   * Destroy the sidebar.
   */
  destroy: function () {
    if (!this._destroyed) {
      this._destroyed = true;

      Services.prefs.setIntPref(this.widthPref, this.frame.width);
      this._sidebar.removeChild(this._splitter);
      this._sidebar.removeChild(this.frame);
    }

    return promise.resolve(null);
  }
};

/**
 * Host object for the toolbox in a separate window
 */
function WindowHost() {
  this._boundUnload = this._boundUnload.bind(this);

  EventEmitter.decorate(this);
}

WindowHost.prototype = {
  type: "window",

  WINDOW_URL: "chrome://devtools/content/framework/toolbox-window.xul",

  /**
   * Create a new xul window to contain the toolbox.
   */
  create: function () {
    let deferred = defer();

    let flags = "chrome,centerscreen,resizable,dialog=no";
    let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank",
                                     flags, null);

    let frameLoad = () => {
      win.removeEventListener("load", frameLoad, true);
      win.focus();

      let key;
      if (system.constants.platform === "macosx") {
        key = win.document.getElementById("toolbox-key-toggle-osx");
      } else {
        key = win.document.getElementById("toolbox-key-toggle");
      }
      key.removeAttribute("disabled");

      this.frame = win.document.getElementById("toolbox-iframe");
      this.emit("ready", this.frame);

      deferred.resolve(this.frame);
    };

    win.addEventListener("load", frameLoad, true);
    win.addEventListener("unload", this._boundUnload);

    this._window = win;

    return deferred.promise;
  },

  /**
   * Catch the user closing the window.
   */
  _boundUnload: function (event) {
    if (event.target.location != this.WINDOW_URL) {
      return;
    }
    this._window.removeEventListener("unload", this._boundUnload);

    this.emit("window-closed");
  },

  /**
   * Raise the host.
   */
  raise: function () {
    this._window.focus();
  },

  /**
   * Set the toolbox title.
   */
  setTitle: function (title) {
    this._window.document.title = title;
  },

  /**
   * Destroy the window.
   */
  destroy: function () {
    if (!this._destroyed) {
      this._destroyed = true;

      this._window.removeEventListener("unload", this._boundUnload);
      this._window.close();
    }

    return promise.resolve(null);
  }
};

/**
 * Host object for the toolbox in its own tab
 */
function CustomHost(hostTab, options) {
  this.frame = options.customIframe;
  this.uid = options.uid;
  EventEmitter.decorate(this);
}

CustomHost.prototype = {
  type: "custom",

  _sendMessageToTopWindow: function (msg, data) {
    // It's up to the custom frame owner (parent window) to honor
    // "close" or "raise" instructions.
    let topWindow = this.frame.ownerDocument.defaultView;
    if (!topWindow) {
      return;
    }
    let json = {name: "toolbox-" + msg, uid: this.uid};
    if (data) {
      json.data = data;
    }
    topWindow.postMessage(JSON.stringify(json), "*");
  },

  /**
   * Create a new xul window to contain the toolbox.
   */
  create: function () {
    return promise.resolve(this.frame);
  },

  /**
   * Raise the host.
   */
  raise: function () {
    this._sendMessageToTopWindow("raise");
  },

  /**
   * Set the toolbox title.
   */
  setTitle: function (title) {
    this._sendMessageToTopWindow("title", { value: title });
  },

  /**
   * Destroy the window.
   */
  destroy: function () {
    if (!this._destroyed) {
      this._destroyed = true;
      this._sendMessageToTopWindow("close");
    }
    return promise.resolve(null);
  }
};

/**
 *  Switch to the given tab in a browser and focus the browser window
 */
function focusTab(tab) {
  let browserWindow = tab.ownerDocument.defaultView;
  browserWindow.focus();
  browserWindow.gBrowser.selectedTab = tab;
}