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

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

    type RouteInfo = {
        path: string;
        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(/\/+$/, "").trim();
    }

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

        return regexPattern;
    }

    /**
     * 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}'.`);
            return;
        }

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

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

    export function isCurrentPath(path: string): boolean {
        if (!currentRoute) {
            return false;
        }

        const match = path.match(currentRoute.pattern);
        if (!match) {
            return false;
        }

        return true;
    }

    /**
     * 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(() => {
        // @ts-ignore
        if (window.navigation) {
            // @ts-ignore
            function handleNavigate(event: NavigateEvent) {
                event.intercept({
                    handler: async () => {
                        currentUrl = new URL(event.destination.url);
                    }
                });
            }

            // @ts-ignore
            window.navigation.addEventListener("navigate", handleNavigate);

            return () => {
                // @ts-ignore
                window.navigation.removeEventListener("navigate", handleNavigate);
            };
        }
    });
</script>

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