import * as Vue from 'vue';
import { deepEqual, exactPathTest, hasKeys, isDangerousProtocol, preloadWarning, removeTrailingSlash, } from '@tanstack/router-core';
import { isServer } from '@tanstack/router-core/isServer';
import { useStore } from '@tanstack/vue-store';
import { useRouter } from './useRouter';
import { useIntersectionObserver } from './utils';
const timeoutMap = new WeakMap();
export function useLinkProps(options) {
    return useLinkPropsImpl(() => options);
}
function useLinkPropsImpl(getOptions) {
    const router = useRouter();
    let renderFetchedHref;
    // Ensure router is defined before proceeding
    if (!router) {
        console.warn('useRouter must be used inside a <RouterProvider> component!');
        return Vue.computed(() => ({}));
    }
    // Determine if the link is external or internal
    const type = Vue.computed(() => {
        const options = getOptions();
        try {
            new URL(`${options.to}`);
            return 'external';
        }
        catch {
            return 'internal';
        }
    });
    const ref = Vue.ref(null);
    // During SSR we render exactly once and do not need reactivity.
    // Avoid store subscriptions, effects and observers on the server.
    if (isServer ?? router.isServer) {
        const options = getOptions();
        if (type.value === 'external') {
            return Vue.ref(getExternalLinkProps(options, router, ref));
        }
        const next = router.buildLocation(options);
        const href = getHref(options, router, next);
        const isActive = getIsActive(router.stores.location.get(), next, options.activeOptions, router);
        const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, } = resolveStyleProps(options, isActive);
        const result = combineResultProps({
            href,
            options,
            isActive,
            resolvedActiveProps,
            resolvedInactiveProps,
            resolvedClassName,
            resolvedStyle,
        });
        return Vue.ref(result);
    }
    const currentLocation = type.value === 'external'
        ? Vue.shallowRef(router.stores.location.get())
        : useStore(router.stores.location, (l) => l, {
            equal: (prev, next) => prev.href === next.href,
        });
    // Links that start external skip useStore above. Subscribe if they later
    // become internal so active state follows subsequent location changes.
    if (type.value === 'external') {
        Vue.watchEffect((onCleanup) => {
            if (type.value === 'external') {
                return;
            }
            const store = router.stores.location;
            const subscription = store.subscribe((location) => {
                if (currentLocation.value.href !== location.href) {
                    currentLocation.value = location;
                }
            });
            onCleanup(() => subscription.unsubscribe());
        });
    }
    const next = Vue.computed(() => {
        // Rebuild when inherited search/hash or the current route context changes.
        const options = getOptions();
        const opts = { _fromLocation: currentLocation.value, ...options };
        return router.buildLocation(opts);
    });
    const preload = Vue.computed(() => {
        const options = getOptions();
        if (type.value === 'external' ||
            options.reloadDocument ||
            options.disabled) {
            return false;
        }
        return options.preload ?? router.options.defaultPreload;
    });
    const preloadDelay = Vue.computed(() => getOptions().preloadDelay ?? router.options.defaultPreloadDelay ?? 0);
    const isActive = Vue.computed(() => {
        const options = getOptions();
        return getIsActive(currentLocation.value, next.value, options.activeOptions, router);
    });
    const doPreload = () => {
        const options = getOptions();
        return router
            .preloadRoute({ ...options, _builtLocation: next.value })
            .catch((err) => {
            console.warn(err);
            console.warn(preloadWarning);
        });
    };
    let pendingPreload;
    const enqueuePreload = (e) => {
        if (!e) {
            clearTimeout(timeoutMap.get(ref));
            timeoutMap.delete(ref);
            pendingPreload = undefined;
            return;
        }
        const isIntersecting = e.isIntersecting;
        const preloadMode = isIntersecting === undefined ? 'intent' : 'viewport';
        if (preload.value !== preloadMode || isIntersecting === false) {
            if (isIntersecting === false && pendingPreload === 'viewport') {
                clearTimeout(timeoutMap.get(ref));
                timeoutMap.delete(ref);
                pendingPreload = undefined;
            }
            return;
        }
        if (!preloadDelay.value) {
            doPreload();
            return;
        }
        if (!timeoutMap.has(ref)) {
            const scheduledHref = next.value.href;
            pendingPreload = preloadMode;
            timeoutMap.set(ref, setTimeout(() => {
                timeoutMap.delete(ref);
                pendingPreload = undefined;
                if (preload.value === preloadMode &&
                    next.value.href === scheduledHref) {
                    doPreload();
                }
            }, preloadDelay.value));
        }
    };
    useIntersectionObserver(ref, enqueuePreload, () => preload.value !== 'viewport');
    Vue.watchEffect(() => {
        if (preload.value !== 'render') {
            return;
        }
        const nextHref = next.value.href;
        if (nextHref && renderFetchedHref !== nextHref) {
            renderFetchedHref = nextHref;
            doPreload();
        }
    });
    // The click handler
    const handleClick = (e) => {
        if (type.value === 'external') {
            return;
        }
        const options = getOptions();
        // Check actual element's target attribute as fallback
        const elementTarget = e.currentTarget?.getAttribute('target');
        const effectiveTarget = options.target !== undefined ? options.target : elementTarget;
        if (!options.disabled &&
            !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) &&
            !e.defaultPrevented &&
            (!effectiveTarget || effectiveTarget === '_self') &&
            e.button === 0) {
            // Don't prevent default or handle navigation if reloadDocument is true
            if (options.reloadDocument) {
                return;
            }
            e.preventDefault();
            // All is well? Navigate!
            router.navigate({
                ...options,
                replace: options.replace,
                resetScroll: options.resetScroll,
                hashScrollIntoView: options.hashScrollIntoView,
                startTransition: options.startTransition,
                viewTransition: options.viewTransition,
                ignoreBlocker: options.ignoreBlocker,
            });
        }
    };
    const handleTouchStart = () => {
        if (preload.value === 'intent') {
            doPreload();
        }
    };
    const handleLeave = () => {
        if (pendingPreload === 'intent') {
            clearTimeout(timeoutMap.get(ref));
            timeoutMap.delete(ref);
            pendingPreload = undefined;
        }
    };
    function composeEventHandlers(getUserHandler, handler) {
        return (event) => {
            getUserHandler()?.(event);
            handler(event);
        };
    }
    // Get the active and inactive props
    const resolvedStyleProps = Vue.computed(() => {
        const options = getOptions();
        return resolveStyleProps(options, isActive.value);
    });
    const href = Vue.computed(() => {
        const options = getOptions();
        return getHref(options, router, next.value);
    });
    // Create static event handlers that don't change between renders
    const staticEventHandlers = {
        onClick: composeEventHandlers(() => getOptions().onClick, handleClick),
        onBlur: composeEventHandlers(() => getOptions().onBlur, handleLeave),
        onFocus: composeEventHandlers(() => getOptions().onFocus, enqueuePreload),
        onMouseenter: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseenter, enqueuePreload),
        onMouseover: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseover, enqueuePreload),
        onMouseleave: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseleave, handleLeave),
        onMouseout: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseout, handleLeave),
        onTouchstart: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onTouchstart, handleTouchStart),
    };
    // Compute all props synchronously to avoid hydration mismatches
    // Using Vue.computed ensures props are calculated at render time, not after
    const computedProps = Vue.computed(() => {
        const options = getOptions();
        if (type.value === 'external') {
            return getExternalLinkProps(options, router, ref, staticEventHandlers);
        }
        const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, } = resolvedStyleProps.value;
        return combineResultProps({
            href: href.value,
            options,
            ref,
            staticEventHandlers,
            isActive: isActive.value,
            resolvedActiveProps,
            resolvedInactiveProps,
            resolvedClassName,
            resolvedStyle,
        });
    });
    // Return the computed ref itself - callers should access .value
    return computedProps;
}
function resolveStyleProps(options, isActive) {
    const activeProps = options.activeProps || (() => ({ class: 'active' }));
    const resolvedActiveProps = (isActive
        ? typeof activeProps === 'function'
            ? activeProps()
            : activeProps
        : {}) || { class: undefined, style: undefined };
    const inactiveProps = options.inactiveProps || (() => ({}));
    const resolvedInactiveProps = (isActive
        ? {}
        : typeof inactiveProps === 'function'
            ? inactiveProps()
            : inactiveProps) || { class: undefined, style: undefined };
    const classes = [
        options.class,
        resolvedActiveProps?.class,
        resolvedInactiveProps?.class,
    ].filter(Boolean);
    const resolvedClassName = classes.length ? classes.join(' ') : undefined;
    const result = {};
    // Merge styles from all sources
    if (options.style) {
        Object.assign(result, options.style);
    }
    if (resolvedActiveProps?.style) {
        Object.assign(result, resolvedActiveProps.style);
    }
    if (resolvedInactiveProps?.style) {
        Object.assign(result, resolvedInactiveProps.style);
    }
    const resolvedStyle = hasKeys(result) ? result : undefined;
    return {
        resolvedActiveProps,
        resolvedInactiveProps,
        resolvedClassName,
        resolvedStyle,
    };
}
function combineResultProps({ href, options, isActive, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, ref, staticEventHandlers, }) {
    const result = {
        ...getPropsSafeToSpread(options),
        ref,
        ...staticEventHandlers,
        href,
        disabled: options._asChild ? !!options.disabled : undefined,
        target: options.target,
    };
    if (resolvedStyle) {
        result.style = resolvedStyle;
    }
    if (resolvedClassName) {
        result.class = resolvedClassName;
    }
    if (options.disabled) {
        result.role = 'link';
        result['aria-disabled'] = true;
    }
    if (isActive) {
        result['data-status'] = 'active';
        result['aria-current'] = 'page';
    }
    for (const key of Object.keys(resolvedActiveProps)) {
        if (key !== 'class' && key !== 'style') {
            result[key] = resolvedActiveProps[key];
        }
    }
    for (const key of Object.keys(resolvedInactiveProps)) {
        if (key !== 'class' && key !== 'style') {
            result[key] = resolvedInactiveProps[key];
        }
    }
    return result;
}
function getExternalLinkProps(options, router, ref, staticEventHandlers) {
    const dangerous = isDangerousProtocol(options.to, router.protocolAllowlist);
    if (dangerous && process.env.NODE_ENV !== 'production') {
        console.warn(`Blocked Link with dangerous protocol: ${options.to}`);
    }
    const eventHandlers = getLinkEventHandlers(options);
    const result = {
        ...getPropsSafeToSpread(options),
        ref,
        href: dangerous || options.disabled ? undefined : options.to,
        target: options.target,
        disabled: options._asChild ? !!options.disabled : undefined,
        style: options.style,
        class: options.class,
        onClick: staticEventHandlers?.onClick ?? options.onClick,
        onBlur: staticEventHandlers?.onBlur ?? options.onBlur,
        onFocus: staticEventHandlers?.onFocus ?? options.onFocus,
        onMouseenter: staticEventHandlers?.onMouseenter ?? eventHandlers.onMouseenter,
        onMouseleave: staticEventHandlers?.onMouseleave ?? eventHandlers.onMouseleave,
        onMouseover: staticEventHandlers?.onMouseover ?? eventHandlers.onMouseover,
        onMouseout: staticEventHandlers?.onMouseout ?? eventHandlers.onMouseout,
        onTouchstart: staticEventHandlers?.onTouchstart ?? eventHandlers.onTouchstart,
    };
    if (options.disabled) {
        result.role = 'link';
        result['aria-disabled'] = true;
    }
    for (const key of Object.keys(result)) {
        if (result[key] === undefined) {
            delete result[key];
        }
    }
    return result;
}
function getLinkEventHandlers(options) {
    return {
        onMouseenter: options.onMouseEnter ?? options.onMouseenter,
        onMouseleave: options.onMouseLeave ?? options.onMouseleave,
        onMouseover: options.onMouseOver ?? options.onMouseover,
        onMouseout: options.onMouseOut ?? options.onMouseout,
        onTouchstart: options.onTouchStart ?? options.onTouchstart,
    };
}
const getPropsSafeToSpread = (options) => {
    const { activeProps: _activeProps, inactiveProps: _inactiveProps, activeOptions: _activeOptions, to: _to, preload: _preload, preloadDelay: _preloadDelay, preloadIntentProximity: _preloadIntentProximity, hashScrollIntoView: _hashScrollIntoView, replace: _replace, startTransition: _startTransition, resetScroll: _resetScroll, viewTransition: _viewTransition, children: _children, target: _target, disabled: _disabled, style: _style, class: _class, onClick: _onClick, onBlur: _onBlur, onFocus: _onFocus, onMouseEnter: _onMouseEnter, onMouseenter: _onMouseenter, onMouseLeave: _onMouseLeave, onMouseleave: _onMouseleave, onMouseOver: _onMouseOver, onMouseover: _onMouseover, onMouseOut: _onMouseOut, onMouseout: _onMouseout, onTouchStart: _onTouchStart, onTouchstart: _onTouchstart, ignoreBlocker: _ignoreBlocker, params: _params, search: _search, hash: _hash, state: _state, mask: _mask, reloadDocument: _reloadDocument, unsafeRelative: _unsafeRelative, _asChild: __asChild, from: _from, additionalProps: _additionalProps, ...propsSafeToSpread } = options;
    return propsSafeToSpread;
};
function getIsActive(loc, nextLoc, activeOptions, router) {
    if (activeOptions?.exact) {
        const testExact = exactPathTest(loc.pathname, nextLoc.pathname, router.basepath);
        if (!testExact) {
            return false;
        }
    }
    else {
        const currentPath = removeTrailingSlash(loc.pathname, router.basepath);
        const nextPath = removeTrailingSlash(nextLoc.pathname, router.basepath);
        const pathIsFuzzyEqual = currentPath.startsWith(nextPath) &&
            (currentPath.length === nextPath.length ||
                currentPath[nextPath.length] === '/');
        if (!pathIsFuzzyEqual) {
            return false;
        }
    }
    if (activeOptions?.includeSearch ?? true) {
        const searchTest = deepEqual(loc.search, nextLoc.search, {
            partial: !activeOptions?.exact,
            ignoreUndefined: !activeOptions?.explicitUndefined,
        });
        if (!searchTest) {
            return false;
        }
    }
    if (activeOptions?.includeHash) {
        return loc.hash === nextLoc.hash;
    }
    return true;
}
function getHref(options, router, nextLocation) {
    if (options.disabled) {
        return undefined;
    }
    const location = nextLocation?.maskedLocation ?? nextLocation;
    // Use publicHref - it contains the correct href for display
    // When a rewrite changes the origin, publicHref is the full URL
    // Otherwise it's the origin-stripped path
    // This avoids constructing URL objects in the hot path
    const publicHref = location?.publicHref;
    if (!publicHref)
        return undefined;
    const external = location?.external;
    if (external)
        return publicHref;
    return router.history.createHref(publicHref) || '/';
}
export function createLink(Comp) {
    return Vue.defineComponent({
        name: 'CreatedLink',
        inheritAttrs: false,
        setup(_, { attrs, slots }) {
            return () => Vue.h(LinkImpl, { ...attrs, _asChild: Comp }, slots);
        },
    });
}
const LinkImpl = Vue.defineComponent({
    name: 'Link',
    inheritAttrs: false,
    props: [
        '_asChild',
        'to',
        'preload',
        'preloadDelay',
        'preloadIntentProximity',
        'activeProps',
        'inactiveProps',
        'activeOptions',
        'from',
        'search',
        'params',
        'hash',
        'state',
        'mask',
        'reloadDocument',
        'disabled',
        'additionalProps',
        'viewTransition',
        'resetScroll',
        'startTransition',
        'hashScrollIntoView',
        'replace',
        'ignoreBlocker',
        'target',
    ],
    setup(props, { attrs, slots }) {
        const attrsSnapshot = Vue.shallowRef({ ...attrs });
        Vue.onBeforeUpdate(() => {
            const keys = Object.keys(attrs);
            const previous = attrsSnapshot.value;
            if (keys.length !== Object.keys(previous).length ||
                keys.some((key) => !Object.is(attrs[key], previous[key]))) {
                attrsSnapshot.value = { ...attrs };
            }
        });
        // Keep a plain cached snapshot so location-only updates do not repeatedly
        // cross Vue's props and attrs proxies for every link computation.
        const allProps = Vue.computed(() => ({
            ...props,
            ...attrsSnapshot.value,
        }));
        const linkPropsSource = useLinkPropsImpl(() => allProps.value);
        return () => {
            const Component = props._asChild || 'a';
            const linkProps = Vue.unref(linkPropsSource);
            const isActive = linkProps['data-status'] === 'active';
            // Create the slot content or empty array if no default slot
            const slotContent = slots.default ? slots.default({ isActive }) : [];
            // Special handling for SVG links - wrap an <a> inside the SVG
            if (Component === 'svg') {
                // Create props without class for svg link
                const svgLinkProps = { ...linkProps };
                delete svgLinkProps.class;
                return Vue.h('svg', {}, [Vue.h('a', svgLinkProps, slotContent)]);
            }
            // For custom functional components (non-string), pass children as a prop
            // since they may expect children as a prop like in Solid
            if (typeof Component !== 'string') {
                return Vue.h(Component, { ...linkProps, children: slotContent }, slotContent);
            }
            // Return the component with props and children
            return Vue.h(Component, linkProps, slotContent);
        };
    },
});
/**
 * Link component with proper TypeScript generics support
 */
export const Link = LinkImpl;
export const linkOptions = (options) => {
    return options;
};
//# sourceMappingURL=link.jsx.map