/* globals define, jQuery, module, require, angular, moment */
/* jslint vars:true */

/**
 * @license angular-bootstrap-datetimepicker
 * Copyright 2016 Knight Rider Consulting, Inc. http://www.knightrider.com
 * License: MIT
 *
 * @author        Dale "Ducky" Lotts
 * @since        2013-Jul-8
 */

;(function (root, factory) {
  'use strict'
  /* istanbul ignore if */
  if (typeof module !== 'undefined' && module.exports) {
    var ng = typeof angular === 'undefined' ? require('angular') : angular
    var mt = typeof moment === 'undefined' ? require('moment') : moment
    factory(ng, mt)
    module.exports = 'ui.bootstrap.datetimepicker'
    /* istanbul ignore next */
  } else if (typeof define === 'function' && /* istanbul ignore next */ define.amd) {
    define(['angular', 'moment'], factory)
  } else {
    factory(root.angular, root.moment)
  }
}(this, function (angular, moment) {
  'use strict'
  angular.module('ui.bootstrap.datetimepicker', [])
    .service('dateTimePickerConfig', DateTimePickerConfigProvider)
    .service('dateTimePickerValidator', DateTimePickerValidatorService)
    .directive('datetimepicker', DatetimepickerDirective)

  DatetimepickerDirective.$inject = ['dateTimePickerConfig', 'dateTimePickerValidator']

  function DatetimepickerDirective (defaultConfig, configurationValidator) {
    var directiveDefinition = {
      bindToController: false,
      controller: DirectiveController,
      controllerAs: 'dateTimePickerController',
      replace: true,
      require: 'ngModel',
      restrict: 'E',
      scope: {
        beforeRender: '&',
        onSetTime: '&'
      },
      templateUrl: 'templates/datetimepicker.html'
    }

    DirectiveController.$inject = ['$scope', '$element', '$attrs']

    function DirectiveController ($scope, $element, $attrs) {
      // Configuration
      var ngModelController = $element.controller('ngModel')

      var configuration = createConfiguration()
      $scope.screenReader = configuration.screenReader

      // Behavior
      $scope.changeView = changeView
      ngModelController.$render = $render

      if (configuration.configureOn) {
        $scope.$on(configuration.configureOn, function () {
          configuration = createConfiguration()
          $scope.screenReader = configuration.screenReader
          ngModelController.$render()
        })
      }

      if (configuration.renderOn) {
        $scope.$on(configuration.renderOn, ngModelController.$render)
      }

      // Implementation

      var viewToModelFactory = {
        year: yearModelFactory,

        month: monthModelFactory,

        day: dayModelFactory,

        hour: hourModelFactory,

        minute: minuteModelFactory,

        setTime: setTime
      }

      function changeView (viewName, dateObject, event) {
        if (event) {
          event.stopPropagation()
          event.preventDefault()
        }

        if (viewName && (dateObject.utcDateValue > -Infinity) && dateObject.selectable && viewToModelFactory[viewName]) {
          var result = viewToModelFactory[viewName](dateObject.utcDateValue)

          var weekDates = []
          if (result.weeks) {
            for (var i = 0; i < result.weeks.length; i += 1) {
              var week = result.weeks[i]
              for (var j = 0; j < week.dates.length; j += 1) {
                var weekDate = week.dates[j]
                weekDates.push(weekDate)
              }
            }
          }

          $scope.beforeRender({
            $view: result.currentView,
            $dates: result.dates || weekDates,
            $leftDate: result.leftDate,
            $upDate: result.previousViewDate,
            $rightDate: result.rightDate
          })

          $scope.data = result
        }
      }

      function yearModelFactory (milliseconds) {
        var selectedDate = moment.utc(milliseconds).startOf('year')
        // View starts one year before the decade starts and ends one year after the decade ends
        // i.e. passing in a date of 1/1/2013 will give a range of 2009 to 2020
        // Truncate the last digit from the current year and subtract 1 to get the start of the decade
        var startDecade = (parseInt(selectedDate.year() / 10, 10) * 10)
        var startDate = moment.utc(startOfDecade(milliseconds)).subtract(1, 'year').startOf('year')

        var yearFormat = 'YYYY'
        var activeFormat = formatValue(ngModelController.$modelValue, yearFormat)
        var currentFormat = moment().format(yearFormat)

        var result = {
          'currentView': 'year',
          'nextView': configuration.minView === 'year' ? 'setTime' : 'month',
          'previousViewDate': new DateObject({
            utcDateValue: null,
            display: startDecade + '-' + (startDecade + 9)
          }),
          'leftDate': new DateObject({utcDateValue: moment.utc(startDate).subtract(9, 'year').valueOf()}),
          'rightDate': new DateObject({utcDateValue: moment.utc(startDate).add(11, 'year').valueOf()}),
          'dates': []
        }

        for (var i = 0; i < 12; i += 1) {
          var yearMoment = moment.utc(startDate).add(i, 'years')
          var dateValue = {
            'active': yearMoment.format(yearFormat) === activeFormat,
            'current': yearMoment.format(yearFormat) === currentFormat,
            'display': yearMoment.format(yearFormat),
            'future': yearMoment.year() > startDecade + 9,
            'past': yearMoment.year() < startDecade,
            'utcDateValue': yearMoment.valueOf()
          }

          result.dates.push(new DateObject(dateValue))
        }

        return result
      }

      function monthModelFactory (milliseconds) {
        var startDate = moment.utc(milliseconds).startOf('year')
        var previousViewDate = startOfDecade(milliseconds)

        var monthFormat = 'YYYY-MMM'
        var activeFormat = formatValue(ngModelController.$modelValue, monthFormat)
        var currentFormat = moment().format(monthFormat)

        var result = {
          'previousView': 'year',
          'currentView': 'month',
          'nextView': configuration.minView === 'month' ? 'setTime' : 'day',
          'previousViewDate': new DateObject({
            utcDateValue: previousViewDate.valueOf(),
            display: startDate.format('YYYY')
          }),
          'leftDate': new DateObject({utcDateValue: moment.utc(startDate).subtract(1, 'year').valueOf()}),
          'rightDate': new DateObject({utcDateValue: moment.utc(startDate).add(1, 'year').valueOf()}),
          'dates': []
        }

        for (var i = 0; i < 12; i += 1) {
          var monthMoment = moment.utc(startDate).add(i, 'months')
          var dateValue = {
            'active': monthMoment.format(monthFormat) === activeFormat,
            'current': monthMoment.format(monthFormat) === currentFormat,
            'display': monthMoment.format('MMM'),
            'utcDateValue': monthMoment.valueOf()
          }

          result.dates.push(new DateObject(dateValue))
        }

        return result
      }

      function dayModelFactory (milliseconds) {
        var selectedDate = moment.utc(milliseconds)
        var startOfMonth = moment.utc(selectedDate).startOf('month')
        var previousViewDate = moment.utc(selectedDate).startOf('year')
        var endOfMonth = moment.utc(selectedDate).endOf('month')

        var startDate = moment.utc(startOfMonth).subtract(Math.abs(startOfMonth.weekday()), 'days')

        var dayFormat = 'YYYY-MMM-DD'
        var activeFormat = formatValue(ngModelController.$modelValue, dayFormat)
        var currentFormat = moment().format(dayFormat)

        var result = {
          'previousView': 'month',
          'currentView': 'day',
          'nextView': configuration.minView === 'day' ? 'setTime' : 'hour',
          'previousViewDate': new DateObject({
            utcDateValue: previousViewDate.valueOf(),
            display: startOfMonth.format('YYYY-MMM')
          }),
          'leftDate': new DateObject({utcDateValue: moment.utc(startOfMonth).subtract(1, 'months').valueOf()}),
          'rightDate': new DateObject({utcDateValue: moment.utc(startOfMonth).add(1, 'months').valueOf()}),
          'dayNames': [],
          'weeks': []
        }

        for (var dayNumber = 0; dayNumber < 7; dayNumber += 1) {
          result.dayNames.push(moment.utc().weekday(dayNumber).format('dd'))
        }

        for (var i = 0; i < 6; i += 1) {
          var week = {dates: []}
          for (var j = 0; j < 7; j += 1) {
            var dayMoment = moment.utc(startDate).add((i * 7) + j, 'days')
            var dateValue = {
              'active': dayMoment.format(dayFormat) === activeFormat,
              'current': dayMoment.format(dayFormat) === currentFormat,
              'display': dayMoment.format('D'),
              'future': dayMoment.isAfter(endOfMonth),
              'past': dayMoment.isBefore(startOfMonth),
              'utcDateValue': dayMoment.valueOf()
            }
            week.dates.push(new DateObject(dateValue))
          }
          result.weeks.push(week)
        }

        return result
      }

      function hourModelFactory (milliseconds) {
        var selectedDate = moment.utc(milliseconds).startOf('day')
        var previousViewDate = moment.utc(selectedDate).startOf('month')

        var hourFormat = 'YYYY-MM-DD H'
        var activeFormat = formatValue(ngModelController.$modelValue, hourFormat)
        var currentFormat = moment().format(hourFormat)

        var result = {
          'previousView': 'day',
          'currentView': 'hour',
          'nextView': configuration.minView === 'hour' ? 'setTime' : 'minute',
          'previousViewDate': new DateObject({
            utcDateValue: previousViewDate.valueOf(),
            display: selectedDate.format('ll')
          }),
          'leftDate': new DateObject({utcDateValue: moment.utc(selectedDate).subtract(1, 'days').valueOf()}),
          'rightDate': new DateObject({utcDateValue: moment.utc(selectedDate).add(1, 'days').valueOf()}),
          'dates': []
        }

        for (var i = 0; i < 24; i += 1) {
          var hourMoment = moment.utc(selectedDate).add(i, 'hours')
          var dateValue = {
            'active': hourMoment.format(hourFormat) === activeFormat,
            'current': hourMoment.format(hourFormat) === currentFormat,
            'display': hourMoment.format('LT'),
            'utcDateValue': hourMoment.valueOf()
          }

          result.dates.push(new DateObject(dateValue))
        }

        return result
      }

      function minuteModelFactory (milliseconds) {
        var selectedDate = moment.utc(milliseconds).startOf('hour')
        var previousViewDate = moment.utc(selectedDate).startOf('day')

        var minuteFormat = 'YYYY-MM-DD H:mm'
        var activeFormat = formatValue(ngModelController.$modelValue, minuteFormat)
        var currentFormat = moment().format(minuteFormat)

        var result = {
          'previousView': 'hour',
          'currentView': 'minute',
          'nextView': 'setTime',
          'previousViewDate': new DateObject({
            utcDateValue: previousViewDate.valueOf(),
            display: selectedDate.format('lll')
          }),
          'leftDate': new DateObject({utcDateValue: moment.utc(selectedDate).subtract(1, 'hours').valueOf()}),
          'rightDate': new DateObject({utcDateValue: moment.utc(selectedDate).add(1, 'hours').valueOf()}),
          'dates': []
        }

        var limit = 60 / configuration.minuteStep

        for (var i = 0; i < limit; i += 1) {
          var hourMoment = moment.utc(selectedDate).add(i * configuration.minuteStep, 'minute')
          var dateValue = {
            'active': hourMoment.format(minuteFormat) === activeFormat,
            'current': hourMoment.format(minuteFormat) === currentFormat,
            'display': hourMoment.format('LT'),
            'utcDateValue': hourMoment.valueOf()
          }

          result.dates.push(new DateObject(dateValue))
        }

        return result
      }

      function setTime (milliseconds) {
        var tempDate = new Date(milliseconds)
        var newDate = new Date(tempDate.getUTCFullYear(), tempDate.getUTCMonth(), tempDate.getUTCDate(), tempDate.getUTCHours(), tempDate.getUTCMinutes(), tempDate.getUTCSeconds(), tempDate.getUTCMilliseconds())

        switch (configuration.modelType) {
          case 'Date':
            // No additional work needed
            break
          case 'moment':
            newDate = moment([tempDate.getUTCFullYear(), tempDate.getUTCMonth(), tempDate.getUTCDate(), tempDate.getUTCHours(), tempDate.getUTCMinutes(), tempDate.getUTCSeconds(), tempDate.getUTCMilliseconds()])
            break
          case 'milliseconds':
            newDate = milliseconds
            break
          default: // It is assumed that the modelType is a formatting string.
            newDate = moment([tempDate.getUTCFullYear(), tempDate.getUTCMonth(), tempDate.getUTCDate(), tempDate.getUTCHours(), tempDate.getUTCMinutes(), tempDate.getUTCSeconds(), tempDate.getUTCMilliseconds()]).format(configuration.modelType)
        }

        var oldDate = ngModelController.$modelValue
        ngModelController.$setViewValue(newDate)

        if (configuration.dropdownSelector) {
          jQuery(configuration.dropdownSelector).dropdown('toggle')
        }

        $scope.onSetTime({newDate: newDate, oldDate: oldDate})

        return viewToModelFactory[configuration.startView](milliseconds)
      }

      function $render () {
        $scope.changeView(configuration.startView, new DateObject({utcDateValue: getUTCTime(ngModelController.$viewValue)}))
      }

      function startOfDecade (milliseconds) {
        var startYear = (parseInt(moment.utc(milliseconds).year() / 10, 10) * 10)
        return moment.utc(milliseconds).year(startYear).startOf('year')
      }

      function formatValue (timeValue, formatString) {
        if (timeValue) {
          return getMoment(timeValue).format(formatString)
        } else {
          return ''
        }
      }

      /**
       * Converts a time value into a moment.
       *
       * This function is now necessary because moment logs a warning when parsing a string without a format.
       * @param modelValue
       *  a time value in any of the supported formats (Date, moment, milliseconds, and string)
       * @returns {moment}
       *  representing the specified time value.
       */

      function getMoment (modelValue) {
        return moment(modelValue, angular.isString(modelValue) ? configuration.parseFormat : undefined)
      }

      /**
       * Converts a time value to UCT/GMT time.
       * @param modelValue
       *  a time value in any of the supported formats (Date, moment, milliseconds, and string)
       * @returns {number}
       *  number of milliseconds since 1/1/1970
       */

      function getUTCTime (modelValue) {
        var tempDate = new Date()
        if (modelValue) {
          var tempMoment = getMoment(modelValue)
          if (tempMoment.isValid()) {
            tempDate = tempMoment.toDate()
          } else {
            throw new Error('Invalid date: ' + modelValue)
          }
        }
        return tempDate.getTime() - (tempDate.getTimezoneOffset() * 60000)
      }

      function createConfiguration () {
        var directiveConfig = {}

        if ($attrs.datetimepickerConfig) {
          directiveConfig = $scope.$parent.$eval($attrs.datetimepickerConfig)
        }

        var configuration = angular.extend({}, defaultConfig, directiveConfig)

        configurationValidator.validate(configuration)

        return configuration
      }
    }

    function DateObject () {
      var tempDate = new Date(arguments[0].utcDateValue)
      var localOffset = tempDate.getTimezoneOffset() * 60000

      this.utcDateValue = tempDate.getTime()
      this.selectable = true

      this.localDateValue = function localDateValue () {
        return this.utcDateValue + localOffset
      }

      var validProperties = ['active', 'current', 'display', 'future', 'past', 'selectable', 'utcDateValue']

      var constructorObject = arguments[0]

      Object.keys(constructorObject).filter(function (key) {
        return validProperties.indexOf(key) >= 0
      }).forEach(function (key) {
        this[key] = constructorObject[key]
      }, this)
    }

    return directiveDefinition
  }

  function DateTimePickerConfigProvider () {
    var defaultConfiguration = {
      configureOn: null,
      dropdownSelector: null,
      minuteStep: 5,
      minView: 'minute',
      modelType: 'Date',
      parseFormat: 'YYYY-MM-DDTHH:mm:ss.SSSZZ',
      renderOn: null,
      startView: 'day'
    }

    var defaultLocalization = {
      'bg': {previous: 'предишна', next: 'следваща'},
      'ca': {previous: 'anterior', next: 'següent'},
      'da': {previous: 'forrige', next: 'næste'},
      'de': {previous: 'vorige', next: 'weiter'},
      'en-au': {previous: 'previous', next: 'next'},
      'en-gb': {previous: 'previous', next: 'next'},
      'en': {previous: 'previous', next: 'next'},
      'es-us': {previous: 'atrás', next: 'siguiente'},
      'es': {previous: 'atrás', next: 'siguiente'},
      'fi': {previous: 'edellinen', next: 'seuraava'},
      'fr': {previous: 'précédent', next: 'suivant'},
      'hu': {previous: 'előző', next: 'következő'},
      'it': {previous: 'precedente', next: 'successivo'},
      'ja': {previous: '前へ', next: '次へ'},
      'ml': {previous: 'മുൻപുള്ളത്', next: 'അടുത്തത്'},
      'nl': {previous: 'vorige', next: 'volgende'},
      'pl': {previous: 'poprzednia', next: 'następna'},
      'pt-br': {previous: 'anteriores', next: 'próximos'},
      'pt': {previous: 'anterior', next: 'próximo'},
      'ro': {previous: 'anterior', next: 'următor'},
      'ru': {previous: 'предыдущая', next: 'следующая'},
      'sk': {previous: 'predošlá', next: 'ďalšia'},
      'sv': {previous: 'föregående', next: 'nästa'},
      'tr': {previous: 'önceki', next: 'sonraki'},
      'uk': {previous: 'назад', next: 'далі'},
      'zh-cn': {previous: '上一页', next: '下一页'},
      'zh-tw': {previous: '上一頁', next: '下一頁'}
    }

    var screenReader = defaultLocalization[moment.locale().toLowerCase()]

    return angular.extend({}, defaultConfiguration, {screenReader: screenReader})
  }

  DateTimePickerValidatorService.$inject = ['$log']

  function DateTimePickerValidatorService ($log) {
    return {
      validate: validator
    }

    function validator (configuration) {
      var validOptions = [
        'configureOn',
        'dropdownSelector',
        'minuteStep',
        'minView',
        'modelType',
        'parseFormat',
        'renderOn',
        'startView',
        'screenReader'
      ]

      var invalidOptions = Object.keys(configuration).filter(function (key) {
        return (validOptions.indexOf(key) < 0)
      })

      if (invalidOptions.length) {
        throw new Error('Invalid options: ' + invalidOptions.join(', '))
      }

      // Order of the elements in the validViews array is significant.
      var validViews = ['minute', 'hour', 'day', 'month', 'year']

      if (validViews.indexOf(configuration.startView) < 0) {
        throw new Error('invalid startView value: ' + configuration.startView)
      }

      if (validViews.indexOf(configuration.minView) < 0) {
        throw new Error('invalid minView value: ' + configuration.minView)
      }

      if (validViews.indexOf(configuration.minView) > validViews.indexOf(configuration.startView)) {
        throw new Error('startView must be greater than minView')
      }

      if (!angular.isNumber(configuration.minuteStep)) {
        throw new Error('minuteStep must be numeric')
      }
      if (configuration.minuteStep <= 0 || configuration.minuteStep >= 60) {
        throw new Error('minuteStep must be greater than zero and less than 60')
      }
      if (configuration.configureOn !== null && !angular.isString(configuration.configureOn)) {
        throw new Error('configureOn must be a string')
      }
      if (configuration.configureOn !== null && configuration.configureOn.length < 1) {
        throw new Error('configureOn must not be an empty string')
      }
      if (configuration.renderOn !== null && !angular.isString(configuration.renderOn)) {
        throw new Error('renderOn must be a string')
      }
      if (configuration.renderOn !== null && configuration.renderOn.length < 1) {
        throw new Error('renderOn must not be an empty string')
      }
      if (configuration.modelType !== null && !angular.isString(configuration.modelType)) {
        throw new Error('modelType must be a string')
      }
      if (configuration.modelType !== null && configuration.modelType.length < 1) {
        throw new Error('modelType must not be an empty string')
      }
      if (configuration.modelType !== 'Date' && configuration.modelType !== 'moment' && configuration.modelType !== 'milliseconds') {
        // modelType contains string format, overriding parseFormat with modelType
        configuration.parseFormat = configuration.modelType
      }
      if (configuration.dropdownSelector !== null && !angular.isString(configuration.dropdownSelector)) {
        throw new Error('dropdownSelector must be a string')
      }

      /* istanbul ignore next */
      if (configuration.dropdownSelector !== null && ((typeof jQuery === 'undefined') || (typeof jQuery().dropdown !== 'function'))) {
        $log.error('Please DO NOT specify the dropdownSelector option unless you are using jQuery AND Bootstrap.js. ' +
          'Please include jQuery AND Bootstrap.js, or write code to close the dropdown in the on-set-time callback. \n\n' +
          'The dropdownSelector configuration option is being removed because it will not function properly.')
        delete configuration.dropdownSelector
      }
    }
  }
}))
