import { rename } from "fs";
import {
    Component,
    TAbstractFile,
    TFile,
    TFolder,
    Vault,
    getAllTags,
    FuzzySuggestModal
} from "obsidian";
import type { Calendar, Event } from "src/@types";
import type FantasyCalendar from "src/main";
//have to ignore until i fix typing issue
//@ts-expect-error
import Worker, {
    CalendarsMessage,
    GetFileCacheMessage,
    FileCacheMessage,
    OptionsMessage,
    QueueMessage,
    UpdateEventMessage,
    SaveMessage,
    DeleteEventMessage
} from "./watcher.worker";

declare global {
    interface Worker {
        postMessage<T>(message: T, transfer?: Transferable[]): void;
    }
}

export type CalendarEventTree = Map<string, Set<number>>;

class CalendarPickerModal extends FuzzySuggestModal<Calendar> {
    chosen: Calendar;
    constructor(public plugin: FantasyCalendar) {
        super(plugin.app);
    }
    getItems() {
        return this.plugin.data.calendars;
    }
    getItemText(item: Calendar) {
        return item.name;
    }
    onChooseItem(item: Calendar, evt: MouseEvent | KeyboardEvent): void {
        this.chosen = item;
        this.close();
    }
}

export class Watcher extends Component {
    queue: Set<string> = new Set();
    get calendars() {
        return this.plugin.data.calendars;
    }
    get metadataCache() {
        return this.plugin.app.metadataCache;
    }
    get vault() {
        return this.plugin.app.vault;
    }
    constructor(public plugin: FantasyCalendar) {
        super();
    }

    tree: CalendarEventTree = new Map();

    worker = new Worker();
    onload() {
        this.plugin.addCommand({
            id: "rescan-events",
            name: "Rescan Events",
            callback: () => {
                if (this.plugin.data.debug) {
                    console.info("Beginning full rescan for calendar events");
                }
                this.start();
            }
        });
        this.plugin.addCommand({
            id: "rescan-events-for-calendar",
            name: "Rescan Events for Calendar",
            callback: () => {
                const modal = new CalendarPickerModal(this.plugin);
                modal.onClose = () => {
                    if (modal.chosen) {
                        if (this.plugin.data.debug) {
                            console.info(
                                "Beginning full rescan for calendar events for calendar " +
                                    modal.chosen.name
                            );
                        }
                        this.start(modal.chosen);
                    }
                };
                modal.open();
            }
        });

        /** Send the worker the calendars so I don't have to with every message. */
        this.worker.postMessage<CalendarsMessage>({
            type: "calendars",
            calendars: this.calendars
        });
        this.registerEvent(
            this.plugin.app.workspace.on("fantasy-calendars-updated", () => {
                this.worker.postMessage<CalendarsMessage>({
                    type: "calendars",
                    calendars: this.calendars
                });
            })
        );
        /** Send the workers the options so I don't have to with every message. */
        this.worker.postMessage<OptionsMessage>({
            type: "options",
            parseTitle: this.plugin.data.parseDates,
            addToDefaultIfMissing: this.plugin.data.addToDefaultIfMissing,
            format: this.plugin.format,
            defaultCalendar: this.plugin.defaultCalendar?.name,
            debug: this.plugin.data.debug
        });
        this.registerEvent(
            this.plugin.app.workspace.on(
                "fantasy-calendar-settings-change",
                () => {
                    this.worker.postMessage<OptionsMessage>({
                        type: "options",
                        parseTitle: this.plugin.data.parseDates,
                        addToDefaultIfMissing:
                            this.plugin.data.addToDefaultIfMissing,
                        format: this.plugin.format,
                        defaultCalendar: this.plugin.defaultCalendar?.name,
                        debug: this.plugin.data.debug
                    });
                }
            )
        );

        /** Metadata for a file has changed and the file should be checked. */
        this.registerEvent(
            this.metadataCache.on("changed", (file) => {
                if (this.queue.has(file.path)) return;
                this.startParsing([file.path]);
            })
        );
        /** A file has been renamed and should be checked for events.
         */
        this.registerEvent(
            this.vault.on("rename", async (abstractFile, oldPath) => {
                if (!this.calendars.length) return;
                if (!(abstractFile instanceof TFile)) return;
                for (const calendar of this.calendars) {
                    calendar.events = calendar.events.filter(
                        (event) => event.note != oldPath
                    );
                }
                this.worker.postMessage<CalendarsMessage>({
                    type: "calendars",
                    calendars: this.calendars
                });
                this.startParsing([abstractFile.path]);
            })
        );
        /** A file has been deleted and should be checked for events to unlink. */
        this.registerEvent(
            this.vault.on("delete", (abstractFile) => {
                if (!(abstractFile instanceof TFile)) return;
                for (let calendar of this.calendars) {
                    const events = calendar.events.filter(
                        (event) => event.note === abstractFile.path
                    );
                    calendar.events = calendar.events.filter(
                        (event) => event.note != abstractFile.path
                    );
                    for (const event of events) {
                        this.addToTree(calendar, event);
                    }
                }
                this.plugin.saveCalendar();
                this.plugin.app.workspace.trigger(
                    "fantasy-calendars-event-update",
                    this.tree
                );
                this.tree = new Map();
            })
        );

        //worker messages
        /** The worker will ask for file information from files in its queue here */
        this.worker.addEventListener(
            "message",
            async (event: MessageEvent<GetFileCacheMessage>) => {
                if (event.data.type == "get") {
                    const { path } = event.data;
                    this.queue.delete(path);
                    const file =
                        this.plugin.app.vault.getAbstractFileByPath(path);
                    if (file instanceof TFile) {
                        const cache = this.metadataCache.getFileCache(file);
                        const allTags = getAllTags(cache);
                        const data = await this.vault.cachedRead(file);
                        this.worker.postMessage<FileCacheMessage>({
                            type: "file",
                            path,
                            cache,
                            file: { path: file.path, basename: file.basename },
                            allTags,
                            data
                        });
                    } else if (file instanceof TFolder) {
                        const paths = file.children.map((f) => f.path);
                        this.startParsing(paths);
                    }
                }
            }
        );

        /** The worker has found an event that should be updated. */
        this.worker.addEventListener(
            "message",
            async (evt: MessageEvent<UpdateEventMessage>) => {
                if (evt.data.type == "update") {
                    const { id, index, event, original } = evt.data;

                    const calendar = this.calendars.find((c) => c.id == id);

                    if (!calendar) return;
                    if (index == -1) {
                        if (this.plugin.data.debug)
                            console.info(
                                `Adding '${event.name}' to ${calendar.name}`
                            );
                        calendar.events.push(event);
                    } else {
                        if (this.plugin.data.debug)
                            console.info(
                                `Updating '${event.name}' in calendar ${calendar.name}`
                            );
                        calendar.events.splice(
                            index,
                            index >= 0 ? 1 : 0,
                            event
                        );
                    }

                    this.addToTree(calendar, event);
                    if (original) {
                        this.addToTree(calendar, original);
                    }
                }
            }
        );

        this.worker.addEventListener(
            "message",
            async (evt: MessageEvent<DeleteEventMessage>) => {
                if (evt.data.type == "delete") {
                    const { id, index, event } = evt.data;
                    if (!event) return;
                    const calendar = this.calendars.find((c) => c.id == id);
                    if (!calendar) return;
                    if (this.plugin.data.debug)
                        console.info(
                            `Removing '${event.name}' from ${calendar.name}`
                        );
                    calendar.events = calendar.events.filter(
                        (e) => e.id != event.id
                    );
                    this.addToTree(calendar, event);
                }
            }
        );

        /** The worker has parsed all files in its queue. */
        this.worker.addEventListener(
            "message",
            async (evt: MessageEvent<SaveMessage>) => {
                if (evt.data.type == "save") {
                    if (this.plugin.data.debug) {
                        console.info("Received save event from file watcher");
                    }
                    this.plugin.app.workspace.trigger(
                        "fantasy-calendars-event-update",
                        this.tree
                    );
                    this.tree = new Map();
                    await this.plugin.saveCalendar();
                }
            }
        );
        this.start();
    }
    start(calendar?: Calendar) {
        const calendars = calendar ? [calendar] : this.calendars;
        if (!calendars.length) return;
        let folders: Set<string> = new Set();

        for (const calendar of calendars) {
            if (!calendar) continue;
            if (!calendar.autoParse) continue;
            const folder = this.vault.getAbstractFileByPath(calendar.path);
            if (!folder || !(folder instanceof TFolder)) continue;
            for (const child of folder.children) {
                folders.add(child.path);
            }
        }

        if (!folders.size) return;
        if (this.plugin.data.debug) {
            if (calendar) {
                console.info(
                    `Starting rescan for ${calendar.name} (${folders.size})`
                );
            } else {
                console.info(
                    `Starting rescan for ${calendars.length} calendars (${folders.size})`
                );
            }
        }
        this.startParsing([...folders]);
    }
    addToTree(calendar: Calendar, event: Event) {
        if (!this.tree.has(calendar.id)) {
            this.tree.set(calendar.id, new Set());
        }
        const calendarTree = this.tree.get(calendar.id);

        if (calendarTree.has(event.date.year)) return;

        calendarTree.add(event.date.year);

        if (event.end && event.end.year != event.date.year) {
            for (let i = event.date.year + 1; i <= event.end.year; i++) {
                calendarTree.add(event.date.year);
            }
        }
    }
    startParsing(paths: string[]) {
        for (const path of paths) {
            this.queue.add(path);
        }
        this.worker.postMessage<QueueMessage>({
            type: "queue",
            paths
        });
    }
    onunload() {
        this.worker.terminate();
        this.worker = null;
    }
}