import { createContext, useContext, createSignal, createEffect, createRenderEffect, onCleanup, Show, getOwner, runWithOwner, JSXElement } from "solid-js";
import { Portal } from "solid-js/web";

export type Placement = "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end";

export interface PositionOptions {
    placement?: Placement;
    offset?: number;
    flip?: boolean;
    shift?: boolean;
    shiftPadding?: number;
}

interface Position {
    top: number;
    left: number;
    placement: string;
}

class FloatingManager {
    activeCloseCallback: (() => void) | null = null;

    register(closeCallback: () => void) {
        if (this.activeCloseCallback && this.activeCloseCallback !== closeCallback) {
            this.activeCloseCallback();
        }
        this.activeCloseCallback = closeCallback;
    }

    unregister(closeCallback: () => void) {
        if (this.activeCloseCallback === closeCallback) {
            this.activeCloseCallback = null;
        }
    }

    closeActive() {
        if (this.activeCloseCallback) {
            this.activeCloseCallback();
            this.activeCloseCallback = null;
        }
    }
}

const floatingManager = new FloatingManager();

// Open floating-content elements in open order, so outside-click handling can tell
// a descendant (opened later, e.g. a menu inside a popover) from an ancestor: a
// floating stays open for clicks inside itself or a later-opened floating, and
// closes for clicks anywhere else (including its parent popover).
const openFloatings: HTMLElement[] = [];

export interface FloatingContextValue {
    isOpen: () => boolean;
    setIsOpen: (open: boolean) => void;
    readonly triggerRef: HTMLElement | undefined;
    setTriggerRef: (el: HTMLElement) => void;
    readonly floatingRef: HTMLElement | undefined;
    setFloatingRef: (el: HTMLElement) => void;
    position: () => Position | null;
    options: Required<PositionOptions>;
    cancelHoverClose: () => void;
    scheduleHoverClose: (delay: number) => void;
}

const FloatingContext = createContext<FloatingContextValue | null>(null);

export function useFloatingContext(): FloatingContextValue {
    const context = useContext(FloatingContext);
    if (!context) {
        throw new Error("Floating components must be used within a FloatingRoot");
    }
    return context;
}

function calculatePosition(triggerRect: DOMRect, floatingRect: DOMRect, options: Required<PositionOptions>): Position {
    const { placement, offset, flip, shift, shiftPadding } = options;
    const parts = placement.split("-");
    const basePlacement = parts[0];
    const alignment = parts[1] || "center";

    let top = 0;
    let left = 0;
    let finalPlacement: string = placement;

    switch (basePlacement) {
        case "top":    top = triggerRect.top - floatingRect.height - offset; break;
        case "bottom": top = triggerRect.bottom + offset; break;
        case "left":   left = triggerRect.left - floatingRect.width - offset; break;
        case "right":  left = triggerRect.right + offset; break;
    }

    if (basePlacement === "top" || basePlacement === "bottom") {
        switch (alignment) {
            case "start": left = triggerRect.left; break;
            case "end":   left = triggerRect.right - floatingRect.width; break;
            default:      left = triggerRect.left + (triggerRect.width - floatingRect.width) / 2;
        }
    } else {
        switch (alignment) {
            case "start": top = triggerRect.top; break;
            case "end":   top = triggerRect.bottom - floatingRect.height; break;
            default:      top = triggerRect.top + (triggerRect.height - floatingRect.height) / 2;
        }
    }

    if (flip) {
        const vh = window.innerHeight;
        const vw = window.innerWidth;
        if (basePlacement === "bottom" && top + floatingRect.height > vh - shiftPadding) {
            const flippedTop = triggerRect.top - floatingRect.height - offset;
            if (flippedTop >= shiftPadding) { top = flippedTop; finalPlacement = placement.replace("bottom", "top"); }
        } else if (basePlacement === "top" && top < shiftPadding) {
            const flippedTop = triggerRect.bottom + offset;
            if (flippedTop + floatingRect.height <= vh - shiftPadding) { top = flippedTop; finalPlacement = placement.replace("top", "bottom"); }
        } else if (basePlacement === "right" && left + floatingRect.width > vw - shiftPadding) {
            const flippedLeft = triggerRect.left - floatingRect.width - offset;
            if (flippedLeft >= shiftPadding) { left = flippedLeft; finalPlacement = placement.replace("right", "left"); }
        } else if (basePlacement === "left" && left < shiftPadding) {
            const flippedLeft = triggerRect.right + offset;
            if (flippedLeft + floatingRect.width <= vw - shiftPadding) { left = flippedLeft; finalPlacement = placement.replace("left", "right"); }
        }
    }

    if (shift) {
        const vh = window.innerHeight;
        const vw = window.innerWidth;
        if (left < shiftPadding) left = shiftPadding;
        else if (left + floatingRect.width > vw - shiftPadding) left = vw - floatingRect.width - shiftPadding;
        if (top < shiftPadding) top = shiftPadding;
        else if (top + floatingRect.height > vh - shiftPadding) top = vh - floatingRect.height - shiftPadding;
    }

    return { top, left, placement: finalPlacement };
}

interface FloatingRootProps {
    // Solid's `h` auto-invokes zero-arg function props on read, so each
    // of these is simply the unwrapped value inside the component body.
    open?: boolean;
    onOpenChange?: (open: boolean) => void;
    placement?: Placement;
    offset?: number;
    flip?: boolean;
    shift?: boolean;
    shiftPadding?: number;
    // Opt out of the global single-open manager. Use for a floating nested
    // inside another (e.g. a tooltip inside a popover) so opening it doesn't
    // close its ancestor, and so the ancestor opening doesn't close it.
    standalone?: boolean;
    children?: JSXElement;
}

export function FloatingRoot(props: FloatingRootProps) {
    const [internalOpen, setInternalOpen] = createSignal(false);
    const isControlled = () => props.open !== undefined;
    const isOpen = () => isControlled() ? !!props.open : internalOpen();

    let triggerRef: HTMLElement | undefined;
    let floatingRef: HTMLElement | undefined;
    const [position, setPosition] = createSignal<Position | null>(null);

    let hoverCloseTimeout: ReturnType<typeof setTimeout> | null = null;

    const options: Required<PositionOptions> = {
        placement: props.placement ?? "bottom-start",
        offset:    props.offset    ?? 4,
        flip:      props.flip      ?? true,
        shift:     props.shift     ?? true,
        shiftPadding: props.shiftPadding ?? 8,
    };

    const closeThis = () => {
        if (isControlled()) props.onOpenChange?.(false);
        else setInternalOpen(false);
    };

    const setIsOpen = (open: boolean) => {
        if (!props.standalone) {
            if (open) floatingManager.register(closeThis);
            else      floatingManager.unregister(closeThis);
        }
        if (isControlled()) props.onOpenChange?.(open);
        else setInternalOpen(open);
        if (open) {
            requestAnimationFrame(() => updatePosition());
        } else {
            setPosition(null);
        }
    };

    onCleanup(() => {
        floatingManager.unregister(closeThis);
        if (hoverCloseTimeout) clearTimeout(hoverCloseTimeout);
    });

    const cancelHoverClose = () => {
        if (hoverCloseTimeout) { clearTimeout(hoverCloseTimeout); hoverCloseTimeout = null; }
    };

    const scheduleHoverClose = (delay: number) => {
        cancelHoverClose();
        hoverCloseTimeout = setTimeout(() => setIsOpen(false), delay);
    };

    const updatePosition = () => {
        if (!triggerRef || !floatingRef) return;
        const triggerRect = triggerRef.getBoundingClientRect();
        const floatingRect = floatingRef.getBoundingClientRect();
        setPosition(calculatePosition(triggerRect, floatingRect, options));
    };

    createRenderEffect(() => {
        if (!isOpen()) { setPosition(null); return; }
        const rafId = requestAnimationFrame(updatePosition);
        const handleUpdate = () => updatePosition();
        window.addEventListener("scroll", handleUpdate, true);
        window.addEventListener("resize", handleUpdate);
        onCleanup(() => {
            cancelAnimationFrame(rafId);
            window.removeEventListener("scroll", handleUpdate, true);
            window.removeEventListener("resize", handleUpdate);
        });
    });

    const value: FloatingContextValue = {
        isOpen,
        setIsOpen,
        get triggerRef() { return triggerRef; },
        setTriggerRef: (el: HTMLElement) => { triggerRef = el; },
        get floatingRef() { return floatingRef; },
        setFloatingRef: (el: HTMLElement) => { floatingRef = el; },
        position,
        options,
        cancelHoverClose,
        scheduleHoverClose,
    };

    return <FloatingContext.Provider value={value}>{props.children}</FloatingContext.Provider>;
}

interface FloatingTriggerProps {
    openOnHover?: boolean;
    hoverDelay?: number;
    hoverCloseDelay?: number;
    class?: string;
    title?: string;
    children?: JSXElement;
}

export function FloatingTrigger(props: FloatingTriggerProps) {
    const ctx = useFloatingContext();
    let hoverOpenTimeout: ReturnType<typeof setTimeout> | null = null;

    const clearOpenTimeout = () => {
        if (hoverOpenTimeout) { clearTimeout(hoverOpenTimeout); hoverOpenTimeout = null; }
    };
    onCleanup(clearOpenTimeout);

    const handleMouseEnter = () => {
        if (!props.openOnHover) return;
        ctx.cancelHoverClose();
        clearOpenTimeout();
        hoverOpenTimeout = setTimeout(() => ctx.setIsOpen(true), props.hoverDelay ?? 0);
    };

    const handleMouseLeave = () => {
        if (!props.openOnHover) return;
        clearOpenTimeout();
        ctx.scheduleHoverClose(props.hoverCloseDelay ?? 150);
    };

    const handleClick = () => {
        if (props.openOnHover) return;
        ctx.setIsOpen(!ctx.isOpen());
    };

    const handleKeyDown = (e: KeyboardEvent) => {
        if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            ctx.setIsOpen(!ctx.isOpen());
        } else if (e.key === "Escape" && ctx.isOpen()) {
            ctx.setIsOpen(false);
        }
    };

    return (
        <button type="button" class={props.class || ""} title={props.title} ref={(el: HTMLElement) => ctx.setTriggerRef(el)} onclick={handleClick} onKeyDown={handleKeyDown} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} aria-expanded={ctx.isOpen()} aria-haspopup="menu">{props.children}</button>
    );
}

interface FloatingContentProps {
    class?: string;
    style?: Record<string, string | number>;
    onMouseEnter?: () => void;
    onMouseLeave?: () => void;
    children?: JSXElement;
}

export function FloatingContent(props: FloatingContentProps) {
    const ctx = useFloatingContext();

    // Capture FloatingContent's own owner (which sits inside whatever provider
    // wraps us — e.g. Menu's MenuContext). We resolve props.children under this
    // owner below instead of letting the <Portal> resolve them in its deferred
    // scope: children handed straight to a Portal get instantiated in the
    // Portal's owner, dropping the surrounding provider from their owner chain,
    // so a MenuItem inside a Menu's Portal throws "must be used within a Menu"
    // (this bites when a solid-js/html page like AppLayout.ts feeds children in).
    // Using runWithOwner (not the children() helper) keeps the exact same one-shot
    // insert behavior the div had before — no extra reactive memo over the content,
    // which that "always render" design is sensitive to.
    const owner = getOwner();

    // Always render the div — toggle visibility via CSS.  Avoids the
    // Show-based mount/unmount thrash where reactive scope disposal
    // was immediately destroying the inner component on open.
    const handleClickOutside = (e: MouseEvent) => {
        if (!ctx.isOpen()) return;
        const el = e.target instanceof Element ? e.target : null;
        const self = ctx.floatingRef;
        // Inside our own content or trigger → keep open.
        if (el && self && self.contains(el)) return;
        if (el && ctx.triggerRef && ctx.triggerRef.contains(el)) return;
        // Keep open when the click lands in a descendant floating layer: either a
        // FloatingContent opened AFTER us (higher in the open stack), or an
        // unregistered floating (e.g. a combobox/select dropdown opened from inside
        // us — these never join the stack and are always leaf descendants). Only a
        // click in an ancestor/sibling FloatingContent, or fully outside, closes us.
        const clicked = el && (el.closest("[data-floating-content]") as HTMLElement | null);
        if (clicked && self) {
            const ci = openFloatings.indexOf(clicked);
            if (ci < 0 || ci > openFloatings.indexOf(self)) return;
        }
        ctx.setIsOpen(false);
    };
    const handleEscape = (e: KeyboardEvent) => {
        if (!ctx.isOpen() || e.key !== "Escape") return;
        // Only the topmost open floating closes on Escape, so dismissing a nested
        // menu (e.g. an insert menu inside an editor popover) doesn't also close
        // its parent. The stack is in open order, so the last entry is innermost.
        const self = ctx.floatingRef;
        if (self && openFloatings.length > 0 && openFloatings[openFloatings.length - 1] !== self) return;
        ctx.setIsOpen(false);
    };
    document.addEventListener("mousedown", handleClickOutside);
    document.addEventListener("keydown", handleEscape);
    // Track open order so descendant vs ancestor can be distinguished above.
    createEffect(() => {
        const self = ctx.floatingRef;
        if (!self) return;
        const i = openFloatings.indexOf(self);
        if (ctx.isOpen()) { if (i < 0) openFloatings.push(self); }
        else if (i >= 0) openFloatings.splice(i, 1);
    });
    onCleanup(() => {
        document.removeEventListener("mousedown", handleClickOutside);
        document.removeEventListener("keydown", handleEscape);
        const self = ctx.floatingRef;
        const i = self ? openFloatings.indexOf(self) : -1;
        if (i >= 0) openFloatings.splice(i, 1);
    });

    // Render through a Portal (to document.body) so the popover escapes any
    // ancestor that establishes a containing block for `position: fixed` — most
    // importantly the Modal's animated `transform`, which would otherwise make
    // our viewport-relative top/left resolve relative to the modal instead.
    return (
        <Portal>
            <div ref={(el: HTMLElement) => ctx.setFloatingRef(el)} role="menu" data-floating-content="true" class={props.class || ""} style={{
                position: "fixed",
                display: ctx.isOpen() ? "block" : "none",
                // Kept laid-out-but-invisible until position() is computed (one
                // rAF after open) so it never flashes at the top-left 0,0 origin.
                visibility: ctx.isOpen() && ctx.position() ? "visible" : "hidden",
                top: (ctx.position()?.top ?? 0) + "px",
                left: (ctx.position()?.left ?? 0) + "px",
                // Above the Modal container (z-[100]) so popovers opened from
                // inside a modal aren't hidden behind it now that we portal.
                "z-index": 110,
                ...(props.style || {}),
            }} onMouseEnter={() => props.onMouseEnter?.()} onMouseLeave={() => props.onMouseLeave?.()}>{runWithOwner(owner, () => props.children)}</div>
        </Portal>
    );
}

export function useFloatingHover(openOnHover: boolean, hoverCloseDelay: number = 150) {
    const ctx = useFloatingContext();
    const handleMouseEnter = () => {
        if (!openOnHover) return;
        ctx.cancelHoverClose();
    };
    const handleMouseLeave = () => {
        if (!openOnHover) return;
        ctx.scheduleHoverClose(hoverCloseDelay);
    };
    return { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave };
}
