import * as Solid from 'solid-js';
import { _isRouteDeparting, deepEqual, functionalUpdate, getUrlScheme, hasKeys, isDangerousProtocol, preloadWarning, removeTrailingSlash, } from '@tanstack/router-core';
import { isServer } from '@tanstack/router-core/isServer';
import { Dynamic } from 'solid-js/web';
import { useRouter } from './useRouter';
import { nearestMatchContext } from './matchContext';
import { useIntersectionObserver } from './utils';
import { useHydrated } from './ClientOnly';
export function useLinkProps(options) {
    return createLinkProps(options, LINK_OPTION_KEYS);
}
// `ownKeys` are the options that never reach the element.
function createLinkProps(options, ownKeys) {
    const router = useRouter();
    // Options are read directly: one props proxy, no merged-default getters.
    const [, propsSafeToSpread] = Solid.splitProps(options, ownKeys);
    // A live match never changes route, so read it once without tracking.
    const routeId = Solid.useContext(nearestMatchContext)[0 /* routeId */]();
    // While a publishing navigation leaves the Link's route, keep the location
    // the route still presents so the Link does no work. The route unmounts
    // when that navigation commits, and any other outcome publishes a newer
    // location. Compare by identity: a same-href navigation can change state.
    // The server renders one location: no reactivity there.
    const currentLocation = (isServer ?? router.isServer)
        ? () => router.stores.location.get()
        : Solid.createMemo((prev) => {
            const location = router.stores.location.get();
            return prev && _isRouteDeparting(router, routeId, location)
                ? prev
                : location;
        });
    // The Link's own destination options, replaced only when a destination prop
    // changes: the router reuses a location built from the same object without
    // reading the current location. A Solid store changes in place, so a store
    // prop value (checked one level deep, not nested) gets a fresh object per
    // location instead, re-reading the store at each navigation as before. A
    // mask holds destination options of its own, so its values are checked too.
    const getDest = () => {
        const dest = {};
        for (const key of NAVIGATION_KEYS) {
            const value = (dest[key] = options[key]);
            if (isStore(value) ||
                (key === 'mask' && value && Object.values(value).some(isStore))) {
                currentLocation();
            }
        }
        return dest;
    };
    const dest = (isServer ?? router.isServer) ? getDest : Solid.createMemo(getDest);
    // Only a hydrating client renders the server's hash-less active state first.
    const hasHydrated = !(isServer ?? router.isServer) && router.options.ssr
        ? useHydrated()
        : undefined;
    // Everything the Link derives from its destination and the location, in
    // one computation: [element href, external href (null when blocked),
    // active]. An unchanged result keeps its identity, so a navigation that
    // leaves the Link as it was notifies nothing downstream.
    const getLinkState = (prev) => {
        const location = currentLocation();
        const to = options.to;
        const disabled = options.disabled;
        const scheme = typeof to === 'string' && getUrlScheme(to);
        let href;
        let external;
        let active = false;
        if (scheme) {
            if (router.protocolAllowlist.has(scheme)) {
                external = to;
            }
            else {
                if (process.env.NODE_ENV !== 'production') {
                    console.warn(`Blocked Link with dangerous protocol: ${to}`);
                }
                external = null;
            }
        }
        else {
            const options_ = dest();
            // Clicks and preloads go where the href points.
            options_._fromLocation = location;
            // untrack because router-core will also access stores, which are signals in solid
            const next = Solid.untrack(() => router.buildLocation(options_));
            if (!disabled) {
                // 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 shown = next.maskedLocation ?? next;
                const publicHref = shown.publicHref;
                const shownHref = shown.external
                    ? publicHref
                    : router.history.createHref(publicHref) || '/';
                if ((shown.external || shownHref !== publicHref) &&
                    isDangerousProtocol(shownHref, router.protocolAllowlist)) {
                    if (process.env.NODE_ENV !== 'production') {
                        console.warn(`Blocked Link with dangerous protocol: ${shownHref}`);
                    }
                }
                else {
                    href = shownHref;
                }
            }
            external =
                !href && !disabled
                    ? null
                    : href && getUrlScheme(href)
                        ? href
                        : undefined;
            if (external === undefined) {
                const activeOptions = options.activeOptions;
                const currentPath = removeTrailingSlash(location.pathname, router.basepath);
                const nextPath = removeTrailingSlash(next.pathname, router.basepath);
                // Both modes compare normalized paths; fuzzy matches need a segment boundary.
                active =
                    (activeOptions?.exact
                        ? currentPath === nextPath
                        : currentPath.startsWith(nextPath) &&
                            (currentPath.length === nextPath.length ||
                                currentPath[nextPath.length] === '/')) &&
                        (!(activeOptions?.includeSearch ?? true) ||
                            deepEqual(location.search, next.search, !activeOptions?.exact, activeOptions?.explicitUndefined)) &&
                        (!activeOptions?.includeHash ||
                            (hasHydrated && !hasHydrated() ? '' : location.hash) === next.hash);
            }
        }
        href = external === null ? undefined : external || href;
        return prev &&
            prev[0 /* href */] === href &&
            prev[1 /* external */] === external &&
            prev[2 /* active */] === active
            ? prev
            : [href, external, active];
    };
    const linkState = (isServer ?? router.isServer)
        ? getLinkState
        : Solid.createMemo(getLinkState);
    const resolveLinkStateProps = ([href, external, active], base) => {
        const disabled = options.disabled || external === null;
        base.href = href;
        base.disabled = disabled;
        base.target = options.target;
        if (disabled) {
            Object.assign(base, STATIC_DISABLED_PROPS);
        }
        if (options.activeProps === undefined &&
            options.inactiveProps === undefined &&
            options.class === undefined &&
            options.style === undefined) {
            return active
                ? Object.assign(base, STATIC_DEFAULT_ACTIVE_ATTRIBUTES)
                : base;
        }
        const stateProps = functionalUpdate((active ? options.activeProps : options.inactiveProps) ??
            (active ? STATIC_ACTIVE_PROPS : EMPTY_OBJECT), {}) ?? EMPTY_OBJECT;
        const baseStyle = options.style;
        const stateStyle = stateProps.style;
        // Snapshot reactive style properties so in-place updates remain observable.
        const style = baseStyle || stateStyle ? { ...baseStyle, ...stateStyle } : undefined;
        const baseClass = options.class;
        const stateClass = stateProps.class;
        const className = baseClass
            ? stateClass
                ? `${baseClass} ${stateClass}`
                : baseClass
            : stateClass;
        return {
            ...base,
            ...stateProps,
            // State props can override element props, but not routing options.
            href,
            disabled,
            target: base.target,
            ...(style && hasKeys(style) && { style }),
            ...(className && { class: className }),
            ...(active && STATIC_ACTIVE_ATTRIBUTES),
        };
    };
    // Keep the guard inline so browser builds can drop the server return.
    if (isServer ?? router.isServer) {
        const props = resolveLinkStateProps(linkState(), {
            onClick: options.onClick,
            onBlur: options.onBlur,
            onFocus: options.onFocus,
            onMouseEnter: options.onMouseEnter,
            onMouseLeave: options.onMouseLeave,
            onMouseOut: options.onMouseOut,
            onMouseOver: options.onMouseOver,
            onTouchStart: options.onTouchStart,
            ref: options.ref,
        });
        // Avoid creating merged-prop getters for absent server event handlers.
        for (const key of STATIC_EVENT_PROPS) {
            if (props[key] === undefined) {
                delete props[key];
            }
        }
        return Solid.mergeProps(propsSafeToSpread, props);
    }
    let hasRenderFetched = false;
    const preload = () => !options.reloadDocument &&
        linkState()[1 /* external */] === undefined &&
        !options.disabled &&
        (options.preload ?? router.options.defaultPreload);
    const preloadDelay = () => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0;
    const doPreload = () => router.preloadRoute(dest()).catch((err) => {
        console.warn(err);
        console.warn(preloadWarning);
    });
    const [ref, setRef] = Solid.createSignal(null);
    let preloadTimeout;
    const cancelPreload = () => {
        clearTimeout(preloadTimeout);
        preloadTimeout = undefined;
    };
    const enqueuePreload = (e) => {
        if (!e) {
            cancelPreload();
            return;
        }
        if (!(e.isIntersecting ??
            preload() === 'intent')) {
            if (e.isIntersecting === false) {
                cancelPreload();
            }
            return;
        }
        if (!preloadDelay()) {
            doPreload();
            return;
        }
        preloadTimeout ?? (preloadTimeout = setTimeout(() => {
            preloadTimeout = undefined;
            doPreload();
        }, preloadDelay()));
    };
    useIntersectionObserver(ref, enqueuePreload, preload);
    Solid.createEffect(() => {
        if (hasRenderFetched) {
            return;
        }
        if (preload() === 'render') {
            doPreload();
            hasRenderFetched = true;
        }
    });
    // 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 &&
            linkState()[1 /* external */] === undefined &&
            !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) &&
            !e.defaultPrevented &&
            (!effectiveTarget || effectiveTarget === '_self') &&
            e.button === 0) {
            e.preventDefault();
            cancelPreload();
            // All is well? Navigate!
            // N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
            router.navigate(dest());
        }
    };
    const handleTouchStart = () => {
        if (preload() !== 'intent')
            return;
        doPreload();
    };
    const handleLeave = () => {
        if (preload() === 'intent') {
            cancelPreload();
        }
    };
    const onClick = createComposedHandler(() => options.onClick, handleClick);
    const onBlur = createComposedHandler(() => options.onBlur, handleLeave);
    const onFocus = createComposedHandler(() => options.onFocus, enqueuePreload);
    const onMouseEnter = createComposedHandler(() => options.onMouseEnter, enqueuePreload);
    const onMouseOver = createComposedHandler(() => options.onMouseOver, enqueuePreload);
    const onMouseLeave = createComposedHandler(() => options.onMouseLeave, handleLeave);
    const onMouseOut = createComposedHandler(() => options.onMouseOut, handleLeave);
    const onTouchStart = createComposedHandler(() => options.onTouchStart, handleTouchStart);
    // One ref callback per element. The selected state props' ref, else the
    // Link's own, receives the element once; a state change does not re-call it.
    const linkRef = (el) => {
        setRef(el);
        const ref = resolved.ref ?? options.ref;
        if (typeof ref === 'function') {
            ref(el);
        }
    };
    // `mergeProps` memoizes this itself; the ref reads its latest result.
    let resolved;
    const resolvedProps = () => (resolved = resolveLinkStateProps(linkState(), {
        onClick,
        onBlur,
        onFocus,
        onMouseEnter,
        onMouseOver,
        onMouseLeave,
        onMouseOut,
        onTouchStart,
    }));
    // The ref sits after the memo so reading it does not track the memo: the
    // element's ref effect then runs once.
    return Solid.mergeProps(propsSafeToSpread, resolvedProps, {
        ref: linkRef,
    });
}
const isStore = (value) => !!value?.[Solid.$PROXY];
// Props that decide where and how a Link navigates.
const NAVIGATION_KEYS = [
    'to',
    'reloadDocument',
    'replace',
    'resetScroll',
    'hashScrollIntoView',
    'startTransition',
    'viewTransition',
    'ignoreBlocker',
    'params',
    'search',
    'hash',
    'state',
    'mask',
    'unsafeRelative',
    'from',
    'href',
];
// Options the Link consumes; every other option is an element prop.
const LINK_OPTION_KEYS = [
    ...NAVIGATION_KEYS,
    'activeProps',
    'inactiveProps',
    'activeOptions',
    'preload',
    'preloadDelay',
    'preloadIntentProximity',
    'target',
    'disabled',
    'style',
    'class',
    'onClick',
    'onBlur',
    'onFocus',
    'onMouseEnter',
    'onMouseLeave',
    'onMouseOver',
    'onMouseOut',
    'onTouchStart',
];
// A Link also consumes its children and component, and drops `type`.
const LINK_ELEMENT_KEYS = [...LINK_OPTION_KEYS, '_asChild', 'children', 'type'];
const STATIC_EVENT_PROPS = [
    'onClick',
    'onBlur',
    'onFocus',
    'onMouseEnter',
    'onMouseLeave',
    'onMouseOut',
    'onMouseOver',
    'onTouchStart',
];
const STATIC_ACTIVE_PROPS = { class: 'active' };
const EMPTY_OBJECT = {};
const STATIC_DEFAULT_ACTIVE_ATTRIBUTES = {
    class: 'active',
    'data-status': 'active',
    'aria-current': 'page',
};
const STATIC_DISABLED_PROPS = {
    role: 'link',
    'aria-disabled': true,
};
const STATIC_ACTIVE_ATTRIBUTES = {
    'data-status': 'active',
    'aria-current': 'page',
};
/** 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 = props;
    // One props proxy between the element and the Link's props.
    const linkProps = createLinkProps(props, LINK_ELEMENT_KEYS);
    // Element insertion tracks this itself; only a custom component, which may
    // read its children more than once, gets a memo.
    const children = () => {
        const ch = local.children;
        if (typeof ch === 'function') {
            return ch({
                get isActive() {
                    return linkProps['data-status'] === 'active';
                },
            });
        }
        return ch;
    };
    if (local._asChild === 'svg') {
        const [_, svgLinkProps] = Solid.splitProps(linkProps, ['class']);
        return (<svg>
        <a {...svgLinkProps}>{children()}</a>
      </svg>);
    }
    if (!local._asChild) {
        return <a {...linkProps}>{children()}</a>;
    }
    const memo = Solid.createMemo(children);
    return (<Dynamic component={local._asChild} {...linkProps}>
      {memo()}
    </Dynamic>);
};
export const linkOptions = (options) => {
    return options;
};
//# sourceMappingURL=link.jsx.map