import {
    AppHistoryEntry,
    AppHistoryEntryInit,
    AppHistoryEntryKnownAs,
    AppHistoryEntryNavigationType, AppHistoryEntrySetState
} from "./app-history-entry";
import {
    AppHistory as AppHistoryPrototype,
    AppHistoryEventMap,
    AppHistoryReloadOptions,
    AppHistoryResult,
    AppHistoryUpdateCurrentOptions,
    AppHistoryTransition as AppHistoryTransitionPrototype, AppHistoryCurrentChangeEvent, AppHistoryNavigationOptions
} from "./spec/app-history";
import {AppHistoryEventTarget} from "./app-history-event-target";
import {InvalidStateError} from "./app-history-errors";
import {EventTargetListeners} from "./event-target";
import {
    AppHistoryTransition,
    AppHistoryTransitionEntry,
    AppHistoryTransitionError,
    AppHistoryTransitionFinally,
    AppHistoryTransitionStart,
    AppHistoryTransitionInitialEntries,
    AppHistoryTransitionInitialIndex,
    AppHistoryTransitionKnown,
    AppHistoryTransitionNavigationType,
    AppHistoryTransitionParentEventTarget,
    AppHistoryTransitionPromises,
    AppHistoryTransitionWait,
    InternalAppHistoryNavigationType,
    Rollback,
    Unset,
    AppHistoryTransitionWhile,
    AppHistoryTransitionStartDeadline,
    AppHistoryTransitionCommit,
    AppHistoryTransitionFinish,
    AppHistoryTransitionAbort,
    AppHistoryTransitionIsOngoing,
    AppHistoryTransitionFinishedDeferred, AppHistoryTransitionCommittedDeferred, AppHistoryTransitionIsPending
} from "./app-history-transition";
import {
    AppHistoryTransitionResult,
    createAppHistoryTransition, EventAbortController,
    InternalAppHistoryNavigateOptions,
    AppHistoryNavigateOptions
} from "./create-app-history-transition";
import {createEvent} from "./event-target/create-event";

export * from "./spec/app-history";

export interface AppHistoryOptions {
    initialUrl?: URL | string;
}

const baseUrl = "https://html.spec.whatwg.org/";

export class AppHistory extends AppHistoryEventTarget<AppHistoryEventMap> implements AppHistoryPrototype {

    // Should be always 0 or 1
    #transitionInProgressCount = 0;

    #entries: AppHistoryEntry[] = [];
    #known = new Set<AppHistoryEntry>();
    #currentIndex = -1;
    #activePromise?: Promise<unknown>;
    #activeTransition?: AppHistoryTransition;
    //
    // #upcomingNonTraverseTransition: AppHistoryTransition;

    #knownTransitions = new WeakSet();
    #initialUrl: string;

    get canGoBack() {
       return !!this.#entries[this.#currentIndex - 1];
    };

    get canGoForward() {
        return !!this.#entries[this.#currentIndex + 1];
    };

    get current() {
        if (this.#currentIndex === -1) {
            return undefined;
        }
        return this.#entries[this.#currentIndex];
    };

    get transition(): AppHistoryTransitionPrototype | undefined {
        const transition = this.#activeTransition;
        // Never let an aborted transition leak, it doesn't need to be accessed any more
        return transition?.signal.aborted ? undefined : transition;
    };

    constructor(options?: AppHistoryOptions) {
        super();
        const initialUrl = options?.initialUrl ?? "/";
        this.#initialUrl = (typeof initialUrl === "string" ? new URL(initialUrl, baseUrl) : initialUrl).toString();
    }

    back(options?: AppHistoryNavigationOptions): AppHistoryResult {
        if (!this.canGoBack) throw new InvalidStateError("Cannot go back");
        const entry = this.#entries[this.#currentIndex - 1];
        return this.#pushEntry("traverse", this.#cloneAppHistoryEntry(entry, {
            ...options,
            navigationType: "traverse"
        }));
    }

    entries(): AppHistoryEntry[] {
        return [...this.#entries];
    }

    forward(options?: AppHistoryNavigationOptions): AppHistoryResult {
        if (!this.canGoForward) throw new InvalidStateError();
        const entry = this.#entries[this.#currentIndex + 1];
        return this.#pushEntry("traverse", this.#cloneAppHistoryEntry(entry, {
            ...options,
            navigationType: "traverse"
        }));
    }

    goTo(key: string, options?: AppHistoryNavigationOptions): AppHistoryResult {
        const found = this.#entries.find(entry => entry.key === key);
        if (found) {
            return this.#pushEntry("traverse", this.#cloneAppHistoryEntry(found, {
                ...options,
                navigationType: "traverse"
            }));
        }
        throw new InvalidStateError();
    }

    navigate(url: string, options?: AppHistoryNavigateOptions): AppHistoryResult {
        const nextUrl = new URL(url, this.#initialUrl).toString();
        console.log({ nextUrl });
        const navigationType = options?.replace ? "replace" : "push";
        const entry = this.#createAppHistoryEntry({
            url: nextUrl,
            ...options,
            navigationType
        });
        return this.#pushEntry(
            navigationType,
            entry,
            undefined,
            options
        );
    }

    #cloneAppHistoryEntry = (entry?: AppHistoryEntry, options?: InternalAppHistoryNavigateOptions): AppHistoryEntry => {
        return this.#createAppHistoryEntry({
            ...entry,
            index: entry?.index ?? undefined,
            state: options?.state ?? entry?.getState() ?? {},
            navigationType: entry?.[AppHistoryEntryNavigationType] ?? (typeof options?.navigationType === "string" ? options.navigationType : "replace"),
            ...options,
            get [AppHistoryEntryKnownAs]() {
              return entry?.[AppHistoryEntryKnownAs];
            },
            get [EventTargetListeners]() {
                return entry?.[EventTargetListeners];
            }
        });
    }

    #createAppHistoryEntry = (options: Partial<AppHistoryEntryInit> & Omit<AppHistoryEntryInit, "index">) => {
        const entry: AppHistoryEntry = new AppHistoryEntry({
            ...options,
            index: options.index ?? (() => {
                return this.#entries.indexOf(entry);
            }),
        });
        return entry;

    }

    #pushEntry = (navigationType: InternalAppHistoryNavigationType, entry: AppHistoryEntry, transition?: AppHistoryTransition, options?: InternalAppHistoryNavigateOptions) => {
        /* c8 ignore start */
        if (entry === this.current) throw new InvalidStateError();
        const existingPosition = this.#entries.findIndex(existing => existing.id === entry.id);
        if (existingPosition > -1) {
            throw new InvalidStateError();
        }
        /* c8 ignore end */
        return this.#commitTransition(navigationType, entry, transition, options);
    };

    #commitTransition = (givenNavigationType: InternalAppHistoryNavigationType, entry: AppHistoryEntry,  transition?: AppHistoryTransition, options?: InternalAppHistoryNavigateOptions) => {
        const nextTransition: AppHistoryTransition = transition ?? new AppHistoryTransition({
            from: entry,
            navigationType: typeof givenNavigationType === "string" ? givenNavigationType : "replace",
            rollback: (options) => {
                return this.#rollback(nextTransition, options);
            },
            [AppHistoryTransitionNavigationType]: givenNavigationType,
            [AppHistoryTransitionInitialEntries]: [...this.#entries],
            [AppHistoryTransitionInitialIndex]: this.#currentIndex,
            [AppHistoryTransitionKnown]: [...this.#known],
            [AppHistoryTransitionEntry]: entry,
            [AppHistoryTransitionParentEventTarget]: this
        });
        const { finished, committed } = nextTransition;
        const handler = () => {
            return this.#immediateTransition(givenNavigationType, entry, nextTransition, options);
        };
        void handler().catch(error => void error);
        // const previousPromise = this.#activePromise;
        // let nextPromise;
        // // console.log({ givenNavigationType });
        // if (givenNavigationType === Rollback) {
        //     nextPromise = handler().then(() => previousPromise);
        // } else {
        //     if (previousPromise) {
        //         nextPromise = previousPromise.then(handler);
        //     } else {
        //         nextPromise = handler();
        //     }
        // }
        // console.log({ previousPromise, nextPromise });
        // const promise = nextPromise
        //     .catch(error => void error)
        //     .then(() => {
        //         if (this.#activePromise === promise) {
        //             this.#activePromise = undefined;
        //         }
        //     })
        this.#queueTransition(nextTransition);
        return { committed, finished };

    }

    #queueTransition = (transition: AppHistoryTransition) => {
        // TODO consume errors that are not abort errors
        // transition.finished.catch(error => void error);
        this.#knownTransitions.add(transition);
    }

    #immediateTransition = (givenNavigationType: InternalAppHistoryNavigationType, entry: AppHistoryEntry, transition: AppHistoryTransition, options?: InternalAppHistoryNavigateOptions) => {
        try {
            this.#transitionInProgressCount += 1;
            if (this.#transitionInProgressCount > 1 && !(givenNavigationType === Rollback)) {
                throw new InvalidStateError("Unexpected multiple transitions");
            }
            return this.#transition(givenNavigationType, entry, transition, options);
        } finally {
            this.#transitionInProgressCount -= 1;
        }
    }

    #rollback = (rollbackTransition: AppHistoryTransition, options?: AppHistoryNavigationOptions): AppHistoryResult => {
        const previousEntries = rollbackTransition[AppHistoryTransitionInitialEntries];
        const previousIndex = rollbackTransition[AppHistoryTransitionInitialIndex];
        const previousCurrent = previousEntries[previousIndex];
        // console.log("z");
        // console.log("Rollback!", { previousCurrent, previousEntries, previousIndex });
        const entry = previousCurrent ? this.#cloneAppHistoryEntry(previousCurrent, options) : undefined;
        const nextOptions: InternalAppHistoryNavigateOptions = {
            ...options,
            index: previousIndex,
            known: new Set([...this.#known, ...previousEntries]),
            navigationType: entry?.[AppHistoryEntryNavigationType] ?? "replace",
            entries: previousEntries,
        } as const;
        const resolvedNavigationType = entry ? Rollback : Unset
        const resolvedEntry = entry ?? this.#createAppHistoryEntry({
            navigationType: "replace",
            index: nextOptions.index,
            sameDocument: true,
            ...options,
        });
        return this.#pushEntry(resolvedNavigationType, resolvedEntry, undefined, nextOptions);
    }

    #transition = (givenNavigationType: InternalAppHistoryNavigationType, entry: AppHistoryEntry, transition: AppHistoryTransition, options?: InternalAppHistoryNavigateOptions): Promise<AppHistoryEntry> => {
        // console.log({ givenNavigationType, transition });
        let navigationType = givenNavigationType;

        const performance = getPerformance();

        if (entry.sameDocument && typeof navigationType === "string") {
            performance.mark(`same-document-navigation:${entry.id}`);
        }

        let committed = false;

        const { current } = this;
        void this.#activeTransition?.finished?.catch(error => error);
        void this.#activeTransition?.[AppHistoryTransitionFinishedDeferred]?.promise?.catch(error => error);
        void this.#activeTransition?.[AppHistoryTransitionCommittedDeferred]?.promise?.catch(error => error);
        this.#activeTransition?.[AppHistoryTransitionAbort]();
        this.#activeTransition = transition;

        const startEventPromise = transition.dispatchEvent({
            type: AppHistoryTransitionStart,
            transition,
            entry
        });

        const unsetTransition = async () => {
            await startEventPromise;
            if (!(typeof options?.index === "number" && options.entries)) throw new InvalidStateError();
            await asyncCommit({
                entries: options.entries,
                index: options.index,
                known: options.known,
            });
            await this.dispatchEvent(
                createEvent({
                    type: "currentchange"
                })
            );
            committed = true;
            return entry;
        }


        const completeTransition = (): Promise<AppHistoryEntry> => {
            if (givenNavigationType === Unset) {
                return unsetTransition();
            }
            
            const transitionResult = createAppHistoryTransition({
                current,
                currentIndex: this.#currentIndex,
                options,
                transition,
                known: this.#known
            });

            const microtask = new Promise<void>(queueMicrotask);
            let promises: Promise<unknown>[] = [];
            const iterator = transitionSteps(transitionResult)[Symbol.iterator]();
            const iterable = { [Symbol.iterator]: () => ({ next: () => iterator.next() })};

            function syncTransition() {
                for (const promise of iterable) {
                    if (promise && typeof promise === "object" && "then" in promise) {
                        promises.push(promise);
                        void promise.catch(error => error);
                    }
                    if (committed) {
                        return asyncTransition();
                    }
                    if (transition.signal.aborted) {
                        break;
                    }
                }
                return Promise.resolve(); // We got through with no async
            }
            async function asyncTransition(): Promise<void> {
                const captured = [...promises];
                if (captured.length) {
                    promises = [];
                    await Promise.all(captured);
                } else if (!transition[AppHistoryTransitionIsOngoing]) {
                    await microtask;
                }
                return syncTransition();
            }

            // console.log("Returning", { entry });
            return syncTransition()
                .then(() => transition[AppHistoryTransitionIsOngoing] ? undefined : microtask)
                .then(() => entry);
        }

        interface Commit  {
            entries: AppHistoryEntry[];
            index: number;
            known?: Set<AppHistoryEntry>;
        }

        const syncCommit = ({ entries, index, known }: Commit) => {
            if (transition.signal.aborted) return;
            this.#entries = entries;
            if (known) {
                this.#known = new Set([...this.#known, ...(known)])
            }
            this.#currentIndex = index;
        }

        const asyncCommit = (commit: Commit) => {
            syncCommit(commit);
            return transition.dispatchEvent(
                createEvent(
                    {
                        type: AppHistoryTransitionCommit,
                        transition,
                        entry
                    }
                )
            );
        }

        const dispose = async () => this.#dispose();

        function *transitionSteps(transitionResult: AppHistoryTransitionResult): Iterable<Promise<unknown>> {
            const microtask = new Promise<void>(queueMicrotask);
            const {
                known,
                entries,
                index,
                currentChange,
                navigate,
            } = transitionResult;

            const navigateAbort = navigate[EventAbortController].abort.bind(navigate[EventAbortController]);
            transition.signal.addEventListener("abort", navigateAbort, { once: true });

            if (typeof navigationType === "string" || navigationType === Rollback) {
                const promise = current?.dispatchEvent(
                    createEvent({
                        type: "navigatefrom",
                        transitionWhile: transition[AppHistoryTransitionWhile],
                    })
                );
                if (promise) yield promise;
            }

            if (typeof navigationType === "string") {
                yield transition.dispatchEvent(navigate);
            }

            yield asyncCommit({
                entries: entries,
                index: index,
                known: known,
            });
            if (entry.sameDocument) {
                yield transition.dispatchEvent(currentChange);
            }
            committed = true;
            if (typeof navigationType === "string") {
                yield entry.dispatchEvent(
                    createEvent({
                        type: "navigateto",
                        transitionWhile: transition[AppHistoryTransitionWhile],
                    })
                );
            }
            yield dispose();
            if (!transition[AppHistoryTransitionPromises].size) {
                yield microtask;
            }
            yield transition.dispatchEvent({
                type: AppHistoryTransitionStartDeadline,
                transition,
                entry
            });
            yield transition[AppHistoryTransitionWait]();
            transition.signal.removeEventListener("abort", navigateAbort);
            yield transition[AppHistoryTransitionFinish]();
            if (typeof navigationType === "string") {
                yield transition.dispatchEvent(
                    createEvent({
                        type: "finish",
                        transitionWhile: transition[AppHistoryTransitionWhile]
                    })
                );
                yield transition.dispatchEvent(
                    createEvent({
                        type: "navigatesuccess",
                        transitionWhile: transition[AppHistoryTransitionWhile]
                    })
                );
            }
            // If we have more length here, we have added more transition
            if (transition[AppHistoryTransitionIsPending]) {
                yield Promise.reject(new InvalidStateError("Unexpected pending promises after finish"));
            }
        }

        const maybeSyncTransition = () => {
            try {
                return completeTransition();
            } catch (error) {
                return Promise.reject(error);
            }
        }

        return Promise.allSettled([
            maybeSyncTransition()
        ])
            .then(async ([detail]) => {
                if (detail.status === "rejected") {
                    await transition.dispatchEvent({
                        type: AppHistoryTransitionError,
                        error: detail.reason,
                        transition,
                        entry
                    });
                }

                await dispose();
                await transition.dispatchEvent({
                    type: AppHistoryTransitionFinally,
                    transition,
                    entry
                });
                await transition[AppHistoryTransitionWait]();
                if (this.#activeTransition === transition) {
                    this.#activeTransition = undefined;
                }
                if (entry.sameDocument && typeof navigationType === "string") {
                    performance.mark(`same-document-navigation-finish:${entry.id}`);
                    performance.measure(
                        `same-document-navigation:${entry.url}`,
                        `same-document-navigation:${entry.id}`,
                        `same-document-navigation-finish:${entry.id}`
                    );
                }
            })
            .then(() => entry)
    }

    #dispose = async () => {
        // console.log(JSON.stringify({ known: [...this.#known], entries: this.#entries }));
        for (const known of this.#known) {
            const index = this.#entries.findIndex(entry => entry.key === known.key);
            if (index !== -1) {
                // Still in use
                continue;
            }
            // No index, no longer known
            this.#known.delete(known);
            const event = createEvent({
                type: "dispose",
                entry: known
            });
            await known.dispatchEvent(event);
            await this.dispatchEvent(event);
        }
        // console.log(JSON.stringify({ pruned: [...this.#known] }));
    }

    reload(options?: AppHistoryReloadOptions): AppHistoryResult {
        const { current } = this;
        if (!current) throw new InvalidStateError();
        const entry = this.#cloneAppHistoryEntry(current, options);
        return this.#pushEntry("reload", entry, undefined, options);
    }

    updateCurrent(options: AppHistoryUpdateCurrentOptions): Promise<void>
    updateCurrent(options: AppHistoryUpdateCurrentOptions): void
    updateCurrent(options: AppHistoryUpdateCurrentOptions): unknown {
        const { current } = this;

        if (!current) {
            throw new InvalidStateError("Expected current entry");
        }

        // Instant change
        current[AppHistoryEntrySetState](options.state);

        const currentChange: AppHistoryCurrentChangeEvent = createEvent({
            from: current,
            type: "currentchange",
            navigationType: undefined,
        });

        return this.dispatchEvent(currentChange);
    }

}

function getPerformance(): {
    now(): number;
    measure(name: string, start: string, finish: string): unknown;
    mark(mark: string): unknown;
} {
    if (typeof performance !== "undefined") {
        return performance;
    }
    /* c8 ignore start */
    return {
        now() {
            return Date.now()
        },
        mark() {

        },
        measure() {
        }
    }
    // const { performance: nodePerformance } = await import("perf_hooks");
    // return nodePerformance;
    /* c8 ignore end */
}
