// #region Imports
import { emit, on } from '@breadstone/mosaik-elements';
/**
 * Cache of `URLPattern` instances keyed by `IPathRouteConfig`.
 * This avoids re-creating patterns when route configs are reused.
 */
const PATTERN_CACHE = new WeakMap();
/**
 * Type guard for `IURLPatternRouteConfig`.
 */
function isPatternConfig(route) {
    return route.pattern !== undefined;
}
/**
 * Retrieves or lazily creates the `URLPattern` for a given route config.
 *
 * @param route - The route configuration.
 * @returns The corresponding `URLPattern`.
 */
function getPattern(route) {
    if (isPatternConfig(route)) {
        return route.pattern;
    }
    let pattern = PATTERN_CACHE.get(route);
    if (pattern === undefined) {
        pattern = new URLPattern({ pathname: route.path });
        PATTERN_CACHE.set(route, pattern);
    }
    return pattern;
}
/**
 * Returns the tail segment of a pathname groups object.
 * This is the match from a wildcard at the end of a pattern like `/foo/*`.
 */
function getTailGroup(groups) {
    let tailKey;
    for (const key of Object.keys(groups)) {
        if ((/\d+/).test(key) && (tailKey === undefined || key > tailKey)) {
            tailKey = key;
        }
    }
    return tailKey !== undefined ? groups[tailKey] : undefined;
}
/**
 * A reactive controller that performs location-based routing using a
 * configuration of URL patterns and associated render callbacks.
 *
 * @public
 */
export class Routes {
    // #region Fields
    _host;
    _childRoutes = [];
    _routes;
    _fallback;
    _parentRoutes;
    _currentPathname;
    _currentRoute;
    _currentParams = {};
    _onDisconnect;
    _hostRoutesConnectedSubscription;
    // #endregion
    // #region Ctor
    /**
     * Creates a new `Routes` reactive controller.
     *
     * @param host - The host element that owns this controller.
     * @param routes - Initial set of route configurations.
     * @param options - Optional settings such as a fallback route.
     *
     * @public
     */
    constructor(host, routes, options) {
        this._host = host;
        host.addController(this);
        this._routes = [...routes];
        this._fallback = options?.fallback;
        this._hostRoutesConnectedSubscription = null;
    }
    // #endregion
    // #region Properties
    /**
     * The currently installed set of routes in precedence order.
     *
     * This array is mutable. To dynamically add a new route:
     *
     * ```ts
     * this._routes.routes.push({
     *     path: '/foo',
     *     render: () => html`<p>Foo</p>`,
     * });
     * ```
     *
     * Mutating this property does not trigger a route transition.
     * Call `goto()` to re-evaluate.
     *
     * @public
     */
    get routes() {
        return this._routes;
    }
    set routes(value) {
        this._routes = value;
    }
    /**
     * A default fallback route matched when none of the `routes` match.
     * Implicitly matches `/*`.
     *
     * @public
     */
    get fallback() {
        return this._fallback;
    }
    set fallback(value) {
        this._fallback = value;
    }
    /**
     * The current parsed route parameters.
     *
     * @public
     * @readonly
     */
    get params() {
        return this._currentParams;
    }
    // #endregion
    // #region Methods
    /**
     * Returns a URL string of the current route, including parent routes,
     * optionally replacing the local path with `pathname`.
     *
     * @param pathname - Optional local pathname override.
     * @returns The composed URL path.
     *
     * @public
     */
    link(pathname) {
        if (pathname?.startsWith('/')) {
            return pathname;
        }
        if (pathname?.startsWith('.')) {
            throw new Error('Relative dot-paths are not yet implemented.');
        }
        pathname ??= this._currentPathname;
        return (this._parentRoutes?.link() ?? '') + pathname;
    }
    /**
     * Navigates this controller to the given `pathname`.
     *
     * Child routes are navigated automatically when the path matches a
     * tail wildcard pattern (`/*`).
     *
     * @param pathname - The target path.
     *
     * @public
     */
    async goto(pathname) {
        let tailGroup;
        if (this.routes.length === 0 && this.fallback === undefined) {
            // No own routes – pass the whole pathname as a tail match.
            tailGroup = pathname;
            this._currentPathname = '';
            this._currentParams = { 0: tailGroup };
        }
        else {
            const route = this.getRoute(pathname);
            if (route === undefined) {
                throw new Error(`No route found for ${pathname}`);
            }
            const pattern = getPattern(route);
            const result = pattern.exec({ pathname });
            const params = result?.pathname.groups ?? {};
            tailGroup = getTailGroup(params);
            if (typeof route.enter === 'function') {
                const success = await route.enter(params);
                if (success === false) {
                    return;
                }
            }
            this._currentRoute = route;
            this._currentParams = params;
            this._currentPathname =
                tailGroup === undefined
                    ? pathname
                    : pathname.substring(0, pathname.length - tailGroup.length);
        }
        // Propagate the tail match to children.
        if (tailGroup !== undefined) {
            for (const childRoutes of this._childRoutes) {
                void childRoutes.goto(tailGroup);
            }
        }
        this._host.requestUpdate();
    }
    /**
     * The result of calling the current route's `render()` callback.
     *
     * @returns The rendered template or `undefined`.
     *
     * @public
     */
    outlet() {
        return this._currentRoute?.render?.(this._currentParams);
    }
    /**
     * @public
     */
    hostConnected() {
        this._hostRoutesConnectedSubscription = on(this._host, 'routesConnected', (e) => this.onRoutesConnected(e));
        const details = { routes: this };
        emit(this._host, 'routesConnected', details);
        this._onDisconnect = details.disconnect;
    }
    /**
     * @public
     */
    hostDisconnected() {
        this._onDisconnect?.();
        this._parentRoutes = undefined;
        this._hostRoutesConnectedSubscription?.dispose();
    }
    /**
     * Matches `pathname` against installed routes and returns the first match.
     *
     * @param pathname - The path to match.
     * @returns The matching `RouteConfig` or `undefined`.
     *
     * @private
     */
    getRoute(pathname) {
        const matchedRoute = this.routes.find((r) => getPattern(r).test({ pathname }));
        if (matchedRoute || this.fallback === undefined) {
            return matchedRoute;
        }
        if (this._fallback) {
            return { ...this._fallback, path: '/*' };
        }
        return undefined;
    }
    /**
     * Handles connection of a child `Routes` controller.
     *
     * @private
     */
    onRoutesConnected(e) {
        // Don't handle the event fired by this routes controller, which we get
        // because we do this.dispatchEvent(...)
        if (e.detail.routes === this) {
            return;
        }
        const childRoutes = e.detail.routes;
        this._childRoutes.push(childRoutes);
        childRoutes._parentRoutes = this;
        e.stopImmediatePropagation();
        e.detail.disconnect = () => {
            this._childRoutes?.splice(this._childRoutes.indexOf(childRoutes) >>> 0, 1);
        };
        const tailGroup = getTailGroup(this._currentParams);
        if (tailGroup !== undefined) {
            void childRoutes.goto(tailGroup);
        }
    }
}
//# sourceMappingURL=Routes.js.map