<script lang="ts">
import { Date, DateTime, cmp } from '@silane/datetime';
import {
  computed, ref, watch, Raw, markRaw, shallowRef, type PropType,
} from 'vue';

import InputCourseDate from './InputCourseDate.vue';
import InputTimeLabel from './InputTimeLabel.vue';
import InputEventColor from './InputEventColor.vue';
import { inject, keys as injectionKeys } from '../injection';
import { comparerByKey, groupBy } from '../common/utils.js';
import { getResourceDisplayText } from '../common/zkan-utils.js';
import { resourceOrdering, type Resource } from '../composables/resourcestore';
import { type TimeLabel } from '../composables/timelabelstore';

function isNotNullish<T>(x: T | null | undefined): x is T {
  return x != null;
}

function eventEquals(a, b) {
  if(a.type !== b.type) return false;
  if(a.resources.length !== b.resources.length) return false;
  if(!a.resources.every(x => b.resources.some(y => y.id === x.id)))
    return false;
  if(cmp(a.startTime, b.startTime) !== 0) return false;
  if(cmp(a.endTime, b.endTime) !== 0) return false;
  if(a.timeLabel?.id !== b.timeLabel?.id) return false;
  if(a.memo !== b.memo) return false;
  if(a.color !== b.color) return false;
  if(a.type === 'lesson') {
    if(a.subject !== b.subject) return false;
    if(a.frame !== b.frame) return false;
  }
  return true;
}

export type ModelValuePropType = {
  id: string | null, type: 'lesson', resources: Resource[],
  startTime: DateTime, endTime: DateTime, timeLabel: TimeLabel | null,
  memo: string, color: string, eventChangeTo: unknown,
  eventChangeFrom: unknown, subject: string, frame: string,
}[];

type ModelValueErrorCode = 'notlesson' | 'overnight' | 'outofcoursedates' | 'multiplestudents';
export type ModelValueErrorsPropType = {
  code: ModelValueErrorCode, event: ModelValuePropType[0],
}[];

export default {
  name: 'InputTeacherLessonList',
  components: { InputCourseDate, InputTimeLabel, InputEventColor },
  props: {
    modelValue: {
      type: Array as PropType<ModelValuePropType>, required: true,
    },
    dates: { type: Array as PropType<Date[]>, required: true },
    // modelValueにこのコンポーネントが扱えないeventが含まれている
    modelValueErrors: {
      type: Array as PropType<ModelValueErrorsPropType>, required: false,
    },
    // ユーザーが入力している入力欄の値が無効
    invalid: { type: Boolean, default: false },
    eventInitialValue: { type: Object as PropType<Partial<{
      resources: Resource[],
    }>>, default: {} }
  },
  emits: {
    'update:modelValue': (payload: ModelValuePropType) => true,
    'update:modelValueErrors': (payload: ModelValueErrorsPropType) => true,
    'update:invalid': (payload: boolean) => true,
  },
  setup(props, { emit }) {
    const colorLabels = inject(injectionKeys.colorLabelStore).colorLabels;
    const resourceStore = inject(injectionKeys.resourceStore);
    const links = inject(injectionKeys.links, null);

    const courses = ref<{
      student: Raw<Resource> | null, subject: string, items: {
        id: string | null, resources: Resource[], date: Raw<Date> | null,
        timeLabel: Raw<TimeLabel> | null, memo: string, frame: string,
        color: string, eventChangeTo: unknown, eventChangeFrom: unknown,
      }[],
    }[]>([]);
    const modelValueErrors = ref<ModelValueErrorsPropType>([]);
    const isLessonSorted = ref<boolean[]>([]);

    let lastEmittedValue: unknown = null;
    watch(() => props.modelValue, () => {
      if(props.modelValue === lastEmittedValue)
          return;

      const errors: {
        code: ModelValueErrorCode, event: typeof props.modelValue[0],
      }[] = [];
      const courseDatesISOString = new Set(
        props.dates.map(x => x.isoFormat())
      );

      for(let event of props.modelValue) {
        if(event.type !== 'lesson') {
          errors.push({code: 'notlesson', event});
          continue;
        }
        if(cmp(event.startTime.date(), event.endTime.date()) !== 0)
          errors.push({code: 'overnight', event});
        if(!courseDatesISOString.has(event.startTime.date().isoFormat()))
          errors.push({code: 'outofcoursedates', event});

        const students = event.resources.filter(x => x.type === 'student');
        if(students.length > 1)
          errors.push({code: 'multiplestudents', event});
      }
      modelValueErrors.value = errors;
      emit('update:modelValueErrors', errors);
      if(errors.length) {
        return;
      }

      const lessons = props.modelValue.map(x => ({
        student: x.resources.find(x => x.type === 'student') ?? null,
        subject: x.subject, item: {
          id: x.id, resources: x.resources.filter(
            x => x.type !== 'student'
          ).map(markRaw), date: markRaw(x.startTime.date()),
          timeLabel: x.timeLabel ? markRaw(x.timeLabel) : null,
          memo: x.memo, frame: x.frame, color: x.color,
          eventChangeTo: x.eventChangeTo, eventChangeFrom: x.eventChangeFrom,
        },
      }));
      courses.value = groupBy(lessons, x => ({
        student: x.student, subject: x.subject,
      }), (a, b) =>
        a.student?.id === b.student?.id && a.subject === b.subject
      ).sort(comparerByKey(x => [
        Boolean(x.key.student),
        x.key.student ? resourceOrdering(x.key.student) : null, x.key.subject,
      ])).map(group => ({
        student: group.key.student ? markRaw(group.key.student) : null,
        subject: group.key.subject,
        items: group.items.map(({ item }) => item),
      }));
      isLessonSorted.value = Array(courses.value.length).fill(false);
    }, { immediate: true });

    const invalid = computed(() => courses.value.some(
      course => course.items.some(x => x.date == null || x.timeLabel == null)
    ));
    watch(invalid, () => {
      emit('update:invalid', invalid.value);
      return;
    }, { immediate: true });

    const modelValue = computed(() => {
      if(invalid.value) return null;
      return courses.value.map(course => course.items.map(x => ({
        type: 'lesson' as const, id: x.id,
        resources: [...x.resources, course.student].filter(isNotNullish),
        startTime: DateTime.combine(x.date!, x.timeLabel!.startTime),
        endTime: DateTime.combine(x.date!, x.timeLabel!.endTime),
        timeLabel: x.timeLabel, memo: x.memo, color: x.color,
        eventChangeTo: x.eventChangeTo, eventChangeFrom: x.eventChangeFrom,
        subject: course.subject, frame: x.frame,
      }))).flat();
    });
    watch(modelValue, () => {
      if(!modelValue.value) {
        return;
      }
      if(
        modelValue.value.length === props.modelValue.length &&
        modelValue.value.every(
          (x, idx) => eventEquals(x, props.modelValue[idx])
        )
      ) return;
      lastEmittedValue = modelValue.value;
      emit('update:modelValue', modelValue.value);
    });

    const studentToAdd = shallowRef<Resource | null>(null);
    const subjectToAdd = shallowRef('');
    function addCourse() {
      const student = studentToAdd.value;
      const subject = subjectToAdd.value.trim();
      if(!student || !subject) return;
      if(courses.value.some(
        x => x.student?.id === student.id && x.subject === subject
      )) return;
      courses.value.push({ student, subject, items: [] });
      isLessonSorted.value.push(false);
    }

    function removeCourse(course: typeof courses.value[0]) {
      const idx = courses.value.indexOf(course);
      courses.value.splice(idx, 1);
      isLessonSorted.value.splice(idx, 1);
    }

    function addEvent(
      course: typeof courses.value[0],
      event: Partial<typeof courses.value[0]['items'][0]>={},
      index=Number.POSITIVE_INFINITY,
    ) {
      course.items.splice(index, 0, {
        id: null, resources: event.resources ?? props.eventInitialValue.resources ?? [],
        date: event.date ?? null, timeLabel: event.timeLabel ?? null,
        memo: event.memo ?? '', frame: event.frame ?? '',
        color: event.color ?? this.colorLabels[0].color, eventChangeTo: null, eventChangeFrom: null,
      });
    }
    
    const subjectEdittingCourse = shallowRef<typeof courses.value[0] | null>(
      null
    );
    const edittingSubject = shallowRef('');

    function startEditSubject(course) {
      subjectEdittingCourse.value = course;
      edittingSubject.value = course.subject;
    }

    function endEditSubject(cancel=false) {
      if(!subjectEdittingCourse.value) return;
      if(!cancel) {
        if(!edittingSubject.value) return;
        subjectEdittingCourse.value.subject = edittingSubject.value;
      }
      subjectEdittingCourse.value = null;
    }

    function sortLesson(course) {
      const idx = courses.value.indexOf(course);
      isLessonSorted.value.splice(idx, 1, true);
      setTimeout(() => isLessonSorted.value.splice(idx, 1, false), 0);
    }

    return {
      cmp, getResourceDisplayText, colorLabels,
      resources: resourceStore.resources,
      eventChangeLinkFn: links?.eventChangeLinkFn ?? (fromEvent => null),
      courses, modelValueErrors, studentToAdd, subjectToAdd,
      subjectEdittingCourse, edittingSubject, isLessonSorted,
      addCourse, removeCourse, addEvent, startEditSubject, endEditSubject,
      sortLesson,
    };
  },
}
</script>

<template>
<div>
  <label>
    生徒:
    <select v-model="studentToAdd">
      <option
        v-for="student of resources.filter(x => x.type === 'student')"
        :key="student.id" :value="student"
      >{{ getResourceDisplayText(student) }}</option>
    </select>
  </label>
  <label>教科: <input type="text" v-model="subjectToAdd" /></label>
  <button type="button" @click="addCourse()">生徒 &amp; 教科追加</button>
  
  <ul class="courses">
    <li
      v-for="course, courseIndex of courses"
      :key="`${course.student?.id ?? ''}${course.subject}`"
    >
      <button
          class="deletecoursebutton"
          type="button"
          @click="removeCourse(course)">
        この生徒 &amp; 教科を削除
      </button>
      <dl class="coursemetadata">
        <dt>生徒</dt>
        <dd>
          {{ course.student ? getResourceDisplayText(
            course.student
           ) : '[生徒なし]' }}
        </dd>
        <dt>教科</dt>
        <dd>
          <span v-if="subjectEdittingCourse !== course">
            {{ course.subject }}
            <button type="button" @click="startEditSubject(course)">編集</button>
          </span>
          <span v-else>
            <input type="text" v-model="edittingSubject" />
            <button type="button" @click="endEditSubject()">確定</button>
            <button type="button" @click="endEditSubject(true)">キャンセル</button>
          </span>
        </dd>
      </dl>
      <button type="button" @click="sortLesson(course)">
        日付時刻順でソート
      </button>
      <DataGrid
          v-show="course.items.length !== 0"
          v-model="course.items"
          :readonly="false"
          :itemKey="'id'"
          :sort="isLessonSorted[courseIndex]
            ? (a, b) => ((a.date == null ? 1 : b.date == null ? -1 : 0)
                        || cmp(a.date, b.date)
                        || (a.timeLabel == null ? 1 : b.timeLabel == null ? -1 : 0)
                        || cmp(a.timeLabel.startTime, b.timeLabel.startTime))
            : null"
          :headers="[
              {text: 'ID', value: 'id'},
              {text: '振替'},
              {text: '枠', value: 'frame'},
              {text: '日付', value: 'date'},
              {text: '時間帯', value: 'timeLabel'},
              {text: 'メモ', value: 'memo'},
              {text: '色', value: 'color'},
              {text: '操作'},
          ]"
          :deleteButton="true">
        <template v-slot:td-ID="{data}">
          <span v-if="data == null">[未割当]</span>
          <span v-else :id="'event-'+data">{{ `...${data.slice(-4)}` }}</span>
        </template>
        <template v-slot:td-振替="{item}">
          <span>
            <template v-if="item.eventChangeTo">
              <template v-if="item.eventChangeTo.toEvent">
                →<EventLink
                v-if="item.eventChangeTo.toEvent"
                :event="item.eventChangeTo.toEvent"
                >振替先</EventLink>
              </template>
              <template v-else>取消済み</template>
            </template>
            <template v-if="item.eventChangeFrom">
              ←<EventLink
                :event="item.eventChangeFrom.fromEvent"
              >振替元</EventLink>
            </template>
          </span>
        </template>
        <template v-slot:td-枠="{item}">
          <input type="text" v-model="item.frame" />
        </template>
        <template v-slot:td-日付="{item}">
          <InputCourseDate v-model="item.date"
                            :class="{ invalid: !item.date }" />
        </template>
        <template v-slot:td-時間帯="{item}">
          <InputTimeLabel v-model="item.timeLabel"
                          :class="{ invalid: !item.timeLabel }" />
        </template>
        <template v-slot:td-メモ="{item}">
          <input type="text" v-model="item.memo" />
        </template>
        <template v-slot:td-色="{item}">
          <InputEventColor v-model="item.color" />
        </template>
        <template v-slot:td-操作="{item, itemIndex}">
          <button type="button" @click="addEvent(course, item, itemIndex + 1)">
            複製
          </button>
          <router-link
            v-if="item.id != null && eventChangeLinkFn(item)"
            :to="eventChangeLinkFn(item)!"
          >振替</router-link>
        </template>
      </DataGrid>
      <hr/>
      <button type="button" @click="addEvent(course)">授業追加</button>
    </li>
  </ul>
</div>
</template>

<style scoped>
.invalid {
  outline: solid 2px red;
}
.courses {
  list-style: none;
  padding: 0;
}
.courses li {
  border: solid 1px black;
  border-radius: 10px;
  padding: 10px;
  margin: 20px 10px;
}
.deletecoursebutton {
  border: dashed 2px red;
}
.coursemetadata  {
  display: flex;
}
.coursemetadata dt {
  display: none;
}
.coursemetadata dd {
  margin: 0 1em;
}
</style>
