/*!
 * AngularJS Material Design
 * https://github.com/angular/material
 * @license MIT
 * v1.2.3
 */
(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',
  'material.components.virtualRepeat'
]);


MdAutocompleteCtrl['$inject'] = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
    .module('material.components.autocomplete')
    .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);

var ITEM_HEIGHT   = 48,
    MAX_ITEMS     = 5,
    MENU_PADDING  = 8,
    INPUT_PADDING = 2, // Padding provided by `md-input-container`
    MODE_STANDARD = 'standard',
    MODE_VIRTUAL = 'virtual';

function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
                             $animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {

  // Internal Variables.
  var ctrl                 = this,
      itemParts            = $scope.itemsExpr.split(/ in /i),
      itemExpr             = itemParts[ 1 ],
      elements             = null,
      cache                = {},
      noBlur               = false,
      selectedItemWatchers = [],
      hasFocus             = false,
      fetchesInProgress    = 0,
      enableWrapScroll     = null,
      inputModelCtrl       = null,
      debouncedOnResize    = $mdUtil.debounce(onWindowResize),
      mode                 = MODE_VIRTUAL; // default

  /**
   * The root document element. This is used for attaching a top-level click handler to
   * close the options panel when a click outside said panel occurs. We use `documentElement`
   * instead of body because, when scrolling is disabled, some browsers consider the body element
   * to be completely off the screen and propagate events directly to the html element.
   * @type {!Object} angular.JQLite
   */
  ctrl.documentElement = angular.element(document.documentElement);

  // Public Exported Variables with handlers
  defineProperty('hidden', handleHiddenChange, true);

  // Public Exported Variables
  ctrl.scope = $scope;
  ctrl.parent = $scope.$parent;
  ctrl.itemName = itemParts[0];
  ctrl.matches = [];
  ctrl.loading = false;
  ctrl.hidden = true;
  ctrl.index = -1;
  ctrl.activeOption = null;
  ctrl.id = $mdUtil.nextUid();
  ctrl.isDisabled = null;
  ctrl.isRequired = null;
  ctrl.isReadonly = null;
  ctrl.hasNotFound = false;
  ctrl.selectedMessage = $scope.selectedMessage || 'selected';
  ctrl.noMatchMessage = $scope.noMatchMessage || 'There are no matches available.';
  ctrl.singleMatchMessage = $scope.singleMatchMessage || 'There is 1 match available.';
  ctrl.multipleMatchStartMessage = $scope.multipleMatchStartMessage || 'There are ';
  ctrl.multipleMatchEndMessage = $scope.multipleMatchEndMessage || ' matches available.';
  ctrl.defaultEscapeOptions = 'clear';

  // Public Exported Methods
  ctrl.keydown = keydown;
  ctrl.blur = blur;
  ctrl.focus = focus;
  ctrl.clear = clearValue;
  ctrl.select = select;
  ctrl.listEnter = onListEnter;
  ctrl.listLeave = onListLeave;
  ctrl.focusInput = focusInputElement;
  ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
  ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
  ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
  ctrl.notFoundVisible = notFoundVisible;
  ctrl.loadingIsVisible = loadingIsVisible;
  ctrl.positionDropdown = positionDropdown;

  /**
   * Report types to be used for the $mdLiveAnnouncer
   * @enum {number} Unique flag id.
   */
  var ReportType = {
    Count: 1,
    Selected: 2
  };

  return init();

  // initialization methods

  /**
   * Initialize the controller, setup watchers, gather elements
   */
  function init () {

    $mdUtil.initOptionalProperties($scope, $attrs, {
      searchText: '',
      selectedItem: null,
      clearButton: false,
      disableVirtualRepeat: false,
    });

    $mdTheming($element);
    configureWatchers();
    $mdUtil.nextTick(function () {

      gatherElements();
      moveDropdown();

      // Touch devices often do not send a click event on tap. We still want to focus the input
      // and open the options pop-up in these cases.
      $element.on('touchstart', focusInputElement);

      // Forward all focus events to the input element when autofocus is enabled
      if ($scope.autofocus) {
        $element.on('focus', focusInputElement);
      }
      if ($scope.inputAriaDescribedBy) {
        elements.input.setAttribute('aria-describedby', $scope.inputAriaDescribedBy);
      }
      if (!$scope.floatingLabel) {
        if ($scope.inputAriaLabel) {
          elements.input.setAttribute('aria-label', $scope.inputAriaLabel);
        } else if ($scope.inputAriaLabelledBy) {
          elements.input.setAttribute('aria-labelledby', $scope.inputAriaLabelledBy);
        } else if ($scope.placeholder) {
          // If no aria-label or aria-labelledby references are defined, then just label using the
          // placeholder.
          elements.input.setAttribute('aria-label', $scope.placeholder);
        }
      }
    });
  }

  function updateModelValidators() {
    if (!$scope.requireMatch || !inputModelCtrl) return;

    inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
  }

  /**
   * Calculates the dropdown's position and applies the new styles to the menu element
   * @returns {*}
   */
  function positionDropdown () {
    if (!elements) {
      return $mdUtil.nextTick(positionDropdown, false, $scope);
    }

    var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
    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,
        offset = getVerticalOffset(),
        position = $scope.dropdownPosition,
        styles, enoughBottomSpace, enoughTopSpace;
    var bottomSpace = root.bottom - vrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
    var topSpace = vrect.top - MENU_PADDING;

    // Automatically determine dropdown placement based on available space in viewport.
    if (!position) {
      enoughTopSpace = topSpace > dropdownHeight;
      enoughBottomSpace = bottomSpace > dropdownHeight;
      if (enoughBottomSpace) {
        position = 'bottom';
      } else if (enoughTopSpace) {
        position = 'top';
      } else {
        position = topSpace > bottomSpace ? 'top' : 'bottom';
      }
    }
    // Adjust the width to account for the padding provided by `md-input-container`
    if ($attrs.mdFloatingLabel) {
      left += INPUT_PADDING;
      width -= INPUT_PADDING * 2;
    }
    styles = {
      left:     left + 'px',
      minWidth: width + 'px',
      maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
    };

    if (position === 'top') {
      styles.top       = 'auto';
      styles.bottom    = bot + 'px';
      styles.maxHeight = Math.min(dropdownHeight, topSpace) + 'px';
    } else {
      bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();

      styles.top       = (top - offset) + 'px';
      styles.bottom    = 'auto';
      styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
    }

    elements.$.scrollContainer.css(styles);
    $mdUtil.nextTick(correctHorizontalAlignment, false, $scope);

    /**
     * Calculates the vertical offset for floating label examples to account for ngMessages
     * @returns {number}
     */
    function getVerticalOffset () {
      var offset = 0;
      var inputContainer = $element.find('md-input-container');
      if (inputContainer.length) {
        var input = inputContainer.find('input');
        offset = inputContainer.prop('offsetHeight');
        offset -= input.prop('offsetTop');
        offset -= input.prop('offsetHeight');
        // add in the height left up top for the floating label text
        offset += inputContainer.prop('offsetTop');
      }
      return offset;
    }

    /**
     * Makes sure that the menu doesn't go off of the screen on either side.
     */
    function correctHorizontalAlignment () {
      var dropdown = elements.scrollContainer.getBoundingClientRect(),
          styles   = {};
      if (dropdown.right > root.right) {
        styles.left = (hrect.right - dropdown.width) + 'px';
      }
      elements.$.scrollContainer.css(styles);
    }
  }

  /**
   * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
   */
  function moveDropdown () {
    if (!elements.$.root.length) return;
    $mdTheming(elements.$.scrollContainer);
    elements.$.scrollContainer.detach();
    elements.$.root.append(elements.$.scrollContainer);
    if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
  }

  /**
   * Sends focus to the input element.
   */
  function focusInputElement () {
    elements.input.focus();
  }

  /**
   * Update the activeOption based on the selected item in the listbox.
   * The activeOption is used in the template to set the aria-activedescendant attribute, which
   * enables screen readers to properly handle visual focus within the listbox and announce the
   * item's place in the list. I.e. "List item 3 of 50". Anytime that `ctrl.index` changes, this
   * function needs to be called to update the activeOption.
   */
  function updateActiveOption() {
    var selectedOption = elements.scroller.querySelector('.selected');
    if (selectedOption) {
      ctrl.activeOption = selectedOption.id;
    } else {
      ctrl.activeOption = null;
    }
  }

  /**
   * Sets up any watchers used by autocomplete
   */
  function configureWatchers () {
    var wait = parseInt($scope.delay, 10) || 0;

    $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
    $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
    $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });

    $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
    $scope.$watch('selectedItem', selectedItemChange);

    angular.element($window).on('resize', debouncedOnResize);

    $scope.$on('$destroy', cleanup);
  }

  /**
   * Removes any events or leftover elements created by this controller
   */
  function cleanup () {
    if (!ctrl.hidden) {
      $mdUtil.enableScrolling();
    }

    angular.element($window).off('resize', debouncedOnResize);

    if (elements){
      var items = ['ul', 'scroller', 'scrollContainer', 'input'];
      angular.forEach(items, function(key){
        elements.$[key].remove();
      });
    }
  }

  /**
   * Event handler to be called whenever the window resizes.
   */
  function onWindowResize() {
    if (!ctrl.hidden) {
      positionDropdown();
    }
  }

  /**
   * Gathers all of the elements needed for this controller
   */
  function gatherElements () {

    var snapWrap = gatherSnapWrap();

    elements = {
      main:  $element[0],
      scrollContainer: $element[0].querySelector('.md-virtual-repeat-container, .md-standard-list-container'),
      scroller: $element[0].querySelector('.md-virtual-repeat-scroller, .md-standard-list-scroller'),
      ul:    $element.find('ul')[0],
      input: $element.find('input')[0],
      wrap:  snapWrap.wrap,
      snap:  snapWrap.snap,
      root:  document.body,
    };

    elements.li   = elements.ul.getElementsByTagName('li');
    elements.$    = getAngularElements(elements);
    mode = elements.scrollContainer.classList.contains('md-standard-list-container') ? MODE_STANDARD : MODE_VIRTUAL;
    inputModelCtrl = elements.$.input.controller('ngModel');
  }

  /**
   * Gathers the snap and wrap elements
   *
   */
  function gatherSnapWrap() {
    var element;
    var value;
    for (element = $element; element.length; element = element.parent()) {
      value = element.attr('md-autocomplete-snap');
      if (angular.isDefined(value)) break;
    }

    if (element.length) {
      return {
        snap: element[0],
        wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
      };
    }

    var wrap = $element.find('md-autocomplete-wrap')[0];
    return {
      snap: wrap,
      wrap: wrap
    };
  }

  /**
   * Gathers angular-wrapped versions of each element
   * @param elements
   * @returns {{}}
   */
  function getAngularElements (elements) {
    var obj = {};
    for (var key in elements) {
      if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
    }
    return obj;
  }

  // event/change handlers

  /**
   * @param {Event} $event
   */
  function preventDefault($event) {
    $event.preventDefault();
  }

  /**
   * @param {Event} $event
   */
  function stopPropagation($event) {
    $event.stopPropagation();
  }

  /**
   * Handles changes to the `hidden` property.
   * @param {boolean} hidden true to hide the options pop-up, false to show it.
   * @param {boolean} oldHidden the previous value of hidden
   */
  function handleHiddenChange (hidden, oldHidden) {
    var scrollContainerElement;

    if (elements) {
      scrollContainerElement = angular.element(elements.scrollContainer);
    }
    if (!hidden && oldHidden) {
      positionDropdown();

      // Report in polite mode, because the screen reader should finish the default description of
      // the input element.
      reportMessages(true, ReportType.Count | ReportType.Selected);

      if (elements) {
        $mdUtil.disableScrollAround(elements.scrollContainer);
        enableWrapScroll = disableElementScrollEvents(elements.wrap);
        if ($mdUtil.isIos) {
          ctrl.documentElement.on('touchend', handleTouchOutsidePanel);
          if (scrollContainerElement) {
            scrollContainerElement.on('touchstart touchmove touchend', stopPropagation);
          }
        }
        ctrl.index = getDefaultIndex();
        $mdUtil.nextTick(function() {
          updateActiveOption();
          updateScroll();
        });
      }
    } else if (hidden && !oldHidden) {
      if ($mdUtil.isIos) {
        ctrl.documentElement.off('touchend', handleTouchOutsidePanel);
        if (scrollContainerElement) {
          scrollContainerElement.off('touchstart touchmove touchend', stopPropagation);
        }
      }
      $mdUtil.enableScrolling();

      if (enableWrapScroll) {
        enableWrapScroll();
        enableWrapScroll = null;
      }
    }
  }

  /**
   * Handling touch events that bubble up to the document is required for closing the dropdown
   * panel on touch outside of the options pop-up panel on iOS.
   * @param {Event} $event
   */
  function handleTouchOutsidePanel($event) {
    ctrl.hidden = true;
    // iOS does not blur the pop-up for touches on the scroll mask, so we have to do it.
    doBlur(true);
  }

  /**
   * Disables scrolling for a specific element.
   * @param {!string|!DOMElement} element to disable scrolling
   * @return {Function} function to call to re-enable scrolling for the element
   */
  function disableElementScrollEvents(element) {
    var elementToDisable = angular.element(element);
    elementToDisable.on('wheel touchmove', preventDefault);

    return function() {
      elementToDisable.off('wheel touchmove', preventDefault);
    };
  }

  /**
   * When the user mouses over the dropdown menu, ignore blur events.
   */
  function onListEnter () {
    noBlur = true;
  }

  /**
   * When the user's mouse leaves the menu, blur events may hide the menu again.
   */
  function onListLeave () {
    if (!hasFocus && !ctrl.hidden) elements.input.focus();
    noBlur = false;
    ctrl.hidden = shouldHide();
  }

  /**
   * Handles changes to the selected item.
   * @param selectedItem
   * @param previousSelectedItem
   */
  function selectedItemChange (selectedItem, previousSelectedItem) {

    updateModelValidators();

    if (selectedItem) {
      getDisplayValue(selectedItem).then(function (val) {
        $scope.searchText = val;
        handleSelectedItemChange(selectedItem, previousSelectedItem);
      });
    } else if (previousSelectedItem && $scope.searchText) {
      getDisplayValue(previousSelectedItem).then(function(displayValue) {
        // Clear the searchText, when the selectedItem is set to null.
        // Do not clear the searchText, when the searchText isn't matching with the previous
        // selected item.
        if (angular.isString($scope.searchText)
          && displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
          $scope.searchText = '';
        }
      });
    }

    if (selectedItem !== previousSelectedItem) {
      announceItemChange();
    }
  }

  /**
   * Use the user-defined expression to announce changes each time a new item is selected
   */
  function announceItemChange () {
    angular.isFunction($scope.itemChange) &&
      $scope.itemChange(getItemAsNameVal($scope.selectedItem));
  }

  /**
   * Use the user-defined expression to announce changes each time the search text is changed
   */
  function announceTextChange () {
    angular.isFunction($scope.textChange) && $scope.textChange();
  }

  /**
   * Calls any external watchers listening for the selected item.  Used in conjunction with
   * `registerSelectedItemWatcher`.
   * @param selectedItem
   * @param previousSelectedItem
   */
  function handleSelectedItemChange (selectedItem, previousSelectedItem) {
    selectedItemWatchers.forEach(function (watcher) {
      watcher(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);
    }
  }

  /**
   * Handles changes to the searchText property.
   * @param {string} searchText
   * @param {string} previousSearchText
   */
  function handleSearchText (searchText, previousSearchText) {
    ctrl.index = getDefaultIndex();

    // do nothing on init
    if (searchText === previousSearchText) return;

    updateModelValidators();

    getDisplayValue($scope.selectedItem).then(function (val) {
      // clear selected item if search text no longer matches it
      if (searchText !== val) {
        $scope.selectedItem = null;

        // trigger change event if available
        if (searchText !== previousSearchText) {
          announceTextChange();
        }

        // cancel results if search text is not long enough
        if (!isMinLengthMet()) {
          ctrl.matches = [];

          setLoading(false);
          reportMessages(true, ReportType.Count);

        } else {
          handleQuery();
        }
      }
    });

  }

  /**
   * Handles input blur event, determines if the dropdown should hide.
   * @param {Event=} $event
   */
  function blur($event) {
    hasFocus = false;

    if (!noBlur) {
      ctrl.hidden = shouldHide();
      evalAttr('ngBlur', { $event: $event });
    } else if (angular.isObject($event)) {
      $event.stopImmediatePropagation();
    }
  }

  /**
   * Force blur on input element
   * @param {boolean} forceBlur
   */
  function doBlur(forceBlur) {
    if (forceBlur) {
      noBlur = false;
      hasFocus = false;
    }
    elements.input.blur();
  }

  /**
   * Handles input focus event, determines if the dropdown should show.
   */
  function focus($event) {
    hasFocus = true;

    if (isSearchable() && isMinLengthMet()) {
      handleQuery();
    }

    ctrl.hidden = shouldHide();

    evalAttr('ngFocus', { $event: $event });
  }

  /**
   * Handles keyboard input.
   * @param event
   */
  function keydown (event) {
    switch (event.keyCode) {
      case $mdConstant.KEY_CODE.DOWN_ARROW:
        if (ctrl.loading || hasSelection()) return;
        event.stopPropagation();
        event.preventDefault();
        ctrl.index = ctrl.index + 1 > ctrl.matches.length - 1 ? 0 : Math.min(ctrl.index + 1, ctrl.matches.length - 1);
        $mdUtil.nextTick(updateActiveOption);
        updateScroll();
        break;
      case $mdConstant.KEY_CODE.UP_ARROW:
        if (ctrl.loading || hasSelection()) return;
        event.stopPropagation();
        event.preventDefault();
        ctrl.index = ctrl.index - 1 < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
        $mdUtil.nextTick(updateActiveOption);
        updateScroll();
        break;
      case $mdConstant.KEY_CODE.TAB:
        // If we hit tab, assume that we've left the list so it will close
        onListLeave();

        if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
        select(ctrl.index);
        break;
      case $mdConstant.KEY_CODE.ENTER:
        if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
        if (hasSelection()) return;
        event.stopImmediatePropagation();
        event.preventDefault();
        select(ctrl.index);
        break;
      case $mdConstant.KEY_CODE.ESCAPE:
        event.preventDefault(); // Prevent browser from always clearing input
        if (!shouldProcessEscape()) return;
        event.stopPropagation();

        clearSelectedItem();
        if ($scope.searchText && hasEscapeOption('clear')) {
          clearSearchText();
        }

        // Manually hide (needed for mdNotFound support)
        ctrl.hidden = true;

        if (hasEscapeOption('blur')) {
          // Force the component to blur if they hit escape
          doBlur(true);
        }

        break;
      default:
    }
  }

  // getters

  /**
   * Returns the minimum length needed to display the dropdown.
   * @returns {*}
   */
  function getMinLength () {
    return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
  }

  /**
   * Returns the display value for an item.
   * @param {*} item
   * @returns {*}
   */
  function getDisplayValue (item) {
    return $q.when(getItemText(item) || item).then(function(itemText) {
      if (itemText && !angular.isString(itemText)) {
        $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
          'Please check the `md-item-text` attribute.');
      }

      return itemText;
    });

    /**
     * Getter function to invoke user-defined expression (in the directive)
     * to convert your object to a single string.
     * @param {*} item
     * @returns {string|null}
     */
    function getItemText (item) {
      return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
    }
  }

  /**
   * Returns the locals object for compiling item templates.
   * @param {*} item
   * @returns {Object|undefined}
   */
  function getItemAsNameVal (item) {
    if (!item) {
      return undefined;
    }

    var locals = {};
    if (ctrl.itemName) {
      locals[ ctrl.itemName ] = item;
    }

    return locals;
  }

  /**
   * Returns the default index based on whether or not autoselect is enabled.
   * @returns {number} 0 if autoselect is enabled, -1 if not.
   */
  function getDefaultIndex () {
    return $scope.autoselect ? 0 : -1;
  }

  /**
   * Sets the loading parameter and updates the hidden state.
   * @param value {boolean} Whether or not the component is currently loading.
   */
  function setLoading(value) {
    if (ctrl.loading !== value) {
      ctrl.loading = value;
    }

    // Always refresh the hidden variable as something else might have changed
    ctrl.hidden = shouldHide();
  }

  /**
   * Determines if the menu should be hidden.
   * @returns {boolean} true if the menu should be hidden
   */
  function shouldHide () {
    return !shouldShow();
  }

  /**
   * Determines whether the autocomplete is able to query within the current state.
   * @returns {boolean} true if the query can be run
   */
  function isSearchable() {
    if (ctrl.loading && !hasMatches()) {
      // No query when query is in progress.
      return false;
    } else if (hasSelection()) {
      // No query if there is already a selection
      return false;
    }
    else if (!hasFocus) {
      // No query if the input does not have focus
      return false;
    }
    return true;
  }

  /**
   * @returns {boolean} if the escape keydown should be processed, return true.
   *  Otherwise return false.
   */
  function shouldProcessEscape() {
    return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
  }

  /**
   * @param {string} option check if this option is set
   * @returns {boolean} if the specified escape option is set, return true. Return false otherwise.
   */
  function hasEscapeOption(option) {
    if (!angular.isString($scope.escapeOptions)) {
      return ctrl.defaultEscapeOptions.indexOf(option) !== -1;
    } else {
      return $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
    }
  }

  /**
   * Determines if the menu should be shown.
   * @returns {boolean} true if the menu should be shown
   */
  function shouldShow() {
    if (ctrl.isReadonly) {
      // Don't show if read only is set
      return false;
    } else if (!isSearchable()) {
      // Don't show if a query is in progress, there is already a selection,
      // or the input is not focused.
      return false;
    }
    return (isMinLengthMet() && hasMatches()) || notFoundVisible();
  }

  /**
   * @returns {boolean} true if the search text has matches.
   */
  function hasMatches() {
    return ctrl.matches.length ? true : false;
  }

  /**
   * @returns {boolean} true if the autocomplete has a valid selection.
   */
  function hasSelection() {
    return ctrl.scope.selectedItem ? true : false;
  }

  /**
   * @returns {boolean} true if the loading indicator is, or should be, visible.
   */
  function loadingIsVisible() {
    return ctrl.loading && !hasSelection();
  }

  /**
   * @returns {*} the display value of the current item.
   */
  function getCurrentDisplayValue () {
    return getDisplayValue(ctrl.matches[ ctrl.index ]);
  }

  /**
   * Determines if the minimum length is met by the search text.
   * @returns {*} true if the minimum length is met by the search text
   */
  function isMinLengthMet () {
    return ($scope.searchText || '').length >= getMinLength();
  }

  // actions

  /**
   * Defines a public property with a handler and a default value.
   * @param {string} key
   * @param {Function} handler function
   * @param {*} defaultValue default value
   */
  function defineProperty (key, handler, defaultValue) {
    Object.defineProperty(ctrl, key, {
      get: function () { return defaultValue; },
      set: function (newValue) {
        var oldValue = defaultValue;
        defaultValue        = newValue;
        handler(newValue, oldValue);
      }
    });
  }

  /**
   * Selects the item at the given index.
   * @param {number} index to select
   */
  function select (index) {
    // force form to update state for validation
    $mdUtil.nextTick(function () {
      getDisplayValue(ctrl.matches[ index ]).then(function (val) {
        var ngModel = elements.$.input.controller('ngModel');
        $mdLiveAnnouncer.announce(val + ' ' + ctrl.selectedMessage, 'assertive');
        ngModel.$setViewValue(val);
        ngModel.$render();
      }).finally(function () {
        $scope.selectedItem = ctrl.matches[ index ];
        setLoading(false);
      });
    }, false);
  }

  /**
   * Clears the searchText value and selected item.
   * @param {Event} $event
   */
  function clearValue ($event) {
    if ($event) {
      $event.stopPropagation();
    }
    clearSelectedItem();
    clearSearchText();
  }

  /**
   * Clears the selected item
   */
  function clearSelectedItem () {
    // Reset our variables
    ctrl.index = -1;
    $mdUtil.nextTick(updateActiveOption);
    ctrl.matches = [];
  }

  /**
   * Clears the searchText value
   */
  function clearSearchText () {
    // Set the loading to true so we don't see flashes of content.
    // The flashing will only occur when an async request is running.
    // So the loading process will stop when the results had been retrieved.
    setLoading(true);

    $scope.searchText = '';

    // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
    // But some browsers are not detecting it properly, which means that we have to trigger the event.
    // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
    // The `change` event is a good alternative and is supported by all supported browsers.
    var eventObj = document.createEvent('CustomEvent');
    eventObj.initCustomEvent('change', true, true, { value: '' });
    elements.input.dispatchEvent(eventObj);

    // For some reason, firing the above event resets the value of $scope.searchText if
    // $scope.searchText has a space character at the end, so we blank it one more time and then
    // focus.
    elements.input.blur();
    $scope.searchText = '';
    elements.input.focus();
  }

  /**
   * Fetches the results for the provided search text.
   * @param searchText
   */
  function fetchResults (searchText) {
    var items = $scope.$parent.$eval(itemExpr),
        term  = searchText.toLowerCase(),
        isList = angular.isArray(items),
        isPromise = !!items.then; // Every promise should contain a `then` property

    if (isList) onResultsRetrieved(items);
    else if (isPromise) handleAsyncResults(items);

    function handleAsyncResults(items) {
      if (!items) return;

      items = $q.when(items);
      fetchesInProgress++;
      setLoading(true);

      $mdUtil.nextTick(function () {
          items
            .then(onResultsRetrieved)
            .finally(function(){
              if (--fetchesInProgress === 0) {
                setLoading(false);
              }
            });
      },true, $scope);
    }

    function onResultsRetrieved(matches) {
      cache[term] = matches;

      // Just cache the results if the request is now outdated.
      // The request becomes outdated, when the new searchText has changed during the result fetching.
      if ((searchText || '') !== ($scope.searchText || '')) {
        return;
      }

      handleResults(matches);
    }
  }


  /**
   * Reports given message types to supported screen readers.
   * @param {boolean} isPolite Whether the announcement should be polite.
   * @param {!number} types Message flags to be reported to the screen reader.
   */
  function reportMessages(isPolite, types) {
    var politeness = isPolite ? 'polite' : 'assertive';
    var messages = [];

    if (types & ReportType.Selected && ctrl.index !== -1) {
      messages.push(getCurrentDisplayValue());
    }

    if (types & ReportType.Count) {
      messages.push($q.resolve(getCountMessage()));
    }

    $q.all(messages).then(function(data) {
      $mdLiveAnnouncer.announce(data.join(' '), politeness);
    });
  }

  /**
   * @returns {string} the ARIA message for how many results match the current query.
   */
  function getCountMessage () {
    switch (ctrl.matches.length) {
      case 0:
        return ctrl.noMatchMessage;
      case 1:
        return ctrl.singleMatchMessage;
      default:
        return ctrl.multipleMatchStartMessage + ctrl.matches.length + ctrl.multipleMatchEndMessage;
    }
  }

  /**
   * Makes sure that the focused element is within view.
   */
  function updateScroll () {
    if (!elements.li[0]) return;
    if (mode === MODE_STANDARD) {
      updateStandardScroll();
    } else {
      updateVirtualScroll();
    }
  }

  function updateVirtualScroll() {
    // elements in virtual scroll have consistent heights
    var optionHeight = elements.li[0].offsetHeight,
        top = optionHeight * Math.max(0, ctrl.index),
        bottom = top + optionHeight,
        containerHeight = elements.scroller.clientHeight,
        scrollTop = elements.scroller.scrollTop;

    if (top < scrollTop) {
      scrollTo(top);
    } else if (bottom > scrollTop + containerHeight) {
      scrollTo(bottom - containerHeight);
    }
  }

  function updateStandardScroll() {
    // elements in standard scroll have variable heights
    var selected =  elements.li[Math.max(0, ctrl.index)];
    var containerHeight = elements.scrollContainer.offsetHeight,
        top = selected && selected.offsetTop || 0,
        bottom = top + selected.clientHeight,
        scrollTop = elements.scrollContainer.scrollTop;

    if (top < scrollTop) {
      scrollTo(top);
    } else if (bottom > scrollTop + containerHeight) {
      scrollTo(bottom - containerHeight);
    }
  }

  function isPromiseFetching() {
    return fetchesInProgress !== 0;
  }

  function scrollTo (offset) {
    if (mode === MODE_STANDARD) {
      elements.scrollContainer.scrollTop = offset;
    } else {
      elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
    }
  }

  function notFoundVisible () {
    var textLength = (ctrl.scope.searchText || '').length;

    return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
  }

  /**
   * Starts the query to gather the results for the current searchText.  Attempts to return cached
   * results first, then forwards the process to `fetchResults` if necessary.
   */
  function handleQuery () {
    var searchText = $scope.searchText || '';
    var term = searchText.toLowerCase();

    // If caching is enabled and the current searchText is stored in the cache
    if (!$scope.noCache && cache[term]) {
      // The results should be handled as same as a normal un-cached request does.
      handleResults(cache[term]);
    } else {
      fetchResults(searchText);
    }

    ctrl.hidden = shouldHide();
  }

  /**
   * Handles the retrieved results by showing them in the autocompletes dropdown.
   * @param results Retrieved results
   */
  function handleResults(results) {
    ctrl.matches = results;
    ctrl.hidden  = shouldHide();

    // If loading is in progress, then we'll end the progress. This is needed for example,
    // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
    if (ctrl.loading) setLoading(false);

    if ($scope.selectOnMatch) selectItemOnMatch();

    positionDropdown();
    reportMessages(true, ReportType.Count);
  }

  /**
   * If there is only one matching item and the search text matches its display value exactly,
   * automatically select that item.  Note: This function is only called if the user uses the
   * `md-select-on-match` flag.
   */
  function selectItemOnMatch () {
    var searchText = $scope.searchText,
        matches    = ctrl.matches,
        item       = matches[ 0 ];
    if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
      var isMatching = searchText === displayValue;
      if ($scope.matchInsensitive && !isMatching) {
        isMatching = searchText.toLowerCase() === displayValue.toLowerCase();
      }

      if (isMatching) {
        select(0);
      }
    });
  }

  /**
   * Evaluates an attribute expression against the parent scope.
   * @param {String} attr Name of the attribute to be evaluated.
   * @param {Object?} locals Properties to be injected into the evaluation context.
   */
 function evalAttr(attr, locals) {
    if ($attrs[attr]) {
      $scope.$parent.$eval($attrs[attr], locals || {});
    }
  }

}


MdAutocomplete['$inject'] = ["$$mdSvgRegistry"];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.
 *
 * To reset the displayed value you must clear both values for `md-search-text` and
 * `md-selected-item`.
 *
 * ### Validation
 *
 * You can use `ng-messages` to include validation the same way that you would normally validate;
 *     however, if you want to replicate a standard input with a floating label, you will have to
 *     do the following:
 *
 * - Make sure that your template is wrapped in `md-item-template`
 * - Add your `ng-messages` code inside of `md-autocomplete`
 * - Add your validation properties to `md-autocomplete` (ie. `required`)
 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
 *
 * There is an example below of how this should look.
 *
 * ### Snapping Drop-Down
 *
 * You can cause the autocomplete drop-down to snap to an ancestor element by applying the
 *     `md-autocomplete-snap` attribute to that element. You can also snap to the width of
 *     the `md-autocomplete-snap` element by setting the attribute's value to `width`
 *     (ie. `md-autocomplete-snap="width"`).
 *
 * ### Notes
 *
 * **Autocomplete Dropdown Items Rendering**
 *
 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeat">
 *   mdVirtualRepeat</a> directive for displaying the results inside of the dropdown.<br/>
 *
 * > When encountering issues regarding the item template please take a look at the
 *   <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
 *
 * **Autocomplete inside of a Virtual Repeat**
 *
 * When using the `md-autocomplete` directive inside of a
 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items
 * might not update properly, because caching of the results is enabled by default.
 *
 * The autocomplete will then show invalid dropdown items, because the Virtual Repeat only updates
 * the scope bindings rather than re-creating the `md-autocomplete`. This means that the previous
 * cached results will be used.
 *
 * > To avoid such problems, ensure that the autocomplete does not cache any results via
 * `md-no-cache="true"`:
 *
 * <hljs lang="html">
 *   <md-autocomplete
 *       md-no-cache="true"
 *       md-selected-item="selectedItem"
 *       md-items="item in items"
 *       md-search-text="searchText"
 *       md-item-text="item.display">
 *     <span>{{ item.display }}</span>
 *   </md-autocomplete>
 * </hljs>
 *
 *
 * @param {expression} md-items An expression in the format of `item in results` to iterate over
 *     matches for your search.<br/><br/>
 *     The `results` expression can be also a function, which returns the results synchronously
 *     or asynchronously (per Promise).
 * @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 {expression=} 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 {expression=} 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 {boolean=} md-require-match When set to true, the autocomplete will add a validator,
 *     which will evaluate to false, when no item is currently selected.
 * @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-clear-button Whether the clear button for the autocomplete input should show
 *     up or not. When `md-floating-label` is set, defaults to false, defaults to true otherwise.
 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
 *     `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
 *     <br/><br/>
 *     Also the autocomplete will immediately focus the input element.
 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating
 *     label.
 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
 *     in the dropdown upon open.
 * @param {string=} md-input-name The name attribute given to the input element to be used with
 *     FormController.
 * @param {string=} md-menu-class This class will be applied to the dropdown menu for styling.
 * @param {string=} md-menu-container-class This class will be applied to the parent container
 *     of the dropdown panel.
 * @param {string=} md-input-class This will be applied to the input for styling. This attribute
 *     is only valid when a `md-floating-label` is defined.
 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
 *     `md-input-container`.
 * @param {string=} md-select-on-focus When present the input's text will be automatically selected
 *     on focus.
 * @param {string=} md-input-id An ID to be added to the input element.
 * @param {number=} md-input-minlength The minimum length for the input's value for validation.
 * @param {number=} md-input-maxlength The maximum length for the input's value for validation.
 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select
 *     the item if the search text is an exact match. <br/><br/>
 *     An exact match is when only one match is displayed.
 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
 *     will select on case-insensitive match.
 * @param {string=} md-escape-options Override escape key logic. Default is `clear`.<br/>
 *     Options: `blur`, `clear`, `none`.
 * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
 *     the dropdown.<br/><br/>
 *     When the dropdown doesn't fit into the viewport, the dropdown will shrink
 *     as much as possible.
 * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`,
 *    `bottom`.
 * @param {string=} input-aria-describedby A space-separated list of element IDs. This should
 *     contain the IDs of any elements that describe this autocomplete. Screen readers will read the
 *     content of these elements at the end of announcing that the autocomplete has been selected
 *     and describing its current state. The descriptive elements do not need to be visible on the
 *     page.
 * @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use case
 *     is that this would contain the ID of a `<label>` element that is associated with this
 *     autocomplete. This will only have affect when `md-floating-label` is not defined.<br><br>
 *     For `<label id="state">US State</label>`, you would set this to
 *     `input-aria-labelledby="state"`.
 * @param {string=} input-aria-label A label that will be applied to the autocomplete's input.
 *    This will be announced by screen readers before the placeholder.
 *    This will only have affect when `md-floating-label` is not defined. If you define both
 *    `input-aria-label` and `input-aria-labelledby`, then `input-aria-label` will take precedence.
 * @param {string=} md-selected-message Attribute to specify the text that the screen reader will
 *    announce after a value is selected. Default is: "selected". If `Alaska` is selected in the
 *    options panel, it will read "Alaska selected". You will want to override this when your app
 *    runs in a non-English locale.
 * @param {string=} md-no-match-message Attribute to specify the text that the screen reader will
 *    announce after a query returns no matching results.
 *    Default is: "There are no matches available.". You will want to override this when your app
 *    runs in a non-English locale.
 * @param {string=} md-single-match-message Attribute to specify the text that the screen reader
 *    will announce after a query returns a single matching result.
 *    Default is: "There is 1 match available.". You will want to override this when your app
 *    runs in a non-English locale.
 * @param {string=} md-multiple-match-start-message Attribute to specify the text that the screen
 *    reader will announce after a query returns multiple matching results. The number of matching
 *    results will be read after this text. Default is: "There are ". You will want to override this
 *    when your app runs in a non-English locale.
 * @param {string=} md-multiple-match-end-message Attribute to specify the text that the screen
 *    reader will announce after a query returns multiple matching results. The number of matching
 *    results will be read before this text. Default is: " matches available.". You will want to
 *    override this when your app runs in a non-English locale.
 * @param {boolean=} ng-trim If set to false, the search text will be not trimmed automatically.
 *     Defaults to true.
 * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
 *     See the [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
 *     for more details.
 * @param {string=} md-mode Specify the repeat mode for suggestion lists. Acceptable values include
 *     `virtual` (md-virtual-repeat) and `standard` (ng-repeat). See the
 *     `Specifying Repeat Mode` example for mode details. Default is `virtual`.
 *
 * @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.
 *
 * ### Clear button for the input
 * By default, the clear button is displayed when there is input. This aligns with the spec's
 * [Search Pattern](https://material.io/archive/guidelines/patterns/search.html#search-in-app-search).
 * In floating label mode, when `md-floating-label="My Label"` is applied, the clear button is not
 * displayed by default (see the spec's
 * [Autocomplete Text Field](https://material.io/archive/guidelines/components/text-fields.html#text-fields-layout)).
 *
 * Nevertheless, developers are able to explicitly toggle the clear button for all autocomplete
 * components with `md-clear-button`.
 *
 * <hljs lang="html">
 *   <md-autocomplete ... md-clear-button="true"></md-autocomplete>
 *   <md-autocomplete ... md-clear-button="false"></md-autocomplete>
 * </hljs>
 *
 * In previous versions, the clear button was always hidden when the component was disabled.
 * This changed in `1.1.5` to give the developer control of this behavior. This example
 * will hide the clear button only when the component is disabled.
 *
 * <hljs lang="html">
 *   <md-autocomplete ... ng-disabled="disabled" md-clear-button="!disabled"></md-autocomplete>
 * </hljs>
 *
 * ### Example with validation
 * <hljs lang="html">
 * <form name="autocompleteForm">
 *   <md-autocomplete
 *       required
 *       md-input-name="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>
 *     <div ng-messages="autocompleteForm.autocomplete.$error">
 *       <div ng-message="required">This field is required</div>
 *     </div>
 *   </md-autocomplete>
 * </form>
 * </hljs>
 *
 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
 *     input validation for the field.
 *
 * ### Asynchronous Results
 * The autocomplete items expression also supports promises, which will resolve with the query
 * results.
 *
 * <hljs lang="js">
 *   function AppController($scope, $http) {
 *     $scope.query = function(searchText) {
 *       return $http
 *         .get(BACKEND_URL + '/items/' + searchText)
 *         .then(function(data) {
 *           // Map the response object to the data object.
 *           return data;
 *         });
 *     };
 *   }
 * </hljs>
 *
 * <hljs lang="html">
 *   <md-autocomplete
 *       md-selected-item="selectedItem"
 *       md-search-text="searchText"
 *       md-items="item in query(searchText)">
 *     <md-item-template>
 *       <span md-highlight-text="searchText">{{item}}</span>
 *     </md-item-template>
 * </md-autocomplete>
 * </hljs>
 *
 * ### Specifying Repeat Mode
 * You can use `md-mode` to specify whether to use standard or virtual lists for
 * rendering autocomplete options.
 * The `md-mode` accepts two values:
 * - `virtual` (default) Uses `md-virtual-repeat` to render list items. Virtual
 *    mode requires you to have consistent heights for all suggestions.
 * - `standard` uses `ng-repeat` to render list items. This allows you to have
 *    options of varying heights.
 *
 * Note that using 'standard' mode will require you to address any list
 * performance issues (e.g. pagination) separately within your application.
 *
 * <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-mode="standard">
 *     <span md-highlight-text="searchText">{{item.display}}</span>
 *   </md-autocomplete>
 * </hljs>
 */
function MdAutocomplete ($$mdSvgRegistry) {
  var REPEAT_STANDARD = 'standard';
  var REPEAT_VIRTUAL = 'virtual';
  var REPEAT_MODES = [REPEAT_STANDARD, REPEAT_VIRTUAL];

  /** get a valid repeat mode from an md-mode attribute string. */
  function getRepeatMode(modeStr) {
    if (!modeStr) { return REPEAT_VIRTUAL; }
    modeStr = modeStr.toLowerCase();
    return  REPEAT_MODES.indexOf(modeStr) > -1 ? modeStr : REPEAT_VIRTUAL;
  }

  return {
    controller:   'MdAutocompleteCtrl',
    controllerAs: '$mdAutocompleteCtrl',
    scope:        {
      inputName:          '@mdInputName',
      inputMinlength:     '@mdInputMinlength',
      inputMaxlength:     '@mdInputMaxlength',
      searchText:         '=?mdSearchText',
      selectedItem:       '=?mdSelectedItem',
      itemsExpr:          '@mdItems',
      itemText:           '&mdItemText',
      placeholder:        '@placeholder',
      inputAriaDescribedBy: '@?inputAriaDescribedby',
      inputAriaLabelledBy: '@?inputAriaLabelledby',
      inputAriaLabel:     '@?inputAriaLabel',
      noCache:            '=?mdNoCache',
      requireMatch:       '=?mdRequireMatch',
      selectOnMatch:      '=?mdSelectOnMatch',
      matchInsensitive:   '=?mdMatchCaseInsensitive',
      itemChange:         '&?mdSelectedItemChange',
      textChange:         '&?mdSearchTextChange',
      minLength:          '=?mdMinLength',
      delay:              '=?mdDelay',
      autofocus:          '=?mdAutofocus',
      floatingLabel:      '@?mdFloatingLabel',
      autoselect:         '=?mdAutoselect',
      menuClass:          '@?mdMenuClass',
      menuContainerClass: '@?mdMenuContainerClass',
      inputClass:         '@?mdInputClass',
      inputId:            '@?mdInputId',
      escapeOptions:      '@?mdEscapeOptions',
      dropdownItems:      '=?mdDropdownItems',
      dropdownPosition:   '@?mdDropdownPosition',
      clearButton:        '=?mdClearButton',
      selectedMessage:    '@?mdSelectedMessage',
      noMatchMessage:     '@?mdNoMatchMessage',
      singleMatchMessage: '@?mdSingleMatchMessage',
      multipleMatchStartMessage: '@?mdMultipleMatchStartMessage',
      multipleMatchEndMessage: '@?mdMultipleMatchEndMessage',
      mdMode: '=?mdMode'
    },
    compile: function(tElement, tAttrs) {
      var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
      var input = tElement.find('input');

      attributes.forEach(function(attribute) {
        var attrValue = tAttrs[tAttrs.$normalize(attribute)];

        if (attrValue !== null) {
          input.attr(attribute, attrValue);
        }
      });

      return function(scope, element, attrs, ctrl) {
        // Retrieve the state of using a md-not-found template by using our attribute, which will
        // be added to the element in the template function.
        ctrl.hasNotFound = !!element.attr('md-has-not-found');

        // By default the inset autocomplete should show the clear button when not explicitly
        // overwritten or in floating label mode.
        if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
          scope.clearButton = true;
        }

        scope.mdMode = getRepeatMode(attrs.mdMode);

        // Stop click events from bubbling up to the document and triggering a flicker of the
        // options panel while still supporting ng-click to be placed on md-autocomplete.
        element.on('click touchstart touchend', function(event) {
          event.stopPropagation();
        });
      };
    },
    template: function (element, attr) {
      var noItemsTemplate = getNoItemsTemplate(),
          itemTemplate    = getItemTemplate(),
          leftover        = element.html(),
          tabindex        = attr.tabindex;

      // Set our attribute for the link function above which runs later.
      // We will set an attribute, because otherwise the stored variables will be trashed when
      // removing the element is hidden while retrieving the template. For example when using ngIf.
      if (noItemsTemplate) element.attr('md-has-not-found', true);

      // Always set our tabindex of the autocomplete directive to -1, because our input
      // will hold the actual tabindex.
      element.attr('tabindex', '-1');

      return '\
        <md-autocomplete-wrap\
            ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
                        \'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
                        \'md-show-clear-button\': !!clearButton }">\
          ' + getInputElement() + '\
          ' + getClearButton() + '\
          <md-progress-linear\
              class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
              ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
              md-mode="indeterminate"></md-progress-linear>\
          ' + getContainer(attr.mdMenuContainerClass, attr.mdMode) + '\
            <ul class="md-autocomplete-suggestions"\
                ng-class="::menuClass"\
                id="ul-{{$mdAutocompleteCtrl.id}}"\
                ng-mouseup="$mdAutocompleteCtrl.focusInput()"\
                role="listbox">\
              <li class="md-autocomplete-suggestion" ' + getRepeatType(attr.mdMode) + ' ="item in $mdAutocompleteCtrl.matches"\
                  ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
                  ng-attr-id="{{\'md-option-\' + $mdAutocompleteCtrl.id + \'-\' + $index}}"\
                  ng-click="$mdAutocompleteCtrl.select($index)"\
                  role="option"\
                  aria-setsize="{{$mdAutocompleteCtrl.matches.length}}"\
                  aria-posinset="{{$index+1}}"\
                  aria-selected="{{$index === $mdAutocompleteCtrl.index ? true : false}}" \
                  md-extra-name="$mdAutocompleteCtrl.itemName">\
                  ' + itemTemplate + '\
                  </li>' + noItemsTemplate + '\
            </ul>\
          '  + getContainerClosingTags(attr.mdMode) + '\
        </md-autocomplete-wrap>';

      function getItemTemplate() {
        var templateTag = element.find('md-item-template').detach(),
            html = templateTag.length ? templateTag.html() : element.html();
        if (!templateTag.length) element.empty();
        return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html +
               '</md-autocomplete-parent-scope>';
      }

      function getNoItemsTemplate() {
        var templateTag = element.find('md-not-found').detach(),
            template = templateTag.length ? templateTag.html() : '';
        return template
            ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()" class="md-autocomplete-suggestion"\
                         md-autocomplete-parent-scope>' + template + '</li>'
            : '';
      }

      function getContainer(menuContainerClass, repeatMode) {
        // prepend a space if needed
        menuContainerClass = menuContainerClass ? ' ' + menuContainerClass : '';

        if (isVirtualRepeatDisabled(repeatMode)) {
          return '\
            <div \
                ng-hide="$mdAutocompleteCtrl.hidden"\
                class="md-standard-list-container md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
                ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
                ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
                ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
                role="presentation">\
              <div class="md-standard-list-scroller" role="presentation">';
        }

        return '\
          <md-virtual-repeat-container\
              md-auto-shrink\
              md-auto-shrink-min="1"\
              ng-hide="$mdAutocompleteCtrl.hidden"\
              class="md-virtual-repeat-container md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
              ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
              ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
              ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
              role="presentation">';
      }

      function getContainerClosingTags(repeatMode) {
        return isVirtualRepeatDisabled(repeatMode) ?
            '   </div>\
              </div>\
            </div>' : '</md-virtual-repeat-container>';
      }

      function getRepeatType(repeatMode) {
        return isVirtualRepeatDisabled(repeatMode)  ?
          'ng-repeat' : 'md-virtual-repeat';
      }

      function isVirtualRepeatDisabled(repeatMode) {
        // ensure we have a valid repeat mode
        var correctedRepeatMode = getRepeatMode(repeatMode);
        return correctedRepeatMode !== REPEAT_VIRTUAL;
      }

      function getInputElement () {
        if (attr.mdFloatingLabel) {
          return '\
            <md-input-container ng-if="floatingLabel">\
              <label>{{floatingLabel}}</label>\
              <input type="text"\
                ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
                id="{{inputId || \'fl-input-\' + $mdAutocompleteCtrl.id}}"\
                name="{{inputName || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
                ng-class="::inputClass"\
                autocomplete="off"\
                ng-required="$mdAutocompleteCtrl.isRequired"\
                ng-readonly="$mdAutocompleteCtrl.isReadonly"\
                ng-minlength="inputMinlength"\
                ng-maxlength="inputMaxlength"\
                ng-disabled="$mdAutocompleteCtrl.isDisabled"\
                ng-model="$mdAutocompleteCtrl.scope.searchText"\
                ng-model-options="{ allowInvalid: true }"\
                ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
                ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
                ng-blur="$mdAutocompleteCtrl.blur($event)"\
                ng-focus="$mdAutocompleteCtrl.focus($event)"\
                aria-label="{{floatingLabel}}"\
                ng-attr-aria-autocomplete="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'list\'}}"\
                ng-attr-role="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'combobox\'}}"\
                aria-haspopup="{{!$mdAutocompleteCtrl.isDisabled}}"\
                aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"\
                ng-attr-aria-owns="{{$mdAutocompleteCtrl.hidden || $mdAutocompleteCtrl.isDisabled ? undefined : \'ul-\' + $mdAutocompleteCtrl.id}}"\
                ng-attr-aria-activedescendant="{{!$mdAutocompleteCtrl.hidden && $mdAutocompleteCtrl.activeOption ? $mdAutocompleteCtrl.activeOption : undefined}}">\
              <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
            </md-input-container>';
        } else {
          return '\
            <input type="text"\
              ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
              id="{{inputId || \'input-\' + $mdAutocompleteCtrl.id}}"\
              name="{{inputName || \'input-\' + $mdAutocompleteCtrl.id }}"\
              ng-class="::inputClass"\
              ng-if="!floatingLabel"\
              autocomplete="off"\
              ng-required="$mdAutocompleteCtrl.isRequired"\
              ng-disabled="$mdAutocompleteCtrl.isDisabled"\
              ng-readonly="$mdAutocompleteCtrl.isReadonly"\
              ng-minlength="inputMinlength"\
              ng-maxlength="inputMaxlength"\
              ng-model="$mdAutocompleteCtrl.scope.searchText"\
              ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
              ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
              ng-blur="$mdAutocompleteCtrl.blur($event)"\
              ng-focus="$mdAutocompleteCtrl.focus($event)"\
              placeholder="{{placeholder}}"\
              aria-label="{{placeholder}}"\
              ng-attr-aria-autocomplete="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'list\'}}"\
              ng-attr-role="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'combobox\'}}"\
              aria-haspopup="{{!$mdAutocompleteCtrl.isDisabled}}"\
              aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"\
              ng-attr-aria-owns="{{$mdAutocompleteCtrl.hidden || $mdAutocompleteCtrl.isDisabled ? undefined : \'ul-\' + $mdAutocompleteCtrl.id}}"\
              ng-attr-aria-activedescendant="{{!$mdAutocompleteCtrl.hidden && $mdAutocompleteCtrl.activeOption ? $mdAutocompleteCtrl.activeOption : undefined}}">';
        }
      }

      function getClearButton() {
        return '' +
          '<button ' +
              'type="button" ' +
              'aria-label="Clear Input" ' +
              'tabindex="0" ' +
              'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText" ' +
              'ng-click="$mdAutocompleteCtrl.clear($event)">' +
            '<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
          '</button>';
        }
    }
  };
}


MdAutocompleteItemScopeDirective['$inject'] = ["$compile", "$mdUtil"];angular
  .module('material.components.autocomplete')
  .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);

function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
  return {
    restrict: 'AE',
    compile: compile,
    terminal: true,
    transclude: 'element'
  };

  function compile(tElement, tAttr, transclude) {
    return function postLink(scope, element, attr) {
      var ctrl = scope.$mdAutocompleteCtrl;
      var newScope = ctrl.parent.$new();
      var itemName = ctrl.itemName;

      // Watch for changes to our scope's variables and copy them to the new scope
      watchVariable('$index', '$index');
      watchVariable('item', itemName);

      // Ensure that $digest calls on our scope trigger $digest on newScope.
      connectScopes();

      // Link the element against newScope.
      transclude(newScope, function(clone) {
        element.after(clone);
      });

      /**
       * Creates a watcher for variables that are copied from the parent scope
       * @param variable
       * @param alias
       */
      function watchVariable(variable, alias) {
        newScope[alias] = scope[variable];

        scope.$watch(variable, function(value) {
          $mdUtil.nextTick(function() {
            newScope[alias] = value;
          });
        });
      }

      /**
       * Creates watchers on scope and newScope that ensure that for any
       * $digest of scope, newScope is also $digested.
       */
      function connectScopes() {
        var scopeDigesting = false;
        var newScopeDigesting = false;

        scope.$watch(function() {
          if (newScopeDigesting || scopeDigesting) {
            return;
          }

          scopeDigesting = true;
          scope.$$postDigest(function() {
            if (!newScopeDigesting) {
              newScope.$digest();
            }

            scopeDigesting = newScopeDigesting = false;
          });
        });

        newScope.$watch(function() {
          newScopeDigesting = true;
        });
      }
    };
  }
}

MdHighlightCtrl['$inject'] = ["$scope", "$element", "$attrs", "$mdUtil"];angular
    .module('material.components.autocomplete')
    .controller('MdHighlightCtrl', MdHighlightCtrl);

function MdHighlightCtrl ($scope, $element, $attrs, $mdUtil) {
  this.$scope = $scope;
  this.$element = $element;
  this.$attrs = $attrs;
  this.$mdUtil = $mdUtil;

  // Cache the Regex to avoid rebuilding each time.
  this.regex = null;
}

MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {

  this.flags = this.$attrs.mdHighlightFlags || '';

  this.unregisterFn = this.$scope.$watch(function($scope) {
    return {
      term: unsafeTermFn($scope),
      contentText: unsafeContentFn($scope)
    };
  }.bind(this), this.onRender.bind(this), true);

  this.$element.on('$destroy', this.unregisterFn);
};

/**
 * Triggered once a new change has been recognized and the highlighted
 * text needs to be updated.
 */
MdHighlightCtrl.prototype.onRender = function(state, prevState) {

  var contentText = state.contentText;

  /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
  if (this.regex === null || state.term !== prevState.term) {
    this.regex = this.createRegex(state.term, this.flags);
  }

  /* If a term is available apply the regex to the content */
  if (state.term) {
    this.applyRegex(contentText);
  } else {
    this.$element.text(contentText);
  }

};

/**
 * Decomposes the specified text into different tokens (whether match or not).
 * Breaking down the string guarantees proper XSS protection due to the native browser
 * escaping of unsafe text.
 */
MdHighlightCtrl.prototype.applyRegex = function(text) {
  var tokens = this.resolveTokens(text);

  this.$element.empty();

  tokens.forEach(function (token) {

    if (token.isMatch) {
      var tokenEl = angular.element('<span class="highlight">').text(token.text);

      this.$element.append(tokenEl);
    } else {
      this.$element.append(document.createTextNode(token));
    }

  }.bind(this));

};

  /**
 * Decomposes the specified text into different tokens by running the regex against the text.
 */
MdHighlightCtrl.prototype.resolveTokens = function(string) {
  var tokens = [];
  var lastIndex = 0;

  // Use replace here, because it supports global and single regular expressions at same time.
  string.replace(this.regex, function(match, index) {
    appendToken(lastIndex, index);

    tokens.push({
      text: match,
      isMatch: true
    });

    lastIndex = index + match.length;
  });

  // Append the missing text as a token.
  appendToken(lastIndex);

  return tokens;

  function appendToken(from, to) {
    var targetText = string.slice(from, to);
    targetText && tokens.push(targetText);
  }
};

/** Creates a regex for the specified text with the given flags. */
MdHighlightCtrl.prototype.createRegex = function(term, flags) {
  var startFlag = '', endFlag = '';
  var regexTerm = this.$mdUtil.sanitize(term);

  if (flags.indexOf('^') >= 0) startFlag = '^';
  if (flags.indexOf('$') >= 0) endFlag = '$';

  return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$^]/g, ''));
};


MdHighlight['$inject'] = ["$interpolate", "$parse"];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" md-highlight-flags="i">
 *     {{result.text}}
 *   </li>
 * </ul>
 * </hljs>
 */

function MdHighlight ($interpolate, $parse) {
  return {
    terminal: true,
    controller: 'MdHighlightCtrl',
    compile: function mdHighlightCompile(tElement, tAttr) {
      var termExpr = $parse(tAttr.mdHighlightText);
      var unsafeContentExpr = $interpolate(tElement.html());

      return function mdHighlightLink(scope, element, attr, ctrl) {
        ctrl.init(termExpr, unsafeContentExpr);
      };
    }
  };
}

})(window, window.angular);