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

import { inject, keys as injectionKeys } from '../../../../../injection';
import {
  comparerByKey, datetimeRange,
} from '../../../../../common/utils';
import {
  getResourceDisplayText, getUniquePeopleNames,
} from '../../../../../common/zkan-utils.js';
import { TimeTable } from '../../../../../common/timetable';
import {
  resourceOrdering, type Resource,
} from '../../../../../composables/resourcestore';
import {
  eventToEventInput, type Event,
} from '../../../../../composables/eventstore';
import { useThisComponentStateMachine } from './timelineeventinput/_this-component-state-machine';
import { ResourceColorPalette } from './timelineeventinput/resourcecolorpalette';
import EventEditDialog from './timelineeventinput/_EventEditDialog.vue';
import EventChangeDialog from './timelineeventinput/_EventChangeDialog.vue';
import eventsAndResourceHoursLoader from './timelineeventinput/_events-and-resourcehours-loader';
import { useTimetableCursor } from './timelineeventinput/_timetable-cursor';

const selfResourceColor = '#000000';

function hexcolorToRGB(hex: string): [number, number, number] {
  if(hex.startsWith('#')) hex = hex.slice(1);
  return Array.from({ length: 3 }, (_, i) => i * 2).map(
    i => hex.slice(i, i + 2)
  ).map(x => Number.parseInt(x, 16)) as [number, number, number];
}

function getContrastColor(hexcolor: string) {
  const [r, g, b] = hexcolorToRGB(hexcolor);
  const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
  return brightness > 128 ? '#000' : '#FFF';
}

function roundTime(t: Time) {
  return time(t.hour, t.minute - t.minute % 5, 0, 0, t.tzInfo, t.fold);
}

function datetimeFloor(datetime: DateTime): DateTime {
  return DateTime.combine(datetime.date(), time());
}
function datetimeCeil(datetime: DateTime): DateTime {
  return DateTime.combine(add(datetime.date(), timedelta({
    days: cmp(datetime.time(), time()) === 0 ? 0 : 1,
  })), time());
}

export default {
  name: 'PagesMasterTimetableResourcesIdTimelineeventinput',
  components: { EventEditDialog, EventChangeDialog },
  props: { id: { type: String, required: true } },
  setup(props) {
    const links = inject(injectionKeys.links);
    const errorHandler = inject(injectionKeys.errorHandler);
    const timeLabelStore = inject(injectionKeys.timeLabelStore);
    const resourceStore = inject(injectionKeys.resourceStore);
    const resourceHourStore = inject(injectionKeys.resourceHourStore);
    const eventStore = inject(injectionKeys.eventStore);
    const generalTimetableScope = inject(injectionKeys.generalTimetableScope);

    async function changeEvent(detail: (
      { type: 'cancel' } | { type: 'change', toEvent: Event }
    ) & {
      fromEvent: Event, causeResources: Resource[],
      resourceHourUnfillRange: 'none' | 'time' | 'day',
    }) {
      if(detail.type === 'cancel') {
        await eventStore.mutate([{ opcode: 'add_eventchange', param: {
          fromEvent: { id: detail.fromEvent.id }, toEvent: null,
          causeResources: detail.causeResources.map(x => x.id),
        } }]);
      } else if(detail.type === 'change') {
        await eventStore.mutate([{ opcode: 'add_event', param: {
          ...eventToEventInput(detail.toEvent), __id: 'A',
        } }, { opcode: 'add_eventchange', param: {
          fromEvent: { id: detail.fromEvent.id }, toEvent: { __id: 'A' },
          causeResources: detail.causeResources.map(x => x.id),
        } }]);
      }
      const resourceHourRangeToUnfill = (
        detail.resourceHourUnfillRange === 'time' ? {
          startTime: detail.fromEvent.startTime, endTime: detail.fromEvent.endTime,
        } : detail.resourceHourUnfillRange === 'day' ? {
          startTime: datetimeFloor(detail.fromEvent.startTime),
          endTime: datetimeCeil(detail.fromEvent.endTime),
        } : null
      );
      if(resourceHourRangeToUnfill) {
        await resourceHourStore.mutate(detail.causeResources.map(x => ({
          opcode: 'unfill', param: {
            ...resourceHourRangeToUnfill, resource: x.id,
          },
        })));
      }
    }

    const classroom = computed(() => resourceStore.resources.value.find(
      x => x.id === generalTimetableScope.classroom.value
    )!);
    const resource = computed(() => resourceStore.resources.value.find(
      x => x.id === props.id
    ) ?? null);

    const columns = computed(() => [...datetimeRange(
      ...generalTimetableScope.dateRange.value, timedelta({ days: 1 }),
    )].map(x => ({ data: x, columnID: x.isoFormat() })));

    const {
      refresh: load, dailyEvents, usedResources, dailyUsedResourceHours,
    } = eventsAndResourceHoursLoader(
      resource, classroom, generalTimetableScope.dateRange,
      errorHandler, eventStore, resourceHourStore,
    );

    const resourceColorPalette = new ResourceColorPalette();
    const usedResourcesAndColors = computed(() => {
      const hasSelf = usedResources.value.some(
        x => x.id === resource.value?.id
      );
      const resources = usedResources.value.filter(
        x => x.id !== resource.value?.id
      ).sort(comparerByKey(resourceOrdering));
      resourceColorPalette.update(resources);
      return [...hasSelf ? [
        { resource: resource.value!, color: selfResourceColor }
      ] : [], ...resourceColorPalette.resourcesAndColors];
    });

    const stateMachine = useThisComponentStateMachine();
    const editingEvent = shallowRef<Event | null>(null);

    function openEventEditDialog(event: Event) {
      if(stateMachine.state.value.type !== 'normal') return;
      editingEvent.value = event;
      stateMachine.startEventEditDialog();
    }
    function closeEventEditDialog() {
      if(stateMachine.state.value.type !== 'eventEditDialog') return;
      stateMachine.endEventEditDialog();
      editingEvent.value = null;
    }
    function openEventChangeDialog(event: Event) {
      if(stateMachine.state.value.type !== 'normal') return;
      stateMachine.startEventChangeDialog(event);
    }
    const onEventChangeDialogFinish = errorHandler.handleErrorAsync(async (
      detail: { type: 'cancel' } | {
        type: 'eventChange' | 'eventCancel'
        causeResources: Resource[],
        resourceHourUnfillRange: 'none' | 'time' | 'day',
      },
    ) => {
      if(stateMachine.state.value.type !== 'eventChangeDialog') return;
      if(detail.type === 'cancel') {
        stateMachine.endEventChangeDialog();
      } else if(detail.type === 'eventChange') {
        stateMachine.proceedToEventChangeTimelineSelection(
          detail.causeResources, detail.resourceHourUnfillRange
        );
      } else if(detail.type === 'eventCancel') {
        await changeEvent({
          type: 'cancel',
          fromEvent: stateMachine.state.value.details.fromEvent,
          causeResources: detail.causeResources,
          resourceHourUnfillRange: detail.resourceHourUnfillRange,
        });
        stateMachine.endEventChangeDialog();
        await load();
      }
    });
    function startTimelineSelection(mode: 'move' | 'copy', event: Event) {
      stateMachine.startTimelineSelection(mode, event);
    }

    const timetableCursor = useTimetableCursor(computed(() => (
      stateMachine.state.value.type === 'timelineSelection'
      ? stateMachine.state.value.details.relatedEvent : null
    )), timeLabelStore.timeLabels);
    const onTimelineMouseEvent = errorHandler.handleErrorAsync(async (
      type: 'move' | 'click', columnID: string, time: Time,
    ) => {
      timetableCursor.timelineMouseEventHandler(type, { columnID, time });

      if(stateMachine.state.value.type === 'normal') {
        if(type !== 'click' || !resource.value) return;
        const date = columns.value.find(
          x => x.columnID === columnID
        )!.data;
        const startTime = DateTime.combine(date, roundTime(time));
        editingEvent.value = {
          id: '', startTime, endTime: startTime, timeLabel: null,
          resources: [classroom.value, resource.value],
          memo: '', color: '#FFFFFF', type: 'standard',
          eventChangeFrom: null, eventChangeTo: null,
        };
        stateMachine.startEventEditDialog();
        return;
      }
      if(stateMachine.state.value.type !== 'timelineSelection') return;
      const selectingRange = timetableCursor.cursor;
      if(type === 'click' && selectingRange.value) {
        const event = stateMachine.state.value.details.relatedEvent;
        const date = columns.value.find(
          x => x.columnID === timetableCursor.cursor.value!.columnID
        )!.data;
        const toEvent = {
          ...event,
          startTime: DateTime.combine(date, selectingRange.value.startTime),
          endTime: DateTime.combine(date, selectingRange.value.endTime),
          timeLabel: selectingRange.value.timeLabel,
        };

        if(stateMachine.state.value.details.mode.type === 'move') {
          await eventStore.mutate([{ opcode: 'update_event', param: {
            ...eventToEventInput(toEvent), id: event.id,
          } }]);
        } else if(stateMachine.state.value.details.mode.type === 'copy') {
          await eventStore.mutate([{ opcode: 'add_event', param: {
            ...eventToEventInput(toEvent),
          } }]);
        } else if(
          stateMachine.state.value.details.mode.type === 'eventchange'
        ) {
          await changeEvent({
            type: 'change', fromEvent: event, toEvent, causeResources: (
              stateMachine.state.value.details.mode.details.causeResources
            ), resourceHourUnfillRange: (
              stateMachine.state.value.details.mode.details.resourceHourUnfillRange
            ),
          });
        }
        await load();
        stateMachine.endTimelineSelection();
      }
    });

    const timetableEventsWithoutSelectingRange = computed(() => [
      ...dailyUsedResourceHours.value.flatMap(d => d.items.map(
        x => ({
          startTime: x.startTime, endTime: x.endTime,
          columnID: d.date.isoFormat(), data: {
            type: 'resourcehour', resource: x.resource,
          },
        })
      )), ...dailyEvents.value.flatMap(x => x.items.map(range => ({
        startTime: range.startTime, endTime: range.endTime,
        columnID: x.date.isoFormat(), data: {
          type: 'event', events: range.events,
        },
      }))),
    ]);
    const timetableEvents = computed(() => [
      ...timetableEventsWithoutSelectingRange.value,
      ...timetableCursor.cursor.value ? [{
        startTime: timetableCursor.cursor.value.startTime,
        endTime: timetableCursor.cursor.value.endTime,
        columnID: timetableCursor.cursor.value.columnID,
        data: { type: 'selecting' },
      }] : [],
    ]);

    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) {
      return resourceDisplayTexts.value.get(
        resource.id
      ) ?? getResourceDisplayText(resource);
    }

    const timetableResourceHourShownResources = ref<Set<string>>(new Set([]));
    watch(resourceStore.resources, () => {
      timetableResourceHourShownResources.value = new Set(
        resourceStore.resources.value.map(x => x.id)
      );
    }, { immediate: true });

    return {
      time, getResourceDisplayText, getResourceDisplayShortText,
      getContrastColor, load, links, resource, columns, usedResourcesAndColors,
      state: stateMachine.state, timetableEvents, editingEvent,
      openEventEditDialog, closeEventEditDialog, openEventChangeDialog,
      onEventChangeDialogFinish, startTimelineSelection, onTimelineMouseEvent,
      timetableResourceHourShownResources,
    };
  },
}
</script>

<template>
<template v-if="resource">
  <h1>
    <router-link to="./index" :style="(color => ({
      'background-color': color,
      'color': color ? getContrastColor(color) : undefined,
    }))(usedResourcesAndColors.find(
      x => x.resource.id === resource!.id
    )?.color)">{{ getResourceDisplayText(resource) }}</router-link>の時間割表イベント入力
  </h1>
  <section>
    <h1>可能時間を表示するリソース</h1>
    <label>
      <input type="checkbox" v-bind="(() => {
        const resourceIds = usedResourcesAndColors.filter(
          x => x.resource.id !== resource!.id
        ).map(x => x.resource.id);
        const checkedCount = resourceIds.filter(
          rid => timetableResourceHourShownResources.has(rid)
        ).length;
        return {
          checked: 0 < checkedCount,
          indeterminate: 0 < checkedCount && checkedCount < resourceIds.length,
        };
      })()" @change="$event => usedResourcesAndColors.filter(
        x => x.resource.id !== resource!.id
      ).map(x => x.resource.id).forEach(rid => (
        ($event.target as HTMLInputElement).checked
        ? timetableResourceHourShownResources.add(rid)
        : timetableResourceHourShownResources.delete(rid)
      ))" />
      全選択/全解除
    </label>
    <ul style="list-style: none; display: flex; flex-wrap: wrap;">
      <li v-for="{ resource: r, color: c } of usedResourcesAndColors.filter(
        x => x.resource.id !== resource!.id
      )" :key="r.id">
        <label>
          <input
            type="checkbox" v-model="timetableResourceHourShownResources"
            :value="r.id"
          />
          <span :style="{ 'background-color': c, 'color': getContrastColor(c) }">
            {{ getResourceDisplayText(r) }}
          </span>
        </label>
      </li>
    </ul>
  </section>
  <time-table
    class="timeline" :startTime="time()" :endTime="time()"
    :columnIDs="columns.map(x => x.columnID)" :events="timetableEvents"
    style="width: 100%; height: calc(100vh - 5em);" :style="{
      '--usedresourcescount': usedResourcesAndColors.filter(
        x => timetableResourceHourShownResources.has(x.resource.id)
      ).length,
    }" @timelinemousemove="
      onTimelineMouseEvent('move', $event.columnID, $event.time);
    " @timelineclick="
      onTimelineMouseEvent('click', $event.columnID, $event.time);
    "
  >
    <template v-slot:columnheader="{ columnID, columnIndex }">
      <component :is="links?.dateTimeTableLinkFn(
        columns[columnIndex].data
      ) ? 'router-link' : 'span'" :to="links?.dateTimeTableLinkFn(
        columns[columnIndex].data
      )" class="dateheader" :class="(weekday => ({
        'dateheader-saturday': weekday === 5,
        'dateheader-sunday': weekday === 6,
      }))(columns[columnIndex].data.weekday())">
        <time :datetime="columnID">
          {{ (columns[columnIndex].data).strftime('%m/%d') }}<br />
          {{ ['月', '火', '水', '木', '金', '土', '日'][
            columns[columnIndex].data.weekday()
          ] }}
        </time>
      </component>
    </template>
    <template v-slot:event="{ event }">
      <div
        v-if="event.data.type === 'event' && event.data.events.length === 1"
        class="event event-event" :style="{
          backgroundColor: `color-mix(in srgb, ${event.data.events[0].color} 30%, transparent)`,
        }"
      >
        <router-link
          :to="`/master/timetable/events/${event.data.events[0].id}/index`"
          class="event-content"
        >
          <div class="event-row">{{ event.data.events[0].subject }}</div>
          <div class="event-row">
            <span
              v-for="resource of event.data.events[0].resources.filter(
                x => x.type !== 'classroom' && x.id !== resource?.id
              )" :key="resource.id" :style="((backgroundColor) => ({
                'background-color': backgroundColor,
                'color': getContrastColor(backgroundColor),
              }))(usedResourcesAndColors.find(
                  x => x.resource.id === resource.id
              )!.color)"
            >{{ getResourceDisplayShortText(resource) }}</span>
          </div>
          <div class="event-row">{{ event.data.events[0].frame }}</div>
          <div
            v-if="event.data.events[0].type === 'standard'" class="event-row"
          >{{ event.data.events[0].memo }}</div>
        </router-link>
        <div class="event-popover">
          <button type="button" @click.stop="
            startTimelineSelection('move', event.data.events[0]);
          ">移動</button>
          <button type="button" @click.stop="
            startTimelineSelection('copy', event.data.events[0]);
          ">複製</button>
          <button type="button" @click.stop="
            openEventChangeDialog(event.data.events[0]);
          ">振替</button>
          <button type="button" @click.stop="
            openEventEditDialog(event.data.events[0]);
          ">編集/削除</button>
        </div>
      </div>
      <div
        v-else-if="event.data.type === 'event' && event.data.events.length !== 1"
        class="event event-collision"
      ></div>
      <div v-else-if="
        event.data.type === 'resourcehour' &&
        timetableResourceHourShownResources.has(event.data.resource.id)
      " class="event event-resourcehour" :style="(([idx, x]) => ({
        '--usedresourceindex': idx,
        '--usedresourcecolor': x.color,
      }))(usedResourcesAndColors.filter(
        x => timetableResourceHourShownResources.has(x.resource.id)
      ).map((x, i) => [i, x] as const).find(
        ([i, x]) => x.resource.id === event.data.resource.id
      )!)"></div>
      <div
        v-else-if="event.data.type === 'selecting'"
        class="event event-selecting"
      ></div>
    </template>
  </time-table>
  <event-edit-dialog
    :open="state.type === 'eventEditDialog'" @update:open="$event => {
      if(!$event) closeEventEditDialog();
    }" v-model:event="editingEvent" @run="() => {
      closeEventEditDialog(); load();
    }"
  />
  <event-change-dialog
    :open="state.type === 'eventChangeDialog'" :from-event="
      state.type === 'eventChangeDialog' ? state.details.fromEvent : null
    " @finish="onEventChangeDialogFinish"
  />
</template>
<p v-else>リソースが見つかりません。</p>
</template>

<style scoped>
.dateheader {
  color: black;
}
.dateheader-saturday {
  color: darkblue;
}
.dateheader-sunday {
  color: tomato;
}


.timeline::v-deep(.timetable-scrollcontent) {
  height: 100em;
}

.event {
  box-sizing: border-box;
  height: 100%;
}
@keyframes eventcollision {
  50% {
    opacity: 0;
  }
}
.event-collision {
  background-color: red;
  animation: eventcollision .75s infinite;
}
.event-resourcehour {
  position: absolute;
  inset-inline-start: calc(var(--usedresourceindex) / var(--usedresourcescount) * 100%);
  width: min(calc(1 / var(--usedresourcescount) * 100%), 20%);
  background-color: var(--usedresourcecolor);
}
.event-selecting {
  border: solid 1px tomato;
  background-color: hsl(9 100% 64% / 20%);
}
.event-content {
  box-sizing: border-box;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1;
  border-block: solid 2px hsl(0, 0%, 50%);
  color: black;
  text-decoration: none;
}
.event-popover {
  display: none;
  position: absolute;
  inset-inline-start: 50%;
  inset-block-end: 0;
  width: max-content;
  transform: translateX(-50%) translateY(100%);
  z-index: 1;
}
.event:hover .event-popover {
  display: flex;
  flex-direction: column;
}
</style>
