‘use strict’;

angular.module(‘mgcrea.ngStrap.datepicker’, [

'mgcrea.ngStrap.helpers.dateParser',
'mgcrea.ngStrap.helpers.dateFormatter',
'mgcrea.ngStrap.tooltip'])

.provider('$datepicker', function() {

  var defaults = this.defaults = {
    animation: 'am-fade',
    prefixClass: 'datepicker',
    placement: 'bottom-left',
    template: 'datepicker/datepicker.tpl.html',
    trigger: 'focus',
    container: false,
    keyboard: true,
    html: false,
    delay: 0,
    // lang: $locale.id,
    useNative: false,
    dateType: 'date',
    dateFormat: 'shortDate',
    modelDateFormat: null,
    dayFormat: 'dd',
    monthFormat: 'MMM',
    yearFormat: 'yyyy',
    monthTitleFormat: 'MMMM yyyy',
    yearTitleFormat: 'yyyy',
    strictFormat: false,
    autoclose: false,
    minDate: -Infinity,
    maxDate: +Infinity,
    startView: 0,
    minView: 0,
    startWeek: 0,
    daysOfWeekDisabled: '',
    iconLeft: 'glyphicon glyphicon-chevron-left',
    iconRight: 'glyphicon glyphicon-chevron-right'
  };

  this.$get = function($window, $document, $rootScope, $sce, $dateFormatter, datepickerViews, $tooltip, $timeout) {

    var bodyEl = angular.element($window.document.body);
    var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent);
    var isTouch = ('createTouch' in $window.document) && isNative;
    if(!defaults.lang) defaults.lang = $dateFormatter.getDefaultLocale();

    function DatepickerFactory(element, controller, config) {

      var $datepicker = $tooltip(element, angular.extend({}, defaults, config));
      var parentScope = config.scope;
      var options = $datepicker.$options;
      var scope = $datepicker.$scope;
      if(options.startView) options.startView -= options.minView;

      // View vars

      var pickerViews = datepickerViews($datepicker);
      $datepicker.$views = pickerViews.views;
      var viewDate = pickerViews.viewDate;
      scope.$mode = options.startView;
      scope.$iconLeft = options.iconLeft;
      scope.$iconRight = options.iconRight;
      var $picker = $datepicker.$views[scope.$mode];

      // Scope methods

      scope.$select = function(date) {
        $datepicker.select(date);
      };
      scope.$selectPane = function(value) {
        $datepicker.$selectPane(value);
      };
      scope.$toggleMode = function() {
        $datepicker.setMode((scope.$mode + 1) % $datepicker.$views.length);
      };

      // Public methods

      $datepicker.update = function(date) {
        // console.warn('$datepicker.update() newValue=%o', date);
        if(angular.isDate(date) && !isNaN(date.getTime())) {
          $datepicker.$date = date;
          $picker.update.call($picker, date);
        }
        // Build only if pristine
        $datepicker.$build(true);
      };

      $datepicker.updateDisabledDates = function(dateRanges) {
        options.disabledDateRanges = dateRanges;
        for(var i = 0, l = scope.rows.length; i < l; i++) {
          angular.forEach(scope.rows[i], $datepicker.$setDisabledEl);
        }
      };

      $datepicker.select = function(date, keep) {
        // console.warn('$datepicker.select', date, scope.$mode);
        if(!angular.isDate(controller.$dateValue)) controller.$dateValue = new Date(date);
        if(!scope.$mode || keep) {
          controller.$setViewValue(angular.copy(date));
          controller.$render();
          if(options.autoclose && !keep) {
            $timeout(function() { $datepicker.hide(true); });
          }
        } else {
          angular.extend(viewDate, {year: date.getFullYear(), month: date.getMonth(), date: date.getDate()});
          $datepicker.setMode(scope.$mode - 1);
          $datepicker.$build();
        }
      };

      $datepicker.setMode = function(mode) {
        // console.warn('$datepicker.setMode', mode);
        scope.$mode = mode;
        $picker = $datepicker.$views[scope.$mode];
        $datepicker.$build();
      };

      // Protected methods

      $datepicker.$build = function(pristine) {
        // console.warn('$datepicker.$build() viewDate=%o', viewDate);
        if(pristine === true && $picker.built) return;
        if(pristine === false && !$picker.built) return;
        $picker.build.call($picker);
      };

      $datepicker.$updateSelected = function() {
        for(var i = 0, l = scope.rows.length; i < l; i++) {
          angular.forEach(scope.rows[i], updateSelected);
        }
      };

      $datepicker.$isSelected = function(date) {
        return $picker.isSelected(date);
      };

      $datepicker.$setDisabledEl = function(el) {
        el.disabled = $picker.isDisabled(el.date);
      };

      $datepicker.$selectPane = function(value) {
        var steps = $picker.steps;
        // set targetDate to first day of month to avoid problems with
        // date values rollover. This assumes the viewDate does not
        // depend on the day of the month
        var targetDate = new Date(Date.UTC(viewDate.year + ((steps.year || 0) * value), viewDate.month + ((steps.month || 0) * value), 1));
        angular.extend(viewDate, {year: targetDate.getUTCFullYear(), month: targetDate.getUTCMonth(), date: targetDate.getUTCDate()});
        $datepicker.$build();
      };

      $datepicker.$onMouseDown = function(evt) {
        // Prevent blur on mousedown on .dropdown-menu
        evt.preventDefault();
        evt.stopPropagation();
        // Emulate click for mobile devices
        if(isTouch) {
          var targetEl = angular.element(evt.target);
          if(targetEl[0].nodeName.toLowerCase() !== 'button') {
            targetEl = targetEl.parent();
          }
          targetEl.triggerHandler('click');
        }
      };

      $datepicker.$onKeyDown = function(evt) {
        if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey) return;
        evt.preventDefault();
        evt.stopPropagation();

        if(evt.keyCode === 13) {
          if(!scope.$mode) {
            return $datepicker.hide(true);
          } else {
            return scope.$apply(function() { $datepicker.setMode(scope.$mode - 1); });
          }
        }

        // Navigate with keyboard
        $picker.onKeyDown(evt);
        parentScope.$digest();
      };

      // Private

      function updateSelected(el) {
        el.selected = $datepicker.$isSelected(el.date);
      }

      function focusElement() {
        element[0].focus();
      }

      // Overrides

      var _init = $datepicker.init;
      $datepicker.init = function() {
        if(isNative && options.useNative) {
          element.prop('type', 'date');
          element.css('-webkit-appearance', 'textfield');
          return;
        } else if(isTouch) {
          element.prop('type', 'text');
          element.attr('readonly', 'true');
          element.on('click', focusElement);
        }
        _init();
      };

      var _destroy = $datepicker.destroy;
      $datepicker.destroy = function() {
        if(isNative && options.useNative) {
          element.off('click', focusElement);
        }
        _destroy();
      };

      var _show = $datepicker.show;
      $datepicker.show = function() {
        _show();
        // use timeout to hookup the events to prevent
        // event bubbling from being processed imediately.
        $timeout(function() {
          // if $datepicker is no longer showing, don't setup events
          if(!$datepicker.$isShown) return;
          $datepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown);
          if(options.keyboard) {
            element.on('keydown', $datepicker.$onKeyDown);
          }
        }, 0, false);
      };

      var _hide = $datepicker.hide;
      $datepicker.hide = function(blur) {
        if(!$datepicker.$isShown) return;
        $datepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown);
        if(options.keyboard) {
          element.off('keydown', $datepicker.$onKeyDown);
        }
        _hide(blur);
      };

      return $datepicker;

    }

    DatepickerFactory.defaults = defaults;
    return DatepickerFactory;

  };

})

.directive('bsDatepicker', function($window, $parse, $q, $dateFormatter, $dateParser, $datepicker) {

  var defaults = $datepicker.defaults;
  var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent);

  return {
    restrict: 'EAC',
    require: 'ngModel',
    link: function postLink(scope, element, attr, controller) {

      // Directive options
      var options = {scope: scope, controller: controller};
      angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'autoclose', 'dateType', 'dateFormat', 'modelDateFormat', 'dayFormat', 'strictFormat', 'startWeek', 'startDate', 'useNative', 'lang', 'startView', 'minView', 'iconLeft', 'iconRight', 'daysOfWeekDisabled', 'id'], function(key) {
        if(angular.isDefined(attr[key])) options[key] = attr[key];
      });

      // Visibility binding support
      attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) {
        if(!datepicker || !angular.isDefined(newValue)) return;
        if(angular.isString(newValue)) newValue = !!newValue.match(/true|,?(datepicker),?/i);
        newValue === true ? datepicker.show() : datepicker.hide();
      });

      // Initialize datepicker
      var datepicker = $datepicker(element, controller, options);
      options = datepicker.$options;
      // Set expected iOS format
      if(isNative && options.useNative) options.dateFormat = 'yyyy-MM-dd';

      var lang = options.lang;

      var formatDate = function(date, format) {
        return $dateFormatter.formatDate(date, format, lang);
      };

      var dateParser = $dateParser({format: options.dateFormat, lang: lang, strict: options.strictFormat});

      // Observe attributes for changes
      angular.forEach(['minDate', 'maxDate'], function(key) {
        // console.warn('attr.$observe(%s)', key, attr[key]);
        angular.isDefined(attr[key]) && attr.$observe(key, function(newValue) {
          // console.warn('attr.$observe(%s)=%o', key, newValue);
          datepicker.$options[key] = dateParser.getDateForAttribute(key, newValue);
          // Build only if dirty
          !isNaN(datepicker.$options[key]) && datepicker.$build(false);
          validateAgainstMinMaxDate(controller.$dateValue);
        });
      });

      // Watch model for changes
      scope.$watch(attr.ngModel, function(newValue, oldValue) {
        datepicker.update(controller.$dateValue);
      }, true);

      // Normalize undefined/null/empty array,
      // so that we don't treat changing from undefined->null as a change.
      function normalizeDateRanges(ranges) {
        if (!ranges || !ranges.length) return null;
        return ranges;
      }

      if (angular.isDefined(attr.disabledDates)) {
        scope.$watch(attr.disabledDates, function(disabledRanges, previousValue) {
          disabledRanges = normalizeDateRanges(disabledRanges);
          previousValue = normalizeDateRanges(previousValue);

          if (disabledRanges) {
            datepicker.updateDisabledDates(disabledRanges);
          }
        });
      }

      function validateAgainstMinMaxDate(parsedDate) {
        if (!angular.isDate(parsedDate)) return;
        var isMinValid = isNaN(datepicker.$options.minDate) || parsedDate.getTime() >= datepicker.$options.minDate;
        var isMaxValid = isNaN(datepicker.$options.maxDate) || parsedDate.getTime() <= datepicker.$options.maxDate;
        var isValid = isMinValid && isMaxValid;
        controller.$setValidity('date', isValid);
        controller.$setValidity('min', isMinValid);
        controller.$setValidity('max', isMaxValid);
        // Only update the model when we have a valid date
        if(isValid) controller.$dateValue = parsedDate;
      }

      // viewValue -> $parsers -> modelValue
      controller.$parsers.unshift(function(viewValue) {
        // console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue);
        // Null values should correctly reset the model value & validity
        if(!viewValue) {
          controller.$setValidity('date', true);
          // BREAKING CHANGE:
          // return null (not undefined) when input value is empty, so angularjs 1.3
          // ngModelController can go ahead and run validators, like ngRequired
          return null;
        }
        var parsedDate = dateParser.parse(viewValue, controller.$dateValue);
        if(!parsedDate || isNaN(parsedDate.getTime())) {
          controller.$setValidity('date', false);
          // return undefined, causes ngModelController to
          // invalidate model value
          return;
        } else {
          validateAgainstMinMaxDate(parsedDate);
        }
        if(options.dateType === 'string') {
          return formatDate(parsedDate, options.modelDateFormat || options.dateFormat);
        } else if(options.dateType === 'number') {
          return controller.$dateValue.getTime();
        } else if(options.dateType === 'unix') {
          return controller.$dateValue.getTime() / 1000;
        } else if(options.dateType === 'iso') {
          return controller.$dateValue.toISOString();
        } else {
          return new Date(controller.$dateValue);
        }
      });

      // modelValue -> $formatters -> viewValue
      controller.$formatters.push(function(modelValue) {
        // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue);
        var date;
        if(angular.isUndefined(modelValue) || modelValue === null) {
          date = NaN;
        } else if(angular.isDate(modelValue)) {
          date = modelValue;
        } else if(options.dateType === 'string') {
          date = dateParser.parse(modelValue, null, options.modelDateFormat);
        } else if(options.dateType === 'unix') {
          date = new Date(modelValue * 1000);
        } else {
          date = new Date(modelValue);
        }
        // Setup default value?
        // if(isNaN(date.getTime())) {
        //   var today = new Date();
        //   date = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0);
        // }
        controller.$dateValue = date;
        return getDateFormattedString();
      });

      // viewValue -> element
      controller.$render = function() {
        // console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue);
        element.val(getDateFormattedString());
      };

      function getDateFormattedString() {
        return !controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : formatDate(controller.$dateValue, options.dateFormat);
      }

      // Garbage collection
      scope.$on('$destroy', function() {
        if(datepicker) datepicker.destroy();
        options = null;
        datepicker = null;
      });

    }
  };

})

.provider('datepickerViews', function() {

  var defaults = this.defaults = {
    dayFormat: 'dd',
    daySplit: 7
  };

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

  // Modulus operator
  function mod(n, m) {
    return ((n % m) + m) % m;
  }

  this.$get = function($dateFormatter, $dateParser, $sce) {

    return function(picker) {

      var scope = picker.$scope;
      var options = picker.$options;

      var lang = options.lang;
      var formatDate = function(date, format) {
        return $dateFormatter.formatDate(date, format, lang);
      };
      var dateParser = $dateParser({format: options.dateFormat, lang: lang, strict: options.strictFormat});

      var weekDaysMin = $dateFormatter.weekdaysShort(lang);
      var weekDaysLabels = weekDaysMin.slice(options.startWeek).concat(weekDaysMin.slice(0, options.startWeek));
      var weekDaysLabelsHtml = $sce.trustAsHtml('<th class="dow text-center">' + weekDaysLabels.join('</th><th class="dow text-center">') + '</th>');

      var startDate = picker.$date || (options.startDate ? dateParser.getDateForAttribute('startDate', options.startDate) : new Date());
      var viewDate = {year: startDate.getFullYear(), month: startDate.getMonth(), date: startDate.getDate()};
      var timezoneOffset = startDate.getTimezoneOffset() * 6e4;

      var views = [{
          format: options.dayFormat,
          split: 7,
          steps: { month: 1 },
          update: function(date, force) {
            if(!this.built || force || date.getFullYear() !== viewDate.year || date.getMonth() !== viewDate.month) {
              angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()});
              picker.$build();
            } else if(date.getDate() !== viewDate.date) {
              viewDate.date = picker.$date.getDate();
              picker.$updateSelected();
            }
          },
          build: function() {
            var firstDayOfMonth = new Date(viewDate.year, viewDate.month, 1), firstDayOfMonthOffset = firstDayOfMonth.getTimezoneOffset();
            var firstDate = new Date(+firstDayOfMonth - mod(firstDayOfMonth.getDay() - options.startWeek, 7) * 864e5), firstDateOffset = firstDate.getTimezoneOffset();
            var today = new Date().toDateString();
            // Handle daylight time switch
            if(firstDateOffset !== firstDayOfMonthOffset) firstDate = new Date(+firstDate + (firstDateOffset - firstDayOfMonthOffset) * 60e3);
            var days = [], day;
            for(var i = 0; i < 42; i++) { // < 7 * 6
              day = dateParser.daylightSavingAdjust(new Date(firstDate.getFullYear(), firstDate.getMonth(), firstDate.getDate() + i));
              days.push({date: day, isToday: day.toDateString() === today, label: formatDate(day, this.format), selected: picker.$date && this.isSelected(day), muted: day.getMonth() !== viewDate.month, disabled: this.isDisabled(day)});
            }
            scope.title = formatDate(firstDayOfMonth, options.monthTitleFormat);
            scope.showLabels = true;
            scope.labels = weekDaysLabelsHtml;
            scope.rows = split(days, this.split);
            this.built = true;
          },
          isSelected: function(date) {
            return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth() && date.getDate() === picker.$date.getDate();
          },
          isDisabled: function(date) {
            var time = date.getTime();

            // Disabled because of min/max date.
            if (time < options.minDate || time > options.maxDate) return true;

            // Disabled due to being a disabled day of the week
            if (options.daysOfWeekDisabled.indexOf(date.getDay()) !== -1) return true;

            // Disabled because of disabled date range.
            if (options.disabledDateRanges) {
              for (var i = 0; i < options.disabledDateRanges.length; i++) {
                if (time >= options.disabledDateRanges[i].start && time <= options.disabledDateRanges[i].end) {
                  return true;
                }
              }
            }

            return false;
          },
          onKeyDown: function(evt) {
            if (!picker.$date) {
              return;
            }
            var actualTime = picker.$date.getTime();
            var newDate;

            if(evt.keyCode === 37) newDate = new Date(actualTime - 1 * 864e5);
            else if(evt.keyCode === 38) newDate = new Date(actualTime - 7 * 864e5);
            else if(evt.keyCode === 39) newDate = new Date(actualTime + 1 * 864e5);
            else if(evt.keyCode === 40) newDate = new Date(actualTime + 7 * 864e5);

            if (!this.isDisabled(newDate)) picker.select(newDate, true);
          }
        }, {
          name: 'month',
          format: options.monthFormat,
          split: 4,
          steps: { year: 1 },
          update: function(date, force) {
            if(!this.built || date.getFullYear() !== viewDate.year) {
              angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()});
              picker.$build();
            } else if(date.getMonth() !== viewDate.month) {
              angular.extend(viewDate, {month: picker.$date.getMonth(), date: picker.$date.getDate()});
              picker.$updateSelected();
            }
          },
          build: function() {
            var firstMonth = new Date(viewDate.year, 0, 1);
            var months = [], month;
            for (var i = 0; i < 12; i++) {
              month = new Date(viewDate.year, i, 1);
              months.push({date: month, label: formatDate(month, this.format), selected: picker.$isSelected(month), disabled: this.isDisabled(month)});
            }
            scope.title = formatDate(month, options.yearTitleFormat);
            scope.showLabels = false;
            scope.rows = split(months, this.split);
            this.built = true;
          },
          isSelected: function(date) {
            return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth();
          },
          isDisabled: function(date) {
            var lastDate = +new Date(date.getFullYear(), date.getMonth() + 1, 0);
            return lastDate < options.minDate || date.getTime() > options.maxDate;
          },
          onKeyDown: function(evt) {
            if (!picker.$date) {
              return;
            }
            var actualMonth = picker.$date.getMonth();
            var newDate = new Date(picker.$date);

            if(evt.keyCode === 37) newDate.setMonth(actualMonth - 1);
            else if(evt.keyCode === 38) newDate.setMonth(actualMonth - 4);
            else if(evt.keyCode === 39) newDate.setMonth(actualMonth + 1);
            else if(evt.keyCode === 40) newDate.setMonth(actualMonth + 4);

            if (!this.isDisabled(newDate)) picker.select(newDate, true);
          }
        }, {
          name: 'year',
          format: options.yearFormat,
          split: 4,
          steps: { year: 12 },
          update: function(date, force) {
            if(!this.built || force || parseInt(date.getFullYear()/20, 10) !== parseInt(viewDate.year/20, 10)) {
              angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()});
              picker.$build();
            } else if(date.getFullYear() !== viewDate.year) {
              angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()});
              picker.$updateSelected();
            }
          },
          build: function() {
            var firstYear = viewDate.year - viewDate.year % (this.split * 3);
            var years = [], year;
            for (var i = 0; i < 12; i++) {
              year = new Date(firstYear + i, 0, 1);
              years.push({date: year, label: formatDate(year, this.format), selected: picker.$isSelected(year), disabled: this.isDisabled(year)});
            }
            scope.title = years[0].label + '-' + years[years.length - 1].label;
            scope.showLabels = false;
            scope.rows = split(years, this.split);
            this.built = true;
          },
          isSelected: function(date) {
            return picker.$date && date.getFullYear() === picker.$date.getFullYear();
          },
          isDisabled: function(date) {
            var lastDate = +new Date(date.getFullYear() + 1, 0, 0);
            return lastDate < options.minDate || date.getTime() > options.maxDate;
          },
          onKeyDown: function(evt) {
            if (!picker.$date) {
              return;
            }
            var actualYear = picker.$date.getFullYear(),
                newDate = new Date(picker.$date);

            if(evt.keyCode === 37) newDate.setYear(actualYear - 1);
            else if(evt.keyCode === 38) newDate.setYear(actualYear - 4);
            else if(evt.keyCode === 39) newDate.setYear(actualYear + 1);
            else if(evt.keyCode === 40) newDate.setYear(actualYear + 4);

            if (!this.isDisabled(newDate)) picker.select(newDate, true);
          }
        }];

      return {
        views: options.minView ? Array.prototype.slice.call(views, options.minView) : views,
        viewDate: viewDate
      };

    };

  };

});