/* 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/. */

 /*
  * ContentAreaObserver manages tracking the viewable area within the browser.
  * It also handles certain tasks like positioning of input elements within
  * content when the viewable area changes.
  *
  * ContentAreaObserver creates styles that content can apply and also fires
  * events when things change. The 'width' and 'height' properties of the
  * styles are updated whenever various parts of the UI are resized.
  *
  *  styles: window-width, window-height
  *  events: SizeChanged
  *
  *  The innerWidth/innerHeight of the main chrome window.
  *
  *  styles: content-width, content-height
  *  events: ContentSizeChanged
  *
  *  The area of the window dedicated to web content; this is equal to the
  *  innerWidth/Height minus any toolbars or similar chrome.
  *
  *  styles: viewable-width, viewable-height
  *  events: ViewableSizeChanged
  *
  *  The portion of the content area that is not obscured by the on-screen
  *  keyboard.
  *
  * ContentAreaObserver also manages soft keyboard related events. Events
  * fired include:
  *
  *  KeyboardChanged - fired after the visibility of the soft keyboard changes
  *  and after any view related changes are made to accomidate view dim
  *  changes as a result.
  *
  *  MozDeckOffsetChanging, MozDeckOffsetChanged - interim events fired when
  *  the browser deck is being repositioned to move focused elements out of
  *  the way of the keyboard.
  */

var ContentAreaObserver = {
  styles: {},
  _shiftAmount: 0,
  _deckTransitioning: false,

  /*
   * Properties
   */

  get width() {
    return window.innerWidth || 1366;
  },

  get height() {
    return window.innerHeight || 768;
  },

  get contentHeight() {
    return this._getContentHeightForWindow(this.height);
  },

  get viewableHeight() {
    return this._getViewableHeightForContent(this.contentHeight);
  },

  get isKeyboardOpened() {
    return Services.metro.keyboardVisible;
  },

  get isKeyboardTransitioning() {
    return this._deckTransitioning;
  },

  get viewstate() {
    if (this.width < Services.prefs.getIntPref("browser.ui.snapped.maxWidth")) {
      return "snapped";
    }
    return (this.height > this.width) ? "portrait" : "landscape";
  },

  /*
   * Public apis
   */

  init: function init() {
    window.addEventListener("resize", this, false);

    // Message manager msgs we listen for
    messageManager.addMessageListener("Content:RepositionInfoResponse", this);

    // Observer msgs we listen for
    Services.obs.addObserver(this, "metro_softkeyboard_shown", false);
    Services.obs.addObserver(this, "metro_softkeyboard_hidden", false);

    // setup initial values for browser form repositioning
    this.shiftBrowserDeck(0);

    // initialize our custom width and height styles
    this._initStyles();

    // apply default styling
    this.update();
  },

  shutdown: function shutdown() {
    messageManager.removeMessageListener("Content:RepositionInfoResponse", this);
    Services.obs.removeObserver(this, "metro_softkeyboard_shown");
    Services.obs.removeObserver(this, "metro_softkeyboard_hidden");
  },

  update: function cao_update (width, height) {
    let oldWidth = parseInt(this.styles["window-width"].width);
    let oldHeight = parseInt(this.styles["window-height"].height);

    let newWidth = width || this.width;
    let newHeight = height || this.height;

    if (newHeight == oldHeight && newWidth == oldWidth) {
      return;
    }

    this.styles["window-width"].width = newWidth + "px";
    this.styles["window-width"].maxWidth = newWidth + "px";
    this.styles["window-height"].height = newHeight + "px";
    this.styles["window-height"].maxHeight = newHeight + "px";

    this._updateViewState();

    this.updateContentArea(newWidth, this._getContentHeightForWindow(newHeight));
    this._dispatchBrowserEvent("SizeChanged");
  },

  updateContentArea: function cao_updateContentArea (width, height) {
    let oldHeight = parseInt(this.styles["content-height"].height);
    let oldWidth = parseInt(this.styles["content-width"].width);

    let newWidth = width || this.width;
    let newHeight = height || this.contentHeight;

    if (newHeight == oldHeight && newWidth == oldWidth) {
      return;
    }

    this.styles["content-height"].height = newHeight + "px";
    this.styles["content-height"].maxHeight = newHeight + "px";
    this.styles["content-width"].width = newWidth + "px";
    this.styles["content-width"].maxWidth = newWidth + "px";

    this.updateViewableArea(newWidth, this._getViewableHeightForContent(newHeight));
    this._dispatchBrowserEvent("ContentSizeChanged");
  },

  updateViewableArea: function cao_updateViewableArea (width, height) {
    let oldHeight = parseInt(this.styles["viewable-height"].height);
    let oldWidth = parseInt(this.styles["viewable-width"].width);

    let newWidth = width || this.width;
    let newHeight = height || this.viewableHeight;

    if (newHeight == oldHeight && newWidth == oldWidth) {
      return;
    }

    this.styles["viewable-height"].height = newHeight + "px";
    this.styles["viewable-height"].maxHeight = newHeight + "px";
    this.styles["viewable-width"].width = newWidth + "px";
    this.styles["viewable-width"].maxWidth = newWidth + "px";

    this.updateAppBarPosition();

    // Update the back/tab button states. If the keyboard is up
    // these are hidden.
    BrowserUI._updateButtons();

    this._dispatchBrowserEvent("ViewableSizeChanged");
  },

  updateAppBarPosition: function updateAppBarPosition(aForceDown) {
    // Adjust the app and find bar position above the soft keyboard
    let keyboardHeight = aForceDown ? 0 : Services.metro.keyboardHeight;
    Elements.navbar.style.bottom = keyboardHeight + "px";
    Elements.contextappbar.style.bottom = keyboardHeight + "px";
    Elements.findbar.style.bottom = keyboardHeight + "px";
  },

  /*
   * Called by BrowserUI right before we blur the nav bar edit. We use
   * this to get a head start on shuffling app bars around before the
   * soft keyboard transitions down.
   */
  navBarWillBlur: function navBarWillBlur() {
    this.updateAppBarPosition(true);
  },

  onBrowserCreated: function onBrowserCreated(aBrowser) {
    let notificationBox = aBrowser.parentNode.parentNode;
    notificationBox.classList.add("content-width");
    notificationBox.classList.add("content-height");
  },

  /*
   * Event handling
   */

  _onKeyboardDisplayChanging: function _onKeyboardDisplayChanging(aNewState) {
    if (aNewState) {
      Elements.stack.setAttribute("keyboardVisible", true);
    } else {
      Elements.stack.removeAttribute("keyboardVisible");
    }

    this.updateViewableArea();

    if (!aNewState) {
      this.shiftBrowserDeck(0);
      return;
    }

    // Request info about the target form element to see if we
    // need to reposition the browser above the keyboard.
    if (SelectionHelperUI.layerMode === 2 /*kContentLayer*/) {
      Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:RepositionInfoRequest", {
        viewHeight: this.viewableHeight,
      });
    }
  },

  _onRepositionResponse: function _onRepositionResponse(aJsonMsg) {
    if (!aJsonMsg.reposition || !this.isKeyboardOpened) {
      this.shiftBrowserDeck(0);
      return;
    }
    this.shiftBrowserDeck(aJsonMsg.raiseContent);
  },

  observe: function cao_observe(aSubject, aTopic, aData) {
    // Note these are fired before the transition starts. Also per MS specs
    // we should not do anything "heavy" here. We have about 100ms before
    // windows just ignores the event and starts the animation.
    switch (aTopic) {
      case "metro_softkeyboard_hidden":
      case "metro_softkeyboard_shown":
        // Mark that we are in a transition state. Auto-complete and other
        // popups will wait for an event before displaying anything.
        this._deckTransitioning = true;
        // This also insures tap events are delivered before we fire
        // this event. Form repositioning requires the selection handler
        // be running before we send this.
        let self = this;
        let state = aTopic == "metro_softkeyboard_shown";
        Util.executeSoon(function () {
          self._onKeyboardDisplayChanging(state);
        });
        break;
    }
  },

  handleEvent: function cao_handleEvent(aEvent) {
    switch (aEvent.type) {
      case 'resize':
        if (aEvent.target != window)
          return;
        ContentAreaObserver.update();
        break;
    }
  },

  receiveMessage: function sh_receiveMessage(aMessage) {
    switch (aMessage.name) {
      case "Content:RepositionInfoResponse":
        this._onRepositionResponse(aMessage.json);
        break;
    }
  },

  /*
   * Internal helpers
   */

  _updateViewState: function (aState) {
    let oldViewstate = Elements.windowState.getAttribute("viewstate");
    let viewstate = aState || this.viewstate;
    if (viewstate != oldViewstate) {
      Elements.windowState.setAttribute("viewstate", viewstate);
      Services.obs.notifyObservers(null, "metro_viewstate_changed", viewstate);
    }
  },

  shiftBrowserDeck: function (aAmount) {
    if (aAmount == 0) {
      this._deckTransitioning = false;
      this._dispatchWindowEvent("KeyboardChanged", this.isKeyboardOpened);
    }

    if (this._shiftAmount == aAmount)
      return;

    this._shiftAmount = aAmount;
    this._dispatchWindowEvent("MozDeckOffsetChanging", aAmount);

    // Elements.browsers is the deck all browsers sit in
    let self = this;
    Elements.browsers.addEventListener("transitionend", function () {
      Elements.browsers.removeEventListener("transitionend", arguments.callee, true);
      self._deckTransitioning = false;
      self._dispatchWindowEvent("MozDeckOffsetChanged", aAmount);
      self._dispatchWindowEvent("KeyboardChanged", self.isKeyboardOpened);
    }, true);

    // selectAtPoint bug 858471
    //Elements.browsers.style.transform = "translateY(" + (-1 * aAmount) + "px)";
    Elements.browsers.style.marginTop = "" + (-1 * aAmount) + "px";
  },

  _dispatchWindowEvent: function _dispatchWindowEvent(aEventName, aDetail) {
    let event = document.createEvent("UIEvents");
    event.initUIEvent(aEventName, true, false, window, aDetail);
    window.dispatchEvent(event);
  },

  _dispatchBrowserEvent: function (aName, aDetail) {
    setTimeout(function() {
      let event = document.createEvent("Events");
      event.initEvent(aName, true, false);
      Elements.browsers.dispatchEvent(event);
    }, 0);
  },

  _initStyles: function _initStyles() {
    let stylesheet = document.styleSheets[0];
    for (let style of ["window-width", "window-height",
                       "content-height", "content-width",
                       "viewable-height", "viewable-width"]) {
      let index = stylesheet.insertRule("." + style + " {}", stylesheet.cssRules.length);
      this.styles[style] = stylesheet.cssRules[index].style;
    }
  },

  _getContentHeightForWindow: function (windowHeight) {
    return windowHeight;
  },

  _getViewableHeightForContent: function (contentHeight) {
    let keyboardHeight = Services.metro.keyboardHeight;
    return contentHeight - keyboardHeight;
  },

  _debugDumpDims: function _debugDumpDims() {
    let width = parseInt(this.styles["window-width"].width);
    let height = parseInt(this.styles["window-height"].height);
    Util.dumpLn("window:", width, height);
    width = parseInt(this.styles["content-width"].width);
    height = parseInt(this.styles["content-height"].height);
    Util.dumpLn("content:", width, height);
    width = parseInt(this.styles["viewable-width"].width);
    height = parseInt(this.styles["viewable-height"].height);
    Util.dumpLn("viewable:", width, height);
  }
};
