import type { Calendar, DateSelectArg, DatesSetArg, EventChangeArg, EventDropArg, EventInput, EventSourceFunc, EventSourceFuncArg, EventSourceInput, PluginDef } from "@fullcalendar/core";
import froca from "../../services/froca.js";
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
import type FNote from "../../entities/fnote.js";
import server from "../../services/server.js";
import { t } from "../../services/i18n.js";
import options from "../../services/options.js";
import dialogService from "../../services/dialog.js";
import attributes from "../../services/attributes.js";
import type { CommandListenerData, EventData } from "../../components/app_context.js";
import utils, { hasTouchBar } from "../../services/utils.js";
import date_notes from "../../services/date_notes.js";
import appContext from "../../components/app_context.js";
import type { EventImpl } from "@fullcalendar/core/internal";
import debounce, { type DebouncedFunction } from "debounce";
import type { TouchBarItem } from "../../components/touch_bar.js";
import type { SegmentedControlSegment } from "electron";
const TPL = /*html*/`
`;
// TODO: Deduplicate
interface CreateChildResponse {
    note: {
        noteId: string;
    };
}
interface Event {
    startDate: string,
    endDate?: string | null,
    startTime?: string | null,
    endTime?: string | null
}
const CALENDAR_VIEWS = [
    "timeGridWeek",
    "dayGridMonth",
    "multiMonthYear",
    "listMonth"
]
export default class CalendarView extends ViewMode<{}> {
    private $root: JQuery;
    private $calendarContainer: JQuery;
    private noteIds: string[];
    private calendar?: Calendar;
    private isCalendarRoot: boolean;
    private lastView?: string;
    private debouncedSaveView?: DebouncedFunction<() => void>;
    constructor(args: ViewModeArgs) {
        super(args, "calendar");
        this.$root = $(TPL);
        this.$calendarContainer = this.$root.find(".calendar-container");
        this.noteIds = args.noteIds;
        this.isCalendarRoot = false;
        args.$parent.append(this.$root);
    }
    get isFullHeight(): boolean {
        return true;
    }
    async renderList(): Promise | undefined> {
        this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot");
        const isEditable = !this.isCalendarRoot;
        const { Calendar } = await import("@fullcalendar/core");
        const plugins: PluginDef[] = [];
        plugins.push((await import("@fullcalendar/daygrid")).default);
        plugins.push((await import("@fullcalendar/timegrid")).default);
        plugins.push((await import("@fullcalendar/list")).default);
        plugins.push((await import("@fullcalendar/multimonth")).default);
        if (isEditable || this.isCalendarRoot) {
            plugins.push((await import("@fullcalendar/interaction")).default);
        }
        let eventBuilder: EventSourceFunc;
        if (!this.isCalendarRoot) {
            eventBuilder = async () => await CalendarView.buildEvents(this.noteIds)
        } else {
            eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e);
        }
        // Parse user's initial view, if valid.
        let initialView = "dayGridMonth";
        const userInitialView = this.parentNote.getLabelValue("calendar:view");
        if (userInitialView && CALENDAR_VIEWS.includes(userInitialView)) {
            initialView = userInitialView;
        }
        const calendar = new Calendar(this.$calendarContainer[0], {
            plugins,
            initialView,
            events: eventBuilder,
            editable: isEditable,
            selectable: isEditable,
            select: (e) => this.#onCalendarSelection(e),
            eventChange: (e) => this.#onEventMoved(e),
            firstDay: options.getInt("firstDayOfWeek") ?? 0,
            weekends: !this.parentNote.hasAttribute("label", "calendar:hideWeekends"),
            weekNumbers: this.parentNote.hasAttribute("label", "calendar:weekNumbers"),
            locale: await CalendarView.#getLocale(),
            height: "100%",
            nowIndicator: true,
            handleWindowResize: false,
            eventDidMount: (e) => {
                const { iconClass, promotedAttributes } = e.event.extendedProps;
                // Prepend the icon to the title, if any.
                if (iconClass) {
                    let titleContainer;
                    switch (e.view.type) {
                        case "timeGridWeek":
                        case "dayGridMonth":
                            titleContainer = e.el.querySelector(".fc-event-title");
                            break;
                        case "multiMonthYear":
                            break;
                        case "listMonth":
                            titleContainer = e.el.querySelector(".fc-list-event-title a");
                            break;
                    }
                    if (titleContainer) {
                        const icon = /*html*/` `;
                        titleContainer.insertAdjacentHTML("afterbegin", icon);
                    }
                }
                // Append promoted attributes to the end of the event container.
                if (promotedAttributes) {
                    let promotedAttributesHtml = "";
                    for (const [name, value] of promotedAttributes) {
                        promotedAttributesHtml = promotedAttributesHtml + /*html*/`\
                        
                            ${name}: ${value}
                        
`;
                    }
                    let mainContainer;
                    switch (e.view.type) {
                        case "timeGridWeek":
                        case "dayGridMonth":
                            mainContainer = e.el.querySelector(".fc-event-main");
                            break;
                        case "multiMonthYear":
                            break;
                        case "listMonth":
                            mainContainer = e.el.querySelector(".fc-list-event-title");
                            break;
                    }
                    $(mainContainer ?? e.el).append($(promotedAttributesHtml));
                }
            },
            // Called upon when clicking the day number in the calendar, opens or creates the day note but only if in a calendar root.
            dateClick: async (e) => {
                if (!this.isCalendarRoot) {
                    return;
                }
                const note = await date_notes.getDayNote(e.dateStr);
                if (note) {
                    appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
                    appContext.triggerCommand("refreshNoteList", { noteId: this.parentNote.noteId });
                }
            },
            datesSet: (e) => this.#onDatesSet(e),
            headerToolbar: {
                start: "title",
                end: `${CALENDAR_VIEWS.join(",")} today prev,next`
            }
        });
        calendar.render();
        this.calendar = calendar;
        new ResizeObserver(() => calendar.updateSize())
            .observe(this.$calendarContainer[0]);
        return this.$root;
    }
    static async #getLocale() {
        const locale = options.get("locale");
        // Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages.
        switch (locale) {
            case "de":
                return (await import("@fullcalendar/core/locales/de")).default;
            case "es":
                return (await import("@fullcalendar/core/locales/es")).default;
            case "fr":
                return (await import("@fullcalendar/core/locales/fr")).default;
            case "cn":
                return (await import("@fullcalendar/core/locales/zh-cn")).default;
            case "tw":
                return (await import("@fullcalendar/core/locales/zh-tw")).default;
            case "ro":
                return (await import("@fullcalendar/core/locales/ro")).default;
            case "en":
            default:
                return undefined;
        }
    }
    #onDatesSet(e: DatesSetArg) {
        const currentView = e.view.type;
        if (currentView === this.lastView) {
            return;
        }
        if (!this.debouncedSaveView) {
            this.debouncedSaveView = debounce(() => {
                if (this.lastView) {
                    attributes.setLabel(this.parentNote.noteId, "calendar:view", this.lastView);
                }
            }, 1_000);
        }
        this.debouncedSaveView();
        this.lastView = currentView;
        if (hasTouchBar) {
            appContext.triggerCommand("refreshTouchBar");
        }
    }
    async #onCalendarSelection(e: DateSelectArg) {
        // Handle start and end date
        const { startDate, endDate } = this.#parseStartEndDateFromEvent(e);
        if (!startDate) {
            return;
        }
        // Handle start and end time.
        const { startTime, endTime } = this.#parseStartEndTimeFromEvent(e);
        // Ask for the title
        const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
        if (!title?.trim()) {
            return;
        }
        // Create the note.
        const { note } = await server.post(`notes/${this.parentNote.noteId}/children?target=into`, {
            title,
            content: "",
            type: "text"
        });
        // Set the attributes.
        attributes.setLabel(note.noteId, "startDate", startDate);
        if (endDate) {
            attributes.setLabel(note.noteId, "endDate", endDate);
        }
        if (startTime) {
            attributes.setLabel(note.noteId, "startTime", startTime);
        }
        if (endTime) {
            attributes.setLabel(note.noteId, "endTime", endTime);
        }
    }
    #parseStartEndDateFromEvent(e: DateSelectArg | EventImpl) {
        const startDate = CalendarView.#formatDateToLocalISO(e.start);
        if (!startDate) {
            return { startDate: null, endDate: null };
        }
        let endDate;
        if (e.allDay) {
            endDate = CalendarView.#formatDateToLocalISO(CalendarView.#offsetDate(e.end, -1));
        } else {
            endDate = CalendarView.#formatDateToLocalISO(e.end);
        }
        return { startDate, endDate };
    }
    #parseStartEndTimeFromEvent(e: DateSelectArg | EventImpl) {
        let startTime: string | undefined | null = null;
        let endTime: string | undefined | null = null;
        if (!e.allDay) {
            startTime = CalendarView.#formatTimeToLocalISO(e.start);
            endTime = CalendarView.#formatTimeToLocalISO(e.end);
        }
        return { startTime, endTime };
    }
    async #onEventMoved(e: EventChangeArg) {
        // Handle start and end date
        let { startDate, endDate } = this.#parseStartEndDateFromEvent(e.event);
        if (!startDate) {
            return;
        }
        const noteId = e.event.extendedProps.noteId;
        // Don't store the end date if it's empty.
        if (endDate === startDate) {
            endDate = undefined;
        }
        // Update start date
        const note = await froca.getNote(noteId);
        if (!note) {
            return;
        }
        // Since they can be customized via calendar:startDate=$foo and calendar:endDate=$bar we need to determine the
        // attributes to be effectively updated
        const startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startDate").shift()?.value||"startDate";
        const endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endDate").shift()?.value||"endDate";
        attributes.setAttribute(note, "label", startAttribute, startDate);
        attributes.setAttribute(note, "label", endAttribute, endDate);
        // Update start time and end time if needed.
        if (!e.event.allDay) {
            const startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
            const endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
            const { startTime, endTime } = this.#parseStartEndTimeFromEvent(e.event);
            attributes.setAttribute(note, "label", startAttribute, startTime);
            attributes.setAttribute(note, "label", endAttribute, endTime);
        }
    }
    onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
        // Refresh note IDs if they got changed.
        if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) {
            this.noteIds = this.parentNote.getChildNoteIds();
        }
        // Refresh calendar on attribute change.
        if (loadResults.getAttributeRows().some((attribute) => attribute.noteId === this.parentNote.noteId && attribute.name?.startsWith("calendar:") && attribute.name !== "calendar:view")) {
            return true;
        }
        // Refresh dataset on subnote change.
        if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) {
            this.calendar.refetchEvents();
        }
    }
    async #buildEventsForCalendar(e: EventSourceFuncArg) {
        const events: EventInput[] = [];
        // Gather all the required date note IDs.
        const dateRange = utils.getMonthsInDateRange(e.startStr, e.endStr);
        let allDateNoteIds: string[] = [];
        for (const month of dateRange) {
            // TODO: Deduplicate get type.
            const dateNotesForMonth = await server.get>(`special-notes/notes-for-month/${month}?calendarRoot=${this.parentNote.noteId}`);
            const dateNoteIds = Object.values(dateNotesForMonth);
            allDateNoteIds = [...allDateNoteIds, ...dateNoteIds];
        }
        // Request all the date notes.
        const dateNotes = await froca.getNotes(allDateNoteIds);
        const childNoteToDateMapping: Record = {};
        for (const dateNote of dateNotes) {
            const startDate = dateNote.getLabelValue("dateNote");
            if (!startDate) {
                continue;
            }
            events.push(await CalendarView.buildEvent(dateNote, { startDate }));
            if (dateNote.hasChildren()) {
                const childNoteIds = dateNote.getChildNoteIds();
                for (const childNoteId of childNoteIds) {
                    childNoteToDateMapping[childNoteId] = startDate;
                }
            }
        }
        // Request all child notes of date notes in a single run.
        const childNoteIds = Object.keys(childNoteToDateMapping);
        const childNotes = await froca.getNotes(childNoteIds);
        for (const childNote of childNotes) {
            const startDate = childNoteToDateMapping[childNote.noteId];
            const event = await CalendarView.buildEvent(childNote, { startDate });
            events.push(event);
        }
        return events.flat();
    }
    static async buildEvents(noteIds: string[]) {
        const notes = await froca.getNotes(noteIds);
        const events: EventSourceInput = [];
        for (const note of notes) {
            const startDate = CalendarView.#getCustomisableLabel(note, "startDate", "calendar:startDate");
            if (note.hasChildren()) {
                const childrenEventData = await this.buildEvents(note.getChildNoteIds());
                if (childrenEventData.length > 0) {
                    events.push(childrenEventData);
                }
            }
            if (!startDate) {
                continue;
            }
            const endDate = CalendarView.#getCustomisableLabel(note, "endDate", "calendar:endDate");
            const startTime = CalendarView.#getCustomisableLabel(note, "startTime", "calendar:startTime");
            const endTime = CalendarView.#getCustomisableLabel(note, "endTime", "calendar:endTime");
            events.push(await CalendarView.buildEvent(note, { startDate, endDate, startTime, endTime }));
        }
        return events.flat();
    }
    /**
     * Allows the user to customize the attribute from which to obtain a particular value. For example, if `customLabelNameAttribute` is `calendar:startDate`
     * and `defaultLabelName` is `startDate` and the note at hand has `#calendar:startDate=myStartDate #myStartDate=2025-02-26` then the value returned will
     * be `2025-02-26`. If there is no custom attribute value, then the value of the default attribute is returned instead (e.g. `#startDate`).
     *
     * @param note the note from which to read the values.
     * @param defaultLabelName the name of the label in case a custom value is not found.
     * @param customLabelNameAttribute the name of the label to look for a custom value.
     * @returns the value of either the custom label or the default label.
     */
    static #getCustomisableLabel(note: FNote, defaultLabelName: string, customLabelNameAttribute: string) {
        const customAttributeName = note.getLabelValue(customLabelNameAttribute);
        if (customAttributeName) {
            const customValue = note.getLabelValue(customAttributeName);
            if (customValue) {
                return customValue;
            }
        }
        return note.getLabelValue(defaultLabelName);
    }
    static async buildEvent(note: FNote, { startDate, endDate, startTime, endTime }: Event) {
        const customTitleAttributeName = note.getLabelValue("calendar:title");
        const titles = await CalendarView.#parseCustomTitle(customTitleAttributeName, note);
        const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color");
        const events: EventInput[] = [];
        const calendarDisplayedAttributes = note.getLabelValue("calendar:displayedAttributes")?.split(",");
        let displayedAttributesData: Array<[string, string]> | null = null;
        if (calendarDisplayedAttributes) {
            displayedAttributesData = await this.#buildDisplayedAttributes(note, calendarDisplayedAttributes);
        }
        for (const title of titles) {
            if (startTime && endTime && !endDate) {
                endDate = startDate;
            }
            startDate = (startTime ? `${startDate}T${startTime}:00` : startDate);
            if (!startTime) {
                const endDateOffset = CalendarView.#offsetDate(endDate ?? startDate, 1);
                if (endDateOffset) {
                    endDate = CalendarView.#formatDateToLocalISO(endDateOffset);
                }
            }
            endDate = (endTime ? `${endDate}T${endTime}:00` : endDate);
            const eventData: EventInput = {
                title: title,
                start: startDate,
                url: `#${note.noteId}?popup`,
                noteId: note.noteId,
                color: color ?? undefined,
                iconClass: note.getLabelValue("iconClass"),
                promotedAttributes: displayedAttributesData
            };
            if (endDate) {
                eventData.end = endDate;
            }
            events.push(eventData);
        }
        return events;
    }
    static async #buildDisplayedAttributes(note: FNote, calendarDisplayedAttributes: string[]) {
        const filteredDisplayedAttributes = note.getAttributes().filter((attr): boolean => calendarDisplayedAttributes.includes(attr.name))
        const result: Array<[string, string]> = [];
        for (const attribute of filteredDisplayedAttributes) {
            if (attribute.type === "label") result.push([attribute.name, attribute.value]);
            else result.push([attribute.name, (await attribute.getTargetNote())?.title || ""])
        }
        return result;
    }
    static async #parseCustomTitle(customTitlettributeName: string | null, note: FNote, allowRelations = true): Promise {
        if (customTitlettributeName) {
            const labelValue = note.getAttributeValue("label", customTitlettributeName);
            if (labelValue) return [labelValue];
            if (allowRelations) {
                const relations = note.getRelations(customTitlettributeName);
                if (relations.length > 0) {
                    const noteIds = relations.map((r) => r.targetNoteId);
                    const notesFromRelation = await froca.getNotes(noteIds);
                    const titles: string[][] = [];
                    for (const targetNote of notesFromRelation) {
                        const targetCustomTitleValue = targetNote.getAttributeValue("label", "calendar:title");
                        const targetTitles = await CalendarView.#parseCustomTitle(targetCustomTitleValue, targetNote, false);
                        titles.push(targetTitles.flat());
                    }
                    return titles.flat();
                }
            }
        }
        return [note.title];
    }
    static #formatDateToLocalISO(date: Date | null | undefined) {
        if (!date) {
            return undefined;
        }
        const offset = date.getTimezoneOffset();
        const localDate = new Date(date.getTime() - offset * 60 * 1000);
        return localDate.toISOString().split("T")[0];
    }
    static #formatTimeToLocalISO(date: Date | null | undefined) {
        if (!date) {
            return undefined;
        }
        const offset = date.getTimezoneOffset();
        const localDate = new Date(date.getTime() - offset * 60 * 1000);
        return localDate.toISOString()
            .split("T")[1]
            .substring(0, 5);
    }
    static #offsetDate(date: Date | string | null | undefined, offset: number) {
        if (!date) {
            return undefined;
        }
        const newDate = new Date(date);
        newDate.setDate(newDate.getDate() + offset);
        return newDate;
    }
    buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) {
        if (!this.calendar) {
            return;
        }
        const items: TouchBarItem[] = [];
        const $toolbarItems = this.$calendarContainer.find(".fc-toolbar-chunk .fc-button-group, .fc-toolbar-chunk > button");
        for (const item of $toolbarItems) {
            // Button groups.
            if (item.classList.contains("fc-button-group")) {
                let mode: "single" | "buttons" = "single";
                let selectedIndex = 0;
                const segments: SegmentedControlSegment[] = [];
                const subItems = item.childNodes as NodeListOf;
                let index = 0;
                for (const subItem of subItems) {
                    if (subItem.ariaPressed === "true") {
                        selectedIndex = index;
                    }
                    index++;
                    // Text button.
                    if (subItem.innerText) {
                        segments.push({ label: subItem.innerText });
                        continue;
                    }
                    // Icon button.
                    const iconEl = subItem.querySelector("span.fc-icon");
                    let icon: string | null = null;
                    if (iconEl?.classList.contains("fc-icon-chevron-left")) {
                        icon = "NSImageNameTouchBarGoBackTemplate";
                        mode = "buttons";
                    } else if (iconEl?.classList.contains("fc-icon-chevron-right")) {
                        icon = "NSImageNameTouchBarGoForwardTemplate";
                        mode = "buttons";
                    }
                    if (icon) {
                        segments.push({
                            icon: buildIcon(icon)
                        });
                    }
                }
                items.push(new TouchBar.TouchBarSegmentedControl({
                    mode,
                    segments,
                    selectedIndex,
                    change: (selectedIndex, isSelected) => subItems[selectedIndex].click()
                }));
                continue;
            }
            // Standalone item.
            if (item.innerText) {
                items.push(new TouchBar.TouchBarButton({
                    label: item.innerText,
                    click: () => item.click()
                }));
            }
        }
        return items;
    }
}