<i18n>
{
  "ru": {
    "WeekSchedule": {
      "daysOfWeek": {
        "1": "Вс",
        "2": "Пн",
        "3": "Вт",
        "4": "Ср",
        "5": "Чт",
        "6": "Пт",
        "7": "Сб"
      },
      "buttons": {
        "ok": "Применить",
        "delete": "Удалить",
        "cancel": "Отменить"
      }
    }
  },
  "en": {
    "WeekSchedule": {
      "daysOfWeek": {
        "1": "Su",
        "2": "Mo",
        "3": "Tu",
        "4": "We",
        "5": "Th",
        "6": "Fr",
        "7": "St"
      },
      "buttons": {
        "ok": "Ok",
        "delete": "Delete",
        "cancel": "Cancel"
      }
    }
  }
}
</i18n>

<template>
  <div class="schedule">
    <div v-for="day in week" :key="day" class="schedule__day">
      <div :key="day" :class="['schedule__day-header', getDayColor(day)]">
        {{ $t(`WeekSchedule.daysOfWeek.${day}`) }}
      </div>
      <div class="schedule__time-slots">
        <div
          v-for="(timeInterval, index) in sortIntervals(daysIndex[day])"
          :key="`day-${timeInterval.dayOfWeek}-${index}`"
          class="schedule__time-slot"
        >
          <q-btn
            size="md"
            class="q-px-none schedule__time-string"
            padding="none"
          >
            <span v-if="timeInterval.startTime" class="q-mx-xs">{{
              formatTime(timeInterval.startTime)
            }}</span>
            <span v-if="timeInterval.endTime" class="q-mx-xs">{{
              formatTime(timeInterval.endTime)
            }}</span>

            <q-menu touch-position context-menu>
              <q-list dense style="min-width: 100px">
                <q-item
                  clickable
                  v-close-popup
                  @click="deleteInterval(timeInterval)"
                >
                  <q-item-section>{{
                    $t('WeekSchedule.buttons.delete')
                  }}</q-item-section>
                </q-item>
              </q-list>
            </q-menu>
            <q-popup-proxy
              cover
              transition-show="scale"
              transition-hide="scale"
              ref="popupInterval"
              @before-show="
                updateProxyRange({
                  startTime: timeInterval.startTime,
                  endTime: timeInterval.endTime,
                })
              "
            >
              <div class="q-pa-md">
                <div class="q-gutter-sm column">
                  <q-input
                    outlined
                    dense
                    mask="time"
                    :rules="[testTime]"
                    v-model="procyRange.startTime"
                    @keydown="keyControl($event, 'startTime')"
                  >
                    <template v-slot:append>
                      <q-icon name="access_time" class="cursor-pointer">
                        <q-popup-proxy
                          @before-show="updateProxyTime(procyRange.startTime)"
                          cover
                          transition-show="scale"
                          transition-hide="scale"
                        >
                          <q-time
                            format24h
                            mask="HH:mm"
                            :minute-options="[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]"
                            v-model="proxyTime"
                          >
                            <div
                              class="row items-center justify-end q-gutter-sm"
                            >
                              <q-btn
                                flat
                                color="primary"
                                :label="$t('WeekSchedule.buttons.cancel')"
                                v-close-popup
                              />
                              <q-btn
                                flat
                                color="primary"
                                :label="$t('WeekSchedule.buttons.ok')"
                                v-close-popup
                                @click="saveTime(procyRange, 'startTime')"
                              />
                            </div>
                          </q-time>
                        </q-popup-proxy>
                      </q-icon>
                    </template>
                  </q-input>
                  <q-input
                    outlined
                    dense
                    mask="time"
                    v-model="procyRange.endTime"
                    :rules="[testTime]"
                    @keydown="keyControl($event, 'endTime')"
                  >
                    <template v-slot:append>
                      <q-icon name="access_time" class="cursor-pointer">
                        <q-popup-proxy
                          @before-show="updateProxyTime(procyRange.endTime)"
                          cover
                          transition-show="scale"
                          transition-hide="scale"
                        >
                          <q-time
                            format24h
                            mask="HH:mm"
                            :minute-options="[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]"
                            v-model="proxyTime"
                          >
                            <div
                              class="row items-center justify-end q-gutter-sm"
                            >
                              <q-btn
                                flat
                                color="primary"
                                :label="$t('WeekSchedule.buttons.cancel')"
                                v-close-popup
                              />
                              <q-btn
                                flat
                                color="primary"
                                :label="$t('WeekSchedule.buttons.ok')"
                                v-close-popup
                                @click="saveTime(procyRange, 'endTime')"
                              />
                            </div>
                          </q-time>
                        </q-popup-proxy>
                      </q-icon>
                    </template>
                  </q-input>
                  <div class="row items-center justify-end">
                    <q-btn
                      flat
                      v-close-popup
                      icon="delete"
                      color="primary"
                      @click="deleteInterval(timeInterval)"
                    />
                    <q-btn
                      flat
                      color="primary"
                      :label="$t('WeekSchedule.buttons.ok')"
                      @click="saveRange(timeInterval)"
                    />
                  </div>
                </div>
              </div>
            </q-popup-proxy>
          </q-btn>
        </div>
        <q-btn
          v-if="hasSpace(day) && isDayAWorkDay(day)"
          class="schedule__add-interval"
          icon="more_time"
          size="sm"
          padding="sm"
          :disable="disable"
          @click="addInterval(day)"
        />
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch, Ref } from 'vue-property-decorator';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import last from 'lodash/last';
import {
  timeAdd,
  timeParse,
  timeFormat,
  timeSubs,
} from '../utils/date-manipulations';
import first from 'lodash/first';
import isNumber from 'lodash/isNumber';
import { WorkCalendarForDomain } from '../utils/WorkCalendarForDomain';
import { notifyError } from '@utils/notify';
import { QPopupProxy } from 'quasar';

export interface TimeInterval {
  deleted?: boolean;
  /**
   * Идентификатор
   */
  id?: number;
  /**
   * День недели
   */
  dayOfWeek?: number;
  /**
   * Время начала приема
   */
  startTime?: string;
  /**
   * Врема окончания приема
   */
  endTime?: string;
}

interface ScheduleIndex {
  [key: string]: TimeInterval[];
}

interface PureTimeInterval {
  startTime: number;
  endTime: number;
}

interface MixTimeInterval {
  startTime: string | number;
  endTime: string | number;
}

interface RestDay {
  numDayOfWeek: number;
  workDay: boolean;
}

@Component({
  components: {},
  methods: {
    get,
  },
})
export default class WeekSchedule extends Vue {
  @Prop({ default: [] }) value: TimeInterval[];
  @Prop({ default: 30 }) timeSlot: number;
  @Prop({ default: 'bg-green' }) workDayColor: string;
  @Prop({ default: 'bg-grey-3' }) emptyWorkDayColor: string;
  @Prop({ default: 'bg-red' }) restDayColor: string;
  @Prop({ default: 'bg-red-3' }) emptyRestDayColor: string;
  @Prop({ default: false }) disable: boolean;
  @Prop({ default: () => ({}) }) workCalendar: WorkCalendarForDomain;
  @Ref('popupInterval') popupInterval: QPopupProxy[];

  dirty = false;
  model: TimeInterval[] = [];

  touch(): void {
    this.dirty = true;
  }

  clear(): void {
    this.dirty = false;
  }

  @Watch('value', { immediate: true })
  onValueChange(value: TimeInterval[]): void {
    this.model = [...value];
  }

  statrOfDay = timeParse('00:00:00');
  endOfDay = timeParse('24:00:00');
  mealTime = 60;
  breakTime = 10;
  proxyTime = '';

  procyRange = {
    startTime: '',
    endTime: '',
  };

  get startOfWorkDay(): number {
    return this.value.reduce((time: number, day) => {
      if (day.startTime) {
        return Math.min(time, timeParse(day.startTime));
      }
      return time;
    }, this.statrOfDay);
  }

  @Watch('procyRange.startTime')
  onProcyRangeChangeStart(startTime: string | number): void {
    if (!/[0-2][0-9]:[0-5][05]/.test(startTime.toString())) return;

    const timeSlot = this.timeSlot * 60;
    startTime = isNumber(startTime) ? startTime : timeParse(startTime);
    let endTime = isNumber(this.procyRange.endTime)
      ? this.procyRange.endTime
      : timeParse(this.procyRange.endTime);
    if (startTime >= endTime) {
      endTime = timeAdd(startTime, 'minute', this.timeSlot);
    }
    if (startTime > this.endOfDay - timeSlot) {
      startTime = this.endOfDay - timeSlot;
      endTime = this.endOfDay;
    }
    if (startTime + timeSlot > endTime) {
      endTime = startTime + timeSlot;
    }
    const coefficient = Math.trunc((endTime - startTime) / timeSlot);
    endTime = startTime + timeSlot * coefficient;
    this.procyRange.startTime = timeFormat(startTime, 'hh:mm');
    this.procyRange.endTime = timeFormat(endTime, 'hh:mm');
  }

  @Watch('procyRange.endTime')
  onProcyRangeChangeEnd(endTime: string | number): void {
    if (!/[0-2][0-9]:[0-5][05]/.test(endTime.toString())) return;

    const timeSlot = this.timeSlot * 60;
    let startTime = isNumber(this.procyRange.startTime)
      ? this.procyRange.startTime
      : timeParse(this.procyRange.startTime);
    endTime = isNumber(endTime) ? endTime : timeParse(endTime);
    if (endTime <= startTime || endTime < startTime + timeSlot) {
      startTime = endTime - timeSlot;
    }
    if (startTime < this.statrOfDay) {
      startTime = this.statrOfDay;
      endTime = startTime + timeSlot;
    }
    if (endTime > this.endOfDay) {
      endTime = this.endOfDay;
    }
    const coefficient = Math.trunc((endTime - startTime) / timeSlot);
    startTime = endTime - timeSlot * coefficient;
    this.procyRange.startTime = timeFormat(startTime, 'hh:mm');
    this.procyRange.endTime = timeFormat(endTime, 'hh:mm');
  }

  updateProxyTime(time: string): void {
    this.proxyTime = time;
  }

  updateProxyRange({ startTime, endTime }: TimeInterval): void {
    this.procyRange = {
      startTime: startTime ? this.formatTime(startTime) : '',
      endTime: endTime ? this.formatTime(endTime) : '',
    };
  }

  sortIntervals(intervals: TimeInterval[] = []): TimeInterval[] {
    return intervals
      .sort((a, b) => {
        const aTime = a.startTime ? timeParse(a.startTime) : 0;
        const bTime = b.startTime ? timeParse(b.startTime) : 0;

        return aTime < bTime ? -1 : 1;
      })
      .map((current, i, arr) => {
        const next = arr[i + 1];
        if (next) {
          const endOfCurrent = current.endTime ? timeParse(current.endTime) : 0;
          const startOfNext = next.startTime ? timeParse(next.startTime) : 0;
          const timeSlot = 600;
          if (endOfCurrent + timeSlot > startOfNext) {
            current.endTime = timeFormat(startOfNext - timeSlot);
          }
        }
        return current;
      });
  }

  saveRange(interval: TimeInterval): void {
    this.touch();
    const startTime = timeParse(this.procyRange.startTime);
    const endTime = timeParse(this.procyRange.endTime);
    let count = '';
    let currentDayOfWeekSlots = this.model.filter(
      (day) => day.dayOfWeek === interval.dayOfWeek && day.id !== interval.id
    );
    currentDayOfWeekSlots = currentDayOfWeekSlots.flatMap((e) => {
      const parsedStartTime = timeParse(e.startTime || '');
      const parsedEndTime = timeParse(e.endTime || '');
      const minBreak = 300;
      if (
        (parsedStartTime <= startTime && startTime <= parsedEndTime) ||
        (parsedStartTime <= endTime && endTime <= parsedEndTime)
      ) {
        const moreStart = startTime < parsedStartTime ? startTime : parsedStartTime;
        const moreEnd = endTime > parsedEndTime ? endTime : parsedEndTime;
        count = count.concat('2');
        return { ...e, startTime: timeFormat(moreStart), endTime: timeFormat(moreEnd) };
      } else if (startTime <= parsedStartTime && endTime >= parsedEndTime) {
        count = count.concat('1');
        return [];
      } else if (
        startTime + minBreak === parsedStartTime || startTime - minBreak === parsedStartTime ||
        endTime + minBreak === parsedEndTime || endTime - minBreak === parsedEndTime ||
        startTime + minBreak === parsedEndTime || startTime - minBreak === parsedEndTime ||
        endTime + minBreak === parsedStartTime || endTime - minBreak === parsedStartTime
      ) {
        count = count.concat('3');
        return [];
      }
      return e;
    });

    if (count.length) {
      if (count.includes('3')) {
        notifyError('Перерыв между интервалами не может быть менее 5 минут');
        return;
      }
      this.$q
        .dialog({
          title: 'Есть пересечение интервалов',
          message: 'Хотите объединить?',
          ok: 'Да',
          cancel: 'Нет',
        })
        .onCancel(() => {
          return undefined;
        })
        .onOk(() => {
          this.model = this.model.filter((day) => day.dayOfWeek !== interval.dayOfWeek);
          this.model = [...this.model, ...currentDayOfWeekSlots];
          if (!count.includes('2')) this.model.push(interval);
          interval.startTime = timeFormat(startTime);
          interval.endTime = timeFormat(endTime);
          this.notifyListeners();
        });
    } else {
      interval.startTime = timeFormat(startTime);
      interval.endTime = timeFormat(endTime);
      this.notifyListeners();
      this.popupInterval.forEach((e) => e.hide());
    }
  }

  saveTime(day: TimeInterval, fieldName: 'startTime' | 'endTime'): void {
    day[fieldName] = `${this.proxyTime}:00`;
  }

  notifyListeners(): void {
    this.$emit('input', this.model);
  }

  get week(): number[] {
    return Array(7)
      .fill('')
      .map((___, i) => {
        return i === 6 ? 1 : i + 2;
      });
  }

  get daysIndex(): ScheduleIndex {
    return this.model.reduce((memo, day) => {
      const dayOfWeek = day.dayOfWeek ?? -1;
      if (!Array.isArray(memo[dayOfWeek])) {
        memo[dayOfWeek] = [day];
      } else {
        memo[dayOfWeek].push(day);
      }
      return memo;
    }, {} as ScheduleIndex);
  }

  isDayNotEmpty(dayNumber: number): boolean {
    return !isEmpty(this.daysIndex[dayNumber]);
  }

  isDayAWorkDay(dayNumber: number): boolean {
    return !!this.workCalendar.getDayofAWeek(dayNumber).workDay;
  }

  getDayColor(dayNumber: number): string {
    const notEmpty = this.isDayNotEmpty(dayNumber);

    if (this.workCalendar.getDayofAWeek(dayNumber).workDay) {
      return notEmpty ? this.workDayColor : this.emptyWorkDayColor;
    } else {
      return notEmpty ? this.restDayColor : this.emptyRestDayColor;
    }
  }

  keyControl(ev: KeyboardEvent, key: keyof PureTimeInterval): void {
    if (!['ArrowUp', 'ArrowDown'].includes(ev.key)) return;
    let value = timeParse(this.procyRange[key]);

    switch (ev.key) {
      case 'ArrowUp':
        ev.preventDefault();
        value = timeAdd(value, 'minute', this.timeSlot);
        break;
      case 'ArrowDown':
        ev.preventDefault();
        value = timeSubs(value, 'minute', this.timeSlot);
        break;
    }
    this.procyRange[key] = timeFormat(value, 'hh:mm');
  }

  balanceTimeInterval({
    startTime,
    endTime,
  }: MixTimeInterval): PureTimeInterval {
    startTime = isNumber(startTime) ? startTime : timeParse(startTime);
    endTime = isNumber(endTime) ? endTime : timeParse(endTime);

    if (startTime >= endTime) {
      endTime = timeAdd(startTime, 'minute', this.timeSlot);
    }
    if (endTime > this.endOfDay) {
      endTime = this.endOfDay;
    }
    if (startTime > this.endOfDay - this.timeSlot) {
      startTime = this.endOfDay - this.timeSlot;
    }
    if (startTime + this.timeSlot * 60 > endTime) {
      endTime = startTime + this.timeSlot;
    }
    return { startTime, endTime };
  }

  hasSpace(dayNumber: number): boolean {
    const time = timeParse(last(this.daysIndex[dayNumber])?.endTime || '00:00');
    return time + this.timeSlot * 60 < 86400;
  }

  addInterval(day: number): void {
    const today = this.daysIndex[day];
    const firstInterval = first(today);
    const lastInterval = last(today);
    const breake = today?.length === 1 ? this.mealTime : this.breakTime;

    const startTime = lastInterval?.endTime
      ? timeAdd(lastInterval?.endTime, 'minute', breake)
      : this.startOfWorkDay;

    const endTime = timeAdd(firstInterval?.startTime || startTime, 'hour', 8);

    const balancedInterlal = this.balanceTimeInterval({ startTime, endTime });

    if (startTime >= this.endOfDay) {
      return;
    }

    this.model.push({
      dayOfWeek: day,
      startTime: timeFormat(balancedInterlal.startTime),
      endTime: timeFormat(balancedInterlal.endTime),
    });
    this.notifyListeners();
  }

  deleteInterval(timeInterval: TimeInterval): void {
    timeInterval.deleted = true;
    this.model = this.model.filter(({ deleted }) => !deleted);
    this.notifyListeners();
  }

  created(): void {
    this.clear();
  }

  formatTime(time: string): string {
    return time.substring(0, 5);
  }

  testTime(time: string): boolean | string {
    return /[0-2][0-9]:[0-5][05]/.test(time) || '';
  }
}
</script>

<style scoped lang="scss">
.schedule {
  display: flex;
  flex-flow: row nowrap;
  justify-content: stretch;
  gap: 1em;

  &__day {
    display: flex;
    flex-flow: column nowrap;
    flex: 1 1 14.258%;
    text-align: center;
    align-items: stretch;
    align-content: flex-start;
    justify-content: flex-start;

    user-select: none;

    &-header {
      display: inline-flex;
      flex-direction: column;
      align-items: stretch;
      position: relative;
      vertical-align: middle;
      font-size: 14px;
      line-height: 1.715em;
      text-decoration: none;
      color: inherit;
      font-weight: 500;
      text-transform: uppercase;
      text-align: center;
      width: auto;
      height: auto;
      flex: 0 1 auto;
      padding: 4px;
      border-radius: 4px 4px 0 0;
    }
  }

  &__time-slot {
    display: flex;
    flex-flow: row nowrap;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    border-bottom: 1px solid #eeeeeeff;
  }

  &__time-string {
    display: block;
    padding: 0.5rem;
    width: 100%;
  }

  &__add-interval {
    display: block;
    width: 100%;
  }
}
</style>
