angular.module('ui.bootstrap', ['ui.bootstrap.buttons','ui.bootstrap.position','ui.bootstrap.datepicker','ui.bootstrap.pagination','ui.bootstrap.rating','ui.bootstrap.timepicker','ui.bootstrap.bindHtml','ui.bootstrap.typeahead']);
angular.module('ui.bootstrap.buttons', [])

  .constant('buttonConfig', {
    activeClass:'active',
    toggleEvent:'click'
  })

  .directive('btnRadio', ['buttonConfig', function (buttonConfig) {
    var activeClass = buttonConfig.activeClass || 'active';
    var toggleEvent = buttonConfig.toggleEvent || 'click';

    return {

      require:'ngModel',
      link:function (scope, element, attrs, ngModelCtrl) {

      //model -> UI
        ngModelCtrl.$render = function () {
          element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
        };

        //ui->model
        element.bind(toggleEvent, function () {
          if (!element.hasClass(activeClass)) {
            scope.$apply(function () {
              ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio));
              ngModelCtrl.$render();
            });
          }
        });
      }
    };
  }])

  .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) {

    var activeClass = buttonConfig.activeClass || 'active';
    var toggleEvent = buttonConfig.toggleEvent || 'click';

    return {
      require:'ngModel',
      link:function (scope, element, attrs, ngModelCtrl) {

        function getTrueValue() {
          var trueValue = scope.$eval(attrs.btnCheckboxTrue);
          return angular.isDefined(trueValue) ? trueValue : true;
        }

        function getFalseValue() {
          var falseValue = scope.$eval(attrs.btnCheckboxFalse);
          return angular.isDefined(falseValue) ? falseValue : false;
        }

        //model -> UI
        ngModelCtrl.$render = function () {
          element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
        };

        //ui->model
        element.bind(toggleEvent, function () {
          scope.$apply(function () {
            ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? getFalseValue() : getTrueValue());
            ngModelCtrl.$render();
          });
        });
      }
    };
  }]);
angular.module('ui.bootstrap.position', [])

/**
 * A set of utility methods that can be use to retrieve position of DOM elements.
 * It is meant to be used where we need to absolute-position DOM elements in
 * relation to other, existing elements (this is the case for tooltips, popovers,
 * typeahead suggestions etc.).
 */
  .factory('$position', ['$document', '$window', function ($document, $window) {

    function getStyle(el, cssprop) {
      if (el.currentStyle) { //IE
        return el.currentStyle[cssprop];
      } else if ($window.getComputedStyle) {
        return $window.getComputedStyle(el)[cssprop];
      }
      // finally try and get inline style
      return el.style[cssprop];
    }

    /**
     * Checks if a given element is statically positioned
     * @param element - raw DOM element
     */
    function isStaticPositioned(element) {
      return (getStyle(element, 'position') || 'static' ) === 'static';
    }

    /**
     * returns the closest, non-statically positioned parentOffset of a given element
     * @param element
     */
    var parentOffsetEl = function (element) {
      var docDomEl = $document[0];
      var offsetParent = element.offsetParent || docDomEl;
      while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
        offsetParent = offsetParent.offsetParent;
      }
      return offsetParent || docDomEl;
    };

    return {
      /**
       * Provides read-only equivalent of jQuery's position function:
       * http://api.jquery.com/position/
       */
      position: function (element) {
        var elBCR = this.offset(element);
        var offsetParentBCR = { top: 0, left: 0 };
        var offsetParentEl = parentOffsetEl(element[0]);
        if (offsetParentEl != $document[0]) {
          offsetParentBCR = this.offset(angular.element(offsetParentEl));
          offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
          offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
        }

        return {
          width: element.prop('offsetWidth'),
          height: element.prop('offsetHeight'),
          top: elBCR.top - offsetParentBCR.top,
          left: elBCR.left - offsetParentBCR.left
        };
      },

      /**
       * Provides read-only equivalent of jQuery's offset function:
       * http://api.jquery.com/offset/
       */
      offset: function (element) {
        var boundingClientRect = element[0].getBoundingClientRect();
        return {
          width: element.prop('offsetWidth'),
          height: element.prop('offsetHeight'),
          top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop),
          left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft  || $document[0].documentElement.scrollLeft)
        };
      }
    };
  }]);

angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])

  .constant('datepickerConfig', {
    dayFormat: 'dd',
    monthFormat: 'MMMM',
    yearFormat: 'yyyy',
    dayHeaderFormat: 'EEE',
    dayTitleFormat: 'MMMM yyyy',
    monthTitleFormat: 'yyyy',
    showWeeks: true,
    startingDay: 0,
    yearRange: 20,
    minDate: null,
    maxDate: null
  })

  .controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function ($scope, $attrs, dateFilter, dtConfig) {
    var format = {
        day:        getValue($attrs.dayFormat,        dtConfig.dayFormat),
        month:      getValue($attrs.monthFormat,      dtConfig.monthFormat),
        year:       getValue($attrs.yearFormat,       dtConfig.yearFormat),
        dayHeader:  getValue($attrs.dayHeaderFormat,  dtConfig.dayHeaderFormat),
        dayTitle:   getValue($attrs.dayTitleFormat,   dtConfig.dayTitleFormat),
        monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat)
      },
      startingDay = getValue($attrs.startingDay,      dtConfig.startingDay),
      yearRange =   getValue($attrs.yearRange,        dtConfig.yearRange);

    this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null;
    this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null;

    function getValue(value, defaultValue) {
      return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue;
    }

    function getDaysInMonth( year, month ) {
      return new Date(year, month, 0).getDate();
    }

    function getDates(startDate, n) {
      var dates = new Array(n);
      var current = startDate, i = 0;
      while (i < n) {
        dates[i++] = new Date(current);
        current.setDate( current.getDate() + 1 );
      }
      return dates;
    }

    function makeDate(date, format, isSelected, isSecondary) {
      return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary };
    }

    this.modes = [
      {
        name: 'day',
        getVisibleDates: function (date, selected) {
          var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1);
          var difference = startingDay - firstDayOfMonth.getDay(),
            numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
            firstDate = new Date(firstDayOfMonth), numDates = 0;

          if ( numDisplayedFromPreviousMonth > 0 ) {
            firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
            numDates += numDisplayedFromPreviousMonth; // Previous
          }
          numDates += getDaysInMonth(year, month + 1); // Current
          numDates += (7 - numDates % 7) % 7; // Next

          var days = getDates(firstDate, numDates), labels = new Array(7);
          for (var i = 0; i < numDates; i ++) {
            var dt = new Date(days[i]);
            days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month);
          }
          for (var j = 0; j < 7; j++) {
            labels[j] = dateFilter(days[j].date, format.dayHeader);
          }
          return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels };
        },
        compare: function (date1, date2) {
          return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
        },
        split: 7,
        step: { months: 1 }
      },
      {
        name: 'month',
        getVisibleDates: function (date, selected) {
          var months = new Array(12), year = date.getFullYear();
          for ( var i = 0; i < 12; i++ ) {
            var dt = new Date(year, i, 1);
            months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year));
          }
          return { objects: months, title: dateFilter(date, format.monthTitle) };
        },
        compare: function (date1, date2) {
          return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
        },
        split: 3,
        step: { years: 1 }
      },
      {
        name: 'year',
        getVisibleDates: function (date, selected) {
          var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1;
          for ( var i = 0; i < yearRange; i++ ) {
            var dt = new Date(startYear + i, 0, 1);
            years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear()));
          }
          return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') };
        },
        compare: function (date1, date2) {
          return date1.getFullYear() - date2.getFullYear();
        },
        split: 5,
        step: { years: yearRange }
      }
    ];

    this.isDisabled = function (date, mode) {
      var currentMode = this.modes[mode || 0];
      return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name})));
    };
  }])

  .directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) {
    return {
      restrict: 'EA',
      replace: true,
      templateUrl: 'template/datepicker/datepicker.html',
      scope: {
        dateDisabled: '&'
      },
      require: ['datepicker', '?^ngModel'],
      controller: 'DatepickerController',
      link: function (scope, element, attrs, ctrls) {
        var datepickerCtrl = ctrls[0], ngModel = ctrls[1];

        if (!ngModel) {
          return; // do nothing if no ng-model
        }

        // Configuration parameters
        var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks;

        if (attrs.showWeeks) {
          scope.$parent.$watch($parse(attrs.showWeeks), function (value) {
            showWeeks = !! value;
            updateShowWeekNumbers();
          });
        } else {
          updateShowWeekNumbers();
        }

        if (attrs.min) {
          scope.$parent.$watch($parse(attrs.min), function (value) {
            datepickerCtrl.minDate = value ? new Date(value) : null;
            refill();
          });
        }
        if (attrs.max) {
          scope.$parent.$watch($parse(attrs.max), function (value) {
            datepickerCtrl.maxDate = value ? new Date(value) : null;
            refill();
          });
        }

        function updateShowWeekNumbers() {
          scope.showWeekNumbers = mode === 0 && showWeeks;
        }

        // Split array into smaller arrays
        function split(arr, size) {
          var arrays = [];
          while (arr.length > 0) {
            arrays.push(arr.splice(0, size));
          }
          return arrays;
        }

        function refill( updateSelected ) {
          var date = null, valid = true;

          if ( ngModel.$modelValue ) {
            date = new Date( ngModel.$modelValue );

            if ( isNaN(date) ) {
              valid = false;
              $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
            } else if ( updateSelected ) {
              selected = date;
            }
          }
          ngModel.$setValidity('date', valid);

          var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date);
          angular.forEach(data.objects, function (obj) {
            obj.disabled = datepickerCtrl.isDisabled(obj.date, mode);
          });

          ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date)));

          scope.rows = split(data.objects, currentMode.split);
          scope.labels = data.labels || [];
          scope.title = data.title;
        }

        function setMode(value) {
          mode = value;
          updateShowWeekNumbers();
          refill();
        }

        ngModel.$render = function () {
          refill( true );
        };

        scope.select = function ( date ) {
          if ( mode === 0 ) {
            var dt = new Date( ngModel.$modelValue );
            dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
            ngModel.$setViewValue( dt );
            refill( true );
          } else {
            selected = date;
            setMode( mode - 1 );
          }
        };
        scope.move = function (direction) {
          var step = datepickerCtrl.modes[mode].step;
          selected.setMonth( selected.getMonth() + direction * (step.months || 0) );
          selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) );
          refill();
        };
        scope.toggleMode = function () {
          setMode( (mode + 1) % datepickerCtrl.modes.length );
        };
        scope.getWeekNumber = function (row) {
          return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null;
        };

        function getISO8601WeekNumber(date) {
          var checkDate = new Date(date);
          checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
          var time = checkDate.getTime();
          checkDate.setMonth(0); // Compare with Jan 1
          checkDate.setDate(1);
          return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
        }
      }
    };
  }])

  .constant('datepickerPopupConfig', {
    dateFormat: 'yyyy-MM-dd',
    closeOnDateSelection: true
  })

  .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig',
    function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig) {
      return {
        restrict: 'EA',
        require: 'ngModel',
        link: function (originalScope, element, attrs, ngModel) {

          var closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
          var dateFormat = attrs.datepickerPopup || datepickerPopupConfig.dateFormat;

          // create a child scope for the datepicker directive so we are not polluting original scope
          var scope = originalScope.$new();
          originalScope.$on('$destroy', function () {
            scope.$destroy();
          });

          var getIsOpen, setIsOpen;
          if ( attrs.isOpen ) {
            getIsOpen = $parse(attrs.isOpen);
            setIsOpen = getIsOpen.assign;

            originalScope.$watch(getIsOpen, function updateOpen(value) {
              scope.isOpen = !! value;
            });
          }
          scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state

          function setOpen( value ) {
            if (setIsOpen) {
              setIsOpen(originalScope, !!value);
            } else {
              scope.isOpen = !!value;
            }
          }

          var documentClickBind = function (event) {
            if (scope.isOpen && event.target !== element[0]) {
              scope.$apply(function () {
                setOpen(false);
              });
            }
          };

          var elementFocusBind = function () {
            scope.$apply(function () {
              setOpen( true );
            });
          };

          // popup element used to display calendar
          var popupEl = angular.element('<datepicker-popup-wrap><datepicker></datepicker></datepicker-popup-wrap>');
          popupEl.attr({
            'ng-model': 'date',
            'ng-change': 'dateSelection()'
          });
          var datepickerEl = popupEl.find('datepicker');
          if (attrs.datepickerOptions) {
            datepickerEl.attr(angular.extend({}, originalScope.$eval(attrs.datepickerOptions)));
          }

          // TODO: reverse from dateFilter string to Date object
          function parseDate(viewValue) {
            if (!viewValue) {
              ngModel.$setValidity('date', true);
              return null;
            } else if (angular.isDate(viewValue)) {
              ngModel.$setValidity('date', true);
              return viewValue;
            } else if (angular.isString(viewValue)) {
              var date = new Date(viewValue);
              if (isNaN(date)) {
                ngModel.$setValidity('date', false);
                return undefined;
              } else {
                ngModel.$setValidity('date', true);
                return date;
              }
            } else {
              ngModel.$setValidity('date', false);
              return undefined;
            }
          }
          ngModel.$parsers.unshift(parseDate);

          // Inner change
          scope.dateSelection = function () {
            ngModel.$setViewValue(scope.date);
            ngModel.$render();

            if (closeOnDateSelection) {
              setOpen( false );
            }
          };

          element.bind('input change keyup', function () {
            scope.$apply(function () {
              updateCalendar();
            });
          });

          // Outter change
          ngModel.$render = function () {
            var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
            element.val(date);

            updateCalendar();
          };

          function updateCalendar() {
            scope.date = ngModel.$modelValue;
            updatePosition();
          }

          function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) {
            if (attribute) {
              originalScope.$watch($parse(attribute), function (value){
                scope[scopeProperty] = value;
              });
              datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty);
            }
          }
          addWatchableAttribute(attrs.min, 'min');
          addWatchableAttribute(attrs.max, 'max');
          if (attrs.showWeeks) {
            addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks');
          } else {
            scope.showWeeks = true;
            datepickerEl.attr('show-weeks', 'showWeeks');
          }
          if (attrs.dateDisabled) {
            datepickerEl.attr('date-disabled', attrs.dateDisabled);
          }

          function updatePosition() {
            scope.position = $position.position(element);
            scope.position.top = scope.position.top + element.prop('offsetHeight');
          }

          var documentBindingInitialized = false, elementFocusInitialized = false;
          scope.$watch('isOpen', function (value) {
            if (value) {
              updatePosition();
              $document.bind('click', documentClickBind);
              if(elementFocusInitialized) {
                element.unbind('focus', elementFocusBind);
              }
              element[0].focus();
              documentBindingInitialized = true;
            } else {
              if(documentBindingInitialized) {
                $document.unbind('click', documentClickBind);
              }
              element.bind('focus', elementFocusBind);
              elementFocusInitialized = true;
            }

            if ( setIsOpen ) {
              setIsOpen(originalScope, value);
            }
          });

          var $setModelValue = $parse(attrs.ngModel).assign;

          scope.today = function () {
            $setModelValue(originalScope, new Date());
          };
          scope.clear = function () {
            $setModelValue(originalScope, null);
          };

          element.after($compile(popupEl)(scope));
        }
      };
    }])

  .directive('datepickerPopupWrap', [function () {
    return {
      restrict:'E',
      replace: true,
      transclude: true,
      templateUrl: 'template/datepicker/popup.html',
      link:function (scope, element, attrs) {
        element.bind('click', function (event) {
          event.preventDefault();
          event.stopPropagation();
        });
      }
    };
  }]);

angular.module('ui.bootstrap.pagination', [])

  .controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) {
    var self = this;

    this.init = function (defaultItemsPerPage) {
      if ($attrs.itemsPerPage) {
        $scope.$parent.$watch($parse($attrs.itemsPerPage), function (value) {
          self.itemsPerPage = parseInt(value, 10);
          $scope.totalPages = self.calculateTotalPages();
        });
      } else {
        this.itemsPerPage = defaultItemsPerPage;
      }
    };

    this.noPrevious = function () {
      return this.page === 1;
    };
    this.noNext = function () {
      return this.page === $scope.totalPages;
    };

    this.isActive = function (page) {
      return this.page === page;
    };

    this.calculateTotalPages = function () {
      return this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
    };

    this.getAttributeValue = function (attribute, defaultValue, interpolate) {
      return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue;
    };

    this.render = function () {
      this.page = parseInt($scope.page, 10) || 1;
      $scope.pages = this.getPages(this.page, $scope.totalPages);
    };

    $scope.selectPage = function (page) {
      if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) {
        $scope.page = page;
        $scope.onSelectPage({ page: page });
      }
    };

    $scope.$watch('totalItems', function () {
      $scope.totalPages = self.calculateTotalPages();
    });

    $scope.$watch('totalPages', function (value) {
      if ( $attrs.numPages ) {
        $scope.numPages = value; // Readonly variable
      }

      if ( self.page > value ) {
        $scope.selectPage(value);
      } else {
        self.render();
      }
    });

    $scope.$watch('page', function () {
      self.render();
    });
  }])

  .constant('paginationConfig', {
    itemsPerPage: 10,
    boundaryLinks: false,
    directionLinks: true,
    firstText: 'First',
    previousText: 'Previous',
    nextText: 'Next',
    lastText: 'Last',
    rotate: true
  })

  .directive('pagination', ['$parse', 'paginationConfig', function ($parse, config) {
    return {
      restrict: 'EA',
      scope: {
        page: '=',
        totalItems: '=',
        onSelectPage:' &',
        numPages: '='
      },
      controller: 'PaginationController',
      templateUrl: 'template/pagination/pagination.html',
      replace: true,
      link: function (scope, element, attrs, paginationCtrl) {

      // Setup configuration parameters
        var maxSize,
          boundaryLinks  = paginationCtrl.getAttributeValue(attrs.boundaryLinks,  config.boundaryLinks      ),
          directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks     ),
          firstText      = paginationCtrl.getAttributeValue(attrs.firstText,      config.firstText,     true),
          previousText   = paginationCtrl.getAttributeValue(attrs.previousText,   config.previousText,  true),
          nextText       = paginationCtrl.getAttributeValue(attrs.nextText,       config.nextText,      true),
          lastText       = paginationCtrl.getAttributeValue(attrs.lastText,       config.lastText,      true),
          rotate         = paginationCtrl.getAttributeValue(attrs.rotate,         config.rotate);

        paginationCtrl.init(config.itemsPerPage);

        if (attrs.maxSize) {
          scope.$parent.$watch($parse(attrs.maxSize), function (value) {
            maxSize = parseInt(value, 10);
            paginationCtrl.render();
          });
        }

        // Create page object used in template
        function makePage(number, text, isActive, isDisabled) {
          return {
            number: number,
            text: text,
            active: isActive,
            disabled: isDisabled
          };
        }

        paginationCtrl.getPages = function (currentPage, totalPages) {
          var pages = [];

          // Default page limits
          var startPage = 1, endPage = totalPages;
          var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages );

          // recompute if maxSize
          if ( isMaxSized ) {
            if ( rotate ) {
            // Current page is displayed in the middle of the visible ones
              startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
              endPage   = startPage + maxSize - 1;

              // Adjust if limit is exceeded
              if (endPage > totalPages) {
                endPage   = totalPages;
                startPage = endPage - maxSize + 1;
              }
            } else {
            // Visible pages are paginated with maxSize
              startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;

              // Adjust last page if limit is exceeded
              endPage = Math.min(startPage + maxSize - 1, totalPages);
            }
          }

          // Add page number links
          for (var number = startPage; number <= endPage; number++) {
            var page = makePage(number, number, paginationCtrl.isActive(number), false);
            pages.push(page);
          }

          // Add links to move between page sets
          if ( isMaxSized && ! rotate ) {
            if ( startPage > 1 ) {
              var previousPageSet = makePage(startPage - 1, '...', false, false);
              pages.unshift(previousPageSet);
            }

            if ( endPage < totalPages ) {
              var nextPageSet = makePage(endPage + 1, '...', false, false);
              pages.push(nextPageSet);
            }
          }

          // Add previous & next links
          if (directionLinks) {
            var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious());
            pages.unshift(previousPage);

            var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext());
            pages.push(nextPage);
          }

          // Add first & last links
          if (boundaryLinks) {
            var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious());
            pages.unshift(firstPage);

            var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext());
            pages.push(lastPage);
          }

          return pages;
        };
      }
    };
  }])

  .constant('pagerConfig', {
    itemsPerPage: 10,
    previousText: '« Previous',
    nextText: 'Next »',
    align: true
  })

  .directive('pager', ['pagerConfig', function (config) {
    return {
      restrict: 'EA',
      scope: {
        page: '=',
        totalItems: '=',
        onSelectPage:' &',
        numPages: '='
      },
      controller: 'PaginationController',
      templateUrl: 'template/pagination/pager.html',
      replace: true,
      link: function (scope, element, attrs, paginationCtrl) {

      // Setup configuration parameters
        var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
          nextText         = paginationCtrl.getAttributeValue(attrs.nextText,     config.nextText,     true),
          align            = paginationCtrl.getAttributeValue(attrs.align,        config.align);

        paginationCtrl.init(config.itemsPerPage);

        // Create page object used in template
        function makePage(number, text, isDisabled, isPrevious, isNext) {
          return {
            number: number,
            text: text,
            disabled: isDisabled,
            previous: ( align && isPrevious ),
            next: ( align && isNext )
          };
        }

        paginationCtrl.getPages = function (currentPage) {
          return [
            makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false),
            makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true)
          ];
        };
      }
    };
  }]);

angular.module('ui.bootstrap.rating', [])

  .constant('ratingConfig', {
    max: 5,
    stateOn: null,
    stateOff: null
  })

  .controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function ($scope, $attrs, $parse, ratingConfig) {

    this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max;
    this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
    this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;

    this.createDefaultRange = function (len) {
      var defaultStateObject = {
        stateOn: this.stateOn,
        stateOff: this.stateOff
      };

      var states = new Array(len);
      for (var i = 0; i < len; i++) {
        states[i] = defaultStateObject;
      }
      return states;
    };

    this.normalizeRange = function (states) {
      for (var i = 0, n = states.length; i < n; i++) {
        states[i].stateOn = states[i].stateOn || this.stateOn;
        states[i].stateOff = states[i].stateOff || this.stateOff;
      }
      return states;
    };

    // Get objects used in template
    $scope.range = angular.isDefined($attrs.ratingStates) ?  this.normalizeRange(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createDefaultRange(this.maxRange);

    $scope.rate = function (value) {
      if ( $scope.readonly || $scope.value === value) {
        return;
      }

      $scope.value = value;
    };

    $scope.enter = function (value) {
      if ( ! $scope.readonly ) {
        $scope.val = value;
      }
      $scope.onHover({value: value});
    };

    $scope.reset = function () {
      $scope.val = angular.copy($scope.value);
      $scope.onLeave();
    };

    $scope.$watch('value', function (value) {
      $scope.val = value;
    });

    $scope.readonly = false;
    if ($attrs.readonly) {
      $scope.$parent.$watch($parse($attrs.readonly), function (value) {
        $scope.readonly = !!value;
      });
    }
  }])

  .directive('rating', function () {
    return {
      restrict: 'EA',
      scope: {
        value: '=',
        onHover: '&',
        onLeave: '&'
      },
      controller: 'RatingController',
      templateUrl: 'template/rating/rating.html',
      replace: true
    };
  });
angular.module('ui.bootstrap.timepicker', [])

  .constant('timepickerConfig', {
    hourStep: 1,
    minuteStep: 1,
    showMeridian: true,
    meridians: ['AM', 'PM'],
    readonlyInput: false,
    mousewheel: true
  })

  .directive('timepicker', ['$parse', '$log', 'timepickerConfig', function ($parse, $log, timepickerConfig) {
    return {
      restrict: 'EA',
      require:'?^ngModel',
      replace: true,
      scope: {},
      templateUrl: 'template/timepicker/timepicker.html',
      link: function (scope, element, attrs, ngModel) {
        if ( !ngModel ) {
          return; // do nothing if no ng-model
        }

        var selected = new Date(), meridians = timepickerConfig.meridians;

        var hourStep = timepickerConfig.hourStep;
        if (attrs.hourStep) {
          scope.$parent.$watch($parse(attrs.hourStep), function (value) {
            hourStep = parseInt(value, 10);
          });
        }

        var minuteStep = timepickerConfig.minuteStep;
        if (attrs.minuteStep) {
          scope.$parent.$watch($parse(attrs.minuteStep), function (value) {
            minuteStep = parseInt(value, 10);
          });
        }

        // 12H / 24H mode
        scope.showMeridian = timepickerConfig.showMeridian;
        if (attrs.showMeridian) {
          scope.$parent.$watch($parse(attrs.showMeridian), function (value) {
            scope.showMeridian = !!value;

            if ( ngModel.$error.time ) {
            // Evaluate from template
              var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
              if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
                selected.setHours( hours );
                refresh();
              }
            } else {
              updateTemplate();
            }
          });
        }

        // Get scope.hours in 24H mode if valid
        function getHoursFromTemplate( ) {
          var hours = parseInt( scope.hours, 10 );
          var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
          if ( !valid ) {
            return undefined;
          }

          if ( scope.showMeridian ) {
            if ( hours === 12 ) {
              hours = 0;
            }
            if ( scope.meridian === meridians[1] ) {
              hours = hours + 12;
            }
          }
          return hours;
        }

        function getMinutesFromTemplate() {
          var minutes = parseInt(scope.minutes, 10);
          return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
        }

        function pad( value ) {
          return ( angular.isDefined(value) && value.toString().length < 2 ) ? `0${  value}` : value;
        }

        // Input elements
        var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1);

        // Respond on mousewheel spin
        var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel;
        if ( mousewheel ) {

          var isScrollingUp = function (e) {
            if (e.originalEvent) {
              e = e.originalEvent;
            }
            //pick correct delta variable depending on event
            var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
            return (e.detail || delta > 0);
          };

          hoursInputEl.bind('mousewheel wheel', function (e) {
            scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() );
            e.preventDefault();
          });

          minutesInputEl.bind('mousewheel wheel', function (e) {
            scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() );
            e.preventDefault();
          });
        }

        scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput;
        if ( ! scope.readonlyInput ) {

          var invalidate = function (invalidHours, invalidMinutes) {
            ngModel.$setViewValue( null );
            ngModel.$setValidity('time', false);
            if (angular.isDefined(invalidHours)) {
              scope.invalidHours = invalidHours;
            }
            if (angular.isDefined(invalidMinutes)) {
              scope.invalidMinutes = invalidMinutes;
            }
          };

          scope.updateHours = function () {
            var hours = getHoursFromTemplate();

            if ( angular.isDefined(hours) ) {
              selected.setHours( hours );
              refresh( 'h' );
            } else {
              invalidate(true);
            }
          };

          hoursInputEl.bind('blur', function (e) {
            if ( !scope.validHours && scope.hours < 10) {
              scope.$apply( function () {
                scope.hours = pad( scope.hours );
              });
            }
          });

          scope.updateMinutes = function () {
            var minutes = getMinutesFromTemplate();

            if ( angular.isDefined(minutes) ) {
              selected.setMinutes( minutes );
              refresh( 'm' );
            } else {
              invalidate(undefined, true);
            }
          };

          minutesInputEl.bind('blur', function (e) {
            if ( !scope.invalidMinutes && scope.minutes < 10 ) {
              scope.$apply( function () {
                scope.minutes = pad( scope.minutes );
              });
            }
          });
        } else {
          scope.updateHours = angular.noop;
          scope.updateMinutes = angular.noop;
        }

        ngModel.$render = function () {
          var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null;

          if ( isNaN(date) ) {
            ngModel.$setValidity('time', false);
            $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
          } else {
            if ( date ) {
              selected = date;
            }
            makeValid();
            updateTemplate();
          }
        };

        // Call internally when we know that model is valid.
        function refresh( keyboardChange ) {
          makeValid();
          ngModel.$setViewValue( new Date(selected) );
          updateTemplate( keyboardChange );
        }

        function makeValid() {
          ngModel.$setValidity('time', true);
          scope.invalidHours = false;
          scope.invalidMinutes = false;
        }

        function updateTemplate( keyboardChange ) {
          var hours = selected.getHours(), minutes = selected.getMinutes();

          if ( scope.showMeridian ) {
            hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
          }
          scope.hours =  keyboardChange === 'h' ? hours : pad(hours);
          scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
          scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
        }

        function addMinutes( minutes ) {
          var dt = new Date( selected.getTime() + minutes * 60000 );
          selected.setHours( dt.getHours(), dt.getMinutes() );
          refresh();
        }

        scope.incrementHours = function () {
          addMinutes( hourStep * 60 );
        };
        scope.decrementHours = function () {
          addMinutes( - hourStep * 60 );
        };
        scope.incrementMinutes = function () {
          addMinutes( minuteStep );
        };
        scope.decrementMinutes = function () {
          addMinutes( - minuteStep );
        };
        scope.toggleMeridian = function () {
          addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
        };
      }
    };
  }]);

angular.module('ui.bootstrap.bindHtml', [])

  .directive('bindHtmlUnsafe', function () {
    return function (scope, element, attr) {
      element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
      scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
        element.html(value || '');
      });
    };
  });
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])

/**
 * A helper service that can parse typeahead's syntax (string provided by users)
 * Extracted to a separate service for ease of unit testing
 */
  .factory('typeaheadParser', ['$parse', function ($parse) {

  //                      00000111000000000000022200000000000000003333333333333330000000000044000
    var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;

    return {
      parse:function (input) {

        var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
        if (!match) {
          throw new Error(
            `${'Expected typeahead specification in form of \'_modelValue_ (as _label_)? for _item_ in _collection_\'' +
            ' but got \''}${  input  }'.`);
        }

        return {
          itemName:match[3],
          source:$parse(match[4]),
          viewMapper:$parse(match[2] || match[1]),
          modelMapper:$parse(match[1])
        };
      }
    };
  }])

  .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
    function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) {

      var HOT_KEYS = [9, 13, 27, 38, 40];

      return {
        require:'ngModel',
        link:function (originalScope, element, attrs, modelCtrl) {

          //SUPPORTED ATTRIBUTES (OPTIONS)

          //minimal no of characters that needs to be entered before typeahead kicks-in
          var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;

          //minimal wait time after last character typed before typehead kicks-in
          var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;

          //should it restrict model values to the ones selected from the popup only?
          var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;

          //binding to a variable that indicates if matches are being retrieved asynchronously
          var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;

          //a callback executed when a match is selected
          var onSelectCallback = $parse(attrs.typeaheadOnSelect);

          var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;

          //INTERNAL VARIABLES

          //model setter executed upon match selection
          var $setModelValue = $parse(attrs.ngModel).assign;

          //expressions used by typeahead
          var parserResult = typeaheadParser.parse(attrs.typeahead);


          //pop-up element used to display matches
          var popUpEl = angular.element('<typeahead-popup></typeahead-popup>');
          popUpEl.attr({
            matches: 'matches',
            active: 'activeIdx',
            select: 'select(activeIdx)',
            query: 'query',
            position: 'position'
          });
          //custom item template
          if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
            popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
          }

          //create a child scope for the typeahead directive so we are not polluting original scope
          //with typeahead-specific data (matches, query etc.)
          var scope = originalScope.$new();
          originalScope.$on('$destroy', function (){
            scope.$destroy();
          });

          var resetMatches = function () {
            scope.matches = [];
            scope.activeIdx = -1;
          };

          var getMatchesAsync = function (inputValue) {

            var locals = {$viewValue: inputValue};
            isLoadingSetter(originalScope, true);
            $q.when(parserResult.source(scope, locals)).then(function (matches) {

              //it might happen that several async queries were in progress if a user were typing fast
              //but we are interested only in responses that correspond to the current view value
              if (inputValue === modelCtrl.$viewValue) {
                if (matches.length > 0) {

                  scope.activeIdx = 0;
                  scope.matches.length = 0;

                  //transform labels
                  for(var i=0; i<matches.length; i++) {
                    locals[parserResult.itemName] = matches[i];
                    scope.matches.push({
                      label: parserResult.viewMapper(scope, locals),
                      model: matches[i]
                    });
                  }

                  scope.query = inputValue;
                  //position pop-up with matches - we need to re-calculate its position each time we are opening a window
                  //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
                  //due to other elements being rendered
                  scope.position = $position.position(element);
                  scope.position.top = scope.position.top + element.prop('offsetHeight');

                } else {
                  resetMatches();
                }
                isLoadingSetter(originalScope, false);
              }
            }, function (){
              resetMatches();
              isLoadingSetter(originalScope, false);
            });
          };

          resetMatches();

          //we need to propagate user's query so we can higlight matches
          scope.query = undefined;

          //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later 
          var timeoutPromise;

          //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
          //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
          modelCtrl.$parsers.unshift(function (inputValue) {

            resetMatches();
            if (inputValue && inputValue.length >= minSearch) {
              if (waitTime > 0) {
                if (timeoutPromise) {
                  $timeout.cancel(timeoutPromise);//cancel previous timeout
                }
                timeoutPromise = $timeout(function () {
                  getMatchesAsync(inputValue);
                }, waitTime);
              } else {
                getMatchesAsync(inputValue);
              }
            }

            if (isEditable) {
              return inputValue;
            } else {
              modelCtrl.$setValidity('editable', false);
              return undefined;
            }
          });

          modelCtrl.$formatters.push(function (modelValue) {

            var candidateViewValue, emptyViewValue;
            var locals = {};

            if (inputFormatter) {

              locals['$model'] = modelValue;
              return inputFormatter(originalScope, locals);

            } else {

              //it might happen that we don't have enough info to properly render input value
              //we need to check for this situation and simply return model value if we can't apply custom formatting
              locals[parserResult.itemName] = modelValue;
              candidateViewValue = parserResult.viewMapper(originalScope, locals);
              locals[parserResult.itemName] = undefined;
              emptyViewValue = parserResult.viewMapper(originalScope, locals);

              return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
            }
          });

          scope.select = function (activeIdx) {
            //called from within the $digest() cycle
            var locals = {};
            var model, item;

            locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
            model = parserResult.modelMapper(originalScope, locals);
            $setModelValue(originalScope, model);
            modelCtrl.$setValidity('editable', true);

            onSelectCallback(originalScope, {
              $item: item,
              $model: model,
              $label: parserResult.viewMapper(originalScope, locals)
            });

            resetMatches();

            //return focus to the input element if a mach was selected via a mouse click event
            element[0].focus();
          };

          //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
          element.bind('keydown', function (evt) {

            //typeahead is open and an "interesting" key was pressed
            if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
              return;
            }

            evt.preventDefault();

            if (evt.which === 40) {
              scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
              scope.$digest();

            } else if (evt.which === 38) {
              scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
              scope.$digest();

            } else if (evt.which === 13 || evt.which === 9) {
              scope.$apply(function () {
                scope.select(scope.activeIdx);
              });

            } else if (evt.which === 27) {
              evt.stopPropagation();

              resetMatches();
              scope.$digest();
            }
          });

          // Keep reference to click handler to unbind it.
          var dismissClickHandler = function (evt) {
            if (element[0] !== evt.target) {
              resetMatches();
              scope.$digest();
            }
          };

          $document.bind('click', dismissClickHandler);

          originalScope.$on('$destroy', function (){
            $document.unbind('click', dismissClickHandler);
          });

          element.after($compile(popUpEl)(scope));
        }
      };

    }])

  .directive('typeaheadPopup', function () {
    return {
      restrict:'E',
      scope:{
        matches:'=',
        query:'=',
        active:'=',
        position:'=',
        select:'&'
      },
      replace:true,
      // Hardcode this path until this is fixed: https://github.com/decipherinc/angular-tags/issues/16#issuecomment-26614629
      templateUrl:'/ui-bootstrap/template/typeahead/typeahead-popup.html',
      link:function (scope, element, attrs) {

        scope.templateUrl = attrs.templateUrl;

        scope.isOpen = function () {
          return scope.matches.length > 0;
        };

        scope.isActive = function (matchIdx) {
          return scope.active == matchIdx;
        };

        scope.selectActive = function (matchIdx) {
          scope.active = matchIdx;
        };

        scope.selectMatch = function (activeIdx) {
          scope.select({activeIdx:activeIdx});
        };
      }
    };
  })

  .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) {
    return {
      restrict:'E',
      scope:{
        index:'=',
        match:'=',
        query:'='
      },
      link:function (scope, element, attrs) {
        var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
        $http.get(tplUrl, {cache: $templateCache}).success(function (tplContent){
          element.replaceWith($compile(tplContent.trim())(scope));
        });
      }
    };
  }])

  .filter('typeaheadHighlight', function () {

    function escapeRegexp(queryToEscape) {
      return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
    }

    return function (matchItem, query) {
      return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
    };
  });