<!--
    Component: ExpandableWrapper
    Animated wrapper for content that can be expanded and collapsed.
-->
<script lang="ts">
import { debounce } from '$shared/lib/utils';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import type { Snippet } from 'svelte';
import { cubicOut } from 'svelte/easing';
import type { HTMLAttributes } from 'svelte/elements';
import { Spring } from 'svelte/motion';
import { slide } from 'svelte/transition';

interface Props extends HTMLAttributes<HTMLDivElement> {
    /**
     * Bindable property to control the expanded state of the wrapper.
     * @default false
     */
    expanded?: boolean;
    /**
     * Disabled flag
     * @default false
     */
    disabled?: boolean;
    /**
     * Bindable property to bind:this
     * @default null
     */
    element?: HTMLElement | null;
    /**
     * Content that's always visible
     */
    visibleContent?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
    /**
     * Content that's hidden when the wrapper is collapsed
     */
    hiddenContent?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
    /**
     * Optional badge to render
     */
    badge?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
    /**
     * Callback for when the element's size changes
     */
    onResize?: (rect: DOMRectReadOnly) => void;
    /**
     * Rotation animation direction
     * @default 'clockwise'
     */
    rotation?: 'clockwise' | 'counterclockwise';
    /**
     * Classes for intermnal container
     */
    containerClassName?: string;
}

let {
    expanded = $bindable(false),
    disabled = false,
    element = $bindable(null),
    visibleContent,
    hiddenContent,
    badge,
    onResize,
    rotation = 'clockwise',
    class: className = '',
    containerClassName = '',
    ...props
}: Props = $props();

let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);

const xSpring = new Spring(0, {
    stiffness: 0.14, // Lower is slower
    damping: 0.5, // Settle
});

const ySpring = new Spring(0, {
    stiffness: 0.32,
    damping: 0.65,
});

const scaleSpring = new Spring(1, {
    stiffness: 0.32,
    damping: 0.65,
});

const rotateSpring = new Spring(0, {
    stiffness: 0.12,
    damping: 0.55,
});

function handleClickOutside(e: MouseEvent) {
    if (element && !element.contains(e.target as Node)) {
        expanded = false;
    }
}

function handleWrapperClick() {
    if (!disabled) {
        expanded = true;
    }
}

function handleKeyDown(e: KeyboardEvent) {
    if (e.key === 'Enter' || (e.key === ' ' && !expanded)) {
        e.preventDefault();
        handleWrapperClick();
    }

    if (expanded && e.key === 'Escape') {
        expanded = false;
    }
}

// Create debounced recize callback
const debouncedResize = debounce((entry: ResizeObserverEntry) => onResize?.(entry.contentRect), 50);

// Elevation and scale on activation
$effect(() => {
    if (expanded && !disabled) {
        // Lift up
        ySpring.target = 8;
        // Slightly bigger
        scaleSpring.target = 1.1;

        rotateSpring.target = rotation === 'clockwise' ? -0.5 : 0.5;

        timeoutId = setTimeout(() => {
            rotateSpring.target = 0;
            scaleSpring.target = 1.05;
        }, 300);
    } else {
        ySpring.target = 0;
        scaleSpring.target = 1;
        rotateSpring.target = 0;
    }

    return () => {
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
    };
});

// Click outside handling
$effect(() => {
    if (typeof window === 'undefined') {
        return;
    }

    document.addEventListener('click', handleClickOutside);
    return () => document.removeEventListener('click', handleClickOutside);
});

$effect(() => {
    if (disabled) {
        expanded = false;
    }
});

// Use an effect to watch the element's actual physical size
$effect(() => {
    if (!element) return;

    const observer = new ResizeObserver(entries => {
        const entry = entries[0];
        if (entry) {
            debouncedResize(entry);
        }
    });

    observer.observe(element);
    return () => observer.disconnect();
});
</script>

<div
    bind:this={element}
    onclick={handleWrapperClick}
    onkeydown={handleKeyDown}
    role="button"
    tabindex={0}
    class={cn(
        'will-change-[transform, width, height] duration-300',
        disabled ? 'pointer-events-none' : 'pointer-events-auto',
        className,
    )}
    style:transform="
        translate({xSpring.current}px, {ySpring.current}px)
        scale({scaleSpring.current})
        rotateZ({rotateSpring.current}deg)
    "
    {...props}
>
    {@render badge?.({ expanded, disabled })}

    <div
        class={cn(
            'relative p-0.5 sm:p-2 rounded-lg sm:rounded-2xl border transition-all duration-250 ease-out flex flex-col gap-1.5 backdrop-blur-lg',
            expanded
                ? 'bg-background-20 border-indigo-400/40 shadow-[0_30px_70px_-10px_rgba(99,102,241,0.25)]'
                : 'bg-background-40 border-background-40 shadow-[0_12px_40px_-12px_rgba(0,0,0,0.12)]',
            disabled && 'opacity-80 grayscale-[0.2]',
            containerClassName,
        )}
    >
        {@render visibleContent?.({ expanded, disabled })}

        {#if expanded}
            <div
                in:slide={{ duration: 250, easing: cubicOut }}
                out:slide={{ duration: 250, easing: cubicOut }}
            >
                {@render hiddenContent?.({ expanded, disabled })}
            </div>
        {/if}
    </div>
</div>
