<template>
	<div v-if="schedule.deleted">
		Error: Unexpected deleted schedule.
	</div>
	<div v-else>
		<Timetable :schedule="schedulePreview" :eventSlotFilter :shiftSlotFilter />
		<table>
			<thead>
				<tr>
					<th>start</th>
					<th>end</th>
					<th>duration</th>
					<th>event</th>
					<th>s</th>
					<th>location</th>
					<th>assigned</th>
					<th v-if="edit"></th>
				</tr>
			</thead>
			<tbody>
				<template v-if="edit">
					<tr
						v-for="es in eventSlots"
						:key='es.slot?.id ?? es.start.toMillis()'
						:class='{
							removed: es.type === "slot" && removed.has(es.id),
							gap: es.type === "gap",
						}'
					>
						<template v-if="es.type === 'gap'">
							<td colspan="2">
								{{ gapFormat(es) }}
								gap
							</td>
							<td>
								<input
									type="time"
									v-model="newEventDuration"
								>
							</td>
							<td>
									<input
										type="text"
										v-model="newEventName"
									>
							</td>
							<td></td>
							<td>
								<select
									v-model="newEventLocation"
								>
									<option
										v-for="location in schedule.locations?.filter(l => !l.deleted)"
										:key="location.id"
										:value="location.id"
										:selected="location.id === newEventLocation"
									>{{ location.name }}</option>
								</select>
							</td>
							<td></td>
							<td>
								Add at
								<button
									type="button"
									@click="newEventSlot({ start: es.start })"
								>Start</button>
								<button
									type="button"
									@click="newEventSlot({ end: es.end })"
								>End</button>
							</td>
						</template>
						<template v-else-if='edit'>
							<td>
								<input
									type="datetime-local"
									:value="es.start.toFormat('yyyy-LL-dd\'T\'HH:mm')"
									@blur="editEventSlot(es, { start: ($event as any).target.value })"
								>
							</td>
							<td>
								<input
									type="time"
									:value="es.end.toFormat('HH:mm')"
									@input="editEventSlot(es, { end: ($event as any).target.value })"
								>
							</td>
							<td>
								<input
									type="time"
									:value='dropDay(es.end.diff(es.start)).toFormat("hh:mm")'
									@input="editEventSlot(es, { duration: ($event as any).target.value })"
								>
							</td>
							<td>
								<input
									type="text"
									:value="es.name"
									@input="editEventSlot(es, { name: ($event as any).target.value })"
								>
							</td>
							<td>{{ status(es) }}</td>
							<td>
								<select
									:value="es.locationId"
									@change="editEventSlot(es, { locationId: parseInt(($event as any).target.value) })"
								>
									<option
										v-for="location in schedule.locations?.filter(l => !l.deleted)"
										:key="location.id"
										:value="location.id"
										:selected="location.id === es.locationId"
									>{{ location.name }}</option>
								</select>
							</td>
							<td>
								<AssignedCrew
									:edit="true"
									:modelValue="es.assigned"
									@update:modelValue="editEventSlot(es, { assigned: $event })"
								/>
							</td>
							<td>
								<button
									:disabled="removed.has(es.id)"
									type="button"
									@click="delEventSlot(es)"
								>Remove</button>
								<button
									v-if="changes.some(c => c.id === es.id)"
									type="button"
									@click="revertEventSlot(es.id)"
								>Revert</button>
							</td>
						</template>
					</tr>
					<tr>
						<td>
							<input
								type="datetime-local"
								v-model="newEventStart"
							>
						</td>
						<td>
							<input
								type="time"
								v-model="newEventEnd"
							>
						</td>
						<td>
							<input
								type="time"
								v-model="newEventDuration"
							>
						</td>
						<td>
								<input
									type="text"
									v-model="newEventName"
								>
						</td>
						<td></td>
						<td></td>
						<td colspan="2">
							<button
								type="button"
								@click="newEventSlot()"
							>Add Event</button>
						</td>
					</tr>
				</template>
				<template v-else>
					<tr
						v-for="es in eventSlots"
						:key='es.slot?.id ?? es.start.toMillis()'
						:class='{
							gap: es.type === "gap",
						}'
					>
						<template v-if="es.type === 'gap'">
							<td colspan="2">
								{{ gapFormat(es) }}
								gap
							</td>
						</template>
						<template v-else>
							<td>{{ es.start.toFormat("yyyy-LL-dd HH:mm") }}</td>
							<td>{{ es.end.toFormat("HH:mm") }}</td>
							<td>{{ es.end.diff(es.start).toFormat('hh:mm') }}</td>
							<td>{{ es.name }}</td>
							<td>{{ status(es) }}</td>
							<td>{{ es.locationId }}</td>
							<td><AssignedCrew :modelValue="es.assigned" :edit="false" /></td>
						</template>
					</tr>
				</template>
			</tbody>
		</table>
		<p v-if="changes.length">
			Changes are not save yet.
			<button
				type="button"
				@click="saveEventSlots"
			>Save Changes</button>
		</p>
		<details>
			<summary>Debug</summary>
			<b>EventSlot changes</b>
			<ol>
				<li v-for="change in changes">
					<pre><code>{{ JSON.stringify((({ event, slot, ...data }) => data)(change as any), undefined, "  ") }}</code></pre>
				</li>
			</ol>
			<b>ScheduleEvent changes</b>
			<ol>
				<li v-for="change in scheduleChanges">
					<pre><code>{{ JSON.stringify((({ event, slot, ...data }) => data)(change as any), undefined, "  ") }}</code></pre>
				</li>
			</ol>
		</details>
	</div>
</template>

<script lang="ts" setup>
import { DateTime, Duration } from 'luxon';
import type { ApiSchedule, ApiScheduleEvent, ApiScheduleEventSlot, ApiScheduleShift, ApiScheduleShiftSlot } from '~/shared/types/api';
import type { Entity } from '~/shared/types/common';
import { enumerate, pairs } from '~/shared/utils/functions';
import { applyUpdatesToArray } from '~/shared/utils/update';

const props = defineProps<{
	edit?: boolean,
	locationId?: number,
	eventSlotFilter?: (slot: ApiScheduleEventSlot) => boolean,
	shiftSlotFilter?: (slot: ApiScheduleShiftSlot) => boolean,
}>();

interface EventSlot {
	type: "slot",
	id: number,
	updatedAt: string,
	deleted?: boolean,
	event?: Extract<ApiScheduleEvent, { deleted?: false }>,
	slot?: ApiScheduleEventSlot,
	origLocation: number,
	name: string,
	locationId: number,
	assigned: number[],
	start: DateTime,
	end: DateTime,
}

interface Gap {
	type: "gap",
	id?: undefined,
	event?: undefined,
	slot?: undefined,
	name?: undefined,
	locationId?: number,
	start: DateTime,
	end: DateTime,
}

function status(eventSlot: EventSlot) {
	if (schedule.value.deleted) {
		throw new Error("Unexpected deleted schedule");
	}
	if (
		!eventSlot.event
		|| eventSlot.event.name !== eventSlot.name
	) {
		const event = schedule.value.events?.find(event => !event.deleted && event.name === eventSlot.name);
		return event ? "L" : "N";
	}
	return eventSlot.event.slots.length === 1 ? "" : eventSlot.event.slots.length;
}

// Filter out set records where a del record exists for the same id.
function filterSetOps<T extends Entity>(changes: T[]) {
	const deleteIds = new Set(changes.filter(c => c.deleted).map(c => c.id));
	return changes.filter(c => c.deleted || !deleteIds.has(c.id));
}

function findEvent(
	eventSlot: EventSlot,
	changes: ApiScheduleEvent[],
	schedule: ApiSchedule,
) {
	if (schedule.deleted) {
		throw new Error("Unexpected deleted schedule");
	}
	let setEvent = changes.filter(
		c => !c.deleted
	).find(
		c => c.name === eventSlot.name
	);
	if (!setEvent && eventSlot.event && eventSlot.event.name === eventSlot.name) {
		setEvent = eventSlot.event;
	}
	if (!setEvent) {
		setEvent = schedule.events?.filter(e => !e.deleted).find(e => e.name === eventSlot.name);
	}
	let delEvent;
	if (eventSlot.event) {
		delEvent = changes.filter(c => !c.deleted).find(
			c => c.name === eventSlot.event!.name
		);
		if (!delEvent) {
			delEvent = schedule.events?.filter(e => !e.deleted).find(e => e.name === eventSlot.event!.name);
		}
	}
	return { setEvent, delEvent };
}

function removeSlotLocation(
	event: Extract<ApiScheduleEvent, { deleted?: false }>,
	oldSlot: ApiScheduleEventSlot,
	locationId: number
) {
	// If location is an exact match remove the whole slot
	if (oldSlot.locationIds.length === 1 && oldSlot.locationIds[0] === locationId) {
		return {
			...event,
			slots: event.slots.filter(s => s.id !== oldSlot.id),
		};
	}
	// Otherwise filter out location
	return {
		...event,
		slots: event.slots.map(
			s => s.id !== oldSlot.id ? s : {
				...s,
				locationIds: s.locationIds.filter(id => id !== locationId)
			}
		),
	};
}

function mergeSlot(
	event: Extract<ApiScheduleEvent, { deleted?: false }>,
	eventSlot: EventSlot,
): Extract<ApiScheduleEvent, { deleted?: false }> {
	if (schedule.value.deleted) {
		throw new Error("Unexpected deleted schedule");
	}
	const oldSlot = event.slots.find(s => s.id === eventSlot.id);
	const nextId = Math.max(0, ...schedule.value.events?.filter(e => !e.deleted).flatMap(e => e.slots.map(slot => slot.id)) ?? []) + 1;
	const start = eventSlot.start.toUTC().toISO({ suppressSeconds: true })!;
	const end = eventSlot.end.toUTC().toISO({ suppressSeconds: true })!;

	// Edit slot in-place if possible
	if (
		oldSlot
		&& oldSlot.id === eventSlot.id
		&& (
			oldSlot.locationIds.length <= 1
			|| oldSlot.start === start && oldSlot.end === end
		)
	) {
		return {
			...event,
			slots: event.slots.map(s => {
				if (s.id !== oldSlot.id)
					return s;
				const locationIds = new Set(s.locationIds);
				locationIds.delete(eventSlot.origLocation)
				locationIds.add(eventSlot.locationId);
				return {
					...s,
					locationIds: [...locationIds],
					assigned: eventSlot.assigned.length ? eventSlot.assigned : undefined,
					start,
					end,
				};
			}),
		};
	}

	// Else remove old slot if it exist and insert a new one
	if (oldSlot) {
		event = removeSlotLocation(event, oldSlot, eventSlot.origLocation);
	}

	return {
		...event,
		slots: [...event.slots, {
			id: oldSlot ? oldSlot.id : nextId,
			locationIds: [eventSlot.locationId],
			assigned: eventSlot.assigned.length ? eventSlot.assigned : undefined,
			start,
			end,
		}],
	};
}

const scheduleChanges = computed(() => {
	let eventChanges: Extract<ApiScheduleEvent, { deleted?: false}>[] = [];
	for (const change of filterSetOps(changes.value)) {
		if (!change.deleted) {
			let { setEvent, delEvent } = findEvent(change, eventChanges, schedule.value);
			if (delEvent && delEvent !== setEvent) {
				eventChanges = removeSlot(eventChanges, delEvent, change);
			}
			if (!setEvent) {
				setEvent = {
					id: Math.floor(Math.random() * -1000), // XXX This wont work.
					updatedAt: "",
					name: change.name,
					crew: true,
					slots: [],
				};
			}

			eventChanges = replaceChange(mergeSlot(setEvent, change), eventChanges);

		} else if (change.deleted) {
			let { delEvent } = findEvent(change, eventChanges, schedule.value);
			if (delEvent) {
				eventChanges = removeSlot(eventChanges, delEvent, change);
			}
		}
	}
	return eventChanges;
});

const schedulePreview = computed(() => {
	if (schedule.value.deleted) {
		throw new Error("Unexpected deleted schedule");
	}
	const events = [...schedule.value.events ?? []]
	applyUpdatesToArray(scheduleChanges.value, events);
	return {
		...schedule.value,
		events,
	};
});

function removeSlot(
	eventChanges: Extract<ApiScheduleEvent, { deleted?: false }>[],
	event: Extract<ApiScheduleEvent, { deleted?: false }>,
	eventSlot: EventSlot,
) {
	let oldSlot = event.slots.find(s => s.id === eventSlot.id);
	if (oldSlot) {
		eventChanges = replaceChange(
			removeSlotLocation(event, oldSlot, eventSlot.origLocation),
			eventChanges,
		);
	}
	return eventChanges;
}

const accountStore = useAccountStore();
const schedule = await useSchedule();

const changes = ref<EventSlot[]>([]);
const removed = computed(() => new Set(changes.value.filter(c => c.deleted).map(c => c.id)));

function replaceChange<T extends Entity>(
	change: T,
	changes: T[],
) {
	const index = changes.findIndex(item => (
		item.deleted === change.deleted && item.id === change.id
	));
	const copy = [...changes];
	if (index !== -1)
		copy.splice(index, 1, change);
	else
		copy.push(change);
	return copy;
}
function revertChange<T extends Entity>(id: number, changes: T[]) {
	return changes.filter(change => change.id !== id);
}

const oneDayMs = 24 * 60 * 60 * 1000;
function dropDay(diff: Duration) {
	if (diff.toMillis() >= oneDayMs) {
		return diff.minus({ days: 1 });
	}
	return diff;
}

const newEventStart = ref("");
const newEventDuration = ref("01:00");
const newEventEnd = computed({
	get: () => (
		DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone, locale: "en-US" })
			.plus(Duration.fromISOTime(newEventDuration.value, { locale: "en-US" }))
			.toFormat("HH:mm")
	),
	set: (value: string) => {
		const start = DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone, locale: "en-US" });
		const end = endFromTime(start, value);
		newEventDuration.value = dropDay(end.diff(start)).toFormat("hh:mm");
	},
});
const newEventLocation = ref(props.locationId);
watch(() => props.locationId, () => {
	newEventLocation.value = props.locationId;
});

function endFromTime(start: DateTime, time: string) {
	let end = start.startOf("day").plus(Duration.fromISOTime(time, { locale: "en-US" }));
	if (end.toMillis() <= start.toMillis()) {
		end = end.plus({ days: 1 });
	}
	return end;
}
function durationFromTime(time: string) {
	let duration = Duration.fromISOTime(time, { locale: "en-US" });
	if (duration.toMillis() === 0) {
		duration = Duration.fromMillis(oneDayMs, { locale: "en-US" });
	}
	return duration;
}
const newEventName = ref("");
function editEventSlot(
	eventSlot: EventSlot,
	edits: {
		start?: string,
		end?: string,
		duration?: string,
		name?: string,
		locationId?: number,
		assigned?: number[],
	}
) {
	if (edits.start) {
		const start = DateTime.fromISO(edits.start, { zone: accountStore.activeTimezone, locale: "en-US" });
		eventSlot = {
			...eventSlot,
			start,
			end: start.plus(eventSlot.end.diff(eventSlot.start)),
		};
	}
	if (edits.end !== undefined) {
		eventSlot = {
			...eventSlot,
			end: endFromTime(eventSlot.start, edits.end),
		};
	}
	if (edits.duration !== undefined) {
		eventSlot = {
			...eventSlot,
			end: eventSlot.start.plus(durationFromTime(edits.duration)),
		};
	}
	if (edits.name !== undefined) {
		eventSlot = {
			...eventSlot,
			name: edits.name,
		};
	}
	if (edits.locationId !== undefined) {
		eventSlot = {
			...eventSlot,
			locationId: edits.locationId,
		};
	}
	if (edits.assigned !== undefined) {
		eventSlot = {
			...eventSlot,
			assigned: edits.assigned,
		};
	}
	changes.value = replaceChange(eventSlot, changes.value);
}
function delEventSlot(eventSlot: EventSlot) {
	const change = {
		...eventSlot,
		deleted: true,
	};
	changes.value = replaceChange(change, changes.value);
}
function revertEventSlot(id: number) {
	changes.value = revertChange(id, changes.value);
}
function newEventSlot(options: { start?: DateTime, end?: DateTime } = {}) {
	const name = newEventName.value;
	const locationId = newEventLocation.value;
	if (!locationId) {
		alert("Invalid location");
		return;
	}
	let start;
	let end;
	const duration = durationFromTime(newEventDuration.value);
	if (!duration.isValid) {
		alert("Invalid duration");
		return;
	}
	if (options.start) {
		start = options.start;
		end = options.start.plus(duration);
	} else if (options.end) {
		end = options.end;
		start = options.end.minus(duration);
	} else {
		start = DateTime.fromISO(newEventStart.value, { zone: accountStore.activeTimezone, locale: "en-US" });
		end = endFromTime(start, newEventEnd.value);
	}
	if (!start.isValid || !end.isValid) {
		alert("Invalid start and/or end time");
		return;
	}
	const change: EventSlot = {
		type: "slot",
		updatedAt: "",
		id: Math.floor(Math.random() * -1000), // XXX this wont work.
		name,
		origLocation: locationId,
		locationId,
		assigned: [],
		start,
		end,
	};
	newEventName.value = "";
	changes.value = replaceChange(change, changes.value);
}

async function saveEventSlots() {
	try {
		await $fetch("/api/schedule", {
			method: "PATCH",
			body: {
				id: 111,
				updatedAt: "",
				events: scheduleChanges.value,
			} satisfies ApiSchedule,
		});
		changes.value = [];
	} catch (err: any) {
		console.error(err);
		alert(err?.data?.message ?? err.message);
	}
}

const oneHourMs = 60 * 60 * 1000;
function gapFormat(gap: Gap) {
	let diff = gap.end.diff(gap.start);
	if (diff.toMillis() % oneHourMs !== 0)
		diff = diff.shiftTo("hours", "minutes");
	else
		diff = diff.shiftTo("hours");
	return diff.toHuman({ listStyle: "short", unitDisplay: "short" });
}

const eventSlots = computed(() => {
	if (schedule.value.deleted) {
		throw new Error("Unexpected deleted schedule");
	}
	const data: (EventSlot | Gap)[] = [];
	for (const event of schedule.value.events ?? []) {
		if (event.deleted)
			continue;
		for (const slot of event.slots) {
			if (props.eventSlotFilter && !props.eventSlotFilter(slot))
				continue;
			for (const locationId of slot.locationIds) {
				if (props.locationId !== undefined && locationId !== props.locationId)
					continue;
				data.push({
					type: "slot",
					id: slot.id,
					updatedAt: "",
					event,
					slot,
					name: event.name,
					locationId,
					assigned: slot.assigned ?? [],
					origLocation: locationId,
					start: DateTime.fromISO(slot.start, { zone: accountStore.activeTimezone, locale: "en-US" }),
					end: DateTime.fromISO(slot.end, { zone: accountStore.activeTimezone, locale: "en-US" }),
				});
			}
		}
	}
	applyUpdatesToArray(changes.value.filter(c => !c.deleted), data as EventSlot[]);
	data.sort((a, b) => a.start.toMillis() - b.start.toMillis() || a.end.toMillis() - b.end.toMillis());

	// Insert gaps
	let maxEnd = 0;
	const gaps: [number, Gap][] = []
	for (const [index, [first, second]] of enumerate(pairs(data))) {
		maxEnd = Math.max(maxEnd, first.end.toMillis());
		if (maxEnd < second.start.toMillis()) {
			gaps.push([index, {
				type: "gap",
				locationId: props.locationId,
				start: DateTime.fromMillis(maxEnd, { locale: "en-US" }),
				end: second.start,
			}]);
		}
	}
	gaps.reverse();
	for (const [index, gap] of gaps) {
		data.splice(index + 1, 0, gap);
	}
	return data;
});
</script>

<style scoped>
label {
	display: inline;
	padding-inline-end: 0.75em;
}
table {
	margin-block-start: 1rem;
	border-spacing: 0;
}
table th {
	text-align: left;
	border-bottom: 1px solid var(--foreground);
}
table :is(th, td) + :is(th, td) {
	padding-inline-start: 0.4em;
}
.gap {
	height: 1.8em;
}
.removed {
	background-color: color-mix(in oklab, var(--background), rgb(255, 0, 0) 40%);
}
.removed :is(td, input) {
	text-decoration: line-through;
}
</style>
