import * as Vue from 'vue';
import invariant from 'tiny-invariant';
import warning from 'tiny-warning';
import { createControlledPromise, getLocationChangeInfo, isNotFound, isRedirect, rootRouteId, } from '@tanstack/router-core';
import { isServer } from '@tanstack/router-core/isServer';
import { CatchBoundary, ErrorComponent } from './CatchBoundary';
import { ClientOnly } from './ClientOnly';
import { useRouterState } from './useRouterState';
import { useRouter } from './useRouter';
import { CatchNotFound } from './not-found';
import { matchContext } 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();
        // Track the last known routeId to handle stale props during same-route transitions
        let lastKnownRouteId = null;
        // Combined selector that returns all needed data including the actual matchId
        // This handles stale props.matchId during same-route transitions
        const matchData = useRouterState({
            select: (s) => {
                // First try to find match by props.matchId
                let match = s.matches.find((d) => d.id === props.matchId);
                let matchIndex = match
                    ? s.matches.findIndex((d) => d.id === props.matchId)
                    : -1;
                // If match found, update lastKnownRouteId
                if (match) {
                    lastKnownRouteId = match.routeId;
                }
                else if (lastKnownRouteId) {
                    // Match not found - props.matchId might be stale during a same-route transition
                    // Try to find the NEW match by routeId
                    match = s.matches.find((d) => d.routeId === lastKnownRouteId);
                    matchIndex = match
                        ? s.matches.findIndex((d) => d.routeId === lastKnownRouteId)
                        : -1;
                }
                if (!match) {
                    return null;
                }
                const routeId = match.routeId;
                const parentRouteId = matchIndex > 0 ? s.matches[matchIndex - 1]?.routeId : null;
                return {
                    matchId: match.id, // Return the actual matchId (may differ from props.matchId)
                    routeId,
                    parentRouteId,
                    loadedAt: s.loadedAt,
                    ssr: match.ssr,
                    _displayPending: match._displayPending,
                };
            },
        });
        invariant(matchData.value, `Could not find routeId for matchId "${props.matchId}". Please file an issue!`);
        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);
        // Create a ref for the current matchId that we provide to child components
        // This ref is updated to the ACTUAL matchId found (which may differ from props during transitions)
        const matchIdRef = Vue.ref(matchData.value?.matchId ?? props.matchId);
        // Watch both props.matchId and matchData to keep matchIdRef in sync
        // This ensures Outlet gets the correct matchId even during transitions
        Vue.watch([() => props.matchId, () => matchData.value?.matchId], ([propsMatchId, dataMatchId]) => {
            // Prefer the matchId from matchData (which handles fallback)
            // Fall back to props.matchId if matchData is null
            matchIdRef.value = dataMatchId ?? propsMatchId;
        }, { immediate: true });
        // Provide the matchId to child components
        Vue.provide(matchContext, matchIdRef);
        return () => {
            // Use the actual matchId from matchData, not props (which may be stale)
            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) => {
                            // 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))
                                throw error;
                            warning(false, `Error in route match: ${actualMatchId}`);
                            routeOnCatch.value?.(error);
                        },
                        children: content,
                    });
                }
                // Add scroll restoration if needed
                const withScrollRestoration = [
                    content,
                    matchData.value?.parentRouteId === rootRouteId &&
                        router.options.scrollRestoration
                        ? Vue.h(Vue.Fragment, null, [
                            Vue.h(OnRendered),
                            Vue.h(ScrollRestoration),
                        ])
                        : 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.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 = useRouterState({
            select: (s) => {
                return s.resolvedLocation?.state.key;
            },
        });
        Vue.watchEffect(() => {
            if (location.value) {
                router.emit({
                    type: 'onRendered',
                    ...getLocationChangeInfo(router.state),
                });
            }
        });
        return () => null;
    },
});
export const MatchInner = Vue.defineComponent({
    name: 'MatchInner',
    props: {
        matchId: {
            type: String,
            required: true,
        },
    },
    setup(props) {
        const router = useRouter();
        // Track the last known routeId to handle stale props during same-route transitions
        // This is stored outside the selector so it persists across selector calls
        let lastKnownRouteId = null;
        // Combined selector for match state AND remount key
        // This ensures both are computed in the same selector call with consistent data
        const combinedState = useRouterState({
            select: (s) => {
                // First try to find match by props.matchId
                let match = s.matches.find((d) => d.id === props.matchId);
                // If match found, update lastKnownRouteId
                if (match) {
                    lastKnownRouteId = match.routeId;
                }
                else if (lastKnownRouteId) {
                    // Match not found - props.matchId might be stale during a same-route transition
                    // (matchId changed due to loaderDepsHash but props haven't updated yet)
                    // Try to find the NEW match by routeId and use that instead
                    const sameRouteMatch = s.matches.find((d) => d.routeId === lastKnownRouteId);
                    if (sameRouteMatch) {
                        match = sameRouteMatch;
                    }
                }
                if (!match) {
                    // Route no longer exists - truly navigating away
                    return null;
                }
                const routeId = match.routeId;
                // Compute remount key
                const remountFn = router.routesById[routeId].options.remountDeps ??
                    router.options.defaultRemountDeps;
                let remountKey;
                if (remountFn) {
                    const remountDeps = remountFn({
                        routeId,
                        loaderDeps: match.loaderDeps,
                        params: match._strictParams,
                        search: match._strictSearch,
                    });
                    remountKey = remountDeps ? JSON.stringify(remountDeps) : undefined;
                }
                return {
                    routeId,
                    match: {
                        id: match.id,
                        status: match.status,
                        error: match.error,
                        ssr: match.ssr,
                        _forcePending: match._forcePending,
                        _displayPending: match._displayPending,
                    },
                    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);
        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') {
                invariant(isNotFound(match.value.error), 'Expected a notFound error');
                return renderRouteNotFound(router, route.value, match.value.error);
            }
            if (match.value.status === 'redirected') {
                invariant(isRedirect(match.value.error), 'Expected a redirect error');
                throw router.getMatch(match.value.id)?._nonReactive.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 matchId = Vue.inject(matchContext);
        const safeMatchId = Vue.computed(() => matchId?.value || '');
        const routeId = useRouterState({
            select: (s) => s.matches.find((d) => d.id === safeMatchId.value)?.routeId,
        });
        const route = Vue.computed(() => router.routesById[routeId.value]);
        const parentGlobalNotFound = useRouterState({
            select: (s) => {
                const matches = s.matches;
                const parentMatch = matches.find((d) => d.id === safeMatchId.value);
                // During navigation transitions, parent match can be temporarily removed
                // Return false to avoid errors - the component will handle this gracefully
                if (!parentMatch) {
                    return false;
                }
                return parentMatch.globalNotFound;
            },
        });
        const childMatchData = useRouterState({
            select: (s) => {
                const matches = s.matches;
                const index = matches.findIndex((d) => d.id === safeMatchId.value);
                const child = matches[index + 1];
                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) {
                return renderRouteNotFound(router, route.value, undefined);
            }
            if (!childMatchData.value) {
                return null;
            }
            const nextMatch = Vue.h(Match, {
                matchId: childMatchData.value.id,
                key: childMatchData.value.paramsKey,
            });
            if (safeMatchId.value === rootRouteId) {
                return Vue.h(Vue.Suspense, {
                    fallback: router.options.defaultPendingComponent
                        ? Vue.h(router.options.defaultPendingComponent)
                        : null,
                }, {
                    default: () => nextMatch,
                });
            }
            return nextMatch;
        };
    },
});
//# sourceMappingURL=Match.jsx.map