/* globals define */
;(function(define){'use strict';define(function(require,exports,module){

/**
 * Dependencies
 */
var component = require('gaia-component');
var fontFit = require('font-fit');

/**
 * Load 'gaia-icons' font-family
 */
require('gaia-icons');

/**
 * Simple logger (toggle 0)
 *
 * @type {Function}
 */
var debug = 0 ? console.log.bind(console) : function() {};

/**
 * Supported action types
 *
 * @type {Object}
 */
var KNOWN_ACTIONS = {
  menu: 'menu',
  back: 'back',
  close: 'close'
};

/**
 * The default title font.
 *
 * @type {String}
 */
var TITLE_FONT = 'italic 300 24px FiraSans';

/**
 * The padding (start/end) used if
 * the title needs padding away from
 * the edge of the header.
 *
 * @type {Number}
 */
var TITLE_PADDING = 10;

/**
 * This is the minimum font size that we can take
 * when the header title is centered in the window.
 */
var MINIMUM_FONT_SIZE_CENTERED = 20;

/**
 * This is the minimum font size that we can take
 * when the header title is not centered in the window.
 */
var MINIMUM_FONT_SIZE_UNCENTERED = 16;

/**
 * This is the maximum font-size
 * for the header title.
 */
var MAXIMUM_FONT_SIZE = 23;

/**
 * Register the element.
 *
 * @return {Element} constructor
 */
module.exports = component.register('gaia-header', {

  /**
   * Called when the element is first created.
   *
   * Here we create the shadow-root and
   * inject our template into it.
   *
   * @private
   */
  created: function() {var this$0 = this;
    debug('created');
    this.setupShadowRoot();

    // Elements
    this.els = {
      actionButton: this.shadowRoot.querySelector('.action-button'),
      titles: this.getElementsByTagName('h1')
    };

    // Events
    this.els.actionButton.addEventListener('click',
      function(e ) {return this$0.onActionButtonClick(e)});
    this.observer = new MutationObserver(this.onMutation.bind(this));

    // Properties
    this.titleEnd = this.getAttribute('title-end');
    this.titleStart = this.getAttribute('title-start');
    this.noFontFit = this.getAttribute('no-font-fit');
    this.notFlush = this.hasAttribute('not-flush');
    this.action = this.getAttribute('action');

    this.unresolved = {};
    this.pending = {};
    this._resizeThrottlingId = null;

    // bind the listener in advance so that we can remove it when detaching.
    this.onResize = this.onResize.bind(this);
  },

  /**
   * Called when the element is
   * attached to the DOM.
   *
   * Run fontFit when we have DOM
   * context and start listening
   * for DOM mutations.
   *
   * We run font-fit on next tick to
   * avoid reading from the DOM when
   * it may not be loaded yet.
   *
   * @private
   */
  attached: function() {
    debug('attached');
    this.runFontFitSoon();
    this.observerStart();
    window.addEventListener('resize', this.onResize);
  },

  /**
   * Called when the element is
   * detached from the DOM.
   *
   * @private
   */
  detached: function() {
    debug('detached');
    window.removeEventListener('resize', this.onResize);
    this.observerStop();
    this.clearPending();
  },

  /**
   * Clears pending `.nextTick()`s and requestAnimationFrame's.
   *
   * @private
   */
  clearPending: function() {
    for (var key in this.pending) {
      this.pending[key].clear();
      delete this.pending[key];
    }

    window.cancelAnimationFrame(this._resizeThrottlingId);
    this._resizeThrottlingId = null;
  },

  /**
   * Run the font-fit logic and
   * center the title.
   *
   * The styles are calculated synchronously,
   * but then set asynchronously. This means
   * this function can be hammered in one turn,
   * and the title styles will only be written
   * once, with the latest styles.
   *
   * @return {Promise}
   * @public
   */
  runFontFit: function() {var this$0 = this;
    debug('run font-fit');

    // Nothing is run if `no-font-fit` attribute
    // is present. We don't `reject()` as this
    // isn't technically an error state.
    if (this.noFontFit) { return Promise.resolve(); }

    var titles = this.els.titles;
    var space = this.getTitleSpace();
    var styles = [].map.call(titles, function(el ) {return this$0.getTitleStyle(el, space)});

    // Update the title styles using the latest
    // styles. This function can be called many
    // times but will only run once in each tick.
    return this.setTitleStylesSoon(styles);
  },

  /**
   * Microtask debounced `runFontFit`
   *
   * @private
   */
  runFontFitSoon: function() {var this$0 = this;
    debug('run font-fit soon');
    if (this.pending.runFontFitSoon) { return; }
    this.pending.runFontFitSoon = this.nextTick(function()  {
      delete this$0.pending.runFontFitSoon;
      this$0.runFontFit();
    });
  },

  /**
   * Get the style properties required
   * to fit and position the title text.
   *
   * @param  {HTMLH1Element} el
   * @param  {Number} space
   * @return {Object} {fontSize, marginStart, overflowing, id}
   * @private
   */
  getTitleStyle: function(el, space) {
    debug('get el style', el, space);
    var text = el.textContent;
    var styleId = space.start + text + space.end + '#' + space.value;

    // Bail when there's no text (or just whitespace)
    if (!text || !text.trim()) { return debug('exit: no text'); }

    // If neither the text or the titleSpace
    // changed, there's no reason to continue.
    if (getStyleId(el) === styleId) { return debug('exit: no change'); }

    var marginStart = this.getTitleMarginStart();
    var textSpace = space.value - Math.abs(marginStart);
    var fontFitResult = this.fontFit(text, textSpace, {
      min: MINIMUM_FONT_SIZE_CENTERED
    });

    var overflowing = fontFitResult.overflowing;
    var padding = { start: 0, end: 0 };

    // If the text is overflowing in the
    // centered title, we remove centering
    // to free up space, rerun fontFit to
    // get a fontSize which fits this space.
    if (overflowing) {
      debug('title overflowing');
      padding.start = !space.start ? TITLE_PADDING : 0;
      padding.end = !space.end ? TITLE_PADDING : 0;
      textSpace = space.value - padding.start - padding.end;
      fontFitResult = this.fontFit(text, textSpace);
      marginStart = 0;
    }

    return {
      id: styleId,
      fontSize: fontFitResult.fontSize,
      marginStart: marginStart,
      overflowing: overflowing,
      padding: padding
    };
  },

  /**
   * Set's styles on the title elements.
   *
   * If there is already an unresolved Promise
   * we return it instead of scheduling another.
   * This means that the function can be hammered
   * in the same sync-turn and will only run
   * once (like a debounce).
   *
   * @param {Array} styles
   * @return {Promise}
   */
  setTitleStylesSoon: function(styles) {var this$0 = this;
    debug('set title styles soon', styles);
    var key = 'setStyleTitlesSoon';

     // Always update styles
    this._titleStyles = styles;

    // Return existing unresolved
    // promise, or make a new one
    if (this.unresolved[key]) { return this.unresolved[key]; }
    this.unresolved[key] = new Promise(function(resolve)  {
      this$0.pending[key] = this$0.nextTick(function()  {
        var styles = this$0._titleStyles;
        var els = this$0.els.titles;

        [].forEach.call(els, function(el, i)  {
          if (!styles[i]) { return debug('exit'); }
          this$0.setTitleStyle(el, styles[i]);
        });

        // Clean up
        delete this$0._titleStyles;
        delete this$0.unresolved[key];
        delete this$0.pending[key];

        resolve();
      });
    });
  },

  /**
   * Fit text and center a title between
   * the buttons before and after.
   *
   * Right now, because gaia-header is not
   * fully rtl-compatible (due to Gaia),
   * we're using `marginLeft` etc. These
   * will be changed to `marginStart` etc
   * when we become fully RTL.
   *
   * @param  {HTMLH1Element} title
   * @param  {Number} space
   * @private
   */
  setTitleStyle: function(el, style) {
    debug('set title style', style);
    this.observerStop();
    el.style.marginLeft = style.marginStart + 'px';
    el.style.paddingLeft = style.padding.start + 'px';
    el.style.paddingRight = style.padding.end + 'px';
    el.style.fontSize = style.fontSize + 'px';
    setStyleId(el, style.id);
    this.observerStart();
  },

  /**
   * Run font-fit on a title with
   * the given amount of content space.

   * @param {String} text
   * @param {Number} space
   * @param {Object} optional {[min]}
   * @return {Object} {fontSize, textWidth}
   * @private
   */
  fontFit: function(text, space) {var opts = arguments[2];if(opts === void 0)opts = {};
    debug('font fit:', text, space, opts);

    var fontFitArgs = {
      font: TITLE_FONT,
      min: opts.min || MINIMUM_FONT_SIZE_UNCENTERED,
      max: MAXIMUM_FONT_SIZE,
      text: text,
      space: space
    };

    return fontFit(fontFitArgs);
  },

  /**
   * Start the observer listening
   * for DOM mutations.
   * Start the listener for 'resize' event.
   *
   * @private
   */
  observerStart: function() {
    if (this.observing) { return; }

    this.observer.observe(this, {
      childList: true,
      attributes: true,
      subtree: true
    });

    this.observing = true;
    debug('observer started');
  },

  /**
   * Stop the observer listening
   * for DOM mutations.
   *
   * @private
   */
  observerStop: function() {
    if (!this.observing) { return; }
    this.observer.disconnect();

    this.observing = false;
    debug('observer stopped');
  },

  /**
   * Handle 'resize' events.
   * @param {Event} The DOM Event that's being handled.
   *
   * @private
   */
  onResize: function(e) {var this$0 = this;
    debug('onResize', this._resizeThrottlingId);

    if (this._resizeThrottlingId !== null) {
      return;
    }

    /* Resize events can arrive at a very high rate, so we're trying to
     * reasonably throttle these events. */
    this._resizeThrottlingId = window.requestAnimationFrame(function()  {
      this$0._resizeThrottlingId = null;
      this$0.runFontFitSoon();
    });
  },

  /**
   * When the components DOM changes we
   * call `.runFontFit()` (sync).
   *
   * If the `textContent` is changed in a
   * mutation observer just after attaching,
   * we end up running twice.
   *
   * If there is a pending async .runFontFit(),
   * then we don't want to run it now.
   *
   * @param  {Array} mutations
   * @private
   */
  onMutation: function(mutations) {
    debug('on mutation', mutations);
    if (!this.pending.runFontFitSoon) { this.runFontFit(); }
  },

  /**
   * Get the title width.
   *
   * Returns the space available for <h1>.
   *
   * @return {Number}
   * @private
   */
  getTitleSpace: function() {
    var start = this.titleStart;
    var end = this.titleEnd;
    var space = this.getWidth() - start - end;
    var result = {
      value: space,
      start: start,
      end: end
    };

    debug('get title space', result);
    return result;
  },

  /**
   * Get the width of the component.
   *
   * For performance reasons we make the
   * assumption that the width is the same
   * as `window.innerWidth` unless the
   * `not-flush` attribute is used.
   *
   * @return {Number}
   * @private
   */
  getWidth: function() {
    var value = this.notFlush ?
      this.clientWidth : window.innerWidth;

    debug('get width', value);
    return value;
  },

  /**
   * Triggers the 'action' button
   * (used in Gaia integration testing).
   *
   * @public
   */
  triggerAction: function() {
    if (this.action) { this.els.actionButton.click(); }
  },

  /**
   * Handle clicks on the action button.
   *
   * Fired pending to allow the 'click' event
   * to finish its event path before
   * dispatching the 'action' event.
   *
   * @param  {Event} e
   * @private
   */
  onActionButtonClick: function() {var this$0 = this;
    debug('action button click');
    var config = { detail: { type: this.action } };
    var e = new CustomEvent('action', config);
    setTimeout(function()  {return this$0.dispatchEvent(e)});
  },

  /**
   * Get the margin-start value required
   * to center the title between
   * surrounding buttons.
   *
   * @param  {Object} title
   * @return {Object}
   * @private
   */
  getTitleMarginStart: function() {
    var start = this.titleStart;
    var end = this.titleEnd;
    var marginStart = end - start;
    debug('get title margin start', marginStart);
    return marginStart;
  },

  /**
   * Get all the buttons (<a> & <button>)
   * before the first <h1>.
   *
   * @return {Array}
   * @private
   */
  getButtonsBeforeTitle: function() {
    var children = this.children;
    var l = children.length;
    var els = [];

    for (var i = 0; i < l; i++) {
      var el = children[i];
      if (el.tagName === 'H1') { break; }
      if (!contributesToLayout(el)) { continue; }

      els.push(el);
    }

    // Don't forget the action button
    if (this.action) { els.push(this.els.actionButton); }
    return els;
  },

  /**
   * Get all the buttons (<a> & <button>)
   * after the last <h1>.
   *
   * @return {Array}
   * @private
   */
  getButtonsAfterTitle: function() {
    var children = this.children;
    var els = [];

    for (var i = children.length - 1; i >= 0; i--) {
      var el = children[i];
      if (el.tagName === 'H1') { break; }
      if (!contributesToLayout(el)) { continue; }

      els.push(el);
    }

    return els;
  },

  /**
   * Get the sum of the width of
   * the given buttons.
   *
   * This function is optimized to avoid reading
   * `element.clientWidth` when possible.
   *
   * If a button is `display: none` it will
   * have a `.clientWidth` of 0, therefore won't
   * contribute anything to the overall sum.
   *
   * @param  {Array} buttons
   * @return {Number}
   * @private
   */
  sumButtonWidths: function(buttons) {var this$0 = this;
    var defaultWidth = 50;
    var sum = buttons.reduce(function(prev, button)  {
      var isStandardButton = button === this$0.els.actionButton;
      var width = isStandardButton ? defaultWidth : button.clientWidth;
      return prev + width;
    }, 0);

    debug('sum button widths', buttons, sum);
    return sum;
  },

  /**
   * Known attribute property
   * descriptors.
   *
   * These setters get called when matching
   * attributes change on the element.
   *
   * @type {Object}
   */
  attrs: {
    action: {
      get: function() { return this._action; },
      set: function(value) {
        var action = KNOWN_ACTIONS[value];
        if (action === this._action) { return; }
        this.setAttr('action', action);
        this._action = action;
      }
    },

    titleStart: {
      get: function() {
        debug('get title-start');
        if ('_titleStart' in this) { return this._titleStart; }
        var buttons = this.getButtonsBeforeTitle();
        var value = this.sumButtonWidths(buttons);
        debug('get title-start', buttons, value);
        return value;
      },

      set: function(value) {
        debug('set title-start', value);
        value = parseInt(value, 10);
        if (value === this._titleStart || isNaN(value)) { return; }
        this.setAttr('title-start', value);
        this._titleStart = value;
        debug('set');
      }
    },

    titleEnd: {
      get: function() {
        debug('get title-end');
        if ('_titleEnd' in this) { return this._titleEnd; }
        var buttons = this.getButtonsAfterTitle();
        return this.sumButtonWidths(buttons);
      },

      set: function(value) {
        debug('set title-end', value);
        value = parseInt(value, 10);
        if (value === this._titleEnd || isNaN(value)) { return; }
        this.setAttr('title-end', value);
        this._titleEnd = value;
      }
    },

    noFontFit: {
      get: function() { return this._noFontFit || false; },
      set: function(value) {
        debug('set no-font-fit', value);
        value = !!(value || value === '');

        if (value === this.noFontFit) { return; }
        this._noFontFit = value;

        if (value) { this.setAttr('no-font-fit', ''); }
        else { this.removeAttr('no-font-fit'); }
      }
    }
  },

  template: ("<div class=\"inner\">\
\n    <button class=\"action-button\">\
\n      <content select=\".l10n-action\"></content>\
\n    </button>\
\n    <content></content>\
\n  </div>\
\n\
\n  <style>\
\n\
\n  :host {\
\n    display: block;\
\n    -moz-user-select: none;\
\n\
\n    --gaia-header-button-color:\
\n      var(--header-button-color,\
\n      var(--header-color,\
\n      var(--link-color,\
\n      inherit)));\
\n  }\
\n\
\n  /**\
\n   * [hidden]\
\n   */\
\n\
\n  :host[hidden] {\
\n    display: none;\
\n  }\
\n\
\n  /** Reset\
\n   ---------------------------------------------------------*/\
\n\
\n  ::-moz-focus-inner { border: 0; }\
\n\
\n  /** Inner\
\n   ---------------------------------------------------------*/\
\n\
\n  .inner {\
\n    display: flex;\
\n    min-height: 50px;\
\n    direction: ltr;\
\n    -moz-user-select: none;\
\n\
\n    background:\
\n      var(--header-background,\
\n      var(--background,\
\n      #fff));\
\n  }\
\n\
\n  /** Action Button\
\n   ---------------------------------------------------------*/\
\n\
\n  /**\
\n   * 1. Hidden by default\
\n   */\
\n\
\n  .action-button {\
\n    position: relative;\
\n\
\n    display: none; /* 1 */\
\n    width: 50px;\
\n    font-size: 30px;\
\n    margin: 0;\
\n    padding: 0;\
\n    border: 0;\
\n    outline: 0;\
\n\
\n    align-items: center;\
\n    background: none;\
\n    cursor: pointer;\
\n    transition: opacity 200ms 280ms;\
\n    color:\
\n      var(--header-action-button-color,\
\n      var(--header-icon-color,\
\n      var(--gaia-header-button-color)));\
\n  }\
\n\
\n  /**\
\n   * [action=back]\
\n   * [action=menu]\
\n   * [action=close]\
\n   *\
\n   * 1. For icon vertical-alignment\
\n   */\
\n\
\n  [action=back] .action-button,\
\n  [action=menu] .action-button,\
\n  [action=close] .action-button {\
\n    display: flex; /* 1 */\
\n  }\
\n\
\n  /**\
\n   * :active\
\n   */\
\n\
\n  .action-button:active {\
\n    transition: none;\
\n    opacity: 0.2;\
\n  }\
\n\
\n  /** Action Button Icon\
\n   ---------------------------------------------------------*/\
\n\
\n  .action-button:before {\
\n    font-family: 'gaia-icons';\
\n    font-style: normal;\
\n    text-rendering: optimizeLegibility;\
\n    font-weight: 500;\
\n  }\
\n\
\n  [action=close] .action-button:before { content: 'close' }\
\n  [action=back] .action-button:before { content: 'back' }\
\n  [action=menu] .action-button:before { content: 'menu' }\
\n\
\n  /** Action Button Icon\
\n   ---------------------------------------------------------*/\
\n\
\n  /**\
\n   * 1. To enable vertical alignment.\
\n   */\
\n\
\n  .action-button:before {\
\n    display: block;\
\n  }\
\n\
\n  /** Action Button Text\
\n   ---------------------------------------------------------*/\
\n\
\n  /**\
\n   * To provide custom localized content for\
\n   * the action-button, we allow the user\
\n   * to provide an element with the class\
\n   * .l10n-action. This node is then\
\n   * pulled inside the real action-button.\
\n   *\
\n   * Example:\
\n   *\
\n   *   <gaia-header action=\"back\">\
\n   *     <span class=\"l10n-action\" aria-label=\"Back\">Localized text</span>\
\n   *     <h1>title</h1>\
\n   *   </gaia-header>\
\n   */\
\n\
\n  ::content .l10n-action {\
\n    position: absolute;\
\n    left: 0;\
\n    top: 0;\
\n    width: 100%;\
\n    height: 100%;\
\n    font-size: 0;\
\n  }\
\n\
\n  /** Title\
\n   ---------------------------------------------------------*/\
\n\
\n  /**\
\n   * 1. Vertically center text. We can't use flexbox\
\n   *    here as it breaks text-overflow ellipsis\
\n   *    without an inner div.\
\n   */\
\n\
\n  ::content h1 {\
\n    flex: 1;\
\n    margin: 0;\
\n    padding: 0;\
\n    overflow: hidden;\
\n\
\n    white-space: nowrap;\
\n    text-overflow: ellipsis;\
\n    text-align: center;\
\n    line-height: 50px; /* 1 */\
\n    font-weight: 300;\
\n    font-style: italic;\
\n    font-size: 24px;\
\n\
\n    color:\
\n      var(--header-title-color,\
\n      var(--header-color,\
\n      var(--title-color,\
\n      var(--text-color,\
\n      inherit))));\
\n  }\
\n\
\n  /**\
\n   * [dir=rtl]\
\n   *\
\n   * When the document is in RTL mode we still\
\n   * want the <h1> text to be reversed to that\
\n   * strings like '1 selected' become 'selected 1'.\
\n   *\
\n   * When we're happy for gaia-header to be fully\
\n   * RTL responsive we won't need this rule anymore,\
\n   * but this depends on all Gaia apps being ready.\
\n   */\
\n\
\n  :host-context([dir=rtl]) ::content h1 {\
\n    direction: rtl;\
\n  }\
\n\
\n  /** Buttons\
\n   ---------------------------------------------------------*/\
\n\
\n  ::content a,\
\n  ::content button {\
\n    position: relative;\
\n    z-index: 1;\
\n    box-sizing: border-box;\
\n    display: flex;\
\n    width: auto;\
\n    height: auto;\
\n    min-width: 50px;\
\n    margin: 0;\
\n    padding: 0 10px;\
\n    outline: 0;\
\n    border: 0;\
\n\
\n    font-size: 14px;\
\n    line-height: 1;\
\n    align-items: center;\
\n    justify-content: center;\
\n    text-decoration: none;\
\n    text-align: center;\
\n    background: none;\
\n    border-radius: 0;\
\n    font-style: italic;\
\n    cursor: pointer;\
\n    transition: opacity 200ms 280ms;\
\n    color: var(--gaia-header-button-color);\
\n  }\
\n\
\n  /**\
\n   * :active\
\n   */\
\n\
\n  ::content a:active,\
\n  ::content button:active {\
\n    transition: none;\
\n    opacity: 0.2;\
\n  }\
\n\
\n  /**\
\n   * [hidden]\
\n   */\
\n\
\n  ::content a[hidden],\
\n  ::content button[hidden] {\
\n    display: none;\
\n  }\
\n\
\n  /**\
\n   * [disabled]\
\n   */\
\n\
\n  ::content a[disabled],\
\n  ::content button[disabled] {\
\n    pointer-events: none;\
\n    color: var(--header-disabled-button-color);\
\n  }\
\n\
\n  /** Icon Buttons\
\n   ---------------------------------------------------------*/\
\n\
\n  /**\
\n   * Icons are a different color to text\
\n   */\
\n\
\n  ::content .icon,\
\n  ::content [data-icon] {\
\n    color:\
\n      var(--header-icon-color,\
\n      var(--gaia-header-button-color));\
\n  }\
\n\
\n  /**\
\n   * If users want their action button\
\n   * to be in the component's light-dom\
\n   * they can add an .action class\
\n   * to make it look like the\
\n   * shadow action button.\
\n   */\
\n\
\n  ::content .action {\
\n    color:\
\n      var(--header-action-button-color,\
\n      var(--header-icon-color,\
\n      var(--gaia-header-button-color)));\
\n  }\
\n\
\n  /**\
\n   * [data-icon]:empty\
\n   *\
\n   * Icon buttons with no textContent,\
\n   * should always be 50px.\
\n   *\
\n   * This is to prevent buttons being\
\n   * larger than they should be before\
\n   * icon-font has loaded.\
\n   */\
\n\
\n  ::content [data-icon]:empty {\
\n    width: 50px;\
\n  }\
\n\
\n  </style>"),

  // Test hook
  nextTick: nextTick
});

/**
 * Utils
 */

/**
 * Determines whether passed element
 * contributes to the layout in gaia-header.
 *
 * @param  {Element}  el
 * @return {Boolean}
 */
function contributesToLayout(el) { return el.tagName !== 'STYLE'; }

/**
 * Set a 'style id' property that
 * can be retrieved later.
 *
 * Used to determine whether a title's
 * `style` needs to be updated or not.
 *
 * @param {Element} el
 * @param {String} id
 */
function setStyleId(el, id) { el._styleId = id; }

/**
 * Get a 'style id' property.
 *
 * Used to determine whether a title's
 * `style` needs to be updated or not.
 *
 * @param {Element} el
 * @param {String} id
 */
function getStyleId(el) { return el._styleId; }

/**
 * Calls callback at next 'microtask'.
 * Returns an object that has
 * a `.clear()` method.
 *
 * @param  {Function} fn
 * @return {Object} { clear }
 */
function nextTick(fn) {
  var cleared;
  Promise.resolve().then(function()  { if (!cleared) { fn(); } });
  return { clear: function() { cleared = true; }};
}

});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this));
