<script>
import { Date, DateTime, cmp, timedelta, time, sub, add } from '@silane/datetime';
import { ref, computed, watch } from 'vue';

import { dateAndTimeRangeToDateTimeRange } from '../../common/utils.js';
import { TimeTable } from '../../common/timetable';


/**
 * Modulo
 * @param {number} a
 * @param {number} n
 * @returns {number}
 */
function mod(a, n) {
  return ((a % n) + n) % n;
}

/**
 * @param {number} end
 * @returns {number[]}
 */
function range(end) {
  return Array.from({ length: end }, (_, i) => i);
}

/**
 * @template T
 * @param {T[]} items
 * @param {(item: T) => boolean} checked
 * @returns {{ checked: boolean, indterminate: boolean}}
 */
function summaryCheckboxState(items, checked) {
  const allCount = items.length, count = items.filter(checked).length;
  return {
    checked: count >= allCount, indeterminate: 0 < count && count < allCount,
  };
}

export default {
  name: 'InputCalendarTimeRanges',
  props: {
    dates: {
      type: /** @type{
        import('vue').PropType<import('@silane/datetime').Date[]>
      } */(Array), required: true,
    },
    modelValue: {
      type: /** @type{import('vue').PropType<{
        startTime: import('@silane/datetime').DateTime,
        endTime: import('@silane/datetime').DateTime,
      }[]>} */(Array), required: true,
    },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    const timeRangesByDay = computed(() => {
      const timetable = new TimeTable();
      for(const range of props.modelValue) {
        timetable.add(range.startTime, range.endTime, true);
      }
      return Object.fromEntries(props.dates.map(date => [
        date.isoFormat(), timetable.get(
          ...dateAndTimeRangeToDateTimeRange(date, time(), time())
        ).filter(x => x.items.has(true)).map(
          x => [x.start.timetz(), x.end.timetz()]
        )
      ]));
    });

    const calendar = computed(() => {
      const datesOrdinal = props.dates.map(x => x.toOrdinal());
      const minDate = Date.fromOrdinal(Math.min(...datesOrdinal));
      const maxDate = Date.fromOrdinal(Math.max(...datesOrdinal));
      const calendarMinDate = sub(
        minDate, timedelta({ days: mod(minDate.weekday() + 1, 7) })
      );
      const calendarMaxDate = add(
        maxDate, timedelta({ days: mod(6 - maxDate.weekday(), 7) })
      );
      return { minDate: calendarMinDate, maxDate: calendarMaxDate };
    });

    const dateCheckboxes = ref(/** @type{Record<string, boolean>} */({}));
    function resetDateCheckboxes() {
      dateCheckboxes.value = {};
      for(const date of props.dates) {
        dateCheckboxes.value[date.isoFormat()] = false;
      }
    }

    watch(() => props.dates, () => {
      resetDateCheckboxes();
    }, { immediate: true });

    const timeRangesInput = ref(/** @type {[
      import('@silane/datetime').Time, import('@silane/datetime').Time,
    ][]} */([]));

    const timeRangesInputInvalidity = computed(() => [
      ...timeRangesInput.value.filter(
        x => !x[0]
      ).map(x => ({ code: 'starttime-null', range: x })),
      ...timeRangesInput.value.filter(
        x => !x[1]
      ).map(x => ({ code: 'endtime-null', range: x })),
      ...timeRangesInput.value.filter(
        x => x[0] && x[1] && cmp(x[1], time(0)) !== 0 && cmp(...x) > 0
      ).map(x => ({ code: 'negativeduration', range: x })),
    ]);

    function addTimeRangeInput() {
      timeRangesInput.value.push([time(0), time(0)]);
    }
    addTimeRangeInput();

    function setRanges() {
      if(timeRangesInputInvalidity.value.length) {
        return;
      }
      const timetable = new TimeTable();
      for(const range of props.modelValue) {
        timetable.add(range.startTime, range.endTime, null);
      }
      for(const date of props.dates.filter(
        x => dateCheckboxes.value[x.isoFormat()]
      )) {
        timetable.remove(
          ...dateAndTimeRangeToDateTimeRange(date, time(0), time(0)), null
        );
        for(const range of timeRangesInput.value) {
          timetable.add(
            ...dateAndTimeRangeToDateTimeRange(date, ...range), null
          );
        }
      }
      emit('update:modelValue', timetable.get().filter(
        x => x.items.size
      ).map(x => ({ startTime: x.start, endTime: x.end })));
      resetDateCheckboxes();
    }

    return {
      range, summaryCheckboxState, cmp, add, sub, time, timedelta,
      timeRangesByDay, calendar, dateCheckboxes, timeRangesInput,
      timeRangesInputInvalidity, addTimeRangeInput, setRanges,
    };
  },
}
</script>

<template>
<div>
  <table class="calendar">
    <thead>
      <tr>
        <th class="calendar-headercell">
          <label>
            <input type="checkbox" v-bind="summaryCheckboxState(
              Object.keys(timeRangesByDay), k => dateCheckboxes[k]
            )" @input="$event => Object.keys(dateCheckboxes).forEach(
              k => dateCheckboxes[k] = $event.target.checked
            )" />
          </label>
        </th>
        <th v-for="weekOfDay of range(7)" class="calendar-headercell">
          <label>
            <input
              type="checkbox" v-bind="summaryCheckboxState(
                Object.keys(timeRangesByDay).filter(
                  x => Date.fromISOFormat(x).weekday() === (weekOfDay + 6) % 7
                ), date => dateCheckboxes[date]
              )" @input="$event => Object.keys(timeRangesByDay).filter(
                x => Date.fromISOFormat(x).weekday() === (weekOfDay + 6) % 7
              ).forEach(k => dateCheckboxes[k] = $event.target.checked)"
            /> {{ ['日', '月', '火', '水', '木', '金', '土'][weekOfDay] }}
          </label>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row of range(
        sub(calendar.maxDate, calendar.minDate).days / 7
      )">
        <th class="calendar-headercell">
          <label>
            <input type="checkbox" v-bind="summaryCheckboxState(
              range(7).map(i => add(calendar.minDate, timedelta(
                { days: row * 7 + i }
              )).isoFormat()).filter(date => dateCheckboxes[date] != null),
              date => dateCheckboxes[date]
            )" @input="$event => range(7).map(i => add(
              calendar.minDate, timedelta({ days: row * 7 + i })
            ).isoFormat()).filter(date => dateCheckboxes[date] != null).forEach(
              date => dateCheckboxes[date] = $event.target.checked
            )" />第{{ row + 1 }}週
          </label>
        </th>
        <td v-for="[col, date] of range(7).map(i => [
          i, add(calendar.minDate, timedelta({ days: row * 7 + i }))
        ])" :key="date.isoFormat()" class="calendar-datecell" :class="{
          'calendar-datecell-selected': dateCheckboxes[date.isoFormat()],
          'calendar-datecell-disabled': !timeRangesByDay[date.isoFormat()],
        }" @click="$event => dateCheckboxes[date.isoFormat()] = !dateCheckboxes[date.isoFormat()]">
          <label v-if="timeRangesByDay[date.isoFormat()]" @click.prevent="">
            <input
              type="checkbox" :checked="dateCheckboxes[date.isoFormat()]" readonly="readonly"
            /><date-time :value="date" format="%m/%d" />
          </label>
          <date-time v-else :value="date" format="%m/%d" />
          <ol v-if="timeRangesByDay[date.isoFormat()]" class="calendar-datecell-rangelist">
            <li v-for="
              range of timeRangesByDay[date.isoFormat()]
            " :key="range[0].isoFormat()">
              <date-time :value="range[0]" /> - <date-time
                :value="range[1]"
                :format="cmp(range[1], time()) ? undefined : '24:00'"
              />
            </li>
          </ol>
        </td>
      </tr>
    </tbody>
  </table>
  <div v-if="
    Object.values(dateCheckboxes).some(x => x)
  " class="timerangessetblock">
    <div>
      <ul>
        <li
          v-for="timeRange, timeRangeIdx of timeRangesInput"
          style=""
        >
          <label>開始<input-time v-model="timeRange[0]" /></label>
          <label>終了<input-time v-model="timeRange[1]" /></label>
          <button
            type="button" @click="timeRangesInput.splice(timeRangeIdx, 1)"
            title="時間帯削除"
          >×</button>
          <span v-if="timeRangesInputInvalidity.some(
            x => x.range === timeRange && x.code === 'starttime-null'
          )">開始時刻が指定されていません</span>
          <span v-if="timeRangesInputInvalidity.some(
            x => x.range === timeRange && x.code === 'endtime-null'
          )">終了時刻が指定されていません</span>
          <span v-if="timeRangesInputInvalidity.some(
            x => x.range === timeRange && x.code === 'negativeduration'
          )">終了時刻が開始時刻より前です</span>
        </li>
      </ul>
      <button
        type="button" @click="addTimeRangeInput()" title="時間帯追加"
      >＋</button>
    </div>
    <button type="button" @click="setRanges()">セット</button>
  </div>
</div>
</template>

<style scoped>
.timerangessetblock {
  width: fit-content;
  position: sticky;
  inset-block-end: 0;
  margin: .5em auto;
  box-shadow: 0 0 .5em skyblue;
  border: solid 2px skyblue;
  border-radius: .5em;
  padding: .5em .5em;
  background-color: white;
  display: grid;
  grid-template-columns: 20em auto;
  column-gap: 1em;
  align-items: end;
}

.calendar {
  border-collapse: collapse;
  height: 100%;
}
.calendar-headercell, .calendar-datecell {
  border: solid 1px black;
}
.calendar-headercell label {
  display: block;
  height: 100%;
}
.calendar-datecell {
  vertical-align: top;
}
.calendar-datecell-selected {
  background-color: skyblue;
}
.calendar-datecell-disabled {
  background-color: lightgray;
}
.calendar-datecell-rangelist {
  list-style: disc;
}
</style>
