text: 'use strict';
  
  (function(exports) {
  
  /**
   * InputAppsTransitionManager takes care of the visual life cycle of
   * the keyboard container. The container basically cycles through
   * the four defined states.
   *
   * Note that there isn't a show() method because the input management
   * currently rely on keyboard app to call resize() first.
   *
   */
  var InputAppsTransitionManager = function() {
    this._started = false;
    this.currentState = undefined;
    this.occupyingHeight = undefined;
    this._element = null;
  };
  
  InputAppsTransitionManager.prototype.onstatechange = null;
  
  InputAppsTransitionManager.prototype.ELEMENT_ID = 'keyboards';
  
  InputAppsTransitionManager.prototype.start = function() {
    if (this._started) {
      throw new Error('InputAppsTransitionManager: ' +
        'Instance should not be start()\'ed twice.');
    }
    this._started = true;
  
    this._element = document.getElementById(this.ELEMENT_ID);
    if (!this._element) {
      throw new Error('InputAppsTransitionManager: element not found.');
    }
  
    this._element.addEventListener('transitionend', this, true);
  
    // Start with hidden state.
    this.currentState = this.STATE_HIDDEN;
  };
  
  InputAppsTransitionManager.prototype.stop = function hb_stop() {
    if (!this._started) {
      throw new Error('InputAppsTransitionManager: ' +
        'Instance was never start()\'ed but stop() is called.');
    }
    this._started = false;
  
    this._element.removeEventListener('transitionend', this, true);
  
    this.currentState = undefined;
    this.occupyingHeight = undefined;
    this._element = null;
  };
  
  InputAppsTransitionManager.prototype.handleEvent = function(evt) {
    // We only care about transition: transform
    if (evt.propertyName !== 'transform') {
      return;
    }
  
    switch (this.currentState) {
      case this.STATE_HIDDEN:
      case this.STATE_VISIBLE:
        // These should never happpen.
        console.error('InputAppsTransitionManager: ' +
          'received transitionend event during incorrect states.');
  
        break;
  
      case this.STATE_TRANSITION_IN:
        this._changeState(this.STATE_VISIBLE);
  
        break;
  
      case this.STATE_TRANSITION_OUT:
        this.occupyingHeight = undefined;
        this._changeState(this.STATE_HIDDEN);
  
        break;
    }
  };
  
  InputAppsTransitionManager.prototype.STATE_HIDDEN = 0;
  InputAppsTransitionManager.prototype.STATE_VISIBLE = 1;
  InputAppsTransitionManager.prototype.STATE_TRANSITION_IN = 2;
  InputAppsTransitionManager.prototype.STATE_TRANSITION_OUT = 3;
  
  InputAppsTransitionManager.prototype.handleResize = function(height) {
    switch (this.currentState) {
      case this.STATE_HIDDEN:
      case this.STATE_TRANSITION_OUT:
        this._setOccupyingHeight(height);
        this._changeState(this.STATE_TRANSITION_IN);
  
        // Start or revert transition
        this._element.classList.remove('no-transition');
        this._element.classList.remove('hide');
  
        break;
  
      case this.STATE_VISIBLE:
        if (height === this.occupyingHeight) {
          return;
  
        }
        this._setOccupyingHeight(height);
        // No state change, but we need to publish a event.
        this._publish('keyboardchange');
  
        break;
  
      case this.STATE_TRANSITION_IN:
        // No state change, simply update the cached height.
        this._setOccupyingHeight(height);
  
        break;
    }
  };
  
  InputAppsTransitionManager.prototype.hide = function() {
    if (this.currentState === this.STATE_TRANSITION_OUT ||
        this.currentState === this.STATE_HIDDEN) {
      // Do nothing.
      return;
    }
    // No matter what the current state is, we now enters transition out state.
    this._changeState(this.STATE_TRANSITION_OUT);
  
    // Start hide transition
    this._element.classList.remove('no-transition');
    this._element.classList.add('hide');
  };
  
  InputAppsTransitionManager.prototype.hideImmediately = function() {
    if (this.currentState === this.STATE_HIDDEN) {
      // Do nothing.
      return;
    }
    // No matter what the current state is, we now enters transition out state.
    this._changeState(this.STATE_TRANSITION_OUT);
  
    // hide without transition
    this._element.classList.add('no-transition');
    this._element.classList.add('hide');
  
    this._changeState(this.STATE_HIDDEN);
  };
  
  InputAppsTransitionManager.prototype._changeState = function(state) {
    if (state === this.currentState) {
      // Should not call onstatechange and dispatch events.
      return;
    }
  
    this.currentState = state;
    if (typeof this.onstatechange === 'function') {
      this.onstatechange();
    }
  
    // Publish some events.
    switch (state) {
      case this.STATE_HIDDEN:
        this._publish('keyboardhidden');
  
        break;
  
      case this.STATE_VISIBLE:
        this._publish('keyboardchange');
  
        break;
  
      case this.STATE_TRANSITION_IN:
        // No event in for this state change
  
        break;
  
      case this.STATE_TRANSITION_OUT:
        this._publish('keyboardhide');
  
        break;
    }
  };
  
  InputAppsTransitionManager.prototype._publish = function (type) {
    var eventInitDict = {
      bubbles: true,
      cancellable: true,
      detail: {
        height: this.occupyingHeight
      }
    };
  
    // We dispatch the events at the body level so we are able to intercept
    // them and prevent page resizing where desired.
    var evt = new CustomEvent(type, eventInitDict);
    document.body.dispatchEvent(evt);
  };
  
  // wrap it so we can mock it for testing
  InputAppsTransitionManager.prototype._getDpx = function () {
    return window.devicePixelRatio;
  };
  
  InputAppsTransitionManager.prototype._setOccupyingHeight = function (height) {
    // bug 1059683: when we're on a HiDPI device with non-integer devicePixelRatio
    // the system may calculate (from available screen height and keyboard height)
    // the available height for current window/layout that is a fraction smaller
    // than the ideal value, which can result in a 1-device-px gap between the
    // current window/layout and keyboard, on such devices.
    // to mitigate this, the keyboard tries to report 1 less pixel of height if
    // it sees that the height of the keyboard is a fraction when expressed in
    // device pixel.
  
    var dpx = this._getDpx();
    if ((height * dpx) % 1 !== 0) {
      height = Math.floor(height * dpx) / dpx;
    }
  
    // XXX: On TV we got 1 additional px. We need to find out the root cause.
    --height;
  
    this.occupyingHeight = height;
  };
  
  exports.InputAppsTransitionManager = InputAppsTransitionManager;
  
  })(window);
  