<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../iron-icons/iron-icons.html">
<link rel="import" href="../paper-button/paper-button.html">
<link rel="import" href="../paper-styles/paper-styles.html">
<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html">
<link rel="import" href="../iron-selector/iron-selector.html">
<link rel="import" href="../moment-element/moment-with-locales-import.html">

<dom-module id="paper-calendar">
  <template>
    <style>
      :host {
        display: block;
        box-sizing: border-box;
        padding: 12px 0;
        position: relative;
        width: 100%;
        height: 100%;
        min-width: 160px;
        min-height: 160px;
        color: var(--primary-text-color);
        -webkit-font-smoothing: antialiased;
        -webkit-tap-highlight-color: rgba(0,0,0,0);
        --ease-in-sine: cubic-bezier(0.47, 0, 0.745, 0.715);
        --ease-out-sine: cubic-bezier(0.39, 0.575, 0.565, 1);
        @apply(--paper-font-body1);
        @apply(--paper-calendar);
        overflow: hidden;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;

        @apply(--paper-calendar);
      }
      #calendar {
        position: relative;
        width: 100%;
        height: 100%;
        @apply(--layout-horizontal);
      }
      #months {
        height: 100%;
        @apply(--layout-horizontal);
      }
      #months.animating .month-nav {
        opacity: 1;
      }
      #months.animating {
        transition-property: transform, opacity;
        transition-duration: 300ms;
      }
      #months.animating.swipe {
        transition-timing-function: var(--ease-in-sine);
        --webkit-transition-timing-function: var(--ease-in-sine);
      }
      #months.animating.reset {
        transition-timing-function: var(--ease-out-sine);
        --webkit-transition-timing-function: var(--ease-out-sine);
      }
      .month {
        flex: 1;
        height: 100%;
        @apply(--layout-vertical);
        @apply(--layout-justified);
      }

      .month-row, .month-nav {
        height: calc(100%/8);
        box-sizing: border-box;
        padding: 0 calc(100%/36);
      }

      .month-col {
        position: relative;
        @apply(--layout-vertical);
        @apply(--layout-flex);
      }

      .month-nav {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        opacity: 1;
      }
      .month-nav .col {
        position: relative;
        @apply(--layout-vertical);
        @apply(--layout-center-center);
      }
      .month-nav .btn .icon {
        cursor: pointer;
      }
      .month-nav .btn .ripple {
        position: absolute;
        width: 48px;
        height: 48px;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }
      .month-nav .btn.right {
        text-align: right;
      }
      .month-name {
        line-height: 24px;
        vertical-align: middle;
        text-align: center;
        font-weight: bold;
        @apply(--paper-font-body2);
      }
      .month-weekdays {
        color: var(--secondary-text-color);
      }
      .month-col .day {
        cursor: default;
        pointer-events: none;
        @apply(--layout-fit);
        @apply(--layout-vertical);
        @apply(--layout-center-center);
      }
      .day-item {
        flex: 1;
        object-fit: contain;
      }
      .day-item::selection {
        background: none;
      }
      .day-item .selection {
        fill: rgba(0,0,0,0);
      }
      .day-item.selected .selection {
        fill: var(--default-primary-color);
      }
      .day-item.selected + .day {
        color: var(--text-primary-color);
      }
      .day-item:not([disabled]) {
        cursor: pointer;
      }
      .day-item[disabled] + .day {
        color: var(--text-disabled-color, #9d9d9d);
      }
      .day-item.today + .day {
        color: var(--default-primary-color);
      }
      .day-item.selected.today + .day {
        color: var(--text-primary-color);
      }
    </style>
    <div id="calendar">
      <div id="months" on-track="_onTrack" class$="{{_contentClass}}">
        <template is="dom-repeat" items="{{_months}}" as="month">
          <div class$="{{_getMonthClass('month', month)}}">
            <div class="month-row month-name layout horizontal center-center flex">
              <span>{{dateFormat(month.date, 'MMMM YYYY', locale)}}</span>
            </div>
            <div class="month-row month-weekdays week layout horizontal justified flex">
              <template is="dom-repeat" items="{{_weekdays}}">
                <div class="month-col layout vertical flex">
                  <div class="day">{{item.0}}</div>
                </div>
              </template>
            </div>
            <template is="dom-repeat" items="{{month.weeks}}" as="row">
              <div class="month-row layout horizontal justified flex">
                <template is="dom-repeat" items="{{row}}">
                  <div class="month-col">
                    <svg version="1.1" viewBox="0 0 100 100"
                      class$="{{_getDayClass('day-item', item.date, today, date)}}"
                      disabled$="{{_isDisabled(item.day, item.date, minDate, maxDate)}}"
                      on-tap="_tapDay" date$="{{item.name}}">
                      <circle cx="50" cy="50" r="49" class="selection"></circle>
                    </svg>
                    <div class="day">{{item.day}}</div>
                  </div>
                </template>
              </div>
            </template>
          </div>
        </template>
      </div>
      <div id="monthNav" class="month-nav layout horizontal center">
        <div class="flex col self-stretch">
          <div class="btn" on-tap="_prevMonth">
            <paper-ripple center class="ripple circle"></paper-ripple>
            <iron-icon class="icon flex" icon="chevron-left"></iron-icon>
          </div>
        </div>
        <div class="flex-5"></div>
        <div class="flex col self-stretch">
          <div class="btn" on-tap="_nextMonth">
            <paper-ripple center class="ripple circle"></paper-ripple>
            <iron-icon class="icon flex" icon="chevron-right"></iron-icon>
          </div>
        </div>
      </div>
    </div>
  </template>
  <script>
    (function() {

    // Ignore movement within this distance (px)
    var WIGGLE_THRESHOLD = 4;

    // what constitues a flick (px/ms)
    var FLICK_SPEED = 0.5;

    // Factor for "springy" resistence effect when swiping too far.
    var RESIST_LENGTH = 40;
    var RESIST_FACTOR = 2;

    // Number of months to preload on both sides of the current month
    var PRELOAD_MONTHS = 1;

    Polymer({
      is: 'paper-calendar',
      properties: {
        /*
         * The selected date
         */
        date: {
          type: Date,
          notify: true,
          value: function() {
            return new Date();
          },
          observer: '_dateChanged'
        },
        /**
         * The current locale
         */
        locale: {
          type: String,
          value: 'en',
          notify: true,
          observer: '_localeChanged'
        },
        /**
         * The minimum allowed date
         */
        minDate: {
          type: Date,
          value: null
        },
        /**
         * The maximum allowed date
         */
        maxDate: {
          type: Date,
          value: null
        },
        /**
         * The currently selected month (1-12) 
         */
        currentMonth: {
          type: Number
        },
        /**
         * The currently selected year
         */
        currentYear: {
          type: Number
        },
        _contentClass: String,
        _months: Array
      },
      behaviors: [
        Polymer.IronResizableBehavior
      ],
      observers: [
        '_populate(currentYear, currentMonth, minDate, maxDate, locale)',
      ],
      listeners: {
        'iron-resize': '_resizeHandler',
        'swiped': '_onSwipe'
      },
      ready: function() {
        this._updateToday();
        this.currentMonth = this.date.getMonth() + 1;
        this.currentYear = this.date.getFullYear();
        this._transitionEvent = this.style.WebkitTransition ? 'transitionEnd' : 'webkitTransitionEnd';
      },
      dateFormat: function(date, format, locale) {
        if (!date) {
          return '';
        }
        var value = moment(date);
        value.locale(locale || this.locale);
        return value.format(format);
      },
      _localeChanged: function(locale) {
        var localeMoment = moment();
        localeMoment.locale(locale);
        var weekdays = [];
        for (var i=0; i<7; i++) {
          weekdays.push(localeMoment.weekday(i).format('dd'));
        }
        this._weekdays = weekdays;
        this._firstDayOfWeek = localeMoment.weekday(0).format('d');
      },
      _populate: function(currentYear, currentMonth, minDate, maxDate) {
        var date, month, weeks, day, d, dayInfo, monthData, months = [];

        // Make sure currentYear/currentMonth are in min/max range
        if (minDate && new Date(currentYear, currentMonth, 0) < minDate) {
          this.currentYear = minDate.getFullYear();
          this.currentMonth = minDate.getMonth() + 1;
          return;
        } else if (maxDate && new Date(currentYear, currentMonth - 1, 1) > maxDate) {
          this.currentYear = maxDate.getFullYear();
          this.currentMonth = maxDate.getMonth() + 1;
          return;
        }

        for (var i=-PRELOAD_MONTHS; i<=PRELOAD_MONTHS; i++) {
          weeks = [[]];
          day = 1;
          date = new Date(currentYear, currentMonth - 1 + i, 1);
          month = date.getMonth();
          monthData = {
            year: date.getFullYear(),
            month: date.getMonth() + 1,
            date: new Date(date)
          };

          if (!this._monthWithinValidRange(monthData.year, monthData.month)) {
            continue;
          }

          // add "padding" days
          var firstDay = date.getDay() - this._firstDayOfWeek;
          if (firstDay < 0) {
            firstDay = 7 + firstDay;
          }
          for (d=0; d<firstDay; d++) {
            weeks[0].push({day: null, date: null});
          }

          // add actual days
          while (date.getMonth() === month) {
            if (weeks[0].length && d % 7 === 0) {
              // start new week
              weeks.push([]);
            }
            dayInfo = {
              date: new Date(date.getFullYear(), month, day),
              name: this.dateFormat(date,'YYYY-MM-DD'),
              year: currentYear,
              month: month,
              day: day,
            };
            weeks[weeks.length-1].push(dayInfo);
            date.setDate(++day);
            d++;
          }

          // add remaining "padding" days
          while (d < 42) {
            if (d % 7 === 0) {
              weeks.push([]);
            }
            weeks[weeks.length-1].push({day: null, date: null});
            d += 1;
          }

          monthData.weeks = weeks;
          months.push(monthData);
        }
        if (!months.length) {
          return;
        }
        this.set('_months', months);
        this._positionSlider();
      },
      _getDayClass: function(cssClass, date) {
        if (!date) {
          return cssClass;
        }
        if (date.getTime() === this.today.getTime()) {
          cssClass += ' today';
        }
        if (this.date && this.date.getTime() === date.getTime()) {
          cssClass += ' selected';
        }
        return cssClass;
      },
      _isDisabled: function(day, date) {
        return !day || !this._withinValidRange(date);
      },
      _getMonthClass: function(name, month) {
        return name + ' month-' + month.year + '-' + month.month;
      },
      _onTrack: function(event) {
        var detail = event.detail;
        var state = detail.state;
        var dx = event.detail.dx;
        var dy = event.detail.dy;
        var adx = Math.abs(dx);
        var ady = Math.abs(dy);
        var width = this._containerWidth;

        if (state === 'start') {
          this._trackStartTime = (new Date()).getTime();
          this._startPos = this._currentPos;
          this._moveQueue = [];
        }

        var x = this._startPos + dx;
        if (state === 'track') {
          if (this._moveQueue.length >= 4) {
            this._moveQueue.shift();
          }
          this._moveQueue.push(event);

          // ignore movement within WIGGLE_THRESHOLD
          var distance = Math.sqrt((dx * dx) + (dy * dy));
          if (!this._gesture && distance > WIGGLE_THRESHOLD) {
            this._gesture = adx > ady ? 'pan-x' : 'pan-y';
          }

          // only drag during pan-x gesture
          if (this._gesture !== 'pan-x') {
            return;
          }

          this._dragging = true;
          var fullWidth = width * this._months.length;

          // If we're dragging outside the bounds, add some resistence
          if (x > 0 || x < (-fullWidth + width)) {
            if (isNaN(parseInt(this._resistStart))) {
              this._resistStart = adx;
            }
            var rdx = adx - this._resistStart;
            var p, d, max = RESIST_LENGTH;
            p = rdx > width ? 1 : rdx / width;
            d = max * (1 - Math.pow(1 - p, RESIST_FACTOR));
            x = (dx < 0 ? -this._scrollWidth + width - d : d);
          } else {
            this._resistStart = null;
          }
          this._translateX(x);
        }
        if (state === 'end') {
          this._resistStart = null;
          var lastIdx = this._getMonthIdx(this._startPos);
          var idx = this._getMonthIdx(this._currentPos);
          var speed = this._getFastestMovement(event).v;
          var move = idx !== lastIdx || speed > FLICK_SPEED;
          if (!this._resistStart && this._gesture === 'pan-x' && move) {
            if (speed > FLICK_SPEED) {
              // calculate an ideal transition duration based on current speed of swipe
              var remainingDistance = width - adx;
              var newDuration = remainingDistance / speed;
              if (newDuration > 300) {
                newDuration = 300;
              }
              this._transitionDuration = newDuration;
            }

            var newX = width * idx * -1;
            var direction = (dx < 0) ? 'left' : 'right';

            this._translateX(newX, 'swipe', function() {
              this.set('_contentClass', '');
              this.transform('translateX(' + this._startPos + 'px)', this.$.months);
              this.fire('swiped', {direction: direction});
            }.bind(this));
          } else {
            this._translateX(this._startPos, 'reset');
          }
          this._gesture = null;
        }
      },
      _getMonthIdx: function(pos) {
        // returns the index for the visible month according to the given position
        var width = this._containerWidth;
        var i = Math.floor((-pos + (width/2)) / width);
        return i < 0 ? 0 : i;
      },
      _translateX: function(x, transition, cb) {
        if (isNaN(parseInt(x))) {
          throw new Error('Not a number: ' + x);
        }
        this._currentPos = x;
        if (transition) {
          if (this._transitionDuration) {
            this.$.months.style.transitionDuration = this._transitionDuration + 'ms';
          }
          this._once(this._transitionEvent, function() {
            this.set('_contentClass', '');
            this.$.months.style.transitionDuration = '';
            this._transitionDuration = null;
            this.$.monthNav.style.removeProperty('opacity');
            if (cb) {
              cb();
            }
          }.bind(this), this.$.months);
          this.set('_contentClass', 'animating ' + transition);
          this.$.monthNav.style.removeProperty('opacity');
          // Fixes odd bug in chrome where we lose touch-events after changing opacity
          this._once('touchstart', function() {});
        }
        window.requestAnimationFrame(function() {
          if (!transition) {
            var halfWidth = this._containerWidth / 2;
            var dx = Math.abs(this._startPos - x);
            var p = (1 - (dx / halfWidth)) * 100;
            p = (100 - Math.pow(p, 2)) / 100 / 100;
            var opacity = Math.abs(parseFloat(p).toFixed(2));
            this.$.monthNav.style.opacity = opacity;
          }
          this.transform('translateX(' + x + 'px)', this.$.months);
        }.bind(this));
      },
      _getFastestMovement: function(event) {
        // detect flick based on fastest segment of movement
        var l = this._moveQueue.length;
        var dt, tx, ty, tv, x = 0, y = 0, v = 0;
        for (var i = 0, m; i < l && (m = this._moveQueue[i]); i++) {
          dt = event.timeStamp - m.timeStamp;
          tx = (event.detail.x - m.detail.x) / dt;
          ty = (event.detail.y - m.detail.y) / dt;
          tv = Math.sqrt(tx * tx + ty * ty);
          if (tv > v) {
            x = tx;
            y = ty;
            v = tv;
          }
        }
        return {x: x, y: y, v: v};
      },
      _onSwipe: function(event) {
        if (event.detail.direction === 'right') {
          this._prevMonth();
        } else {
          this._nextMonth();
        }
      },
      _once: function(eventName, callback, node) {
        node = node || this;
        function onceCallback() {
          node.removeEventListener(eventName, onceCallback);
          callback.apply(null, arguments);
        }
        node.addEventListener(eventName, onceCallback);
      },
      _incrMonth: function(i) {
        var date = new Date(this.currentYear, this.currentMonth - 1 + i);
        var year = date.getFullYear();
        var month = date.getMonth() + 1;
        if (this._monthWithinValidRange(year, month)) {
          this.currentYear = year;
          this.currentMonth = month;
        }
      },
      _prevMonth: function() {
        this._incrMonth(-1);
      },
      _nextMonth: function() {
        this._incrMonth(1);
      },
      _dateChanged: function(date, oldValue) {
        if (!this._isValidDate(date)) {
          console.warn('Invalid date: ' + date);
          this.date = date = oldValue;
        }
        if (!this._withinValidRange(date)) {
          console.warn('Date outside of valid range: ' + date);
          this.date = date = oldValue;
        }
        this.currentYear = date.getFullYear();
        this.currentMonth = date.getMonth() + 1;
        if (oldValue && date.getTime && oldValue.getTime && date.getTime() === oldValue.getTime()) {
          return;
        }
        // clone the date object so as not to have unintended effects on bindings
        date = new Date(date.getTime());
        date.setHours(0, 0, 0, 0);
        this.set('date', date);
      },
      _tapDay: function(event) {
        if (!this._withinValidRange(event.model.item.date)) {
          return false;
        }
        this.date = event.model.item.date;
      },
      _isValidDate: function(date) {
        return date && date.getTime && !isNaN(date.getTime());
      },
      _withinValidRange: function(date) {
        if (this._isValidDate(date)) {
          return (!this.minDate || date >= this.minDate) && (!this.maxDate || date <= this.maxDate);
        }
        return false;
      },
      _monthWithinValidRange: function(year, month) {
        var monthStart = new Date(year, month-1, 1);
        var monthEnd = new Date(year, month, 0);
        return this._withinValidRange(monthStart) || this._withinValidRange(monthEnd);
      },
      _positionSlider: function() {
        if (!this._months || !this._containerWidth) {
          return;
        }
        this._scrollWidth = (this.$.calendar.offsetWidth * this._months.length);
        this.$.months.style.minWidth = this._scrollWidth + 'px';
        var i = ((this.currentYear * 12) + this.currentMonth) - 
                 ((this._months[0].year * 12) + this._months[0].month);
        this._translateX(-i * this._containerWidth);
      },
      _resizeHandler: function() {
        this._containerWidth = this.$.calendar.offsetWidth;
        this._positionSlider();
      },
      _getDayName: function(date) {
        return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
      },
      _updateToday: function() {
        this.today = new Date();
        this.today.setHours(0, 0, 0, 0);
      },
    });
   })();
  </script>
</dom-module>