<script>
  import addMinutes from 'date-fns/addMinutes';
  import addDays from 'date-fns/addDays';
  import startOfDay from 'date-fns/startOfDay';
  import isSameMinute from 'date-fns/isSameMinute';
  import formatDate from 'date-fns/format';
  import getHours from 'date-fns/getHours';
  import startOfWeek from 'date-fns/startOfWeek';
  import squareSelectionScheme from './selectionScheme';

  const formatHour = (hour) => `${hour === 0 ? '24' : hour}:00`;

  function unique(arr) {
    return arr.filter((item, index, arr) => arr.indexOf(item, 0) === index);
  }

  export default {
    name: 'HnScheduleSelector',

    model: {
      prop: 'selection'
    },

    props: {
      startTime: {
        type: Number,
        default: 0
      },
      endTime: {
        type: Number,
        default: 23
      },
      numDays: {
        type: Number,
        default: 7
      },
      startDate: {
        type: Date,
        default: () => startOfWeek(new Date())
      },
      selection: {
        type: Array,
        default: () => ([])
      },
      halfHour: Boolean,
      autoComplete: Boolean,
      dateFormat: {
        type: String,
        default: 'M/d'
      },
      formatDate: Function,
      border: Boolean,
      height: {
        type: String,
        default: '360px'
      }
    },

    data() {
      return {
        dates: [],
        selectionDraft: [...this.selection],
        selectionType: null,
        selectionStart: null
      };
    },

    created() {
      const startDate = startOfDay(this.startDate);
      this.dates = [];
      const { halfHour, startTime, endTime } = this;

      for (let d = 0; d < this.numDays; d += 1) {
        const minTime = halfHour ? startTime + 0.5 : startTime;
        const maxTime = halfHour ? endTime + 1 : endTime;
        this.dates.push(this.generateRangeDate(addDays(startDate, d), minTime, maxTime));
      }
    },

    mounted() {
      document.addEventListener('mouseup', this.endSelection);
    },

    beforeDestroy() {
      document.removeEventListener('mouseup', this.endSelection);
    },

    methods: {
      /**
       * 根据起点时间，生成时间范围内的时间类型数组
       * @param {Date} startDate 起点时间
       * @param {Number} startTime 起点
       * @param {Number} endTime 终点
       * @return {Date[]} 时间类型数组
       */
      generateRangeDate(startDate, startTime, endTime) {
        const rangeDate = [];
        const step = this.halfHour ? 0.5 : 1;
        let h = startTime;
        while (h <= endTime) {
          rangeDate.push(addMinutes(startDate, h * 60));
          h += step;
        }
        return rangeDate;
      },

      /**
       * 根据日期分组
       * @param {Date[]} dates 时间类型数组
       * @return {Object} 以时间日期 yyyy/MM/dd 分组的对象 { '2019/12/25': [7, 7.5, 8] }
       */
      groupByDay(dates) {
        const dayObj = {};
        for (let i = 0; i < dates.length; i++) {
          let day = formatDate(dates[i], 'yyyy/MM/dd');

          if (!dayObj[day]) {
            dayObj[day] = [];
          }

          const dayHour = getHours(dates[i]);
          if (new Date(dates[i]).getMinutes() === 30) {
            dayObj[day].push(dayHour + 0.5);
          } else {
            dayObj[day].push(dayHour);
          }
        }

        return dayObj;
      },

      /**
       * 覆盖一天中起始时间内的所有时间
       * @param {Date[]} dates 时间类型数组
       */
      autoCompleteDate(dates) {
        let newDates = [];
        const dayObj = this.groupByDay(dates);

        Object.keys(dayObj)
          .forEach(k => {
            const minTime = Math.min.apply(null, dayObj[k]);
            const maxTime = Math.max.apply(null, dayObj[k]);
            newDates = newDates.concat(this.generateRangeDate(new Date(k), minTime, maxTime));
          });

        this.selectionDraft = newDates;
      },

      endSelection() {
        if (this.autoComplete) {
          this.autoCompleteDate(this.selectionDraft);
        }

        this.$emit('input', this.selectionDraft);
        this.$emit('change', this.selectionDraft);
        this.selectionType = null;
        this.selectionStart = null;
      },

      // Given an ending Date, determines all the dates that should be selected in this draft
      updateAvailabilityDraft(selectionEnd) {
        const { selectionType, selectionStart } = this;

        if (selectionType === null || selectionStart === null) return;

        let newSelection = [];
        if (selectionStart && selectionEnd && selectionType) {
          newSelection = squareSelectionScheme(selectionStart, selectionEnd, this.dates);
        }

        let nextDraft = [...this.selection];

        if (selectionType === 'add') {
          nextDraft = unique([...nextDraft, ...newSelection]);
        } else if (selectionType === 'remove') {
          nextDraft = nextDraft.filter(a => !newSelection.find(b => isSameMinute(a, b)));
        }

        this.selectionDraft = nextDraft;
      },

      handleSelectionStartEvent(startTime) {
        // Check if the startTime cell is selected/unselected to determine if this drag-select should
        // add values or remove values
        const timeSelected = this.selection.find(a => isSameMinute(a, startTime));

        this.selectionType = timeSelected ? 'remove' : 'add';
        this.selectionStart = startTime;
      },

      handleMouseEnterEvent(time) {
        // Need to update selection draft on mouseup as well in order to catch the cases
        // where the user just clicks on a single cell (because no mouseenter events fire
        // in this scenario)
        this.updateAvailabilityDraft(time);
      },

      handleMouseUpEvent(time) {
        this.updateAvailabilityDraft(time);
        // Don't call this.endSelection() here because the document mouseup handler will do it
      },

      genTitle() {
        return (
          <div class="hn-schedule-selector__title-wrap">
            {
              this.dates.map((dayOfTimes, index) => {
                const formatDateFn = this.formatDate || formatDate;
                return (
                  <div class="hn-schedule-selector__date-day">
                    {formatDateFn(dayOfTimes[0], this.dateFormat)}
                  </div>
                );
              })
            }
          </div>
        );
      },

      genTimeLabels() {
        const labels = [];
        const { startTime, endTime, halfHour } = this;

        for (let t = startTime; t <= endTime; t += 1) {
          labels.push(
            <div
              class={{
                'hn-schedule-selector__time-label': true,
                'hn-schedule-selector__time-label--halfHour': halfHour
              }}
              key={t}>{formatHour(t)}</div>
          );
        }
        return <div class="hn-schedule-selector__time-wrap">{labels}</div>;
      },

      genDateCell(time) {
        const selected = Boolean(this.selectionDraft.find(a => isSameMinute(a, time)));
        const { halfHour } = this;
        return (
          <div
            class={{
              'hn-schedule-selector__date-cell': true,
              'hn-schedule-selector__date-cell--selected': selected,
              'hn-schedule-selector__date-cell--unselected': !selected,
              'hn-schedule-selector__date-cell--halfHour': halfHour
            }}
            // Mouse handlers
            onMousedown={this.handleSelectionStartEvent.bind(this, time)}
            onMouseenter={this.handleMouseEnterEvent.bind(this, time)}
            onMouseup={this.handleMouseUpEvent.bind(this, time)}
          />
        );
      },

      genDateColumn(dayOfTimes, index) {
        return <div class="hn-schedule-selector__date-wrap">
          {dayOfTimes.map(time => this.genDateCell(time))}
        </div>;
      }
    },

    render() {
      return (
        <div class={{
          'hn-schedule-selector': true,
          'hn-schedule-selector--border': this.border
        }}>
          {this.genTitle()}
          <div
            class="hn-schedule-selector__content"
            style={{ height: this.height }}
          >
            {this.genTimeLabels()}
            {this.dates.map(this.genDateColumn)}
          </div>
        </div>
      );
    }
  };
</script>
