<script module lang="ts">
    import { SvelteMap } from 'svelte/reactivity';
    import { onMount, type Component, type Snippet } from 'svelte';

    type RouteInfo = {
        pattern: RegExp;
        paramNames: string[];
        component: Component<{}>;
    };

    const routes = new SvelteMap<string, RouteInfo>();

    let currentUrl = $state.raw(new URL(window.location.href));

    const currentRoute = $derived.by(() => {
        const normalizedPath = normalizePath(currentUrl.pathname);

        for (const [_, routeInfo] of routes) {
            if (normalizedPath.match(routeInfo.pattern)) {
                return routeInfo;
            }
        }

        return null;
    });

    const currentParams = $derived.by(() => {
        if (!currentRoute) {
            return {};
        }

        const match = currentUrl.pathname.match(currentRoute.pattern);
        if (!match) {
            return {};
        }

        const result: Record<string, string> = {};

        currentRoute.paramNames.forEach((name, index) => {
            result[name] = match[index + 1];
        });

        return result;
    });

    function normalizePath(path: string): string {
        return path.replace(/\/+$/, '');
    }

    /**
     * Registers a new route with the router.
     */
    export function route(path: string, component: Component<{}>): void {
        if (routes.has(path)) {
            console.warn(`Route already registered for path: '${path}'.`);
        }

        const params = Array.from(path.matchAll(/\[([^\]]+)\]/g).map((x) => x[1]));

        const normalizedPath = normalizePath(path);
        const escapedPath = normalizedPath.replace(/[.*+?^${}()|\\/]/g, '\\$&');
        const pathPattern = escapedPath.replace(/\[([^\]]+)\]/g, '([^/]+)');
        const regexPattern = `^${pathPattern}\\/?$`;

        routes.set(path, {
            pattern: new RegExp(regexPattern),
            paramNames: params,
            component: component,
        });
    }

    export class RouteError extends Error {}
    export class MissingRouteError extends RouteError {}
    export class RouteFormatError extends RouteError {}

    /**
     * Gets the value of a route parameter.
     */
    export function paramString(name: string): string {
        const value = currentParams[name];
        if (!value) {
            throw new MissingRouteError(`Route does not have a parameter matching the name '${name}''`);
        }

        return value;
    }

    /**
     * Gets the value of a route parameter as an integer.
     */
    export function paramNumber(name: string): number {
        const value = paramString(name);
        const parsed = parseInt(value, 10);
        if (Number.isNaN(parsed)) {
            throw new RouteFormatError(`parameter ${value} could not be parsed as an integer`);
        }

        return parsed;
    }

    /**
     * Gets the value of a query parameter.
     * If the query parameter is not present, the default value (if provided) will be used instead, otherwise, null.
     */
    export function queryString(name: string): string | null;
    export function queryString(name: string, defaultValue: string): string;
    export function queryString(name: string, defaultValue?: string): string | null {
        const value = currentUrl.searchParams.get(name);
        if (value) {
            return value;
        }

        return defaultValue ?? null;
    }

    /**
     * Gets the value of a query parameter as an integer.
     * If the query parameter cannot be parsed, the default value (if provided) will be used instead, otherwise, null.
     */
    export function queryNumber(name: string): number | null;
    export function queryNumber(name: string, defaultValue: number): number;
    export function queryNumber(name: string, defaultValue?: number): number | null {
        const value = currentUrl.searchParams.get(name);

        if (value) {
            const parsed = parseInt(value, 10);
            if (!Number.isNaN(parsed)) {
                return parsed;
            }
        }

        return defaultValue ?? null;
    }
</script>

<script lang="ts">
    interface Props {
        notfound: Snippet;
    }

    let { notfound }: Props = $props();

    onMount(() => {
        if (window.navigation) {
            function handleNavigate(event: NavigateEvent) {
                event.intercept({
                    handler: async () => {
                        currentUrl = new URL(event.destination.url);
                    },
                });
            }

            window.navigation.addEventListener('navigate', handleNavigate);

            return () => {
                window.navigation.removeEventListener('navigate', handleNavigate);
            };
        } else {
            function refresh() {
                currentUrl = new URL(window.location.href);
            }

            function makeProxy(target: any) {
                return new Proxy(target, {
                    apply: (target, thisArg, argArray) => {
                        target.apply(thisArg, argArray);
                        refresh();
                    },
                });
            }

            window.history.pushState = makeProxy(window.history.pushState);
            window.history.replaceState = makeProxy(window.history.replaceState);
            window.history.go = makeProxy(window.history.go);
            window.history.forward = makeProxy(window.history.forward);
            window.history.back = makeProxy(window.history.back);

            function handleClick(e: MouseEvent) {
                if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
                    return;
                }

                if (e.target instanceof HTMLAnchorElement) {
                    if (e.target.target === '_blank') {
                        return;
                    }

                    const targetUrl = new URL(e.target.href, document.baseURI);
                    const documentUrl = new URL(document.baseURI);

                    if (targetUrl.origin == documentUrl.origin) {
                        e.preventDefault();
                        history.pushState({}, '', targetUrl);
                        currentUrl = targetUrl;
                    }
                }
            }

            window.addEventListener('popstate', refresh);
            document.addEventListener('click', handleClick);

            return () => {
                window.removeEventListener('popstate', refresh);
                document.removeEventListener('click', handleClick);
            };
        }
    });
</script>

{#if currentRoute}
    <currentRoute.component />
{:else}
    {@render notfound()}
{/if}