/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/typedef */
// #region Imports
import { emit, on } from '@breadstone/mosaik-elements';
import { PathLocationStrategy } from './../Strategies/PathLocationStrategy';
import { getPattern, getTailGroup } from './../Utils';
/**
 * A reactive controller that performs location-based routing using a
 * configuration of URL patterns and associated render callbacks.
 *
 * @fires routesConnected {RoutesConnectedEvent} - Fired when a child routes controller is connected.
 *
 * @public
 */
export class RouterController {
    // #region Fields
    static ROUTING_STRATEGIES = new Map([
        ['anchor', nativeAnchorStrategy],
        ['pseudo', pseudoAnchorStrategy]
    ]);
    static ORIGIN = location.origin || `${location.protocol}//${location.host}`;
    _childRoutes = [];
    _host;
    _callback;
    _fallback;
    _currentPathname;
    _currentRoute;
    _currentParams = {};
    _parentRoutes;
    _onDisconnect;
    _windowClickSubscription;
    _windowPopStateSubscription;
    _hostRoutesConnectedSubscription;
    _locationStrategy;
    _routes;
    // #endregion
    // #region Ctor
    /**
     * Constructs a new instance of the `MutationController` class.
     *
     * @public
     */
    constructor(host, routes, config) {
        this._host = host;
        host.addController(this);
        this._routes = [...routes];
        this._fallback = config?.fallback;
        this._callback = config?.callback;
        this._locationStrategy = config?.locationStrategy ?? new PathLocationStrategy();
        this.scrollRestoration = config?.scrollRestoration ?? 'manual';
        this._windowClickSubscription = null;
        this._windowPopStateSubscription = null;
        this._hostRoutesConnectedSubscription = null;
        this._callback = config?.callback;
        this._fallback = config?.fallback;
        if ('scrollRestoration' in history) {
            window.history.scrollRestoration = config?.scrollRestoration ?? 'manual';
        }
    }
    // #endregion
    // #region Properties
    /**
     * The currently installed set of routes in precedence order.
     *
     * This array is mutable. To dynamically add a new route you can write:
     *
     * ```ts
     * this._routes.routes.push({
     *   path: '/foo',
     *   render: () => html`<p>Foo</p>`,
     * });
     * ```
     *
     * Mutating this property does not trigger any route transitions. If the
     * changes may result is a different route matching for the current path, you
     * must instigate a route update with `navigate()`.
     *
     * @public
     */
    get routes() {
        return this._routes;
    }
    set routes(value) {
        this._routes = value;
    }
    /**
     * Gets or sets the `locationStrategy` used by the router.
     *
     * The strategy determines how the browser URL is read and manipulated
     * (hash-based vs. path-based routing).
     *
     * @public
     */
    get locationStrategy() {
        return this._locationStrategy;
    }
    set locationStrategy(value) {
        this._locationStrategy = value;
    }
    /**
     * Gets or sets the `scrollRestoration` property.
     *
     * @public
     */
    get scrollRestoration() {
        return window.history.scrollRestoration;
    }
    set scrollRestoration(value) {
        window.history.scrollRestoration = value;
    }
    /**
     * A default fallback route which will always be matched if none of the
     * `routes` match. Implicitly matches to the path "/*".
     *
     * @public
     * @readonly
     */
    get fallback() {
        return this._fallback;
    }
    /**
     * Returns the `callback` property.
     *
     * @public
     * @readonly
     */
    get callback() {
        return this._callback;
    }
    /**
     * The current parsed route parameters.
     *
     * @public
     * @readonly
     */
    get params() {
        return this._currentParams;
    }
    // #endregion
    // #region Methods
    hostConnected() {
        this._hostRoutesConnectedSubscription = on(this._host, 'routesConnected', (e) => this.onRoutesConnected(e));
        const details = { routes: this };
        emit(this._host, 'routesConnected', details);
        this._onDisconnect = details.disconnect;
        this._windowClickSubscription = on(window, 'click', (e) => this.onWindowClick(e));
        this._windowPopStateSubscription = on(window, 'popstate', (e) => this.onWindowPopState(e));
        // initial kick off routed rendering by going to the current URL
        const initialPath = this._locationStrategy.pathname;
        void this.navigate(initialPath);
    }
    /**
     * @public
     */
    hostDisconnected() {
        // When this child routes controller is disconnected because a parent
        // outlet rendered a different template, disconnecting will ensure that
        // this controller doesn't receive a tail match meant for another route.
        this._onDisconnect?.();
        this._parentRoutes = undefined;
        this._hostRoutesConnectedSubscription?.dispose();
        this._windowClickSubscription?.dispose();
        this._windowPopStateSubscription?.dispose();
    }
    /**
     * Navigates this routes controller to `pathname`.
     *
     * This does not navigate parent routes, so it isn't (yet) a general page
     * navigation API. It does navigate child routes if pathname matches a
     * pattern with a tail wildcard pattern (`/*`).
     *
     * @public
     */
    async navigate(pathname) {
        pathname = pathname.toLowerCase();
        let tailGroup = undefined;
        if (this.routes.length === 0 && this.fallback === undefined) {
            // If a routes controller has none of its own routes it acts like it has
            // one route of `/*` so that it passes the whole pathname as a tail
            // match.
            tailGroup = pathname;
            this._currentPathname = '';
            // Simulate a tail group with the whole pathname
            this._currentParams = { 0: tailGroup };
        }
        else {
            const route = this.getRoute(pathname);
            if (route === undefined) {
                return;
            }
            const pattern = getPattern(route);
            const result = pattern.reg.exec(pathname);
            const params = result?.groups ?? {};
            tailGroup = getTailGroup(params);
            if (typeof route.enter === 'function') {
                const isSuccess = await route.enter(params);
                // If enter() returns false, cancel this navigation
                if (!isSuccess) {
                    return;
                }
            }
            // Only update route state if the enter handler completes successfully
            this._currentRoute = route;
            this._currentParams = params;
            this._currentPathname = tailGroup === undefined
                ? pathname
                : pathname.substring(0, pathname.length - tailGroup.length);
            if (this.callback) {
                this.callback(this._currentRoute);
            }
        }
        // Propagate the tail match to children
        if (tailGroup !== undefined) {
            for (const childRoutes of this._childRoutes) {
                return childRoutes.navigate(tailGroup);
            }
        }
        this._host.requestUpdate();
        return Promise.resolve();
    }
    /**
     * Returns a URL string of the current route, including parent routes,
     * optionally replacing the local path with `pathname`.
     *
     * @public
     */
    link(pathname) {
        if (pathname?.startsWith('/')) {
            return pathname;
        }
        if (pathname?.startsWith('.')) {
            throw new Error('Not implemented');
        }
        pathname ??= this._currentPathname;
        const linkPath = (this._parentRoutes?.link() ?? '') + pathname;
        return this._locationStrategy.prepareExternalUrl(linkPath);
    }
    /**
     * The result of calling the current route's render() callback.
     *
     * @public
     */
    outlet() {
        const view = this._currentRoute?.view;
        if (view) {
            return view;
        }
        return undefined;
    }
    /**
     * Matches `url` against the installed routes and returns the first match.
     *
     * @private
     */
    getRoute(pathname) {
        // first, fix the case-insensitives
        pathname = pathname.toLowerCase();
        const matchedRoute = this.routes.find((r) => {
            try {
                return getPattern(r).reg.test(pathname);
            }
            catch {
                return false;
            }
        });
        if (matchedRoute !== undefined || this._fallback === undefined) {
            return matchedRoute;
        }
        // The fallback route behaves like it has a "/*" path. This is hidden from
        // the public API but is added here to return a valid RouteConfig.
        return {
            ...this._fallback,
            path: '/*'
        };
    }
    /**
     * @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 = () => {
            // Remove route from this._childRoutes:
            // `>>> 0` converts -1 to 2**32-1
            this._childRoutes.splice(this._childRoutes.indexOf(childRoutes) >>> 0, 1);
        };
        const tailGroup = getTailGroup(this._currentParams);
        if (tailGroup !== undefined) {
            void childRoutes.navigate(tailGroup);
        }
    }
    /**
     * @private
     */
    onWindowClick(event) {
        if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) {
            return;
        }
        const anchor = RouterController.ROUTING_STRATEGIES.get('anchor')?.(event.composedPath()) ?? RouterController.ROUTING_STRATEGIES.get('pseudo')?.(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 !== RouterController.ORIGIN)) {
            return;
        }
        event.preventDefault();
        if (anchor.href !== window.location.href) {
            const search = new URLSearchParams(anchor.search);
            const fragment = anchor.hash ?? '';
            const path = search.size ? `${anchor.pathname}?${search.toString()}` : anchor.pathname;
            this._locationStrategy.pushState(`${path}${fragment}`);
            void this.navigate(anchor.pathname);
        }
    }
    /**
     * @private
     */
    onWindowPopState(_) {
        const pathname = this._locationStrategy.pathname;
        void this.navigate(pathname);
    }
}
/**
 * Extracts routing information from a native `<a>` (HTMLAnchorElement) found
 * in the event's composed path. This strategy is optimized for fully qualified,
 * browser-native anchor elements that expose the complete set of link-related
 * properties (`href`, `origin`, `pathname`, `search`, etc.).
 *
 * This strategy is ideal for handling routing behavior when the developer uses
 * traditional `<a>` tags in the DOM that trigger navigation via standard browser behavior.
 *
 * @param path - The composed event path from a click event.
 * @returns A `IRouterLinkLike` object if a valid anchor is found, otherwise `undefined`.
 *
 * @example
 * const result = nativeAnchorStrategy(event.composedPath());
 * if (result) { navigate(result.pathname); }
 */
function nativeAnchorStrategy(path) {
    const anchor = path.find((x) => x instanceof HTMLAnchorElement && x.hasAttribute('href'));
    if (!anchor) {
        return;
    }
    // eslint-disable-next-line @typescript-eslint/consistent-return
    return {
        download: anchor.download,
        href: anchor.href,
        rel: anchor.rel,
        target: anchor.target,
        origin: anchor.origin,
        pathname: anchor.pathname,
        search: anchor.search,
        hash: anchor.hash
    };
}
/**
 * Extracts routing information from a non-anchor element (e.g., `<div>`, `<button>`)
 * that has an `href` attribute set - typically by a directive like `RouterLinkDirective`.
 *
 * This strategy compensates for the lack of native `HTMLAnchorElement` properties
 * by dynamically creating a temporary `<a>` element and assigning the `href`
 * value to it. The browser will automatically parse the `href` into usable
 * properties like `origin`, `pathname`, and `search`.
 *
 * This approach allows generic DOM elements to participate in link-style routing
 * while preserving URL semantics (e.g. navigation logic, parameter extraction).
 *
 * @param path - The composed event path from a click event.
 * @returns A `RouterClickInfo` object if a valid pseudo-anchor is found, otherwise `undefined`.
 *
 * @example
 * const result = pseudoAnchorStrategy(event.composedPath());
 * if (result) { navigate(result.pathname); }
 */
function pseudoAnchorStrategy(path) {
    const el = path.find((x) => x instanceof HTMLElement && x.hasAttribute('href'));
    if (!el) {
        return;
    }
    const href = el.getAttribute('href') ?? '';
    if (!href) {
        return;
    }
    const a = document.createElement('a');
    try {
        a.href = new URL(href, window.location.origin).toString();
    }
    catch {
        return;
    }
    // eslint-disable-next-line @typescript-eslint/consistent-return
    return {
        download: el.getAttribute('download') ?? '',
        href: href,
        rel: el.getAttribute('rel') ?? '',
        target: el.getAttribute('target') ?? '',
        origin: a.origin,
        pathname: a.pathname,
        search: a.search,
        hash: a.hash
    };
}
//# sourceMappingURL=RouterController.js.map