<script lang="ts">
/**
 * src/lib/elements/element_qr_scanner.svelte
 * QR Scanner v3 — Svelte 5 runes, auto-starts, no manual permission step.
 *
 * html5-qrcode's .start() handles camera permission internally.
 * A unique viewfinder ID is generated per instance so multiple scanners
 * can coexist on the same page without collision.
 *
 * Props:
 *   start_qr_scanner  (bindable) — true = scan, false = stop
 *   on_qr_scan_result — callback fired with { detail: { result, entry_method } }
 *   qr_fps            — scan frames per second (default 10)
 *   qr_facing_mode    — 'environment' (rear) or 'user' (front)
 */
import { onDestroy, untrack } from 'svelte';
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode';
import { RefreshCw } from '@lucide/svelte';

interface Props {
    start_qr_scanner?: boolean;
    on_qr_scan_result?: (event: {
        detail: { result: string; entry_method: string };
    }) => void;
    qr_fps?: number;
    qr_facing_mode?: string;
}

let {
    start_qr_scanner = $bindable(true),
    on_qr_scan_result,
    qr_fps = 25,
    qr_facing_mode = 'environment'
}: Props = $props();

// Unique DOM ID per instance — prevents conflicts when multiple scanners mount
const viewfinder_id = `qr_vf_${Math.random().toString(36).substring(2, 9)}`;

let scanner: Html5Qrcode | null = null;
let status = $state<'idle' | 'starting' | 'scanning' | 'error'>('idle');
let error_msg = $state('');
let start_timeout: ReturnType<typeof setTimeout> | null = null;

// React to start_qr_scanner prop changes from the parent
$effect(() => {
    const should_scan = start_qr_scanner;

    untrack(() => {
        if (should_scan && (status === 'idle' || status === 'error')) {
            start_scanning();
        } else if (!should_scan && status === 'scanning') {
            stop_scanning();
        }
    });
});

onDestroy(() => {
    clear_start_timeout();
    stop_scanning();
});

function clear_start_timeout() {
    if (start_timeout !== null) {
        clearTimeout(start_timeout);
        start_timeout = null;
    }
}

async function start_scanning() {
    if (status === 'starting' || status === 'scanning') return;
    status = 'starting';
    error_msg = '';

    // If the camera hasn't started within 7 seconds, show a helpful nudge.
    // Common causes: permission dialog left open, camera in use by another app, slow device.
    start_timeout = setTimeout(() => {
        if (status === 'starting') {
            status = 'error';
            error_msg =
                'Camera is taking too long to start. Try refreshing the page, or check that no other app is using your camera.';
        }
    }, 7000);

    try {
        scanner = new Html5Qrcode(viewfinder_id, {
            formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE],
            verbose: false
        });

        await scanner.start(
            { facingMode: qr_facing_mode },
            {
                fps: qr_fps,
                // Use a percentage of the viewfinder so it scales on any screen size
                qrbox: (w: number, h: number) => {
                    const side = Math.floor(Math.min(w, h) * 0.88);
                    return { width: side, height: side };
                },
                // Use native BarcodeDetector API on Chrome/Edge — significantly faster
                // than the JS ZXing fallback used on Firefox/older Safari.
                // Cast: experimentalFeatures exists at runtime but is missing from the
                // html5-qrcode TypeScript type definitions for this version.
                ...({
                    experimentalFeatures: {
                        useBarCodeDetectorIfSupported: true
                    }
                } as {
                    experimentalFeatures: {
                        useBarCodeDetectorIfSupported: boolean;
                    };
                })
            },
            on_scan_success,
            on_scan_error
        );

        clear_start_timeout();
        status = 'scanning';
    } catch (e: any) {
        clear_start_timeout();
        status = 'error';
        if (e?.name === 'NotAllowedError') {
            error_msg =
                'Camera access denied. Please allow camera in your browser settings and try again.';
        } else {
            error_msg = 'Could not start camera. Please try again.';
        }
        console.warn('[QR v3] Scanner start failed:', e);
    }
}

async function stop_scanning() {
    clear_start_timeout();
    if (!scanner) return;
    const s = scanner;
    scanner = null;
    try {
        await s.stop();
        await s.clear();
    } catch {
        // Ignore cleanup errors — component may be unmounting
    }
    status = 'idle';
}

function on_scan_success(decoded_text: string) {
    // Stop scanning before notifying parent so the camera shuts down cleanly
    stop_scanning().then(() => {
        start_qr_scanner = false;
    });

    if (on_qr_scan_result) {
        on_qr_scan_result({
            detail: { result: decoded_text, entry_method: 'QR' }
        });
    }
}

function on_scan_error(_msg: string) {
    // Called on every frame that doesn't contain a QR code — expected, not an error
}
</script>

<!-- Viewfinder fills whatever container the parent provides -->
<div
    class="qr-scanner-v3 flex h-full w-full flex-col items-center justify-center">
    <div id={viewfinder_id} class="h-full w-full"></div>

    {#if status === 'starting'}
        <div
            class="absolute inset-0 flex items-center justify-center rounded-xl bg-black/30">
            <span
                class="animate-pulse rounded-full bg-black/60 px-4 py-2 text-sm font-semibold text-white shadow-lg">
                Starting camera...
            </span>
        </div>
    {:else if status === 'error'}
        <div
            class="absolute inset-0 flex flex-col items-center justify-center gap-5 rounded-xl bg-black/75 p-6 text-center">
            <p
                class="text-sm leading-snug font-semibold text-white drop-shadow">
                {error_msg}
            </p>
            <!-- bg-white + dark text: always readable on the dark camera overlay regardless of theme.
                 preset-filled-primary is theme-adaptive and has poor contrast on black/75 backgrounds. -->
            <button
                type="button"
                class="text-surface-950 hover:bg-surface-100 flex cursor-pointer items-center gap-2 rounded-xl bg-white px-8 py-3 text-base font-bold shadow-lg transition-colors"
                onclick={start_scanning}>
                <RefreshCw size="1.2em" />
                Try Again
            </button>
        </div>
    {/if}
</div>

<style>
/* html5-qrcode injects a <video> with inline width/height. Override with cover so the
       camera stream fills the container completely on any camera aspect ratio (4:3, 16:9, etc.).
       Without this, portrait-mode mobile cameras leave a gray letterbox at the bottom. */
:global(.qr-scanner-v3 video) {
    object-fit: cover !important;
    width: 100% !important;
    height: 100% !important;
}
</style>
