import * as Vue from 'vue';
import { deepEqual, exactPathTest, isDangerousProtocol, preloadWarning, removeTrailingSlash, } from '@tanstack/router-core';
import { useRouterState } from './useRouterState';
import { useRouter } from './useRouter';
import { useIntersectionObserver } from './utils';
import { useMatches } from './Matches';
const timeoutMap = new WeakMap();
export function useLinkProps(options) {
    const router = useRouter();
    const isTransitioning = Vue.ref(false);
    let hasRenderFetched = false;
    // 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(() => {
        try {
            new URL(`${options.to}`);
            return 'external';
        }
        catch {
            return 'internal';
        }
    });
    const buildLocationKey = useRouterState({
        select: (s) => {
            const leaf = s.matches[s.matches.length - 1];
            return {
                search: leaf?.search,
                hash: s.location.hash,
                path: leaf?.pathname, // path + params
            };
        },
    });
    // when `from` is not supplied, use the leaf route of the current matches as the `from` location
    const from = useMatches({
        select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
    });
    const _options = Vue.computed(() => ({
        ...options,
        from: from.value,
    }));
    const next = Vue.computed(() => {
        // Depend on search to rebuild when search changes
        buildLocationKey.value;
        return router.buildLocation(_options.value);
    });
    const preload = Vue.computed(() => {
        if (_options.value.reloadDocument) {
            return false;
        }
        return options.preload ?? router.options.defaultPreload;
    });
    const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0);
    const isActive = useRouterState({
        select: (s) => {
            const activeOptions = options.activeOptions;
            if (activeOptions?.exact) {
                const testExact = exactPathTest(s.location.pathname, next.value.pathname, router.basepath);
                if (!testExact) {
                    return false;
                }
            }
            else {
                const currentPathSplit = removeTrailingSlash(s.location.pathname, router.basepath).split('/');
                const nextPathSplit = removeTrailingSlash(next.value?.pathname, router.basepath)?.split('/');
                const pathIsFuzzyEqual = nextPathSplit?.every((d, i) => d === currentPathSplit[i]);
                if (!pathIsFuzzyEqual) {
                    return false;
                }
            }
            if (activeOptions?.includeSearch ?? true) {
                const searchTest = deepEqual(s.location.search, next.value.search, {
                    partial: !activeOptions?.exact,
                    ignoreUndefined: !activeOptions?.explicitUndefined,
                });
                if (!searchTest) {
                    return false;
                }
            }
            if (activeOptions?.includeHash) {
                return s.location.hash === next.value.hash;
            }
            return true;
        },
    });
    const doPreload = () => router.preloadRoute(_options.value).catch((err) => {
        console.warn(err);
        console.warn(preloadWarning);
    });
    const preloadViewportIoCallback = (entry) => {
        if (entry?.isIntersecting) {
            doPreload();
        }
    };
    const ref = Vue.ref(null);
    useIntersectionObserver(ref, preloadViewportIoCallback, { rootMargin: '100px' }, { disabled: () => !!options.disabled || !(preload.value === 'viewport') });
    Vue.effect(() => {
        if (hasRenderFetched) {
            return;
        }
        if (!options.disabled && preload.value === 'render') {
            doPreload();
            hasRenderFetched = true;
        }
    });
    // Create safe props that can be spread
    const getPropsSafeToSpread = () => {
        const result = {};
        const optionRecord = options;
        for (const key in options) {
            if (![
                'activeProps',
                'inactiveProps',
                'activeOptions',
                'to',
                'preload',
                'preloadDelay',
                'hashScrollIntoView',
                'replace',
                'startTransition',
                'resetScroll',
                'viewTransition',
                'children',
                'target',
                'disabled',
                'style',
                'class',
                'onClick',
                'onBlur',
                'onFocus',
                'onMouseEnter',
                'onMouseLeave',
                'onMouseOver',
                'onMouseOut',
                'onTouchStart',
                'ignoreBlocker',
                'params',
                'search',
                'hash',
                'state',
                'mask',
                'reloadDocument',
                '_asChild',
                'from',
                'additionalProps',
            ].includes(key)) {
                result[key] = optionRecord[key];
            }
        }
        return result;
    };
    if (type.value === 'external') {
        // Block dangerous protocols like javascript:, blob:, data:
        if (isDangerousProtocol(options.to, router.protocolAllowlist)) {
            if (process.env.NODE_ENV !== 'production') {
                console.warn(`Blocked Link with dangerous protocol: ${options.to}`);
            }
            // Return props without href to prevent navigation
            const safeProps = {
                ...getPropsSafeToSpread(),
                ref,
                // No href attribute - blocks the dangerous protocol
                target: options.target,
                disabled: options.disabled,
                style: options.style,
                class: options.class,
                onClick: options.onClick,
                onBlur: options.onBlur,
                onFocus: options.onFocus,
                onMouseEnter: options.onMouseEnter,
                onMouseLeave: options.onMouseLeave,
                onMouseOver: options.onMouseOver,
                onMouseOut: options.onMouseOut,
                onTouchStart: options.onTouchStart,
            };
            // Remove undefined values
            Object.keys(safeProps).forEach((key) => {
                if (safeProps[key] === undefined) {
                    delete safeProps[key];
                }
            });
            return Vue.computed(() => safeProps);
        }
        // External links just have simple props
        const externalProps = {
            ...getPropsSafeToSpread(),
            ref,
            href: options.to,
            target: options.target,
            disabled: options.disabled,
            style: options.style,
            class: options.class,
            onClick: options.onClick,
            onBlur: options.onBlur,
            onFocus: options.onFocus,
            onMouseEnter: options.onMouseEnter,
            onMouseLeave: options.onMouseLeave,
            onMouseOver: options.onMouseOver,
            onMouseOut: options.onMouseOut,
            onTouchStart: options.onTouchStart,
        };
        // Remove undefined values
        Object.keys(externalProps).forEach((key) => {
            if (externalProps[key] === undefined) {
                delete externalProps[key];
            }
        });
        return Vue.computed(() => externalProps);
    }
    // The click handler
    const handleClick = (e) => {
        // 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 &&
            !isCtrlEvent(e) &&
            !e.defaultPrevented &&
            (!effectiveTarget || effectiveTarget === '_self') &&
            e.button === 0) {
            // Don't prevent default or handle navigation if reloadDocument is true
            if (_options.value.reloadDocument) {
                return;
            }
            e.preventDefault();
            isTransitioning.value = true;
            const unsub = router.subscribe('onResolved', () => {
                unsub();
                isTransitioning.value = false;
            });
            // All is well? Navigate!
            router.navigate({
                ..._options.value,
                replace: options.replace,
                resetScroll: options.resetScroll,
                hashScrollIntoView: options.hashScrollIntoView,
                startTransition: options.startTransition,
                viewTransition: options.viewTransition,
                ignoreBlocker: options.ignoreBlocker,
            });
        }
    };
    const enqueueIntentPreload = (e) => {
        if (options.disabled || preload.value !== 'intent')
            return;
        if (!preloadDelay.value) {
            doPreload();
            return;
        }
        const eventTarget = e.currentTarget || e.target;
        if (!eventTarget || timeoutMap.has(eventTarget))
            return;
        timeoutMap.set(eventTarget, setTimeout(() => {
            timeoutMap.delete(eventTarget);
            doPreload();
        }, preloadDelay.value));
    };
    const handleTouchStart = (_) => {
        if (options.disabled || preload.value !== 'intent')
            return;
        doPreload();
    };
    const handleLeave = (e) => {
        if (options.disabled)
            return;
        const eventTarget = e.currentTarget || e.target;
        if (eventTarget) {
            const id = timeoutMap.get(eventTarget);
            clearTimeout(id);
            timeoutMap.delete(eventTarget);
        }
    };
    // Helper to compose event handlers - with explicit return type and better type handling
    function composeEventHandlers(handlers) {
        return (event) => {
            for (const handler of handlers) {
                if (handler) {
                    handler(event);
                }
            }
        };
    }
    // Get the active and inactive props
    const resolvedActiveProps = Vue.computed(() => {
        const activeProps = options.activeProps || (() => ({ class: 'active' }));
        const props = isActive.value
            ? typeof activeProps === 'function'
                ? activeProps()
                : activeProps
            : {};
        return props || { class: undefined, style: undefined };
    });
    const resolvedInactiveProps = Vue.computed(() => {
        const inactiveProps = options.inactiveProps || (() => ({}));
        const props = isActive.value
            ? {}
            : typeof inactiveProps === 'function'
                ? inactiveProps()
                : inactiveProps;
        return props || { class: undefined, style: undefined };
    });
    const resolvedClassName = Vue.computed(() => {
        const classes = [
            options.class,
            resolvedActiveProps.value?.class,
            resolvedInactiveProps.value?.class,
        ].filter(Boolean);
        return classes.length ? classes.join(' ') : undefined;
    });
    const resolvedStyle = Vue.computed(() => {
        const result = {};
        // Merge styles from all sources
        if (options.style) {
            Object.assign(result, options.style);
        }
        if (resolvedActiveProps.value?.style) {
            Object.assign(result, resolvedActiveProps.value.style);
        }
        if (resolvedInactiveProps.value?.style) {
            Object.assign(result, resolvedInactiveProps.value.style);
        }
        return Object.keys(result).length > 0 ? result : undefined;
    });
    const href = Vue.computed(() => {
        if (options.disabled) {
            return undefined;
        }
        const nextLocation = next.value;
        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) || '/';
    });
    // Create static event handlers that don't change between renders
    const staticEventHandlers = {
        onClick: composeEventHandlers([
            options.onClick,
            handleClick,
        ]),
        onBlur: composeEventHandlers([
            options.onBlur,
            handleLeave,
        ]),
        onFocus: composeEventHandlers([
            options.onFocus,
            enqueueIntentPreload,
        ]),
        onMouseenter: composeEventHandlers([
            options.onMouseEnter,
            enqueueIntentPreload,
        ]),
        onMouseover: composeEventHandlers([
            options.onMouseOver,
            enqueueIntentPreload,
        ]),
        onMouseleave: composeEventHandlers([
            options.onMouseLeave,
            handleLeave,
        ]),
        onMouseout: composeEventHandlers([
            options.onMouseOut,
            handleLeave,
        ]),
        onTouchstart: composeEventHandlers([
            options.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 result = {
            ...getPropsSafeToSpread(),
            href: href.value,
            ref,
            ...staticEventHandlers,
            disabled: !!options.disabled,
            target: options.target,
        };
        // Add style if present
        if (resolvedStyle.value) {
            result.style = resolvedStyle.value;
        }
        // Add class if present
        if (resolvedClassName.value) {
            result.class = resolvedClassName.value;
        }
        // Add disabled props
        if (options.disabled) {
            result.role = 'link';
            result['aria-disabled'] = true;
        }
        // Add active status
        if (isActive.value) {
            result['data-status'] = 'active';
            result['aria-current'] = 'page';
        }
        // Add transitioning status
        if (isTransitioning.value) {
            result['data-transitioning'] = 'transitioning';
        }
        // Merge active/inactive props (excluding class and style which are handled above)
        const activeP = resolvedActiveProps.value;
        const inactiveP = resolvedInactiveProps.value;
        for (const key of Object.keys(activeP)) {
            if (key !== 'class' && key !== 'style') {
                result[key] = activeP[key];
            }
        }
        for (const key of Object.keys(inactiveP)) {
            if (key !== 'class' && key !== 'style') {
                result[key] = inactiveP[key];
            }
        }
        return result;
    });
    // Return the computed ref itself - callers should access .value
    return computedProps;
}
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',
        'activeProps',
        'inactiveProps',
        'activeOptions',
        'from',
        'search',
        'params',
        'hash',
        'state',
        'mask',
        'reloadDocument',
        'disabled',
        'additionalProps',
        'viewTransition',
        'resetScroll',
        'startTransition',
        'hashScrollIntoView',
        'replace',
        'ignoreBlocker',
        'target',
    ],
    setup(props, { attrs, slots }) {
        // Call useLinkProps ONCE during setup with combined props and attrs
        // The returned object is a computed ref that updates reactively
        const allProps = { ...props, ...attrs };
        const linkPropsComputed = useLinkProps(allProps);
        return () => {
            const Component = props._asChild || 'a';
            // Access the computed value to get fresh props each render
            const linkProps = linkPropsComputed.value;
            const isActive = linkProps['data-status'] === 'active';
            const isTransitioning = linkProps['data-transitioning'] === 'transitioning';
            // Create the slot content or empty array if no default slot
            const slotContent = slots.default
                ? slots.default({
                    isActive,
                    isTransitioning,
                })
                : [];
            // 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;
function isCtrlEvent(e) {
    return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
}
export const linkOptions = (options) => {
    return options;
};
//# sourceMappingURL=link.jsx.map