import * as Solid from 'solid-js';
import { deepEqual, exactPathTest, functionalUpdate, hasKeys, isDangerousProtocol, preloadWarning, removeTrailingSlash, } from '@tanstack/router-core';
import { isServer } from '@tanstack/router-core/isServer';
import { Dynamic } from '@solidjs/web';
import { useRouter } from './useRouter';
import { useIntersectionObserver } from './utils';
import { useHydrated } from './ClientOnly';
function mergeRefs(...refs) {
    const setRef = (ref, el) => {
        if (typeof ref === 'function') {
            ref(el);
        }
        else if (Array.isArray(ref)) {
            for (const nestedRef of ref) {
                setRef(nestedRef, el);
            }
        }
    };
    return (el) => {
        for (const ref of refs) {
            setRef(ref, el);
        }
    };
}
function splitProps(props, keys) {
    const _local = {};
    const _rest = {};
    // A safe way to polyfill splitProps if native getter copy is too complex
    // is just to return [props, Solid.omit(props, keys)] but it modifies typing.
    // Actually, Solid.omit exists!
    // Note: Solid.omit uses rest params (...keys), so we must spread the array.
    return [props, Solid.omit(props, ...keys)];
}
const timeoutMap = new WeakMap();
const cancelPreload = (eventTarget) => {
    clearTimeout(timeoutMap.get(eventTarget));
    timeoutMap.delete(eventTarget);
};
export function useLinkProps(options) {
    const router = useRouter();
    const shouldHydrateHash = !isServer && !!router.options.ssr;
    const hasHydrated = useHydrated();
    let hasRenderFetched = false;
    // Defaults are resolved through accessors at the use sites instead of
    // merging them into the props. Every merge/omit proxy layered here gets
    // re-enumerated by spread() on each navigation, and V8 dispatches proxy
    // traps in native runtime code — keeping this path proxy-free is what
    // keeps Link updates cheap.
    const local = options;
    const activeProps = () => local.activeProps ?? STATIC_ACTIVE_PROPS_GET;
    const inactiveProps = () => local.inactiveProps ?? STATIC_INACTIVE_PROPS_GET;
    const propsSafeToSpread = Solid.omit(options, 'activeProps', 'inactiveProps', 'activeOptions', 'to', 'preload', 'preloadDelay', 'preloadIntentProximity', 'hashScrollIntoView', 'replace', 'startTransition', 'resetScroll', 'viewTransition', 'target', 'disabled', 'style', 'class', 'onClick', 'onBlur', 'onFocus', 'onMouseEnter', 'onMouseLeave', 'onMouseOver', 'onMouseOut', 'onTouchStart', 'ignoreBlocker', 'params', 'search', 'hash', 'state', 'mask', 'reloadDocument', 'unsafeRelative', 'from');
    const currentLocation = Solid.createMemo(() => router.stores.location.get(), {
        equals: (prev, next) => prev.href === next.href,
    });
    const _options = () => options;
    const next = Solid.createMemo(() => {
        // Rebuild when inherited search/hash or the current route context changes.
        const _fromLocation = currentLocation();
        const options = { _fromLocation, ..._options() };
        // untrack because router-core will also access stores, which are signals in solid
        return Solid.untrack(() => router.buildLocation(options));
    }, {
        lazy: true,
        // Navigations usually leave most links' built locations unchanged;
        // comparing hrefs lets downstream memos (href, isActive) skip work.
        equals: (prev, next) => prev.href === next.href &&
            prev.external === next.external &&
            prev.maskedLocation?.href === next.maskedLocation?.href,
    });
    const hrefOption = Solid.createMemo(() => {
        if (_options().disabled)
            return undefined;
        // 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 location = next().maskedLocation ?? next();
        const publicHref = location.publicHref;
        const external = location.external;
        if (external) {
            return { href: publicHref, external: true };
        }
        return {
            href: router.history.createHref(publicHref) || '/',
            external: false,
        };
    }, { lazy: true });
    const externalLink = Solid.createMemo(() => {
        const _href = hrefOption();
        if (_href?.external) {
            // Block dangerous protocols for external links
            if (isDangerousProtocol(_href.href, router.protocolAllowlist)) {
                if (process.env.NODE_ENV !== 'production') {
                    console.warn(`Blocked Link with dangerous protocol: ${_href.href}`);
                }
                return undefined;
            }
            return _href.href;
        }
        const to = _options().to;
        const safeInternal = isSafeInternal(to);
        if (safeInternal)
            return undefined;
        if (typeof to !== 'string' || to.indexOf(':') === -1)
            return undefined;
        try {
            new URL(to);
            // Block dangerous protocols like javascript:, blob:, data:
            if (isDangerousProtocol(to, router.protocolAllowlist)) {
                if (process.env.NODE_ENV !== 'production') {
                    console.warn(`Blocked Link with dangerous protocol: ${to}`);
                }
                return undefined;
            }
            return to;
        }
        catch { }
        return undefined;
    }, { lazy: true });
    const preload = Solid.createMemo(() => {
        if (_options().reloadDocument || externalLink() || local.disabled) {
            return false;
        }
        return local.preload ?? router.options.defaultPreload;
    }, { lazy: true });
    const preloadDelay = () => local.preloadDelay ?? router.options.defaultPreloadDelay ?? 0;
    const isActive = Solid.createMemo(() => {
        if (externalLink())
            return false;
        const activeOptions = local.activeOptions;
        const current = currentLocation();
        const nextLocation = next();
        if (activeOptions?.exact) {
            const testExact = exactPathTest(current.pathname, nextLocation.pathname, router.basepath);
            if (!testExact) {
                return false;
            }
        }
        else {
            const currentPath = removeTrailingSlash(current.pathname, router.basepath);
            const nextPath = removeTrailingSlash(nextLocation.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(current.search, nextLocation.search, {
                partial: !activeOptions?.exact,
                ignoreUndefined: !activeOptions?.explicitUndefined,
            });
            if (!searchTest) {
                return false;
            }
        }
        if (activeOptions?.includeHash) {
            const currentHash = shouldHydrateHash && !hasHydrated() ? '' : current.hash;
            return currentHash === nextLocation.hash;
        }
        return true;
    }, { lazy: true });
    const doPreload = () => router
        .preloadRoute({ ...options, _builtLocation: next() })
        .catch((err) => {
        console.warn(err);
        console.warn(preloadWarning);
    });
    const [ref, setRefSignal] = Solid.createSignal(null);
    const setRef = (el) => {
        Solid.runWithOwner(null, () => {
            setRefSignal(el);
        });
    };
    const enqueuePreload = (e) => {
        if (!e) {
            cancelPreload(ref);
            return;
        }
        if (!(e.isIntersecting ??
            preload() === 'intent')) {
            if (e.isIntersecting === false) {
                cancelPreload(ref);
            }
            return;
        }
        if (!preloadDelay()) {
            doPreload();
            return;
        }
        if (!timeoutMap.has(ref)) {
            timeoutMap.set(ref, setTimeout(() => {
                timeoutMap.delete(ref);
                doPreload();
            }, preloadDelay()));
        }
    };
    useIntersectionObserver(ref, enqueuePreload, () => preload() !== 'viewport');
    Solid.createEffect(preload, (preloadValue) => {
        if (hasRenderFetched) {
            return;
        }
        if (preloadValue === 'render') {
            Solid.untrack(() => doPreload());
            hasRenderFetched = true;
        }
    });
    if (Solid.untrack(externalLink)) {
        const externalHref = Solid.untrack(externalLink);
        return Solid.merge(propsSafeToSpread, {
            ref: mergeRefs(setRef, options.ref),
            href: externalHref,
        }, splitProps(local, [
            'target',
            'disabled',
            'style',
            'class',
            'onClick',
            'onBlur',
            'onFocus',
            'onMouseEnter',
            'onMouseLeave',
            'onMouseOut',
            'onMouseOver',
            'onTouchStart',
        ])[0]);
    }
    // The click handler
    const handleClick = (e) => {
        // Check actual element's target attribute as fallback
        const elementTarget = e.currentTarget.getAttribute('target');
        const effectiveTarget = local.target !== undefined ? local.target : elementTarget;
        if (!local.disabled &&
            !isCtrlEvent(e) &&
            !e.defaultPrevented &&
            (!effectiveTarget || effectiveTarget === '_self') &&
            e.button === 0) {
            e.preventDefault();
            // All is well? Navigate!
            // N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
            router.navigate({
                ...options,
                replace: local.replace,
                resetScroll: local.resetScroll,
                hashScrollIntoView: local.hashScrollIntoView,
                startTransition: local.startTransition,
                viewTransition: local.viewTransition,
                ignoreBlocker: local.ignoreBlocker,
            });
        }
    };
    const handleTouchStart = () => {
        if (preload() !== 'intent')
            return;
        doPreload();
    };
    const handleLeave = () => {
        if (preload() === 'intent') {
            cancelPreload(ref);
        }
    };
    const simpleStyling = Solid.createMemo(() => activeProps() === STATIC_ACTIVE_PROPS_GET &&
        inactiveProps() === STATIC_INACTIVE_PROPS_GET &&
        local.class === undefined &&
        local.style === undefined, { lazy: true });
    const onClick = createComposedHandler(() => local.onClick, handleClick);
    const onBlur = createComposedHandler(() => local.onBlur, handleLeave);
    const onFocus = createComposedHandler(() => local.onFocus, enqueuePreload);
    const onMouseEnter = createComposedHandler(() => local.onMouseEnter, enqueuePreload);
    const onMouseOver = createComposedHandler(() => local.onMouseOver, enqueuePreload);
    const onMouseLeave = createComposedHandler(() => local.onMouseLeave, handleLeave);
    const onMouseOut = createComposedHandler(() => local.onMouseOut, handleLeave);
    const onTouchStart = createComposedHandler(() => local.onTouchStart, handleTouchStart);
    const resolvedStateProps = Solid.createMemo(() => (isActive()
        ? functionalUpdate(activeProps(), {})
        : functionalUpdate(inactiveProps(), {})) ?? EMPTY_OBJECT, { lazy: true });
    const resolvedClass = Solid.createMemo(() => {
        if (simpleStyling())
            return isActive() ? 'active' : undefined;
        return ([local.class, resolvedStateProps().class].filter(Boolean).join(' ') ||
            undefined);
    }, { lazy: true });
    const resolvedStyle = Solid.createMemo(() => {
        if (simpleStyling())
            return local.style;
        const style = { ...local.style, ...resolvedStateProps().style };
        return hasKeys(style) ? style : undefined;
    }, { lazy: true });
    // The returned object must be a plain object with a stable key set so the
    // consuming spread() never enumerates through proxy traps. Reactivity lives
    // in the property getters; values that no longer apply resolve to undefined,
    // which spread()/assign() treats as attribute removal. Keys returned by
    // activeProps/inactiveProps are discovered once at setup.
    const extraStateKeys = new Set();
    Solid.untrack(() => {
        for (const stateProps of [
            functionalUpdate(activeProps(), {}),
            functionalUpdate(inactiveProps(), {}),
        ]) {
            if (stateProps) {
                for (const key of Object.keys(stateProps)) {
                    if (key !== 'class' && key !== 'style')
                        extraStateKeys.add(key);
                }
            }
        }
    });
    const composedRef = mergeRefs(setRef, (el) => {
        const r = _options().ref;
        if (typeof r === 'function')
            r(el);
    });
    const linkProps = {};
    for (const key of Object.keys(propsSafeToSpread)) {
        Object.defineProperty(linkProps, key, Object.getOwnPropertyDescriptor(propsSafeToSpread, key));
    }
    for (const key of extraStateKeys) {
        Object.defineProperty(linkProps, key, {
            get: () => resolvedStateProps()[key],
            enumerable: true,
            configurable: true,
        });
    }
    const defineGetters = (getters) => {
        for (const key of Object.keys(getters)) {
            Object.defineProperty(linkProps, key, {
                get: getters[key],
                enumerable: true,
                configurable: true,
            });
        }
    };
    linkProps.ref = composedRef;
    linkProps.onClick = onClick;
    linkProps.onBlur = onBlur;
    linkProps.onFocus = onFocus;
    linkProps.onMouseEnter = onMouseEnter;
    linkProps.onMouseOver = onMouseOver;
    linkProps.onMouseLeave = onMouseLeave;
    linkProps.onMouseOut = onMouseOut;
    linkProps.onTouchStart = onTouchStart;
    defineGetters({
        href: () => hrefOption()?.href,
        disabled: () => !!local.disabled,
        target: () => local.target,
        role: () => (local.disabled ? 'link' : undefined),
        'aria-disabled': () => (local.disabled ? 'true' : undefined),
        'data-status': () => (isActive() ? 'active' : undefined),
        'aria-current': () => (isActive() ? 'page' : undefined),
        class: resolvedClass,
        style: resolvedStyle,
    });
    return linkProps;
}
const STATIC_ACTIVE_PROPS = { class: 'active' };
const STATIC_ACTIVE_PROPS_GET = () => STATIC_ACTIVE_PROPS;
const EMPTY_OBJECT = {};
const STATIC_INACTIVE_PROPS_GET = () => EMPTY_OBJECT;
/** Call a JSX.EventHandlerUnion with the event. */
function callHandler(event, handler) {
    if (typeof handler === 'function') {
        handler(event);
    }
    else {
        handler[0](handler[1], event);
    }
    return event.defaultPrevented;
}
function createComposedHandler(getHandler, fallback) {
    return (event) => {
        const handler = getHandler();
        if (!handler || !callHandler(event, handler))
            fallback(event);
    };
}
export function createLink(Comp) {
    return (props) => <Link {...props} _asChild={Comp}/>;
}
export const Link = (props) => {
    const [local, rest] = splitProps(props, [
        '_asChild',
        'children',
    ]);
    const [_, linkProps] = splitProps(useLinkProps(rest), [
        'type',
    ]);
    // Resolve children once using Solid.children to avoid
    // re-accessing the children getter (which in Solid 2.0 would
    // re-invoke createComponent each time for JSX children).
    const resolvedChildren = Solid.children(() => local.children);
    const children = () => {
        const ch = resolvedChildren();
        if (typeof ch === 'function') {
            return ch({
                get isActive() {
                    return linkProps['data-status'] === 'active';
                },
            });
        }
        return ch;
    };
    if (local._asChild === 'svg') {
        const [_, svgLinkProps] = splitProps(linkProps, ['class']);
        return (<svg>
        <a {...svgLinkProps}>{children()}</a>
      </svg>);
    }
    if (!local._asChild) {
        return <a {...linkProps}>{children()}</a>;
    }
    return (<Dynamic component={local._asChild} {...linkProps}>
      {children()}
    </Dynamic>);
};
function isCtrlEvent(e) {
    return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
}
function isSafeInternal(to) {
    if (typeof to !== 'string')
        return false;
    const zero = to.charCodeAt(0);
    if (zero === 47)
        return to.charCodeAt(1) !== 47; // '/' but not '//'
    return zero === 46; // '.', '..', './', '../'
}
export const linkOptions = (options) => {
    return options;
};
//# sourceMappingURL=link.jsx.map