import * as Vue from 'vue';
import { createControlledPromise, getLocationChangeInfo, invariant, isNotFound, isRedirect, rootRouteId, } from '@tanstack/router-core';
import { isServer } from '@tanstack/router-core/isServer';
import { useStore } from '@tanstack/vue-store';
import { CatchBoundary, ErrorComponent } from './CatchBoundary';
import { ClientOnly } from './ClientOnly';
import { useRouter } from './useRouter';
import { CatchNotFound } from './not-found';
import { matchContext, pendingMatchContext, routeIdContext, } from './matchContext';
import { renderRouteNotFound } from './renderRouteNotFound';
import { ScrollRestoration } from './scroll-restoration';
export const Match = Vue.defineComponent({
    name: 'Match',
    props: {
        matchId: {
            type: String,
            required: true,
        },
    },
    setup(props) {
        const router = useRouter();
        // Derive routeId from initial props.matchId — stable for this component's
        // lifetime. The routeId never changes for a given route position in the
        // tree, even when matchId changes (loaderDepsHash, etc).
        const routeId = router.stores.matchStores.get(props.matchId)?.routeId;
        if (!routeId) {
            if (process.env.NODE_ENV !== 'production') {
                throw new Error(`Invariant failed: Could not find routeId for matchId "${props.matchId}". Please file an issue!`);
            }
            invariant();
        }
        // Static route-tree check: is this route a direct child of the root?
        // parentRoute is set at build time, so no reactive tracking needed.
        const isChildOfRoot = router.routesById[routeId]?.parentRoute?.id === rootRouteId;
        // Single stable store subscription — getRouteMatchStore returns a
        // cached computed store that resolves routeId → current match state
        // through the signal graph. No bridge needed.
        const activeMatch = useStore(router.stores.getRouteMatchStore(routeId), (value) => value);
        const isPendingMatchRef = useStore(router.stores.pendingRouteIds, (pendingRouteIds) => Boolean(pendingRouteIds[routeId]), { equal: Object.is });
        const loadedAt = useStore(router.stores.loadedAt, (value) => value);
        const matchData = Vue.computed(() => {
            const match = activeMatch.value;
            if (!match) {
                return null;
            }
            return {
                matchId: match.id,
                routeId,
                loadedAt: loadedAt.value,
                ssr: match.ssr,
                _displayPending: match._displayPending,
            };
        });
        const route = Vue.computed(() => matchData.value ? router.routesById[matchData.value.routeId] : null);
        const PendingComponent = Vue.computed(() => route.value?.options?.pendingComponent ??
            router?.options?.defaultPendingComponent);
        const pendingElement = Vue.computed(() => PendingComponent.value ? Vue.h(PendingComponent.value) : undefined);
        const routeErrorComponent = Vue.computed(() => route.value?.options?.errorComponent ??
            router?.options?.defaultErrorComponent);
        const routeOnCatch = Vue.computed(() => route.value?.options?.onCatch ?? router?.options?.defaultOnCatch);
        const routeNotFoundComponent = Vue.computed(() => route.value?.isRoot
            ? // If it's the root route, use the globalNotFound option, with fallback to the notFoundRoute's component
                (route.value?.options?.notFoundComponent ??
                    router?.options?.notFoundRoute?.options?.component)
            : route.value?.options?.notFoundComponent);
        const hasShellComponent = Vue.computed(() => {
            if (!route.value?.isRoot)
                return false;
            return !!route.value.options.shellComponent;
        });
        const ShellComponent = Vue.computed(() => hasShellComponent.value
            ? route.value.options.shellComponent
            : null);
        // Provide routeId context (stable string) for children.
        // MatchInner, Outlet, and useMatch all consume this.
        Vue.provide(routeIdContext, routeId);
        // Provide reactive nearest-match context for hooks that slice the active
        // matches array relative to the current match.
        const matchIdRef = Vue.computed(() => activeMatch.value?.id ?? props.matchId);
        Vue.provide(matchContext, matchIdRef);
        Vue.provide(pendingMatchContext, isPendingMatchRef);
        return () => {
            const actualMatchId = matchData.value?.matchId ?? props.matchId;
            const resolvedNoSsr = matchData.value?.ssr === false || matchData.value?.ssr === 'data-only';
            const shouldClientOnly = resolvedNoSsr || !!matchData.value?._displayPending;
            const renderMatchContent = () => {
                const matchInner = Vue.h(MatchInner, { matchId: actualMatchId });
                let content = shouldClientOnly
                    ? Vue.h(ClientOnly, {
                        fallback: pendingElement.value,
                    }, {
                        default: () => matchInner,
                    })
                    : matchInner;
                // Wrap in NotFound boundary if needed
                if (routeNotFoundComponent.value) {
                    content = Vue.h(CatchNotFound, {
                        fallback: (error) => {
                            error.routeId ?? (error.routeId = matchData.value?.routeId);
                            // If the current not found handler doesn't exist or it has a
                            // route ID which doesn't match the current route, rethrow the error
                            if (!routeNotFoundComponent.value ||
                                (error.routeId && error.routeId !== matchData.value?.routeId) ||
                                (!error.routeId && route.value && !route.value.isRoot))
                                throw error;
                            return Vue.h(routeNotFoundComponent.value, error);
                        },
                        children: content,
                    });
                }
                // Wrap in error boundary if needed
                if (routeErrorComponent.value) {
                    content = CatchBoundary({
                        getResetKey: () => matchData.value?.loadedAt ?? 0,
                        errorComponent: routeErrorComponent.value || ErrorComponent,
                        onCatch: (error) => {
                            // Forward not found errors (we don't want to show the error component for these)
                            if (isNotFound(error)) {
                                error.routeId ?? (error.routeId = matchData.value?.routeId);
                                throw error;
                            }
                            if (process.env.NODE_ENV !== 'production') {
                                console.warn(`Warning: Error in route match: ${actualMatchId}`);
                            }
                            routeOnCatch.value?.(error);
                        },
                        children: content,
                    });
                }
                // Add scroll restoration if needed
                const withScrollRestoration = [
                    content,
                    isChildOfRoot
                        ? Vue.h(Vue.Fragment, null, [
                            Vue.h(OnRendered),
                            router.options.scrollRestoration &&
                                (isServer ?? router.isServer)
                                ? Vue.h(ScrollRestoration)
                                : null,
                        ])
                        : null,
                ].filter(Boolean);
                // Return single child directly to avoid Fragment wrapper that causes hydration mismatch
                if (withScrollRestoration.length === 1) {
                    return withScrollRestoration[0];
                }
                return Vue.h(Vue.Fragment, null, withScrollRestoration);
            };
            if (!hasShellComponent.value) {
                return renderMatchContent();
            }
            return Vue.h(ShellComponent.value, null, {
                // Important: return a fresh VNode on each slot invocation so that shell
                // components can re-render without reusing a cached VNode instance.
                default: () => renderMatchContent(),
            });
        };
    },
});
// On Rendered can't happen above the root layout because it actually
// renders a dummy dom element to track the rendered state of the app.
// We render a script tag with a key that changes based on the current
// location state.__TSR_key. Also, because it's below the root layout, it
// allows us to fire onRendered events even after a hydration mismatch
// error that occurred above the root layout (like bad head/link tags,
// which is common).
const OnRendered = Vue.defineComponent({
    name: 'OnRendered',
    setup() {
        const router = useRouter();
        const location = useStore(router.stores.resolvedLocation, (resolvedLocation) => resolvedLocation?.state.__TSR_key);
        let prevHref;
        Vue.watch(location, () => {
            if (location.value) {
                const currentHref = router.latestLocation.href;
                if (prevHref === undefined || prevHref !== currentHref) {
                    router.emit({
                        type: 'onRendered',
                        ...getLocationChangeInfo(router.stores.location.get(), router.stores.resolvedLocation.get()),
                    });
                    prevHref = currentHref;
                }
            }
        }, { immediate: true });
        return () => null;
    },
});
export const MatchInner = Vue.defineComponent({
    name: 'MatchInner',
    props: {
        matchId: {
            type: String,
            required: true,
        },
    },
    setup(props) {
        const router = useRouter();
        // Use routeId from context (provided by parent Match) — stable string.
        const routeId = Vue.inject(routeIdContext);
        const activeMatch = useStore(router.stores.getRouteMatchStore(routeId), (value) => value);
        // Combined selector for match state AND remount key
        // This ensures both are computed in the same selector call with consistent data
        const combinedState = Vue.computed(() => {
            const match = activeMatch.value;
            if (!match) {
                // Route no longer exists - truly navigating away
                return null;
            }
            const matchRouteId = match.routeId;
            // Compute remount key
            const remountFn = router.routesById[matchRouteId].options.remountDeps ??
                router.options.defaultRemountDeps;
            let remountKey;
            if (remountFn) {
                const remountDeps = remountFn({
                    routeId: matchRouteId,
                    loaderDeps: match.loaderDeps,
                    params: match._strictParams,
                    search: match._strictSearch,
                });
                remountKey = remountDeps ? JSON.stringify(remountDeps) : undefined;
            }
            return {
                routeId: matchRouteId,
                match: {
                    id: match.id,
                    status: match.status,
                    error: match.error,
                    ssr: match.ssr,
                    _forcePending: match._forcePending,
                    _displayPending: match._displayPending,
                    _nonReactive: match._nonReactive,
                },
                remountKey,
            };
        });
        const route = Vue.computed(() => {
            if (!combinedState.value)
                return null;
            return router.routesById[combinedState.value.routeId];
        });
        const match = Vue.computed(() => combinedState.value?.match);
        const remountKey = Vue.computed(() => combinedState.value?.remountKey);
        const getMatchPromise = (match, key) => {
            return (router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key]);
        };
        return () => {
            // If match doesn't exist, return null (component is being unmounted or not ready)
            if (!combinedState.value || !match.value || !route.value)
                return null;
            // Handle different match statuses
            if (match.value._displayPending) {
                const PendingComponent = route.value.options.pendingComponent ??
                    router.options.defaultPendingComponent;
                return PendingComponent ? Vue.h(PendingComponent) : null;
            }
            if (match.value._forcePending) {
                const PendingComponent = route.value.options.pendingComponent ??
                    router.options.defaultPendingComponent;
                return PendingComponent ? Vue.h(PendingComponent) : null;
            }
            if (match.value.status === 'notFound') {
                if (!isNotFound(match.value.error)) {
                    if (process.env.NODE_ENV !== 'production') {
                        throw new Error('Invariant failed: Expected a notFound error');
                    }
                    invariant();
                }
                return renderRouteNotFound(router, route.value, match.value.error);
            }
            if (match.value.status === 'redirected') {
                if (!isRedirect(match.value.error)) {
                    if (process.env.NODE_ENV !== 'production') {
                        throw new Error('Invariant failed: Expected a redirect error');
                    }
                    invariant();
                }
                throw getMatchPromise(match.value, 'loadPromise');
            }
            if (match.value.status === 'error') {
                // Check if this route or any parent has an error component
                const RouteErrorComponent = route.value.options.errorComponent ??
                    router.options.defaultErrorComponent;
                // If this route has an error component, render it directly
                // This is more reliable than relying on Vue's error boundary
                if (RouteErrorComponent) {
                    return Vue.h(RouteErrorComponent, {
                        error: match.value.error,
                        reset: () => {
                            router.invalidate();
                        },
                        info: {
                            componentStack: '',
                        },
                    });
                }
                // If there's no error component for this route, throw the error
                // so it can bubble up to the nearest parent with an error component
                throw match.value.error;
            }
            if (match.value.status === 'pending') {
                const pendingMinMs = route.value.options.pendingMinMs ?? router.options.defaultPendingMinMs;
                const routerMatch = router.getMatch(match.value.id);
                if (pendingMinMs &&
                    routerMatch &&
                    !routerMatch._nonReactive.minPendingPromise) {
                    // Create a promise that will resolve after the minPendingMs
                    if (!(isServer ?? router.isServer)) {
                        const minPendingPromise = createControlledPromise();
                        routerMatch._nonReactive.minPendingPromise = minPendingPromise;
                        setTimeout(() => {
                            minPendingPromise.resolve();
                            // We've handled the minPendingPromise, so we can delete it
                            routerMatch._nonReactive.minPendingPromise = undefined;
                        }, pendingMinMs);
                    }
                }
                // In Vue, we render the pending component directly instead of throwing a promise
                // because Vue's Suspense doesn't catch thrown promises like React does
                const PendingComponent = route.value.options.pendingComponent ??
                    router.options.defaultPendingComponent;
                if (PendingComponent) {
                    return Vue.h(PendingComponent);
                }
                // If no pending component, return null while loading
                return null;
            }
            // Success status - render the component with remount key
            const Comp = route.value.options.component ?? router.options.defaultComponent;
            const key = remountKey.value;
            if (Comp) {
                // Pass key as a prop - Vue.h properly handles 'key' as a special prop
                return Vue.h(Comp, key !== undefined ? { key } : undefined);
            }
            return Vue.h(Outlet, key !== undefined ? { key } : undefined);
        };
    },
});
export const Outlet = Vue.defineComponent({
    name: 'Outlet',
    setup() {
        const router = useRouter();
        const parentRouteId = Vue.inject(routeIdContext);
        if (!parentRouteId) {
            return () => null;
        }
        // Parent state via stable routeId store — single subscription
        const parentMatch = useStore(router.stores.getRouteMatchStore(parentRouteId), (v) => v);
        const route = Vue.computed(() => parentMatch.value
            ? router.routesById[parentMatch.value.routeId]
            : undefined);
        const parentGlobalNotFound = Vue.computed(() => parentMatch.value?.globalNotFound ?? false);
        // Child match lookup: read the child matchId from the shared derived
        // map (one reactive node for the whole tree), then grab match state
        // directly from the pool.
        const childMatchIdMap = useStore(router.stores.childMatchIdByRouteId, (v) => v);
        const childMatchData = Vue.computed(() => {
            const childId = childMatchIdMap.value[parentRouteId];
            if (!childId)
                return null;
            const child = router.stores.matchStores.get(childId)?.get();
            if (!child)
                return null;
            return {
                id: child.id,
                // Key based on routeId + params only (not loaderDeps)
                // This ensures component recreates when params change,
                // but NOT when only loaderDeps change
                paramsKey: child.routeId + JSON.stringify(child._strictParams),
            };
        });
        return () => {
            if (parentGlobalNotFound.value) {
                if (!route.value) {
                    return null;
                }
                return renderRouteNotFound(router, route.value, undefined);
            }
            if (!childMatchData.value) {
                return null;
            }
            const nextMatch = Vue.h(Match, {
                matchId: childMatchData.value.id,
                key: childMatchData.value.paramsKey,
            });
            // Note: We intentionally do NOT wrap in Suspense here.
            // The top-level Suspense in Matches already covers the root.
            // The old code compared matchId (e.g. "__root__/") with rootRouteId ("__root__")
            // which never matched, so this Suspense was effectively dead code.
            // With routeId-based lookup, parentRouteId === rootRouteId would match,
            // causing a double-Suspense that corrupts Vue's DOM during updates.
            return nextMatch;
        };
    },
});
//# sourceMappingURL=Match.jsx.map