// #region Imports
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { on } from '@breadstone/mosaik-elements';
import { Task } from '@lit/task';
import { LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { MissingComponentError, MissingPathError, RouteAlreadyExistsError, RouteNotFoundError } from './Errors';
import { Route } from './Route';
import { back, forward } from './Utils';
/**
 * The `RouterElement2` element.
 *
 * Usage:
 * ```html
 * <mosaik-router-2></mosaik-router-2>
 * ```
 *
 * ```ts
 * const router = document.querySelector('mosaik-router-2');
 * router.setRoutes([
 *     { path: '/', component: 'home-page' },
 *     { path: '/about', component: 'about-page' },
 * ]);
 * ```
 *
 * @public
 */
let RouterElement2 = class RouterElement2 extends LitElement {
    // #region Fields
    _currentRoute = null;
    _routes = [];
    _renderingTemplateTask = new Task(this, {
        task: async ([currentRoute]) => currentRoute?.resolve(),
        args: () => [this._currentRoute]
    });
    _windowPopStateSubscription = null;
    _windowClickSubscription = null;
    // #endregion
    // #region Methods
    /**
     * @public
     * @override
     */
    connectedCallback() {
        super.connectedCallback();
        this._windowPopStateSubscription = on(window, 'popstate', (e) => this.onHandlePopState(e));
        this._windowClickSubscription = on(window, 'click', (e) => this.onHandleAnchorClick(e));
    }
    /**
     * @public
     * @override
     */
    disconnectedCallback() {
        super.disconnectedCallback();
        this._windowPopStateSubscription?.dispose();
        this._windowClickSubscription?.dispose();
    }
    /**
     * Returns the list of registered routes.
     *
     * @public
     */
    routes() {
        return this._routes;
    }
    /**
     * Retrieves query-string parameters from the current URL.
     *
     * @param name - Optional key to retrieve a single parameter value.
     * @returns An object of all query parameters, or a single value when `name` is provided.
     *
     * @example
     * ```ts
     * // URL: https://example.com?foo=bar&baz=qux
     * router.qs()      // { foo: 'bar', baz: 'qux' }
     * router.qs('foo') // 'bar'
     * ```
     *
     * @public
     */
    qs(name = '') {
        const urlSearchParams = new URLSearchParams(window.location.search);
        return this.extractParameters(urlSearchParams, name);
    }
    /**
     * Retrieves path parameters from the current route.
     *
     * @param name - Optional key to retrieve a single parameter value.
     * @returns An object of all path parameters, or a single value when `name` is provided.
     *
     * @example
     * ```ts
     * // URL: https://example.com/users/1
     * router.params() // { userId: '1' }
     * ```
     *
     * @public
     */
    params(name) {
        const { origin, pathname } = window.location;
        const route = this.findRouteByPath(pathname);
        if (!route) {
            return {};
        }
        const { pathname: pathnameObject } = route.urlPattern.exec(origin + pathname) || {};
        if (!pathnameObject || !pathnameObject.groups) {
            return {};
        }
        return name == null ? pathnameObject.groups : pathnameObject.groups[name];
    }
    /**
     * Registers a callback that fires whenever the router state changes
     * (e.g. on `popstate`).
     *
     * @param callback - The callback to invoke.
     * @returns An `ISubscription` that can be used to unsubscribe.
     *
     * @public
     */
    onChange(callback) {
        const handler = () => callback(this);
        const subscription = on(window, 'popstate', handler);
        return {
            unsubscribe: () => subscription.dispose()
        };
    }
    /**
     * Adds a list of routes to the router configuration.
     *
     * @param routes - An array of partial route configurations.
     *
     * @example
     * ```ts
     * router.setRoutes([
     *     { path: '/', component: HomePage },
     *     { path: '/about', component: AboutPage },
     * ]);
     * ```
     *
     * @public
     */
    setRoutes(routes) {
        for (const route of routes) {
            const built = this.buildNestedRouteFromConfig(route);
            this._routes.push(built);
        }
        // Navigate to the current URL to trigger initial rendering.
        const { href } = window.location;
        void this.navigate({ href });
    }
    /**
     * Navigates to a new route.
     *
     * @param navigation - The navigation target (path or href).
     * @param options - Optional navigation options.
     *
     * @example
     * ```ts
     * router.navigate({ path: '/about' });
     * router.navigate({ path: '/users', query: { page: '1' } });
     * ```
     *
     * @public
     */
    async navigate(navigation, options = {}) {
        const { enableHistoryPushState } = Object.assign({ enableHistoryPushState: true }, options);
        const { path, href } = navigation;
        if (!path && !href) {
            console.error(new MissingPathError());
            return;
        }
        const urlInstance = new URL(path || href || '', window.location.origin);
        Object.keys(navigation.query || {}).forEach((key) => {
            const value = navigation.query[key];
            urlInstance.searchParams.append(key, value);
        });
        const { pathname } = urlInstance;
        const route = this.findRouteByPath(pathname);
        if (!route) {
            console.warn(new RouteNotFoundError(pathname));
            return;
        }
        // Wrap qs and params so guards see the navigation target,
        // not the current location.
        const qs = (name) => this.extractParameters(urlInstance.searchParams, name);
        const params = (name) => {
            const { pathname: targetPath, origin: targetOrigin } = urlInstance;
            const targetRoute = this.findRouteByPath(targetPath);
            if (!targetRoute) {
                return {};
            }
            const { pathname: pathnameObject } = targetRoute.urlPattern.exec(targetOrigin + targetPath) || {};
            if (!pathnameObject || !pathnameObject.groups) {
                return {};
            }
            return name == null ? pathnameObject.groups : pathnameObject.groups[name];
        };
        const isAllowed = await route.resolveRecursiveGuard({
            navigate: this.navigate.bind(this),
            forward: this.forward.bind(this),
            back: this.back.bind(this),
            qs,
            params
        });
        if (!isAllowed) {
            return;
        }
        this._currentRoute = route;
        if (!enableHistoryPushState) {
            return;
        }
        window.history.pushState({}, '', urlInstance.href);
    }
    /**
     * Navigates forward in session history.
     *
     * @public
     */
    forward() {
        forward();
    }
    /**
     * Navigates backward in session history.
     *
     * @public
     */
    back() {
        back();
    }
    /**
     * @override
     */
    createRenderRoot() {
        return this;
    }
    /**
     * @override
     */
    render() {
        return this._renderingTemplateTask.render({
            complete: (template) => template
        });
    }
    /**
     * Finds a route matching the given path, searching recursively through
     * child routes when present.
     *
     * @param path - The pathname to match.
     * @param routes - The route list to search (defaults to all routes).
     * @param onlyLeafRoute - When `true`, only childless routes are returned.
     *
     * @private
     */
    findRouteByPath(path, routes = this.routes(), onlyLeafRoute = false) {
        const hasLeafRoute = (route) => route.children.length === 0;
        for (const route of routes) {
            if (onlyLeafRoute && route?.match(path)) {
                return route;
            }
            if (onlyLeafRoute && route.match(path) && hasLeafRoute(route)) {
                return route;
            }
            if (!onlyLeafRoute && route.match(path)) {
                return route;
            }
            const childRoute = this.findRouteByPath(path, route.children, onlyLeafRoute);
            if (childRoute) {
                return childRoute;
            }
        }
        return undefined;
    }
    /**
     * Creates a `Route` from a partial config, validating required fields.
     *
     * @private
     */
    createRouteFromConfig(routeConfig) {
        const { path, component } = routeConfig;
        if (!path) {
            throw new MissingPathError();
        }
        if (!component) {
            throw new MissingComponentError();
        }
        const route = new Route(path, component);
        if (routeConfig.beforeEnter) {
            route.beforeEnter = routeConfig.beforeEnter;
        }
        return route;
    }
    /**
     * Recursively builds a route tree from a partial config, including
     * nested children.
     *
     * @private
     */
    buildNestedRouteFromConfig(routeConfig) {
        const { children } = routeConfig;
        const route = this.createRouteFromConfig(routeConfig);
        (children || []).forEach((child) => {
            const childRoute = this.buildNestedRouteFromConfig({
                ...child,
                path: `${route.path}${child.path}`
            });
            childRoute.setParent(route);
            if (this.findRouteByPath(child.path, (route.children || []))) {
                console.error(new RouteAlreadyExistsError(child.path));
                return;
            }
            route.children.push(childRoute);
        });
        return route;
    }
    /**
     * Extracts parameters from a `URLSearchParams` or regex match.
     *
     * @private
     */
    extractParameters(source, name) {
        if (!source) {
            return {};
        }
        if (name) {
            return source instanceof URLSearchParams ? source.get(name) : null;
        }
        return source instanceof URLSearchParams
            ? Object.fromEntries(source.entries())
            : source.groups || {};
    }
    /**
     * Handles anchor clicks for in-page navigation.
     *
     * @private
     */
    onHandleAnchorClick(event) {
        const isNonNavigationClick = event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey;
        if (event.defaultPrevented || isNonNavigationClick) {
            return;
        }
        const anchor = this.findAnchor(event.composedPath());
        if (!anchor) {
            return;
        }
        if (anchor.target !== '' ||
            anchor.download ||
            anchor.rel === 'external' ||
            anchor.href === '' ||
            anchor.href?.startsWith('mailto:') ||
            anchor.href?.startsWith('tel:') ||
            (anchor.origin && anchor.origin !== window.location.origin)) {
            return;
        }
        event.preventDefault();
        if (anchor.href === window.location.href) {
            return;
        }
        void this.navigate({ href: anchor.href });
    }
    /**
     * Handles popstate events (browser back/forward).
     *
     * @private
     */
    onHandlePopState(_event) {
        const { href } = window.location;
        void this.navigate({ href }, { enableHistoryPushState: false });
    }
    /**
     * Finds an anchor element in the event's composed path.
     *
     * @param path - The composed event path.
     * @returns An `IRouterLinkLike` object if found, otherwise `undefined`.
     *
     * @private
     */
    findAnchor(path) {
        const anchor = path.find((x) => x instanceof HTMLAnchorElement && x.hasAttribute('href'));
        if (!anchor) {
            return undefined;
        }
        return {
            download: anchor.download,
            href: anchor.href,
            rel: anchor.rel,
            target: anchor.target,
            origin: anchor.origin,
            pathname: anchor.pathname,
            search: anchor.search
        };
    }
};
__decorate([
    state(),
    __metadata("design:type", Object)
], RouterElement2.prototype, "_currentRoute", void 0);
__decorate([
    state(),
    __metadata("design:type", Array)
], RouterElement2.prototype, "_routes", void 0);
RouterElement2 = __decorate([
    customElement('mosaik-router-2')
], RouterElement2);
export { RouterElement2 };
/**
 * Alias for backward compatibility with auto-generated wrappers.
 *
 * @public
 */
export { RouterElement2 as Router2Element };
//# sourceMappingURL=RouterElement2.js.map