/*
 * typeahead.js
 * https://github.com/twitter/typeahead.js
 * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
 */

var Dropdown = (function() {
  'use strict';

  // constructor
  // -----------

  function Dropdown(o) {
    var that = this,
      onSuggestionClick,
      onSuggestionMouseEnter,
      onSuggestionMouseLeave;

    o = o || {};

    if (!o.menu) {
      $.error('menu is required');
    }

    this.isOpen = false;
    this.isEmpty = true;

    this.datasets = _.map(o.datasets, initializeDataset);

    // bound functions
    onSuggestionClick = _.bind(this._onSuggestionClick, this);
    onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this);
    onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this);

    this.$menu = $(o.menu)
      .on('click.tt', '.tt-suggestion', onSuggestionClick)
      .on('mouseenter.tt', '.tt-suggestion', onSuggestionMouseEnter)
      .on('mouseleave.tt', '.tt-suggestion', onSuggestionMouseLeave);

    _.each(this.datasets, function(dataset) {
      that.$menu.append(dataset.getRoot());
      dataset.onSync('rendered', that._onRendered, that);
    });
  }

  // instance methods
  // ----------------

  _.mixin(Dropdown.prototype, EventEmitter, {
    // ### private

    _onSuggestionClick: function onSuggestionClick($e) {
      this.trigger('suggestionClicked', $($e.currentTarget));
    },

    _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
      this._removeCursor();
      this._setCursor($($e.currentTarget), true);
    },

    _onSuggestionMouseLeave: function onSuggestionMouseLeave() {
      this._removeCursor();
    },

    _onRendered: function onRendered() {
      this.isEmpty = _.every(this.datasets, isDatasetEmpty);

      this.isEmpty ? this._hide() : this.isOpen && this._show();

      this.trigger('datasetRendered');

      function isDatasetEmpty(dataset) {
        return dataset.isEmpty();
      }
    },

    _hide: function() {
      this.$menu.hide();
    },

    _show: function() {
      // can't use jQuery#show because $menu is a span element we want
      // display: block; not dislay: inline;
      this.$menu.css('display', 'block');
    },

    _getSuggestions: function getSuggestions() {
      return this.$menu.find('.tt-suggestion');
    },

    _getCursor: function getCursor() {
      return this.$menu.find('.tt-cursor').first();
    },

    _setCursor: function setCursor($el, silent) {
      $el.first().addClass('tt-cursor');

      !silent && this.trigger('cursorMoved');
    },

    _removeCursor: function removeCursor() {
      this._getCursor().removeClass('tt-cursor');
    },

    _moveCursor: function moveCursor(increment) {
      var $suggestions, $oldCursor, newCursorIndex, $newCursor;

      if (!this.isOpen) {
        return;
      }

      $oldCursor = this._getCursor();
      $suggestions = this._getSuggestions();

      this._removeCursor();

      // shifting before and after modulo to deal with -1 index
      newCursorIndex = $suggestions.index($oldCursor) + increment;
      newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;

      if (newCursorIndex === -1) {
        this.trigger('cursorRemoved');

        return;
      } else if (newCursorIndex < -1) {
        newCursorIndex = $suggestions.length - 1;
      }

      this._setCursor(($newCursor = $suggestions.eq(newCursorIndex)));

      // in the case of scrollable overflow
      // make sure the cursor is visible in the menu
      this._ensureVisible($newCursor);
    },

    _ensureVisible: function ensureVisible($el) {
      var elTop, elBottom, menuScrollTop, menuHeight;

      elTop = $el.position().top;
      elBottom = elTop + $el.outerHeight(true);
      menuScrollTop = this.$menu.scrollTop();
      menuHeight =
        this.$menu.height() +
        parseInt(this.$menu.css('paddingTop'), 10) +
        parseInt(this.$menu.css('paddingBottom'), 10);

      if (elTop < 0) {
        this.$menu.scrollTop(menuScrollTop + elTop);
      } else if (menuHeight < elBottom) {
        this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
      }
    },

    // ### public

    close: function close() {
      if (this.isOpen) {
        this.isOpen = false;

        this._removeCursor();
        this._hide();

        this.trigger('closed');
      }
    },

    open: function open() {
      if (!this.isOpen) {
        this.isOpen = true;

        !this.isEmpty && this._show();

        this.trigger('opened');
      }
    },

    setLanguageDirection: function setLanguageDirection(dir) {
      this.$menu.css(dir === 'ltr' ? css.ltr : css.rtl);
    },

    moveCursorUp: function moveCursorUp() {
      this._moveCursor(-1);
    },

    moveCursorDown: function moveCursorDown() {
      this._moveCursor(+1);
    },

    getDatumForSuggestion: function getDatumForSuggestion($el) {
      var datum = null;

      if ($el.length) {
        datum = {
          raw: Dataset.extractDatum($el),
          value: Dataset.extractValue($el),
          datasetName: Dataset.extractDatasetName($el),
        };
      }

      return datum;
    },

    getDatumForCursor: function getDatumForCursor() {
      return this.getDatumForSuggestion(this._getCursor().first());
    },

    getDatumForTopSuggestion: function getDatumForTopSuggestion() {
      return this.getDatumForSuggestion(this._getSuggestions().first());
    },

    update: function update(query) {
      _.each(this.datasets, updateDataset);

      function updateDataset(dataset) {
        dataset.update(query);
      }
    },

    empty: function empty() {
      _.each(this.datasets, clearDataset);
      this.isEmpty = true;

      function clearDataset(dataset) {
        dataset.clear();
      }
    },

    isVisible: function isVisible() {
      return this.isOpen && !this.isEmpty;
    },

    destroy: function destroy() {
      this.$menu.off('.tt');

      this.$menu = null;

      _.each(this.datasets, destroyDataset);

      function destroyDataset(dataset) {
        dataset.destroy();
      }
    },
  });

  return Dropdown;

  // helper functions
  // ----------------

  function initializeDataset(oDataset) {
    return new Dataset(oDataset);
  }
})();
