/*!
 * Angular Material Design
 * https://github.com/angular/material
 * @license MIT
 * v0.9.4
 */
(function( window, angular, undefined ){
"use strict";

/**
 * @ngdoc module
 * @name material.components.autocomplete
 */
/*
 * @see js folder for autocomplete implementation
 */
angular.module('material.components.autocomplete', [
  'material.core',
  'material.components.icon'
]);

angular
    .module('material.components.autocomplete')
    .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);

var ITEM_HEIGHT = 41,
    MAX_HEIGHT = 5.5 * ITEM_HEIGHT,
    MENU_PADDING = 8;

function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $mdTheming, $window, $animate, $rootElement) {

  //-- private variables

  var self      = this,
      itemParts = $scope.itemsExpr.split(/ in /i),
      itemExpr  = itemParts[1],
      elements  = null,
      promise   = null,
      cache     = {},
      noBlur    = false,
      selectedItemWatchers = [],
      hasFocus  = false;

  //-- public variables

  self.scope    = $scope;
  self.parent   = $scope.$parent;
  self.itemName = itemParts[0];
  self.matches  = [];
  self.loading  = false;
  self.hidden   = true;
  self.index    = null;
  self.messages = [];
  self.id       = $mdUtil.nextUid();

  //-- public methods

  self.keydown  = keydown;
  self.blur     = blur;
  self.focus    = focus;
  self.clear    = clearValue;
  self.select   = select;
  self.getCurrentDisplayValue         = getCurrentDisplayValue;
  self.registerSelectedItemWatcher    = registerSelectedItemWatcher;
  self.unregisterSelectedItemWatcher  = unregisterSelectedItemWatcher;

  self.listEnter = function () { noBlur = true; };
  self.listLeave = function () {
    noBlur = false;
    if (!hasFocus) self.hidden = true;
  };
  self.mouseUp   = function () { elements.input.focus(); };

  return init();

  //-- initialization methods

  function init () {
    configureWatchers();
    $timeout(function () {
      gatherElements();
      focusElement();
      moveDropdown();
    });
  }

  function positionDropdown () {
    if (!elements) return $timeout(positionDropdown, 0, false);
    var hrect  = elements.wrap.getBoundingClientRect(),
        vrect  = elements.snap.getBoundingClientRect(),
        root   = elements.root.getBoundingClientRect(),
        top    = vrect.bottom - root.top,
        bot    = root.bottom - vrect.top,
        left   = hrect.left - root.left,
        width  = hrect.width,
        styles = {
          left:     left + 'px',
          minWidth: width + 'px',
          maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
        };
    if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
      styles.top = 'auto';
      styles.bottom = bot + 'px';
      styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
    } else {
      styles.top = top + 'px';
      styles.bottom = 'auto';
      styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom - hrect.bottom - MENU_PADDING) + 'px';
    }
    elements.$.ul.css(styles);
    $timeout(correctHorizontalAlignment, 0, false);

    function correctHorizontalAlignment () {
      var dropdown = elements.ul.getBoundingClientRect(),
          styles   = {};
      if (dropdown.right > root.right - MENU_PADDING) {
        styles.left = (hrect.right - dropdown.width) + 'px';
      }
      elements.$.ul.css(styles);
    }
  }

  function moveDropdown () {
    if (!elements.$.root.length) return;
    $mdTheming(elements.$.ul);
    elements.$.ul.detach();
    elements.$.root.append(elements.$.ul);
    if ($animate.pin) $animate.pin(elements.$.ul, $rootElement);
  }

  function focusElement () {
    if ($scope.autofocus) elements.input.focus();
  }

  function configureWatchers () {
    var wait = parseInt($scope.delay, 10) || 0;
    $scope.$watch('searchText', wait
        ? $mdUtil.debounce(handleSearchText, wait)
        : handleSearchText);
    registerSelectedItemWatcher(selectedItemChange);
    $scope.$watch('selectedItem', handleSelectedItemChange);
    $scope.$watch('$mdAutocompleteCtrl.hidden', function (hidden, oldHidden) {
      if (!hidden && oldHidden) positionDropdown();
    });
    angular.element($window).on('resize', positionDropdown);
  }

  function gatherElements () {
    elements = {
      main:  $element[0],
      ul:    $element.find('ul')[0],
      input: $element.find('input')[0],
      wrap:  $element.find('md-autocomplete-wrap')[0],
      root:  document.body
    };
    elements.li = elements.ul.getElementsByTagName('li');
    elements.snap = getSnapTarget();
    elements.$ = getAngularElements(elements);
  }

  function getSnapTarget () {
    for (var element = $element; element.length; element = element.parent()) {
      if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[0];
    }
    return elements.wrap;
  }

  function getAngularElements (elements) {
    var obj = {};
    for (var key in elements) {
      obj[key] = angular.element(elements[key]);
    }
    return obj;
  }

  //-- event/change handlers

  function selectedItemChange (selectedItem, previousSelectedItem) {
    if (selectedItem) {
      $scope.searchText = getDisplayValue(selectedItem);
    }
    if ($scope.itemChange && selectedItem !== previousSelectedItem)
      $scope.itemChange(getItemScope(selectedItem));
  }

  function handleSelectedItemChange(selectedItem, previousSelectedItem) {
    for (var i = 0; i < selectedItemWatchers.length; ++i) {
      selectedItemWatchers[i](selectedItem, previousSelectedItem);
    }
  }

  /**
   * Register a function to be called when the selected item changes.
   * @param cb
   */
  function registerSelectedItemWatcher(cb) {
    if (selectedItemWatchers.indexOf(cb) == -1) {
      selectedItemWatchers.push(cb);
    }
  }

  /**
   * Unregister a function previously registered for selected item changes.
   * @param cb
   */
  function unregisterSelectedItemWatcher(cb) {
    var i = selectedItemWatchers.indexOf(cb);
    if (i != -1) {
      selectedItemWatchers.splice(i, 1);
    }
  }

  function handleSearchText (searchText, previousSearchText) {
    self.index = getDefaultIndex();
    //-- do nothing on init
    if (searchText === previousSearchText) return;
    //-- clear selected item if search text no longer matches it
    if (searchText !== getDisplayValue($scope.selectedItem)) $scope.selectedItem = null;
    else return;
    //-- trigger change event if available
    if ($scope.textChange && searchText !== previousSearchText)
      $scope.textChange(getItemScope($scope.selectedItem));
    //-- cancel results if search text is not long enough
    if (!isMinLengthMet()) {
      self.loading = false;
      self.matches = [];
      self.hidden = shouldHide();
      updateMessages();
    } else {
      handleQuery();
    }
  }

  function blur () {
    hasFocus = false;
    if (!noBlur) self.hidden = true;
  }

  function focus () {
    hasFocus = true;
    //-- if searchText is null, let's force it to be a string
    if (!angular.isString($scope.searchText)) $scope.searchText = '';
    if ($scope.minLength > 0) return;
    self.hidden = shouldHide();
    if (!self.hidden) handleQuery();
  }

  function keydown (event) {
    switch (event.keyCode) {
      case $mdConstant.KEY_CODE.DOWN_ARROW:
        if (self.loading) return;
        event.preventDefault();
        self.index = Math.min(self.index + 1, self.matches.length - 1);
        updateScroll();
        updateSelectionMessage();
        break;
      case $mdConstant.KEY_CODE.UP_ARROW:
        if (self.loading) return;
        event.preventDefault();
        self.index = self.index < 0 ? self.matches.length - 1 : Math.max(0, self.index - 1);
        updateScroll();
        updateSelectionMessage();
        break;
      case $mdConstant.KEY_CODE.TAB:
      case $mdConstant.KEY_CODE.ENTER:
        if (self.hidden || self.loading || self.index < 0 || self.matches.length < 1) return;
        event.preventDefault();
        select(self.index);
        break;
      case $mdConstant.KEY_CODE.ESCAPE:
        self.matches = [];
        self.hidden = true;
        self.index = getDefaultIndex();
        break;
      default:
    }
  }

  //-- getters

  function getMinLength () {
    return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
  }

  function getDisplayValue (item) {
    return (item && $scope.itemText) ? $scope.itemText(getItemScope(item)) : item;
  }

  function getItemScope (item) {
    if (!item) return;
    var locals = {};
    if (self.itemName) locals[self.itemName] = item;
    return locals;
  }

  function getDefaultIndex () {
    return $scope.autoselect ? 0 : -1;
  }

  function shouldHide () {
    if (!isMinLengthMet()) return true;
  }

  function getCurrentDisplayValue () {
    return getDisplayValue(self.matches[self.index]);
  }

  function isMinLengthMet () {
    return $scope.searchText.length >= getMinLength();
  }

  //-- actions

  function select (index) {
    $scope.selectedItem = self.matches[index];
    $scope.searchText = getDisplayValue($scope.selectedItem) || $scope.searchText;
    self.hidden = true;
    self.index = 0;
    self.matches = [];
  }

  function clearValue () {
    $scope.searchText = '';
    select(-1);

    // Per http://www.w3schools.com/jsref/event_oninput.asp
    var eventObj = document.createEvent('CustomEvent');
    eventObj.initCustomEvent('input', true, true, {value: $scope.searchText});
    elements.input.dispatchEvent(eventObj);

    elements.input.focus();
  }

  function fetchResults (searchText) {
    var items = $scope.$parent.$eval(itemExpr),
        term = searchText.toLowerCase();
    if (angular.isArray(items)) {
      handleResults(items);
    } else {
      self.loading = true;
      if (items.success) items.success(handleResults);
      if (items.then)    items.then(handleResults);
      if (items.error)   items.error(function () { self.loading = false; });
    }
    function handleResults (matches) {
      cache[term] = matches;
      self.loading = false;
      if (searchText !== $scope.searchText) return; //-- just cache the results if old request
      promise = null;
      self.matches = matches;
      self.hidden = shouldHide();
      updateMessages();
      positionDropdown();
    }
  }

  function updateMessages () {
    if (self.hidden) return;
    switch (self.matches.length) {
      case 0:  return self.messages.splice(0);
      case 1:  return self.messages.push({ display: 'There is 1 match available.' });
      default: return self.messages.push({ display: 'There are '
          + self.matches.length
          + ' matches available.' });
    }
  }

  function updateSelectionMessage () {
    self.messages.push({ display: getCurrentDisplayValue() });
  }

  function updateScroll () {
    var li  = elements.li[self.index],
        top = li.offsetTop,
        bot = top + li.offsetHeight,
        hgt = elements.ul.clientHeight;
    if (top < elements.ul.scrollTop) {
      elements.ul.scrollTop = top;
    } else if (bot > elements.ul.scrollTop + hgt) {
      elements.ul.scrollTop = bot - hgt;
    }
  }

  function handleQuery () {
    var searchText = $scope.searchText,
        term = searchText.toLowerCase();
    //-- cancel promise if a promise is in progress
    if (promise && promise.cancel) {
      promise.cancel();
      promise = null;
    }
    //-- if results are cached, pull in cached results
    if (!$scope.noCache && cache[term]) {
      self.matches = cache[term];
      updateMessages();
    } else {
      fetchResults(searchText);
    }
    self.hidden = shouldHide();
  }

}
MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$timeout", "$mdTheming", "$window", "$animate", "$rootElement"];

angular
    .module('material.components.autocomplete')
    .directive('mdAutocomplete', MdAutocomplete);

/**
 * @ngdoc directive
 * @name mdAutocomplete
 * @module material.components.autocomplete
 *
 * @description
 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a custom query.
 * This component allows you to provide real-time suggestions as the user types in the input area.
 *
 * To start, you will need to specify the required parameters and provide a template for your results.
 * The content inside `md-autocomplete` will be treated as a template.
 *
 * In more complex cases, you may want to include other content such as a message to display when
 * no matches were found.  You can do this by wrapping your template in `md-item-template` and adding
 * a tag for `md-not-found`.  An example of this is shown below.
 *
 * @param {expression} md-items An expression in the format of `item in items` to iterate over matches for your search.
 * @param {expression} md-selected-item-change An expression to be run each time a new item is selected
 * @param {expression} md-search-text-change An expression to be run each time the search text updates
 * @param {string=} md-search-text A model to bind the search query text to
 * @param {object=} md-selected-item A model to bind the selected item to
 * @param {string=} md-item-text An expression that will convert your object to a single string.
 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will make suggestions
 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking for results
 * @param {boolean=} md-autofocus If true, will immediately focus the input element
 * @param {boolean=} md-autoselect If true, the first item will be selected by default
 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
 *
 * @usage
 * ###Basic Example
 * <hljs lang="html">
 *   <md-autocomplete
 *       md-selected-item="selectedItem"
 *       md-search-text="searchText"
 *       md-items="item in getMatches(searchText)"
 *       md-item-text="item.display">
 *     <span md-highlight-text="searchText">{{item.display}}</span>
 *   </md-autocomplete>
 * </hljs>
 *
 * ###Example with "not found" message
 * <hljs lang="html">
 * <md-autocomplete
 *     md-selected-item="selectedItem"
 *     md-search-text="searchText"
 *     md-items="item in getMatches(searchText)"
 *     md-item-text="item.display">
 *   <md-item-template>
 *     <span md-highlight-text="searchText">{{item.display}}</span>
 *   </md-item-template>
 *   <md-not-found>
 *     No matches found.
 *   </md-not-found>
 * </md-autocomplete>
 * </hljs>
 *
 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different
 * parts that make up our component.
 */

function MdAutocomplete ($mdTheming, $mdUtil) {
  return {
    controller:   'MdAutocompleteCtrl',
    controllerAs: '$mdAutocompleteCtrl',
    link:         link,
    scope:        {
      name:          '@',
      searchText:    '=?mdSearchText',
      selectedItem:  '=?mdSelectedItem',
      itemsExpr:     '@mdItems',
      itemText:      '&mdItemText',
      placeholder:   '@placeholder',
      noCache:       '=?mdNoCache',
      itemChange:    '&?mdSelectedItemChange',
      textChange:    '&?mdSearchTextChange',
      minLength:     '=?mdMinLength',
      delay:         '=?mdDelay',
      autofocus:     '=?mdAutofocus',
      floatingLabel: '@?mdFloatingLabel',
      autoselect:    '=?mdAutoselect',
      menuClass:     '@?mdMenuClass'
    },
    template: function (element, attr) {
      return '\
        <md-autocomplete-wrap role="listbox">\
          ' + getInputElement() + '\
          <button\
              type="button"\
              tabindex="-1"\
              ng-if="$mdAutocompleteCtrl.scope.searchText && !isDisabled"\
              ng-click="$mdAutocompleteCtrl.clear()">\
            <md-icon md-svg-icon="md-cancel"></md-icon>\
            <span class="md-visually-hidden">Clear</span>\
          </button>\
          <md-progress-linear\
              ng-if="$mdAutocompleteCtrl.loading"\
              md-mode="indeterminate"></md-progress-linear>\
          <ul role="presentation"\
              class="md-autocomplete-suggestions {{menuClass || \'\'}}"\
              id="ul-{{$mdAutocompleteCtrl.id}}"\
              ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
              ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
              ng-mouseup="$mdAutocompleteCtrl.mouseUp()">\
            <li ng-repeat="(index, item) in $mdAutocompleteCtrl.matches"\
                ng-class="{ selected: index === $mdAutocompleteCtrl.index }"\
                ng-hide="$mdAutocompleteCtrl.hidden"\
                ng-click="$mdAutocompleteCtrl.select(index)"\
                md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\
                ' + getItemTemplate() + '\
            </li>\
            ' + getNoItemsTemplate() + '\
          </ul>\
        </md-autocomplete-wrap>\
        <aria-status\
            class="md-visually-hidden"\
            role="status"\
            aria-live="assertive">\
          <p ng-repeat="message in $mdAutocompleteCtrl.messages">{{message.display}}</p>\
        </aria-status>';

      function getItemTemplate() {
        var templateTag = element.find('md-item-template').remove();
        return templateTag.length ? templateTag.html() : element.html();
      }

      function getNoItemsTemplate() {
        var templateTag = element.find('md-not-found').remove(),
            template = templateTag.length ? templateTag.html() : '';
        return template
            ? '<li ng-if="!$mdAutocompleteCtrl.matches.length && !$mdAutocompleteCtrl.loading\
                         && !$mdAutocompleteCtrl.hidden"\
                         ng-hide="$mdAutocompleteCtrl.hidden"\
                         md-autocomplete-parent-scope>' + template + '</li>'
            : '';

      }

      function getInputElement() {
        if (attr.mdFloatingLabel) {
          return '\
            <md-input-container ng-if="floatingLabel">\
              <label>{{floatingLabel}}</label>\
              <input type="search"\
                  id="fl-input-{{$mdAutocompleteCtrl.id}}"\
                  name="{{name}}"\
                  autocomplete="off"\
                  ng-disabled="isDisabled"\
                  ng-model="$mdAutocompleteCtrl.scope.searchText"\
                  ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                  ng-blur="$mdAutocompleteCtrl.blur()"\
                  ng-focus="$mdAutocompleteCtrl.focus()"\
                  aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
                  aria-label="{{floatingLabel}}"\
                  aria-autocomplete="list"\
                  aria-haspopup="true"\
                  aria-activedescendant=""\
                  aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
            </md-input-container>';
        } else {
          return '\
            <input type="search"\
                id="input-{{$mdAutocompleteCtrl.id}}"\
                name="{{name}}"\
                ng-if="!floatingLabel"\
                autocomplete="off"\
                ng-disabled="isDisabled"\
                ng-model="$mdAutocompleteCtrl.scope.searchText"\
                ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                ng-blur="$mdAutocompleteCtrl.blur()"\
                ng-focus="$mdAutocompleteCtrl.focus()"\
                placeholder="{{placeholder}}"\
                aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
                aria-label="{{placeholder}}"\
                aria-autocomplete="list"\
                aria-haspopup="true"\
                aria-activedescendant=""\
                aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>';
        }
      }
    }
  };

  function link (scope, element, attr) {
    attr.$observe('disabled', function (value) { scope.isDisabled = value; });

    $mdUtil.initOptionalProperties(scope, attr, {searchText:null, selectedItem:null} );

    $mdTheming(element);
  }
}
MdAutocomplete.$inject = ["$mdTheming", "$mdUtil"];

angular
    .module('material.components.autocomplete')
    .controller('MdHighlightCtrl', MdHighlightCtrl);

function MdHighlightCtrl ($scope, $element, $interpolate) {
  var term = $element.attr('md-highlight-text'),
      text = $interpolate($element.text())($scope),
      flags = $element.attr('md-highlight-flags') || '',
      watcher = $scope.$watch(term, function (term) {
        var regex = getRegExp(term, flags),
            html = text.replace(regex, '<span class="highlight">$&</span>');
        $element.html(html);
      });
  $element.on('$destroy', function () { watcher(); });

  function sanitize (term) {
    if (!term) return term;
    return term.replace(/[\\\^\$\*\+\?\.\(\)\|\{\}\[\]]/g, '\\$&');
  }

  function getRegExp (text, flags) {
    var str = '';
    if (flags.indexOf('^') >= 1) str += '^';
    str += text;
    if (flags.indexOf('$') >= 1) str += '$';
    return new RegExp(sanitize(str), flags.replace(/[\$\^]/g, ''));
  }
}
MdHighlightCtrl.$inject = ["$scope", "$element", "$interpolate"];

angular
    .module('material.components.autocomplete')
    .directive('mdHighlightText', MdHighlight);

/**
 * @ngdoc directive
 * @name mdHighlightText
 * @module material.components.autocomplete
 *
 * @description
 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
 * an element.  Highlighted text will be wrapped in `<span class="highlight"></span>` which can
 * be styled through CSS.  Please note that child elements may not be used with this directive.
 *
 * @param {string} md-highlight-text A model to be searched for
 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
 *    #### **Supported flags**:
 *    - `g`: Find all matches within the provided text
 *    - `i`: Ignore case when searching for matches
 *    - `$`: Only match if the text ends with the search term
 *    - `^`: Only match if the text begins with the search term
 *
 * @usage
 * <hljs lang="html">
 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
 * <ul>
 *   <li ng-repeat="result in results" md-highlight-text="searchTerm">
 *     {{result.text}}
 *   </li>
 * </ul>
 * </hljs>
 */

function MdHighlight () {
  return {
    terminal: true,
    scope: false,
    controller: 'MdHighlightCtrl'
  };
}

angular
    .module('material.components.autocomplete')
    .directive('mdAutocompleteListItem', MdAutocompleteListItem);

function MdAutocompleteListItem ($compile, $mdUtil) {
  return {
    terminal: true,
    link: postLink,
    scope: false
  };
  function postLink (scope, element, attr) {
    var ctrl     = scope.$parent.$mdAutocompleteCtrl,
        newScope = ctrl.parent.$new(false, ctrl.parent),
        itemName = ctrl.scope.$eval(attr.mdAutocompleteListItem);
    newScope[itemName] = scope.item;
    $compile(element.contents())(newScope);
    element.attr({
      role: 'option',
      id: 'item_' + $mdUtil.nextUid()
    });
  }
}
MdAutocompleteListItem.$inject = ["$compile", "$mdUtil"];

angular
    .module('material.components.autocomplete')
    .directive('mdAutocompleteParentScope', MdAutocompleteParentScope);

function MdAutocompleteParentScope ($compile, $mdUtil) {
  return {
    restrict: 'A',
    terminal: true,
    link: postLink,
    scope: false
  };
  function postLink (scope, element, attr) {
    var ctrl     = scope.$parent.$mdAutocompleteCtrl;
    $compile(element.contents())(ctrl.parent);
  }
}
MdAutocompleteParentScope.$inject = ["$compile", "$mdUtil"];

})(window, window.angular);