<script lang="ts">
import { watch, computed, shallowRef, type PropType } from 'vue';
import {
  DateTime, add, sub, cmp, time, timedelta, type Date, type Time,
} from '@silane/datetime';

import { inject, keys as injectionKeys } from '../injection';
import {
  getResourceDisplayText, getUniquePeopleNames,
} from '../common/zkan-utils.js';
import { getTimeLine } from '../common/timeline.js';
import { kanaToRoman } from '../common/kana-roman';
import type { Resource } from '../composables/resourcestore';
import type { Event } from '../composables/eventstore';


function dateToDateTimeRange(date: Date) {
  return {
    startTime: DateTime.combine(date, time()),
    endTime: DateTime.combine(add(date, timedelta({ days: 1 })), time()),
  };
}

function* zip<T extends readonly Iterable<any>[]>(...iterables: [...T]) {
  const iterators = iterables.map(x => x[Symbol.iterator]());
  while(true) {
    const iteratorResults = iterators.map(x => x.next());
    if(iteratorResults.some(x => x.done)) break;
    yield iteratorResults.map(x => x.value) as T extends { [P in keyof infer U]: Iterable<(infer U)[P]> } ? U : never;
  }
}

function* iterGroupby<T, K>(iterable: Iterable<T>, key: (element: T) => K) {
  let items: T[] = [];
  let currentKey: K | null = null;
  for(const x of iterable) {
    const nextKey = key(x);
    if(items.length && nextKey !== currentKey) {
      yield { key: currentKey, items };
      items = [];
    }
    currentKey = nextKey;
    items.push(x);
  }
  if(items.length) {
    yield { key: currentKey, items };
  }
}

export default {
  name: 'DateAndResourceTimeTable',
  props: {
    classroom: { type: Object as PropType<Resource>, required: true },
    mode: { type: String as PropType<'date'| 'resource'>, required: true },
    dates: { type: Array as PropType<Date[]>, required: true },
    resources: { type: Array as PropType<Resource[]>, required: true },
    hideConfidence: { type: Boolean, default: false },
  },
  setup(props) {
    const links = inject(injectionKeys.links, null);
    const handleError = inject(injectionKeys.errorHandler).handleErrorAsync;
    const resourceStore = inject(injectionKeys.resourceStore);
    const resourceHourStore = inject(injectionKeys.resourceHourStore);
    const eventStore = inject(injectionKeys.eventStore);

    const loading = shallowRef(false);
    const timeLineTimeRange = shallowRef<[Time, Time] | null>(null);
    const columns = shallowRef<{ type: 'resource', items: {
      columnID: string, resource: Resource
    }[] } | { type: 'date', items: {
      columnID: string, holiday: boolean, dates: Date[],
    }[] }>({ type: 'resource', items: [] });
    const timeLines = shallowRef<({
      startTime: Time, endTime: Time, available: boolean, events: Event[],
    }[] | null)[]>([]);

    const load = handleError(async () => {
      loading.value = true;
      try {
        if(props.mode === 'resource') {
          if(!props.resources[0]) return;
          const dates = props.dates;
          const resource = props.resources[0];

          const classRoomResourceTimes = (await Promise.all(dates.map(
            date => resourceHourStore.query({
              resource: props.classroom.id,
              startTime: DateTime.combine(date, time()),
              endTime: DateTime.combine(
                add(date, timedelta({ days: 1 })), time()
              ),
            })
          ))).map(x => [
            x[0]?.startTime.time() ?? null,
            x[x.length - 1]?.endTime.time() ?? null,
          ]);
          const isDatesHoliday = classRoomResourceTimes.map(x => x[0] == null);
          const timeLineTimeRangeStart = classRoomResourceTimes.reduce<
            Time | null
          >((prev, cur) => prev != null && cur[0] != null
            ? cmp(prev, cur[0]) < 0 ? prev : cur[0]
            : prev ?? cur[0] ?? null, null
          );
          const timeLineTimeRangeEnd = classRoomResourceTimes.reduce<
            Time | null
          >((prev, cur) => prev != null && cur[1] != null
            ? cmp(prev, time()) === 0 || cmp(cur[0], time()) === 0 ? time()
            : cmp(prev, cur[1]) > 0 ? prev : cur[1]
            : prev ?? cur[1] ?? null, null
          );
          if(timeLineTimeRangeStart && timeLineTimeRangeEnd) {
            timeLineTimeRange.value = [
              timeLineTimeRangeStart, timeLineTimeRangeEnd,
            ];
          } else {
            timeLineTimeRange.value = null;
          }

          const cols = [...iterGroupby(
            dates.map((x, i) => [i, x] as const), ([i, _]) => isDatesHoliday[i]
          )].map(
            ({ key, items }) => ({ key, items: items.map(([_, x]) => x) })
          ).flatMap(x => (
            x.key
            ? [{ holiday: true, dates: x.items }]
            : x.items.map(d => ({ holiday: false, dates: [d] }))
          )).map(x => ({
            ...x, columnID: x.dates[0].isoFormat(),
          }));

          columns.value = { type: 'date', items: cols };

          const events = new Map(zip(dates.map(x => x.isoFormat()), (
            await Promise.all(dates.map(
              date => eventStore.query({ ...dateToDateTimeRange(date) })
            ))
          ).map(events => events.filter(x => !x.eventChangeTo).filter(x => [
            props.classroom.id, resource.id,
          ].every(r => x.resources.map(x => x.id).includes(r))))));
          const resourceHours = new Map(zip(
            dates.map(x => x.isoFormat()),
            await Promise.all(dates.map(
              date => resourceHourStore.query({
                ...dateToDateTimeRange(date), resource: resource.id,
              })
            ))
          ));

          timeLines.value = cols.map(col => {
            if(col.holiday) return null;
            const date = col.dates[0];
            const start = DateTime.combine(date, time());
            return getTimeLine(
              events.get(date.isoFormat()) ?? [],
              resourceHours.get(date.isoFormat()) ?? [],
              start, add(start, timedelta({ days: 1 })),
            ).map(x => ({
              ...x, startTime: x.startTime.time(), endTime: x.endTime.time(),
            }));
          });
        } else if(props.mode === 'date') {
          if(!props.dates[0]) return;
          const date = props.dates[0];
          const allResources = props.resources;

          const classRoomResourceTimes = await resourceHourStore.query({
            resource: props.classroom.id,
            startTime: DateTime.combine(date, time()),
            endTime: DateTime.combine(
              add(date, timedelta({ days: 1 })), time()
            ),
          });

          const isDateHoliday = classRoomResourceTimes.length === 0;
          if(isDateHoliday) {
            timeLineTimeRange.value = null;
          } else {
            timeLineTimeRange.value = [
              classRoomResourceTimes[0].startTime.time(),
              classRoomResourceTimes[
                classRoomResourceTimes.length - 1
              ].endTime.time(),
            ];
          }

          const sourceResourceHours = (await Promise.all(allResources.map(
            resource => resourceHourStore.query({
              ...dateToDateTimeRange(date), resource: resource.id,
            })
          ))).flatMap((x, resourceIdx) => x.map(rh => ({
            ...rh, resource: allResources[resourceIdx],
          })));

          const sourceEvents = (await eventStore.query({
            ...dateToDateTimeRange(date),
          })).filter(x => !x.eventChangeTo && x.resources.map(
            r => r.id
          ).includes(props.classroom.id));

          const resources = allResources.filter(x => (
            sourceResourceHours.map(rh => rh.resource.id).includes(x.id) ||
            sourceEvents.flatMap(e => e.resources.map(r => r.id)).includes(x.id)
          ));

          const resourceHours = resources.map(resource => sourceResourceHours.filter(
            x => x.resource.id === resource.id
          ));
          const events = resources.map(resource => sourceEvents.filter(
            x => x.resources.map(r => r.id).includes(resource.id)
          ));

          columns.value = { type: 'resource', items: resources.map(
            x => ({ columnID: x.id, resource: x })
          )};
          timeLines.value = resources.map((resource, resourceIdx) => {
            const start = DateTime.combine(date, time());
            const segments = getTimeLine(
              events[resourceIdx], resourceHours[resourceIdx],
              start, add(start, timedelta({ days: 1 })),
            );
            return segments.map(x => ({
              ...x, startTime: x.startTime.time(), endTime: x.endTime.time(),
            }));
          });
        }
      } finally {
        loading.value = false;
      }
    });
    watch([
      () => props.mode, () => props.dates, () => props.resources
    ], async () => {
      await load();
    }, { immediate: true });

    const timeTableEvents = computed(() => {
      return timeLines.value.flatMap((x, columnIdx) => !x ? {
        startTime: time(), endTime: time(),
        columnID: columns.value.items[columnIdx].columnID,
        data: { available: false, events: [], holiday: true }
      } : x.filter(
        segment => !(segment.available && segment.events.length === 0)
      ).map(segment => ({
        startTime: segment.startTime, endTime: segment.endTime,
        columnID: columns.value.items[columnIdx].columnID,
        data: {
          available: segment.available, events: segment.events, holiday: false
        },
      })));
    });

    const resourceDisplayTexts = computed(() => {
      const resources = resourceStore.resources.value.filter((x): x is Extract<
        Resource, { type: 'teacher' } | { type: 'student' }
      > => ['teacher', 'student'].includes(x.type));
      return new Map(getUniquePeopleNames(resources).map(
        (x, i) => [resources[i].id, x]
      ));
    });

    function getResourceDisplayShortText(resource: Resource) {
      if(resource.type === 'student' && props.hideConfidence) {
        return `${kanaToRoman(resource.lastNameKana)[0].toUpperCase()}. ${
          kanaToRoman(resource.firstNameKana)[0].toUpperCase()
        }.`;
      }
      return resourceDisplayTexts.value.get(
        resource.id
      ) ?? getResourceDisplayText(resource);
    }

    function getResourceTableColumnHeaderText(
      columnData: { holiday: boolean, dates: Date[] }
    ) {
      const data = columnData;
      return data.dates.length > 1 ? [
        data.dates[0].strftime('%m/%d'),
        `-${data.dates[data.dates.length - 1].strftime('%m/%d')}`,
      ] : [
        data.dates[0].strftime('%m/%d'),
        ['月', '火', '水', '木', '金', '土', '日'][data.dates[0].weekday()],
      ];
    }

    function getResourceTableColumnHeaderLink(
      columnData: { holiday: boolean, dates: Date[] }
    ) {
      return columnData.dates.length === 1 ? links?.dateTimeTableLinkFn(
        columnData.dates[0]
      ) ?? null : null;
    }

    return {
      sub, links, getResourceDisplayShortText,
      getResourceTableColumnHeaderText, getResourceTableColumnHeaderLink,
      loading, timeLineTimeRange, columns, timeTableEvents,
    };
  },
}
</script>

<template>
<p v-if="loading">読み込み中</p>
<p v-else-if="!timeLineTimeRange">指定日付内に教室が開室している時間帯がありません。</p>
<p v-else-if="columns.items.length === 0">表示すべき列がありません</p>
<time-table
  v-else v-bind="$attrs" class="timeline" :style="{ '--timeline-durationseconds': `${
    sub(timeLineTimeRange[1], timeLineTimeRange[0]).totalSeconds()
  }` }"
  :start-time="timeLineTimeRange[0]" :end-time="timeLineTimeRange[1]"
  :columnIDs="columns.items.map(x => x.columnID)" :events="timeTableEvents"
>
  <template v-slot:columnheader="{ columnID, columnIndex }">
    <template v-if="columns.type === 'date'">
      <component :is="getResourceTableColumnHeaderLink(
        columns.items[columnIndex]
      ) ? 'router-link' : 'span'" :to="getResourceTableColumnHeaderLink(
        columns.items[columnIndex]
      )" style="white-space: pre-wrap;">{{
        getResourceTableColumnHeaderText(columns.items[columnIndex]).join('\n')
      }}</component>
    </template>
    <template v-if="columns.type === 'resource'">
      <component :is="links?.resourceTimeTableLinkFn(
        columns.items[columnIndex].resource
      ) ? 'router-link' : 'span'" :to="links?.resourceTimeTableLinkFn(
        columns.items[columnIndex].resource
      )">
        {{ getResourceDisplayShortText(columns.items[columnIndex].resource) }}
      </component>
    </template>
  </template>
  <template v-slot:event="{ event }">
    <component
      :is="event.data.events.length === 1 ? 'event-link' :'div'"
      :event="event.data.events[0]"
      class="event" :class="'event-' + (event.data.holiday ? 'holiday' : (
        event.data.available
        ? (event.data.events.length === 1 ? 'event' : 'error')
        : (event.data.events.length === 0 ? 'occupied' : 'error')
      ))"
      :style="{'--event-color': event.data.events.length === 1 ? event.data.events[0].color : undefined}"
    >
      <span v-if="event.data.holiday" class="eventcontent-row">休室日</span>
      <span
        v-if="!event.data.available && event.data.events.length !== 0"
        class="eventcontent-row"
      >!時間外!<br /></span>
      <template v-if="event.data.events.length >= 2">
        <span class="eventcontent-row">!衝突!</span>
        <span class="eventcontent-row">
          <EventLink
            v-for="e of event.data.events" :key="e.id" :event="e"
            class="eventpopover"
          >{{ e.id.slice(-4) }}</EventLink>
        </span>
      </template>
      <template v-if="event.data.events.length === 1">
        <span
          v-if="event.data.events[0].timeLabel" class="eventcontent-row"
        >{{ event.data.events[0].timeLabel.label }}</span>
        <span class="eventcontent-row">
          <template v-if="event.data.events[0].resources.filter(
            x => x.type === 'student'
          ).length === 1">
            {{
              event.data.events[0].resources.find(
                x => x.type === 'student'
              ).grade.label
            }}
          </template>
          <template v-if="event.data.events[0].subject">
            {{ event.data.events[0].subject }}
          </template>
        </span>
        <span class="eventcontent-row">
          {{
            event.data.events[0].resources.filter(
              x => x.type !== 'classroom'
            ).filter(x => mode === 'resource'
              ? x.id !== resources[0].id : x.id !== event.columnID
            ).map(getResourceDisplayShortText).join()
          }}
        </span>
        <span v-if="event.data.events[0].frame" class="eventcontent-row">
          {{ event.data.events[0].frame }}
        </span>
        <span class="eventcontent-row">{{ event.data.events[0].memo }}</span>
      </template>
    </component>
  </template>
</time-table>
</template>

<style scoped>
.timeline::v-deep(.timetable-scrollcontent) {
  height: calc(var(--timeline-durationseconds) * var(
    --date-and-resource-time-table--scroll-content-height-per-duration,
    100% / var(--timeline-durationseconds)
  ));
}

.event {
  box-sizing: border-box;
  display: flex;
  height: 100%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1;
  border-block: solid 1px hsl(0, 0%, 75%);
  background-color: var(--event-color, #FFF);
}
/* .event-event {
}
.event-occupied {
} */
.event-error {
  border: solid 3px red;
}
/* .event-holiday {
} */
.eventcontent-row {
  display: block;
}
</style>
