<script lang="ts">
let log_lvl: number = $state(0);
interface Props {
    /** @type {import('./$types').LayoutData} */
    data: any;
    children?: import('svelte').Snippet;
}

let { data, children }: Props = $props();

// *** Import Svelte specific
import { untrack } from 'svelte';
// import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
import { sineIn } from 'svelte/easing';

// *** Import other supporting libraries
import { liveQuery } from 'dexie';
import { Drawer, Modal } from 'flowbite-svelte';
import { fade } from 'svelte/transition';
import { listen, idle, onIdle, restartCountdown } from 'svelte-idle';

import {
    LoaderCircle,
    Satellite,
    MapPin,
    Laptop,
    BedDouble,
    MousePointer,
    Wifi,
    Network,
    X,
    CalendarDays,
    Clock,
    Biohazard,
    Search,
    GraduationCap,
    Info,
    ZoomIn,
    Minimize2,
    Monitor,
    List
} from '@lucide/svelte';

// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { api } from '$lib/ae_api/api';
import { db_events } from '$lib/ae_events/db_events';
import {
    ae_snip,
    ae_loc,
    ae_sess,
    ae_api,
    ae_trig,
    slct,
    slct_trigger,
    time
} from '$lib/stores/ae_stores';
import {
    EVENTS_MODULE_TITLE,
    events_sess,
    events_slct,
    events_trigger,
    events_trig
} from '$lib/stores/ae_events_stores';
import { launcher_loc } from '$lib/stores/ae_events_stores__launcher.svelte';
import { events_func } from '$lib/ae_events/ae_events_functions';

import Launcher_cfg from '../launcher_cfg.svelte';
import Launcher_menu from '../launcher_menu.svelte';
import Launcher_session_view from '../launcher_session_view.svelte';
import Element_websocket from '$lib/elements/element_websocket.svelte';

// *** Set initial variables
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]);

import { online } from 'svelte/reactivity/window';

$ae_sess.disable_sys_nav = true;
$ae_sess.disable_sys_header = true;
$ae_sess.disable_sys_footer = true;

// WHY: launcher_loc is a PersistedState store — always initialized with defaults.
// No need for initialization guards; all fields have default values in launcher_loc_defaults.

let modal_cfg_open = $state(!launcher_loc.current.hide_drawer__cfg);

// Sync store → modal: biohazard button writes hide_drawer__cfg = false to open.
$effect(() => {
    const should_open = !launcher_loc.current.hide_drawer__cfg;
    if (modal_cfg_open !== should_open) {
        modal_cfg_open = should_open;
    }
});

// Sync modal → store: direct assignment to PersistedState triggers localStorage persistence.
$effect(() => {
    launcher_loc.current.hide_drawer__cfg = !modal_cfg_open;
});

// Called by backdrop click. Writes to store immediately (don't rely on effect timing).
function close_cfg() {
    modal_cfg_open = false;
    launcher_loc.current.hide_drawer__cfg = true;
}

// Generate a stable per-device client ID on first load and persist it.
// launcher_loc is a PersistedState store (localStorage) so this survives
// page reloads. Without this, client_id falls back to Date.now() inside
// element_websocket — a new ID on every reload, which breaks direct-target
// WS messages and doesn't match V3 Vision ID expectations.
if (!launcher_loc.current.controller_client_id) {
    launcher_loc.current.controller_client_id = crypto.randomUUID();
}

// Unified Selection Sync (Refactored 2026-02-11)
// WHY: We track URL params directly to ensure the UI reacts instantly to navigation.
// We use untrack for store writes to prevent circular dependency loops.
$effect(() => {
    const url_session_id = data.url.searchParams.get('session_id');
    const path_location_id = data.params.event_location_id;
    const path_event_id = data.params.event_id;

    // WHY: URL params for display overrides — lets you share a clean link
    // (e.g. tablet PWA, digital poster kiosk) without the recipient needing
    // to configure settings manually on their device.
    // These are read on every URL change (reactive to SPA navigation), which
    // is better than the root layout's onMount which only fires once on load.
    // Values persist in localStorage so they survive page reloads within the
    // same device; the URL param always wins when present.
    //
    // Supported params:
    //   ?iframe=true/false         — hide/show global sys & debug menus
    //   ?launcher_menu=hide/show   — hide/show left session/location panel
    //   ?launcher_header=hide/show — hide/show "Æ Launcher v3" header bar
    //   ?launcher_footer=hide/show — hide/show the status footer
    //
    // Example clean poster-kiosk link:
    //   /events/{id}/launcher/{loc_id}?session_id={sess_id}
    //     &iframe=true&launcher_menu=hide&launcher_header=hide
    const param_iframe = data.url.searchParams.get('iframe');
    const param_launcher_menu = data.url.searchParams.get('launcher_menu');
    const param_launcher_header = data.url.searchParams.get('launcher_header');
    const param_launcher_footer = data.url.searchParams.get('launcher_footer');

    if (log_lvl > 1) {
        console.log(
            `[Launcher Sync] URL Change: event=${path_event_id}, loc=${path_location_id}, sess=${url_session_id}`
        );
    }

    untrack(() => {
        if ($events_slct.event_id !== path_event_id) {
            $events_slct.event_id = path_event_id;
        }
        if ($events_slct.event_location_id !== path_location_id) {
            $events_slct.event_location_id = path_location_id;
        }
        // CRITICAL: Ensure session_id is synced to store so LiveQueries react
        if ($events_slct.event_session_id !== url_session_id) {
            if (log_lvl)
                console.log(
                    `[Launcher Sync] Updating store session_id: ${url_session_id}`
                );
            $events_slct.event_session_id = url_session_id;
        }

        // Apply display overrides from URL params (when present)
        if (param_iframe === 'true') $ae_loc.iframe = true;
        else if (param_iframe === 'false') $ae_loc.iframe = false;

        if (param_launcher_menu === 'hide')
            launcher_loc.current.hide__launcher_menu = true;
        else if (param_launcher_menu === 'show')
            launcher_loc.current.hide__launcher_menu = false;

        if (param_launcher_header === 'hide')
            launcher_loc.current.hide__launcher_header = true;
        else if (param_launcher_header === 'show')
            launcher_loc.current.hide__launcher_header = false;

        if (param_launcher_footer === 'hide')
            launcher_loc.current.hide__launcher_footer = true;
        else if (param_launcher_footer === 'show')
            launcher_loc.current.hide__launcher_footer = false;
    });

    // Strip launcher display params from the URL after applying them — same pattern
    // as root layout does for ?theme / ?theme_mode.  These are "one-shot" apply params;
    // keeping them in the URL is misleading because toggling the CFG panel would desync.
    // ?iframe is intentionally left in the URL because it is a persistent mode flag.
    if (param_launcher_menu || param_launcher_header || param_launcher_footer) {
        const clean_url = new URL(data.url.href);
        clean_url.searchParams.delete('launcher_menu');
        clean_url.searchParams.delete('launcher_header');
        clean_url.searchParams.delete('launcher_footer');
        goto(clean_url.pathname + clean_url.search + clean_url.hash, {
            replaceState: true,
            noScroll: true,
            keepFocus: true
        });
    }
});

// String-Only ID Vision: Sync the device ID from the native environment
const native_dev = $derived($ae_loc.native_device);
$effect(() => {
    if (native_dev) {
        untrack(() => {
            $events_slct.event_device_id =
                native_dev.event_device_id ||
                native_dev.id;
        });
    }
});

$effect(() => {
    if (ae_acct) {
        untrack(() => {
            const new_location_obj_li = ae_acct.slct.event_location_obj_li ?? [
                ''
            ];
            // Compare by extracting IDs only — object identity (===) won't work for
            // plain JS objects from the store. Joining IDs is cheap and avoids a full
            // JSON.stringify of potentially large location objects on every navigation.
            const current_obj_ids = ($events_slct.event_location_obj_li ?? [])
                .map((o: any) => o?.event_location_id ?? o)
                .join(',');
            const new_obj_ids = new_location_obj_li
                .map((o: any) => o?.event_location_id ?? o)
                .join(',');
            if (current_obj_ids !== new_obj_ids) {
                $events_slct.event_location_obj_li = new_location_obj_li;
            }

            const new_id_li__event_location = ae_acct.slct
                .id_li__event_location ?? [''];
            // ID list contains plain strings — join-compare is O(n) and avoids JSON.stringify.
            if (
                ($events_slct.id_li__event_location ?? []).join(',') !==
                new_id_li__event_location.join(',')
            ) {
                $events_slct.id_li__event_location = new_id_li__event_location;
            }
        });
    }
});

// *** Functions and Logic

// Event
let lq__event_obj = liveQuery(async () => {
    const id = $events_slct?.event_id;
    if (!id) return null;
    if (log_lvl > 1) console.log(`lq__event_obj: event_id = ${id}`);
    return await db_events.event.get(id);
});

// Event Device
let lq__event_device_obj = liveQuery(async () => {
    const id = $events_slct.event_device_id;
    if (!id) return null;
    return await db_events.device.get(id);
});

// Event File - For Event
let lq__event_event_file_obj_li = liveQuery(async () => {
    const id = $events_slct.event_id;
    if (!id) return [];
    return await db_events.file.where('for_id').equals(id).sortBy('filename');
});

// Event File - For Location
// $derived.by: must recreate the observable when event_location_id changes.
// Plain liveQuery() only re-fires when Dexie detects a change in the initially
// watched index range — if the id starts as null and changes to a real value,
// Dexie never fires because a different range was added (not the null range).
let lq__location_event_file_obj_li = $derived.by(() => {
    const id = $events_slct.event_location_id;
    return liveQuery(async () => {
        if (!id) return [];
        return await db_events.file
            .where('for_id')
            .equals(id)
            .sortBy('filename');
    });
});

// Event Location — same reason as above
let lq__event_location_obj = $derived.by(() => {
    const id = $events_slct.event_location_id;
    return liveQuery(async () => {
        if (!id) return null;
        return await db_events.location.get(id);
    });
});

let lq__event_location_obj_li = liveQuery(async () => {
    const id = $events_slct.event_id;
    if (!id) return [];
    return await db_events.location.where('event_id').equals(id).sortBy('name');
});

// $derived.by: must recreate when event_location_id changes (see comment above).
let lq__event_session_obj_li = $derived.by(() => {
    const id = $events_slct.event_location_id;
    return liveQuery(async () => {
        if (!id) return [];
        if (log_lvl > 1)
            console.log(`LQ - Event Session list location_id: ${id}`);
        return await db_events.session
            .where('event_location_id')
            .equals(id)
            .sortBy('start_datetime');
    });
});

// Event Session (Main View Trigger - Needed for Global Header/Idle)
// $derived.by: capture ID in outer closure so Svelte tracks it as a dependency.
// liveQuery callback runs in Dexie's async context where Svelte tracking is off.
let lq__event_session_obj = $derived.by(() => {
    const id = $events_slct.event_session_id;
    return liveQuery(() => db_events.session.get(id));
});

// Store sync effects — keep liveQuery closures pure (data-only) and sync to
// $events_slct here in reactive effects instead. Comparing updated_on + id is
// O(1) vs O(serialized-bytes) for JSON.stringify and avoids running inside a
// Dexie async context where Svelte's reactivity tracking is undefined.
$effect(() => {
    const result = $lq__event_obj;
    if (result) {
        untrack(() => {
            if (
                result.updated_on !== $events_slct.event_obj?.updated_on ||
                result.id !== $events_slct.event_obj?.id
            ) {
                $events_slct.event_obj = { ...result };
            }
        });
    }
});

$effect(() => {
    const result = $lq__event_device_obj;
    if (result) {
        untrack(() => {
            if (
                result.updated_on !==
                    $events_slct.event_device_obj?.updated_on ||
                result.id !== $events_slct.event_device_obj?.id
            ) {
                $events_slct.event_device_obj = { ...result };
            }
        });
    }
});

$effect(() => {
    const results = $lq__event_session_obj_li;
    if (results) {
        untrack(() => {
            const current = $events_slct.event_session_obj_li ?? [];
            // Compare by joining IDs — O(n) string compare vs O(n*m) JSON.stringify.
            const new_ids = (results as any[])
                .map((r: any) => r.id ?? r.event_session_id)
                .join(',');
            const cur_ids = current
                .map((r: any) => r.id ?? r.event_session_id)
                .join(',');
            if (new_ids !== cur_ids) {
                $events_slct.event_session_obj_li = [...(results as any[])];
            }
        });
    }
});

let trigger_handle_ws_conn = $state(false);
let trigger_handle_ws_recv = $state(false);
let trigger_handle_ws_sent = $state(false);

/* *** BEGIN *** Handle WebSocket events */
function handle_ws_conn(ws_conn_status: any) {
    if (log_lvl) console.log('*** handle_ws_conn() ***', ws_conn_status);
    if (ws_conn_status.status == 'connected') {
        $events_sess.launcher.ws = { status: 'connected' };
    } else {
        $events_sess.launcher.ws = { status: 'disconnected' };
    }
}

function handle_ws_recv(ws_recv_status: any) {
    if (log_lvl) console.log('*** handle_ws_recv() ***', ws_recv_status);
    // V3 schema uses msg_type instead of type
    if (ws_recv_status.msg_type == 'cmd' && ws_recv_status.cmd) {
        let cmd = ws_recv_status.cmd;
        if (launcher_loc.current.controller != 'remote') return;

        if (cmd.startsWith('ae_load:')) {
            let cmd_parts = cmd.split(':');
            let obj_parts = cmd_parts[1].split('=');
            let obj_type = obj_parts[0];
            let obj_id = obj_parts[1];

            if (obj_type == 'event_session') {
                $events_slct.event_session_id = obj_id;
                let new_url = new URL(data.url);
                new_url.pathname = `/events/${data.params.event_id}/launcher/${$events_slct.event_location_id}`;
                new_url.searchParams.set(
                    'session_id',
                    $events_slct.event_session_id
                );
                goto(new_url.toString(), { replaceState: false });
            }
        } else if (cmd.startsWith('ae_download:')) {
            let cmd_parts = cmd.split(':');
            let obj_parts = cmd_parts[1].split('=');
            let obj_type = obj_parts[0];
            let obj_id = obj_parts[1];
            let obj_filename = cmd_parts[2];

            api.get_object({
                api_cfg: $ae_api,
                endpoint: `/v3/action/hosted_file/${obj_id}/download`,
                params: {
                    filename: obj_filename,
                    key: $ae_api.account_id
                },
                filename: obj_filename,
                return_blob: true,
                auto_download: true,
                log_lvl: 1
            });
        } else if (cmd.startsWith('ae_open:')) {
            let cmd_parts = cmd.split(':');
            let obj_parts = cmd_parts[1].split('=');
            let obj_id = obj_parts[1];

            if (obj_parts[0] == 'event_file') {
                $events_sess.launcher.modal__open_event_file_id = null;
                clearInterval(idle_timer_interval);
                saver_looping = false;
                restartCountdown();
                $events_slct.event_file_id = obj_id;
                $events_sess.launcher.modal__open_event_file_id = obj_id;
                // Look up the file object from Dexie so the modal can render the image.
                // The remote device has no event_file_obj in scope — only the ID was sent over WS.
                // Also look up the parent presentation to use its name as the modal title
                // (cleaner for the LCD display than a raw filename).
                db_events.file.get(obj_id).then(async (file_obj: any) => {
                    if (file_obj) {
                        $events_sess.launcher.modal__event_file_obj = file_obj;
                        const presentation = file_obj.for_id
                            ? await db_events.presentation.get(file_obj.for_id)
                            : null;
                        $events_sess.launcher.modal__title =
                            presentation?.name ?? file_obj.filename ?? '';
                    }
                });
            }
        } else if (cmd.startsWith('ae_close:')) {
            if (cmd.split(':')[1] == 'event_file_modal') {
                $events_sess.launcher.modal__open_event_file_id = null;
            }
            clearInterval(idle_timer_interval);
            saver_looping = false;
            restartCountdown();
        } else if (cmd.startsWith('ae_zoom:')) {
            // WHY: Controller can push zoom state to the remote display so both
            // devices stay in sync (e.g. operator zooms to show detail to an attendee
            // while the wall screen also zooms in for the room to see).
            const zoom_target = cmd.split(':')[1];
            if (zoom_target === 'fit') modal_zoom_fit = true;
            else if (zoom_target === 'zoom') modal_zoom_fit = false;
        } else if (cmd.startsWith('ae_refresh:')) {
            if (cmd.split(':')[1] == 'now') location.reload();
        } else if (cmd.startsWith('ae_mode:')) {
            // WHY: Allows a controller to remotely push a display mode preset
            // (e.g. switch all poster kiosks into kiosk mode without touching each device).
            const mode_target = cmd.split(':')[1];
            if (mode_target === 'poster') {
                $ae_loc.iframe = true;
                launcher_loc.current.hide__launcher_menu = true;
                launcher_loc.current.hide__launcher_header = true;
                launcher_loc.current.hide__launcher_footer = true;
            } else if (mode_target === 'oral') {
                $ae_loc.iframe = false;
                launcher_loc.current.hide__launcher_menu = false;
                launcher_loc.current.hide__launcher_header = false;
                launcher_loc.current.hide__launcher_footer = false;
            }
        }
    }
}

function handle_ws_sent(ws_sent_status: any) {
    $events_sess.launcher.controller_cmd = null;
    $events_sess.launcher.controller_trigger_send = null;
}

$effect(() => {
    if (trigger_handle_ws_conn) {
        handle_ws_conn(trigger_handle_ws_conn);
        trigger_handle_ws_conn = false;
    }
});

$effect(() => {
    if (trigger_handle_ws_recv) {
        handle_ws_recv(trigger_handle_ws_recv);
        trigger_handle_ws_recv = false;
    }
});

$effect(() => {
    if (trigger_handle_ws_sent) {
        handle_ws_sent(trigger_handle_ws_sent);
        trigger_handle_ws_sent = false;
    }
});

if (!launcher_loc.current.idle_timer)
    launcher_loc.current.idle_timer = 5 * 60 * 1000;
if (!launcher_loc.current.idle_cycle)
    launcher_loc.current.idle_cycle = 5 * 1000;
if (!launcher_loc.current.idle_loop_period)
    launcher_loc.current.idle_loop_period = 3 * 60 * 1000;

listen({
    timer: launcher_loc.current.idle_timer,
    cycle: launcher_loc.current.idle_cycle
});

let idle_timer_interval: any = $state();
let saver_looping: boolean = $state(false);
// Tracks fit vs. zoom mode for the poster modal.
// Reset to fit whenever a new poster opens so every poster starts clean.
let modal_zoom_fit: boolean = $state(true);

function handle_idle_client() {
    if (
        $lq__event_session_obj &&
        $lq__event_session_obj?.type_code == 'poster'
    ) {
        if (saver_looping) return false;
        saver_looping = true;

        idle_timer_interval = setInterval(
            () => {
                if (launcher_loc.current.screen_saver_img_kv) {
                    const keys = Object.keys(
                        launcher_loc.current.screen_saver_img_kv
                    );
                    const rand_index = Math.floor(Math.random() * keys.length);
                    let event_file_obj =
                        launcher_loc.current.screen_saver_img_kv[
                            keys[rand_index]
                        ];

                    $events_slct.event_file_id = event_file_obj.event_file_id;
                    $events_slct.event_file_obj = event_file_obj;
                    $events_sess.launcher.modal__open_event_file_id = null;
                    $events_sess.launcher.modal__title =
                        event_file_obj.filename ?? '*';
                    $events_sess.launcher.modal__open_event_file_id =
                        $events_slct.event_file_id;
                    $events_sess.launcher.modal__event_file_obj =
                        event_file_obj;
                    return true;
                }
                return false;
            },
            launcher_loc.current.idle_loop_period ?? 2 * 60 * 1000
        );
    } else {
        saver_looping = false;
        return false;
    }
}

onIdle(() => {
    clearInterval(idle_timer_interval);
    handle_idle_client();
});

$effect(() => {
    if (!$idle) {
        clearInterval(idle_timer_interval);
        saver_looping = false;
    }
});

// Reset to fit-mode whenever a different poster is opened.
$effect(() => {
    if ($events_sess.launcher.modal__open_event_file_id) {
        modal_zoom_fit = true;
    }
});
</script>

<svelte:head>
    <title>
        &AElig;:
        {$lq__event_location_obj?.name ?? '-- not set --'}
        ({$lq__event_session_obj?.name ?? 'Æ loading...'}) - Launcher v3 -
        {EVENTS_MODULE_TITLE}
    </title>
</svelte:head>

<div
    class:mt-12={!launcher_loc.current.hide__launcher_header}
    class:mt-2={launcher_loc.current.hide__launcher_header}
    class="
        static
        m-auto
        mb-16
        lg:min-h-8/12 h-full max-h-screen
        w-full max-w-7xl
        border-x
        border-gray-200
        transition-all sm:mb-12
        dark:border-gray-600
        ">
    <header
        id="Main-Header"
        class:hidden={launcher_loc.current.hide__launcher_header}
        class="
        absolute
        top-0 right-0 left-0 z-20
        m-auto flex
        h-12
        w-full max-w-7xl flex-row

        items-center justify-around bg-slate-200 p-1 px-12

        text-sm

        opacity-95 transition-colors

        duration-300 hover:opacity-100
        sm:justify-between dark:bg-slate-800
        ">
        <h3 class="h4 text-surface-600-400 text-center italic">
            <!-- Menu toggle: needs a real tap target for tablet/touch operators -->
            <button
                type="button"
                class="hover:bg-surface-500/10 rounded px-2 py-1 transition-colors"
                onclick={() => {
                    launcher_loc.current.hide__launcher_menu =
                        !launcher_loc.current.hide__launcher_menu;
                }}
                title="Toggle Launcher menu">
                <Satellite class="mx-1 inline-block text-base text-gray-500" />
                <abbr title="Aether - Events Module Launcher">
                    Æ Launcher
                    <span
                        class="align-super text-xs font-normal"
                        title="Version 3">v3</span>
                </abbr>
            </button>
        </h3>

        {#if $lq__event_obj}
            <h2
                class="h3 text-surface-600-400 hidden text-center md:inline-block">
                {$lq__event_obj.cfg_json?.short_name}
            </h2>
            <h3
                class="h4 text-surface-600-400 text-center italic"
                title="Location ID: {$lq__event_location_obj?.event_location_id} Name: {$lq__event_location_obj?.name}">
                <button
                    type="button"
                    class="text-base"
                    onclick={() => {
                        $ae_loc.edit_mode = !$ae_loc.edit_mode;
                    }}
                    title="Toggle Edit Mode to show location options and more">
                    <MapPin size="1em" />
                    <span class="sr-only">Location:</span>
                </button>
                {$lq__event_location_obj?.name}
            </h3>
        {:else}
            <div class="flex flex-row items-center justify-center gap-1">
                <LoaderCircle size="1em" class="mx-1 animate-spin" />
                <span>Loading event...</span>
            </div>
        {/if}
    </header>

    <div
        class="
        flex h-full w-full max-w-full
        min-w-full flex-col flex-wrap items-center justify-start gap-0
        bg-gray-100
        px-0.5 py-1
        sm:flex-row sm:flex-nowrap
        sm:justify-center dark:bg-gray-900

        ">
        <section
            id="Main-Nav-Menu"
            class="event_launcher_menu
        flex
        h-full
        max-w-xs min-w-56 basis-1/5
        flex-col
        items-center justify-start
        gap-1 overflow-y-auto border-r border-gray-200 pt-0.5
        pr-0.5

        md:min-w-64 lg:min-w-72 dark:border-gray-700
        "
            class:hidden={launcher_loc.current.hide__launcher_menu}>
            <Launcher_menu
                {lq__event_obj}
                {lq__event_event_file_obj_li}
                {lq__location_event_file_obj_li}
                slct__event_file_id={$events_slct.event_file_id}
                {lq__event_location_obj_li}
                {lq__event_location_obj}
                slct__event_location_id={$events_slct.event_location_id}
                bind:loading__session_li_status={
                    $events_sess.launcher.loading__session_li_status
                }
                {lq__event_session_obj_li}
                bind:loading__session_id_status={
                    $events_sess.launcher.loading__session_id_status
                }
                bind:slct__event_session_id={$events_slct.event_session_id}
                bind:trigger_reload__event_session_obj_id={
                    $events_sess.launcher.trigger_reload__event_session_obj_id
                }
                bind:trigger_reload__event_session_obj_li={
                    $events_sess.launcher.trigger_reload__event_session_obj_li
                }
                bind:trigger_reload__event_location_obj_li={
                    $events_sess.launcher.trigger_reload__event_location_obj_li
                }></Launcher_menu>
        </section>

        <section
            id="Main-Content"
            class="event_launcher_main
        flex
        h-full
        max-w-full
        min-w-xs basis-4/5
        flex-col
        items-center justify-center gap-1
        overflow-y-auto
        px-0.5
        py-1
        ">
            {#if !$events_slct.event_location_id}
                <div
                    class="flex flex-row items-center justify-center p-8 opacity-50">
                    <MapPin size="1.5em" class="mx-2" />
                    <span>Please select a location from the menu</span>
                </div>
            {/if}

            {#if $events_slct.event_session_id}
                <Launcher_session_view
                    bind:slct__event_session_id={$events_slct.event_session_id}
                ></Launcher_session_view>
            {:else if $events_slct.event_location_id}
                <!-- Location selected but no session chosen yet — prompt operator -->
                <div
                    class="flex flex-col items-center justify-center p-8 opacity-50">
                    <LoaderCircle class="mb-2 animate-spin" />
                    <span>Select a session from the menu</span>
                </div>
            {/if}
        </section>
    </div>

    {@render children?.()}
</div>

<footer
    id="Main-Footer"
    class:hidden={launcher_loc.current.hide__launcher_footer}
    class="
        absolute
        right-0 bottom-0 left-0 z-20
        m-auto flex
        w-full max-w-7xl

        flex-row items-center justify-between border-t

        border-gray-300

        bg-gray-200 p-1 text-xs
        opacity-70 transition-opacity

        duration-500 hover:opacity-100
        dark:border-gray-600 dark:bg-gray-800
        ">
    <div
        class="slct_location_name transition-colors duration-300"
        title="Location ID: {$lq__event_location_obj?.event_location_id} Name: {$lq__event_location_obj?.name} | Device ID: {$lq__event_device_obj?.event_device_id} Name: {$lq__event_device_obj?.name}">
        <!-- Edit mode toggle: needs tap target for tablet operators -->
        <button
            type="button"
            class="hover:bg-surface-500/10 rounded px-1.5 py-1 transition-colors"
            onclick={() => {
                $ae_loc.edit_mode = !$ae_loc.edit_mode;
            }}
            title="Toggle Edit Mode to show location options and more">
            <span class="sr-only">Location:</span>
            <MapPin size="1em" />
        </button>
        {$lq__event_location_obj?.name}
        {#if $lq__event_device_obj?.name}
            <Laptop size="1em" class="mx-1" />
            {$lq__event_device_obj?.name}
        {/if}
    </div>

    <span
        class:preset-tonal-warning={!$idle}
        class:preset-tonal-success={$idle}
        class="group rounded-md px-2 py-0.5 transition-colors duration-300"
        title="The user is currently {$idle ? 'idle' : 'active'}">
        {#if $idle}
            <BedDouble size="1em" class="mx-1" />
            <span class="hidden group-hover:inline"> Idle </span>
        {:else}
            <MousePointer size="1em" class="mx-1" />
            <span class="hidden group-hover:inline"> Active </span>
        {/if}
    </span>

    <span
        class="group rounded-md px-2 py-0.5 transition-colors duration-300"
        title="Online status = {online?.current}">
        <Wifi size="1em" class="mx-1" />
        {online?.current ? '' : 'Offline!'}
    </span>

    <span
        class:hidden={!launcher_loc.current.ws_connect}
        class:preset-tonal-warning={$events_sess.launcher.ws_connect_status !=
            'connected'}
        class:preset-tonal-success={$events_sess.launcher.ws_connect_status ==
            'connected'}
        class="group rounded-md px-2 py-0.5 transition-colors duration-300"
        title="WebSocket is {$events_sess.launcher.ws_connect_status ==
        'connected'
            ? 'connected'
            : 'disconnected'} API: {$ae_api?.base_url}">
        {#if $events_sess.launcher.ws_connect_status == 'connected'}
            <Network size="1em" class="mx-1 text-green-700" />
            <span class="hidden group-hover:inline"> WS Connected </span>
        {:else}
            <X size="1em" class="mx-1" />
            <span class="hidden group-hover:inline"> WS Disconnected </span>
        {/if}
    </span>

    <!-- Font size cycler: allows onsite operators to adjust text size without needing
         access to the global sys menu, which may be unavailable in kiosk/locked setups. -->
    <button
        type="button"
        onclick={() => {
            const mode = $ae_loc.font_size_mode;
            if (!mode || mode === 'default') $ae_loc.font_size_mode = 'larger';
            else if (mode === 'larger') $ae_loc.font_size_mode = 'smaller';
            else $ae_loc.font_size_mode = 'default';
        }}
        class="group hover:bg-surface-500/10 rounded-md px-2 py-0.5 font-mono font-bold transition-colors duration-200"
        title="Font size: {$ae_loc.font_size_mode ??
            'default'} — tap to cycle (default → larger → smaller)">
        {#if $ae_loc.font_size_mode === 'larger'}
            <span>A+</span>
        {:else if $ae_loc.font_size_mode === 'smaller'}
            <span class="text-[0.7em]">A−</span>
        {:else}
            <span>A</span>
        {/if}
    </button>

    <div
        class="current_datetime px-2 font-mono transition-colors hover:bg-white hover:font-bold dark:hover:bg-slate-700">
        <span class="hidden md:inline">
            <CalendarDays size="1em" />
            {ae_util.iso_datetime_formatter($time, 'date_full_no_year')}
        </span>
        <span class="hidden sm:inline">
            <Clock size="1em" />
        </span>
        {#if launcher_loc.current?.time_hours == 12}
            {ae_util.iso_datetime_formatter($time, 'time_12_long')}
        {:else}
            {ae_util.iso_datetime_formatter($time, 'time_long')}
        {/if}
    </div>
</footer>

<div class="absolute top-0 left-0 z-20 text-center">
    <button
        type="button"
        onclick={() => (launcher_loc.current.hide_drawer__cfg = false)}
        class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 p-3 transition-colors duration-300"
        class:opacity-25={!$ae_loc.trusted_access}
        class:hover:opacity-75={!$ae_loc.trusted_access}>
        <Biohazard size="1em" />
        <span class="hidden">Launcher Config</span>
    </button>
</div>

{#if modal_cfg_open}
    <!-- Backdrop: full-screen overlay. Clicking anywhere on it closes the cfg panel. -->
    <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
    <div
        role="presentation"
        class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/60 p-4"
        onclick={close_cfg}
        transition:fade={{ duration: 200 }}>
        <!-- Panel: stop-propagation so clicks inside don't reach the backdrop handler. -->
        <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
        <div
            role="dialog"
            aria-modal="true"
            aria-label="Launcher Settings"
            tabindex="-1"
            class="flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-lg shadow-2xl"
            onclick={(e) => e.stopPropagation()}>
            <Launcher_cfg></Launcher_cfg>

            <div
                class="bg-surface-100-900 flex flex-row flex-wrap items-center justify-center gap-2 border-t border-surface-500/10 p-4">
                <a
                    href="/events/{$events_slct.event_id}"
                    class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
                    <Search size="1em" class="m-1" />
                    Session Search
                </a>
                {#if $events_slct?.event_location_id}
                    <a
                        href="/events/{$events_slct.event_id}/location/{$events_slct.event_location_id}"
                        class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
                        <MapPin size="1em" class="m-1" />
                        View Selected Location
                    </a>
                {/if}
                {#if $events_slct?.event_session_id}
                    <a
                        href="/events/{$events_slct.event_id}/session/{$events_slct.event_session_id}"
                        class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500">
                        <GraduationCap size="1em" class="m-1" />
                        View Selected Session
                    </a>
                {/if}
            </div>
        </div>
    </div>
{/if}

<Drawer
    activateClickOutside={false}
    class="bg-red-50 opacity-75 transition-all duration-300 hover:opacity-95 dark:bg-slate-900"
    placement="bottom"
    {...{
        transitionType: 'fly',
        transitionParams: {
            y: 320,
            duration: 200,
            easing: sineIn
        }
    }}
    bind:hidden={launcher_loc.current.hide_drawer__debug}
    id="sidebar2">
    <div class="flex flex-row items-center justify-between">
        <h2
            class="mb-4 text-center text-base font-semibold text-gray-500 dark:text-gray-400">
            Debug
        </h2>
        <button
            type="button"
            onclick={() => (launcher_loc.current.hide_drawer__debug = true)}
            class="mb-4 dark:text-white">
            <X size="1em" />
            <span class="hidden">Close Debug Drawer</span>
        </button>
    </div>

    <div>
        <pre class="text-xs">
        {JSON.stringify(launcher_loc.current, null, 2)}
    </pre>
        <hr />
        <pre class="text-xs">
        {JSON.stringify($ae_api, null, 2)}
    </pre>
    </div>
</Drawer>

<Modal
    open={$events_sess.launcher?.modal__open_event_file_id}
    autoclose={false}
    placement="top-center"
    class="
        relative flex flex-col items-center
        justify-center divide-y divide-gray-200
        rounded-lg border-gray-200 bg-gray-500/90 text-gray-800
        shadow-md
        dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800/90 dark:text-gray-200
        {launcher_loc.current.controller == 'remote' ? 'min-h-full' : ''}
        min-w-full
        "
    bodyClass="p-0 space-y-0 overflow-auto flex flex-col gap-1 items-center justify-center pb-14"
    headerClass={`fixed top-0 right-0 left-0 p-1 md:p-2 flex flex-row items-center ${launcher_loc.current.controller == 'remote' ? 'hidden' : ''} bg-white dark:bg-gray-800 opacity-50 ${launcher_loc.current.hide__modal_header_title ? 'justify-center' : 'justify-between'}`}
    footerClass="text-center hidden">
    {#snippet header()}
        <h3
            class:hidden={launcher_loc.current.hide__modal_header_title}
            class="text-lg font-semibold opacity-20 transition-all hover:opacity-100">
            {$events_sess.launcher?.modal__title ?? 'Digital Poster Display'}
        </h3>
        <button
            type="button"
            class="btn group flex-row-reverse justify-self-end transition-all"
            onclick={() => {
                $events_sess.launcher.modal__open_event_file_id = null;
            }}
            title="Close Modal">
            <X size="1em" class="my-1.5" />
            <span class="hidden group-hover:inline"> Close</span>
        </button>
    {/snippet}

    <!-- WHY: overflow-auto on wrapper + touch-action: pinch-zoom on the img enables
         native pinch-to-zoom on mobile/tablet without any JS library.
         Toggling modal_zoom_fit switches between 'fit to viewport' (default, clean display)
         and 'natural size' mode where the operator can pan/scroll and pinch-zoom freely
         for closer inspection or accessibility accommodation. -->
    <div
        class="flex w-full flex-1 items-center justify-center"
        class:overflow-auto={!modal_zoom_fit}
        class:overflow-hidden={modal_zoom_fit}>
        {#if $events_sess.launcher.modal__event_file_obj?.hosted_file_id}
            <!-- WHY: Use hosted_file endpoint (not event_file) — the event_file download
                 endpoint requires auth headers that a plain <img> tag cannot send (→ 403).
                 The hosted_file endpoint accepts key=account_id as a query param and is
                 the proven browser-compatible path for direct file display. -->
            <img
                src="{$ae_api.base_url}/v3/action/hosted_file/{$events_sess
                    .launcher.modal__event_file_obj
                    .hosted_file_id}/download?return_file=true&filename={encodeURIComponent(
                    $events_sess.launcher.modal__event_file_obj.filename ?? ''
                )}&key={$ae_api.account_id}"
                alt="Poster: {$events_sess.launcher.modal__title}"
                ondblclick={() => {
                    modal_zoom_fit = !modal_zoom_fit;
                    // Sync zoom state to the remote display when acting as controller.
                    if (
                        launcher_loc.current.controller == 'local_push' &&
                        $events_sess.launcher.ws_connect_status == 'connected'
                    ) {
                        $events_sess.launcher.controller_cmd = `ae_zoom:${modal_zoom_fit ? 'fit' : 'zoom'}`;
                        $events_sess.launcher.controller_trigger_send = true;
                    }
                }}
                title="Double-tap to toggle zoom / fit"
                class="block transition-[max-height,max-width,width] duration-200"
                class:max-h-[85dvh]={modal_zoom_fit}
                class:max-w-full={modal_zoom_fit}
                class:w-auto={modal_zoom_fit}
                class:object-contain={modal_zoom_fit}
                class:cursor-zoom-in={modal_zoom_fit}
                class:cursor-zoom-out={!modal_zoom_fit}
                style="touch-action: pinch-zoom;" />
        {:else}
            <div class="flex flex-row items-center justify-center p-4">
                <Info size="1em" class="mx-1" />
                <span>No image selected</span>
            </div>
        {/if}
    </div>

    <!-- Bottom control bar — hidden on the remote display (operator-free kiosk) -->
    <!-- WHY: pb-14 on bodyClass reserves space so these buttons don't obscure poster content. -->
    <div
        class="
            absolute right-0 bottom-0 left-0
            flex flex-row items-center justify-between gap-2
            bg-black/30
            p-1.5 backdrop-blur-sm
            "
        class:hidden={launcher_loc.current.controller == 'remote'}>
        <!-- Zoom / Fit toggle: accessibility accommodation — lets operators and general
             public zoom in to read details, pinch on mobile, or double-tap the image. -->
        <button
            type="button"
            onclick={() => {
                modal_zoom_fit = !modal_zoom_fit;
                // Sync zoom state to the remote display when acting as controller.
                if (
                    launcher_loc.current.controller == 'local_push' &&
                    $events_sess.launcher.ws_connect_status == 'connected'
                ) {
                    $events_sess.launcher.controller_cmd = `ae_zoom:${modal_zoom_fit ? 'fit' : 'zoom'}`;
                    $events_sess.launcher.controller_trigger_send = true;
                }
            }}
            class="btn btn-sm preset-tonal-surface opacity-80 transition-opacity hover:opacity-100"
            title={modal_zoom_fit
                ? 'Pan / Zoom mode — pinch or double-tap image to zoom'
                : 'Fit image to screen'}>
            {#if modal_zoom_fit}
                <ZoomIn size="1em" class="mr-1" />
                <span class="hidden sm:inline">Zoom</span>
            {:else}
                <Minimize2 size="1em" class="mr-1" />
                <span class="hidden sm:inline">Fit</span>
            {/if}
        </button>

        <!-- Close Both: sends WS close to remote display AND dismisses this modal.
             Remote screensaver will resume cycling after idle timeout. -->
        <button
            type="button"
            onclick={() => {
                $events_sess.launcher.controller_cmd = `ae_close:event_file_modal`;
                $events_sess.launcher.controller_trigger_send = true;
                $events_sess.launcher.modal__title = '';
                $events_sess.launcher.modal__open_event_file_id = null;
                $events_sess.launcher.modal__event_file_obj = null;
            }}
            class="btn btn-sm preset-tonal-error opacity-80 transition-all hover:opacity-100"
            class:hidden={launcher_loc.current.controller != 'local_push' ||
                $events_sess.launcher.ws_connect_status != 'connected'}
            title="Close poster on this device and on the remote display (screensaver resumes)">
            <Monitor size="1em" class="mr-1" />
            <X size="1em" />
            <span class="ml-1 hidden sm:inline">Close Both</span>
        </button>

        <!-- Back to List: dismisses this controller's view only.
             Remote display keeps showing the current poster until next action or screensaver. -->
        <button
            type="button"
            onclick={() => {
                $events_sess.launcher.modal__title = '';
                $events_sess.launcher.modal__open_event_file_id = null;
                $events_sess.launcher.modal__event_file_obj = null;
            }}
            class="btn btn-sm preset-tonal-surface border-surface-400/50 border opacity-80 transition-all hover:opacity-100"
            class:hidden={!$ae_loc.trusted_access &&
                (launcher_loc.current.controller != 'local_push' ||
                    $events_sess.launcher.ws_connect_status != 'connected')}
            title="Close poster on this device only — remote display keeps showing">
            <List size="1em" class="mr-1" />
            <span class="hidden sm:inline">Back to List</span>
        </button>
    </div>
</Modal>

{#if launcher_loc.current.controller_group_code && launcher_loc.current.ws_connect}
    <Element_websocket
        {log_lvl}
        bind:ws_connect={launcher_loc.current.ws_connect}
        bind:ws_connect_status={$events_sess.launcher.ws_connect_status}
        ws_server={$ae_api.fqdn}
        api_key={$ae_api.api_secret_key}
        jwt={$ae_loc.jwt}
        bind:group_id={launcher_loc.current.controller_group_code}
        bind:client_id={launcher_loc.current.controller_client_id}
        bind:cmd={$events_sess.launcher.controller_cmd}
        bind:trigger_send={$events_sess.launcher.controller_trigger_send}
        bind:trigger_connect={$events_sess.launcher.trigger__ws_connect}
        bind:trigger_disconnect={$events_sess.launcher.trigger__ws_disconnect}
        bind:hide__ws_element={launcher_loc.current.hide__ws_element}
        bind:hide__ws_form={launcher_loc.current.hide__ws_form}
        bind:hide__ws_messages={launcher_loc.current.hide__ws_messages}
        bind:hide__ws_commands={launcher_loc.current.hide__ws_commands}
        bind:ws_conn_status={trigger_handle_ws_conn}
        bind:ws_recv_status={trigger_handle_ws_recv}
        bind:ws_sent_status={trigger_handle_ws_sent} />
{/if}
