<!--
    Component: ComparisonSlider
    A multiline text comparison slider that morphs between two fonts.
    Features:
    - Multiline support with precise line breaking matching container width.
    - Character-level morphing: Font changes exactly when the slider passes the character's global position.
    - Responsive layout with Tailwind breakpoints for font sizing.
    - Performance optimized using offscreen canvas for measurements and transform-based animations.
-->
<script lang="ts">
import {
    MULTIPLIER_L,
    MULTIPLIER_M,
    MULTIPLIER_S,
} from '$entities/Font';
import { TypographyMenu } from '$features/AdjustTypography';
import { typographySettingsStore } from '$features/AdjustTypography/model';
import {
    type ResponsiveManager,
    debounce,
} from '$shared/lib';
import { cn } from '$shared/lib';
import {
    CharacterComparisonEngine,
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
import { Loader } from '$shared/ui';
import { getContext } from 'svelte';
import { Spring } from 'svelte/motion';
import { fade } from 'svelte/transition';
import {
    ensureCanvasFonts,
    getPretextFontString,
} from '../../lib';
import { comparisonStore } from '../../model';
import Character from '../Character/Character.svelte';
import Line from '../Line/Line.svelte';
import Thumb from '../Thumb/Thumb.svelte';

interface Props {
    /**
     * Sidebar open state
     * @default false
     */
    isSidebarOpen?: boolean;
    /**
     * CSS classes
     */
    class?: string;
}

let { isSidebarOpen = false, class: className }: Props = $props();

/**
 * Spring tuning for the comparison slider thumb. Lower stiffness = slower
 * follow; higher damping = less overshoot.
 */
const SLIDER_SPRING_CONFIG = { stiffness: 0.2, damping: 0.7 } as const;

/**
 * Debounce wait before persisting the slider position to the store.
 * High frequency during drag → batched writes.
 */
const SLIDER_PERSIST_DEBOUNCE_MS = 100;

/**
 * Horizontal layout padding subtracted from container width before laying
 * out the comparison text. Different per breakpoint to match the gutters
 * around the slider track.
 */
const SLIDER_PADDING_MOBILE_PX = 48;
const SLIDER_PADDING_DESKTOP_PX = 96;

const fontA = $derived(comparisonStore.fontA);
const fontB = $derived(comparisonStore.fontB);
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
const typography = $derived(typographySettingsStore);

let container = $state<HTMLElement>();

const responsive = getContext<ResponsiveManager>('responsive');
const isMobile = $derived(responsive?.isMobile ?? false);

let isDragging = $state(false);
let isTypographyMenuOpen = $state(false);
let containerWidth = $state(0);

// New high-performance layout engine
const comparisonEngine = new CharacterComparisonEngine();

let layoutResult = $state<ReturnType<typeof comparisonEngine.layout>>({ lines: [], totalHeight: 0 });

// Track container width changes (window resize, sidebar toggle, etc.)
$effect(() => {
    if (!container) {
        return;
    }

    const observer = new ResizeObserver(entries => {
        for (const entry of entries) {
            // Use borderBoxSize if available, fallback to contentRect
            const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
            if (width > 0) {
                containerWidth = width;
            }
        }
    });

    observer.observe(container);
    return () => observer.disconnect();
});

const sliderSpring = new Spring(50, SLIDER_SPRING_CONFIG);
const sliderPos = $derived(sliderSpring.current);

function handleMove(e: PointerEvent) {
    if (!isDragging || !container) {
        return;
    }
    const rect = container.getBoundingClientRect();
    const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
    const percentage = (x / rect.width) * 100;
    sliderSpring.target = percentage;
}

function startDragging(e: PointerEvent) {
    e.preventDefault();
    // Close typography menu popover
    isTypographyMenuOpen = false;
    isDragging = true;
    handleMove(e);
}

const storeSliderPosition = debounce((value: number) => {
    comparisonStore.sliderPosition = value;
}, SLIDER_PERSIST_DEBOUNCE_MS);

$effect(() => {
    storeSliderPosition(sliderPos);
});

$effect(() => {
    if (!responsive) {
        return;
    }
    switch (true) {
        case responsive.isMobile:
            typography.multiplier = MULTIPLIER_S;
            break;
        case responsive.isTablet:
            typography.multiplier = MULTIPLIER_M;
            break;
        case responsive.isDesktop:
            typography.multiplier = MULTIPLIER_L;
            break;
        default:
            typography.multiplier = MULTIPLIER_L;
    }
});

$effect(() => {
    if (isDragging) {
        window.addEventListener('pointermove', handleMove);
        const stop = () => (isDragging = false);
        window.addEventListener('pointerup', stop);
        return () => {
            window.removeEventListener('pointermove', handleMove);
            window.removeEventListener('pointerup', stop);
        };
    }
});

// Layout effect — depends on content, settings AND containerWidth.
// Awaits font loading into the canvas measurement context before invoking
// the engine; otherwise pretext caches fallback-font widths globally per
// font string, and the morph boundary drifts from the thumb visually.
$effect(() => {
    const _text = comparisonStore.text;
    const _weight = typography.weight;
    const _size = typography.renderedSize;
    const _height = typography.height;
    const _spacing = typography.spacing;
    const _width = containerWidth;
    const _isMobile = isMobile;

    if (!container || !fontA || !fontB || _width <= 0) {
        return;
    }

    const fontAStr = getPretextFontString(_weight, _size, fontA.name);
    const fontBStr = getPretextFontString(_weight, _size, fontB.name);

    const padding = _isMobile ? SLIDER_PADDING_MOBILE_PX : SLIDER_PADDING_DESKTOP_PX;
    const availableWidth = Math.max(0, _width - padding);
    const lineHeight = _size * _height;

    let cancelled = false;
    ensureCanvasFonts([fontAStr, fontBStr]).then(() => {
        if (cancelled) {
            return;
        }
        layoutResult = comparisonEngine.layout(
            _text,
            fontAStr,
            fontBStr,
            availableWidth,
            lineHeight,
            _spacing,
            _size,
        );
    });

    return () => {
        cancelled = true;
    };
});

// Dynamic backgroundSize based on isMobile — can't express this in Tailwind.
// Color is set to currentColor so it respects dark mode via text color.
const gridStyle = $derived(
    `background-image: linear-gradient(currentColor 1px, transparent 1px), linear-gradient(90deg, currentColor 1px, transparent 1px); `
        + `background-size: ${isMobile ? '10px 10px' : '20px 20px'};`,
);

// Replaces motion.div animate={{ scale: isSidebarOpen && !isMobile ? 0.94 :1 }}
const scaleClass = $derived(
    isSidebarOpen && !isMobile
        ? 'scale-[0.94]'
        : 'scale-100',
);
</script>

<!--
    Outer flex container — fills parent.
    The paper div inside scales down when the sidebar opens on desktop.
-->
<div class={cn('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}>
    <!-- Paper surface -->
    <div
        class={cn(
            'w-full h-full flex flex-col items-center justify-center relative',
            'bg-paper dark:bg-dark-card',
            'shadow-2xl shadow-black/5 dark:shadow-black/20',
            'transition-transform duration-300 ease-out',
            scaleClass,
        )}
    >
        <!-- Subtle grid overlay — pointer-events-none, very low opacity -->
        <div
            class="absolute inset-0 pointer-events-none opacity-[0.03] dark:opacity-[0.05] text-swiss-black dark:text-swiss-white"
            style={gridStyle}
            aria-hidden="true"
        >
        </div>

        <!-- Slider interaction area -->
        <div class="w-full h-full flex items-center justify-center p-4 md:p-8 overflow-hidden">
            {#if isLoading}
                <div out:fade={{ duration: 300 }}>
                    <Loader size={24} />
                </div>
            {:else}
                <div
                    bind:this={container}
                    role="slider"
                    tabindex="0"
                    aria-valuenow={Math.round(sliderPos)}
                    aria-label="Font comparison slider"
                    onpointerdown={startDragging}
                    class="
                        relative w-full max-w-6xl h-full
                        flex flex-col justify-center
                        select-none touch-none outline-none cursor-ew-resize
                        py-8 px-4 sm:py-12 sm:px-8 md:py-16 md:px-12 lg:py-20 lg:px-24
                    "
                    in:fade={{ duration: 300, delay: 300 }}
                    out:fade={{ duration: 300 }}
                >
                    <!-- Character lines -->
                    <div
                        class="
                            relative flex flex-col items-center gap-3 sm:gap-4
                            z-10 pointer-events-none text-center
                            my-auto
                        "
                    >
                        {#each layoutResult.lines as line}
                            {@const lineStates = comparisonEngine.getLineCharStates(line, sliderPos, containerWidth)}
                            <Line chars={line.chars}>
                                {#snippet character({ char, index })}
                                    <Character
                                        {char}
                                        proximity={lineStates[index]?.proximity ?? 0}
                                        isPast={lineStates[index]?.isPast ?? false}
                                    />
                                {/snippet}
                            </Line>
                        {/each}
                    </div>

                    <Thumb {sliderPos} {isDragging} />
                </div>
            {/if}
        </div>
    </div>

    <TypographyMenu
        bind:open={isTypographyMenuOpen}
        class={cn(
            'absolute z-10',
            responsive.isMobileOrTablet
                ? 'bottom-0 right-0 -translate-1/2'
                : 'bottom-2.5 left-1/2 -translate-x-1/2',
        )}
    />
</div>
