/**
 * Дев-панель игры (трек 28): список объектов сцены + вьювер движка.
 * Движок даёт ModelViewer (орбита мышью, клипы, статы) — панель только
 * рисует DOM. Каталог берётся из реестра объектов (registry.ts): объекты
 * регистрируются там же, где создаются — devview ничего не перечисляет.
 * F3 (или «) — открыть/закрыть; панель не ставит мир на паузу — время и мир
 * живут, метка часов следует за потоком времени.
 */
import { ModelViewer, mannequin, resolveTiles } from '@rpg/engine';
import type { MannequinPalette, ModelStats, WeatherSpec } from '@rpg/engine';

/** Снапшот погоды панели: активный spec + счётчики слоёв + режим. */
export interface WeatherPanelInfo extends WeatherSpec {
    rainDrops?: number;
    snowFlakes?: number;
    /** Правки панели активны (null/undefined — база области). */
    manual?: Partial<WeatherSpec> | null;
}
import { registryAll, registryGet, type RegistryBuild, type RegistryEntry } from './registry';

/** Каталог дев-панели (мосту тоже нужен): id + подпись в порядке регистрации. */
export function devEntries(): Array<{ id: string; label: string }> {
    return registryAll().map((e) => ({ id: e.id, label: e.label }));
}

/** Построить объект каталога (мосту). */
export function buildDevEntry(id: string): RegistryBuild | null {
    return registryGet(id)?.build() ?? null;
}

/**
 * Панель: DOM-список (#dev) + вьювер-оверлей (#devview). onOpenChange —
 * игра показывает подсказку/обновляет HUD. getMinutes/setMinutes — часы
 * суток (слайдер и пресеты секции «Время суток»; время течёт и при
 * открытой панели — метка обновляется каждый тик, слайдер синхронизируется
 * на открытии и после перемоток).
 */
export class DevPanel {
    private viewer: ModelViewer | null = null;
    private raf = 0;
    private last = 0;
    private spinOn = false;

    constructor(
        private readonly onOpenChange: (open: boolean) => void,
        private readonly getMinutes: () => number,
        private readonly setMinutes: (m: number) => void,
        private readonly getWeather: () => WeatherPanelInfo,
        private readonly setWeather: (spec: Partial<WeatherSpec> | null) => void,
    ) {
        document.getElementById('dev-open')!.addEventListener('click', () => this.closeViewer());
        const playBtn = document.getElementById('dev-play')!;
        playBtn.addEventListener('click', () => {
            this.playing = !this.playing;
            this.viewer?.play(this.playing);
            playBtn.textContent = this.playing ? '⏸' : '⏵';
        });
        document.getElementById('dev-spin')!.addEventListener('change', (e) => {
            this.spinOn = (e.target as HTMLInputElement).checked;
            this.viewer?.setSpin(this.spinOn ? 0.8 : 0);
        });
        document.getElementById('dev-skin')!.addEventListener('change', (e) => {
            const i = Number((e.target as HTMLSelectElement).value);
            this.applySkin(i);
        });
        // Время суток: слайдер тянет часы сразу, пресеты — быстрая перемотка
        const slider = document.getElementById('dev-clock-slider') as HTMLInputElement;
        slider.addEventListener('input', () => {
            this.setMinutes(Number(slider.value));
            this.refreshClock();
        });
        for (const btn of document.querySelectorAll<HTMLButtonElement>('#dev-presets button')) {
            btn.addEventListener('click', () => {
                this.setMinutes(Number(btn.dataset.min));
                this.refreshClock();
            });
        }
        // Погода: слайдер тянет правку сразу (поверх базы области), сброс —
        // вернуть погоду области; счётчики слоёв обновляет живая погода
        const bind = (id: string, patch: (v: number) => Partial<WeatherSpec>): void => {
            const el = document.getElementById(id) as HTMLInputElement;
            el.addEventListener('input', () => {
                this.setWeather(patch(Number(el.value)));
                this.refreshWeather();
            });
        };
        bind('dev-w-cloud', (v) => ({ cloud: v }));
        bind('dev-w-rain', (v) => ({ rain: v }));
        bind('dev-w-snow', (v) => ({ snow: v }));
        bind('dev-w-wind', (v) => ({ windStrength: v }));
        bind('dev-w-dir', (deg) => ({ windDir: (deg * Math.PI) / 180 })); // слайдер в градусах
        document.getElementById('dev-w-reset')!.addEventListener('click', () => {
            this.setWeather(null);
            this.refreshWeather();
        });
    }

    private playing = true;
    private currentSkins: Array<[string, MannequinPalette]> = [];
    private currentEntry: RegistryEntry | null = null;

    /** Открыта ли панель (список или вьювер). */
    get open(): boolean {
        const el = document.getElementById('dev');
        return el !== null && !el.hidden;
    }

    /** Показать список объектов (время суток — синхронизировать с часами). */
    openPanel(): void {
        document.getElementById('dev')!.hidden = false;
        const list = document.getElementById('dev-list')!;
        list.replaceChildren();
        for (const e of devEntries()) {
            const btn = document.createElement('button');
            btn.textContent = e.label;
            btn.addEventListener('click', () => this.openViewer(e.id));
            list.appendChild(btn);
        }
        this.refreshClock();
        this.refreshWeather();
        this.onOpenChange(true);
    }

    /** Слайдер/метка из текущих часов игры (полная синхронизация). */
    refreshClock(): void {
        (document.getElementById('dev-clock-slider') as HTMLInputElement).value =
            String(this.getMinutes());
        this.refreshClockLabel();
    }

    /** Только метка времени: тик игры тянет её за часами (слайдер не трогаем —
     *  не мешаем тянущему его пользователю). */
    refreshClockLabel(): void {
        const m = this.getMinutes();
        const h = Math.floor(m / 60);
        const mm = m % 60;
        document.getElementById('dev-clock')!.textContent = `${h}:${mm < 10 ? '0' : ''}${mm}`;
    }

    /** Слайдеры/счётчики из активной погоды (база области + правки панели). */
    refreshWeather(): void {
        const w = this.getWeather();
        const set = (id: string, v: string): void => {
            document.getElementById(id)!.textContent = v;
        };
        const input = (id: string, v: number): void => {
            (document.getElementById(id) as HTMLInputElement).value = String(v);
        };
        input('dev-w-cloud', w.cloud);
        set('dev-w-cloud-v', w.cloud.toFixed(2));
        input('dev-w-rain', w.rain);
        set('dev-w-rain-v', w.rain.toFixed(2));
        input('dev-w-snow', w.snow);
        set('dev-w-snow-v', w.snow.toFixed(2));
        input('dev-w-wind', w.windStrength);
        set('dev-w-wind-v', w.windStrength.toFixed(2));
        input('dev-w-dir', Math.round((w.windDir * 180) / Math.PI));
        set('dev-w-dir-v', `${Math.round((w.windDir * 180) / Math.PI / 5) * 5}°`);
        const counts = `дождь: ${w.rainDrops ?? 0} · снег: ${w.snowFlakes ?? 0}`;
        set('dev-w-info', w.manual ? `${counts} · правка панели` : `${counts} · база области`);
    }

    /** Закрыть всё (панель и вьювер). */
    close(): void {
        this.closeViewer();
        document.getElementById('dev')!.hidden = true;
        this.onOpenChange(false);
    }

    /** Статы текущего объекта вьювера (мост/гейт). */
    stats(): ModelStats | null {
        return this.viewer?.stats() ?? null;
    }

    /** Открыть вьювер с объектом каталога. */
    openViewer(id: string): void {
        const entry = registryGet(id);
        if (!entry) return;
        const built = entry.build();
        this.closeViewer();
        const canvas = document.getElementById('dev-canvas') as HTMLCanvasElement;
        this.viewer = new ModelViewer(canvas, { width: 480, height: 360, viewSize: 24 });
        this.currentEntry = entry;
        this.currentSkins = built.skins ?? [];
        const select = document.getElementById('dev-skin') as HTMLSelectElement;
        select.replaceChildren();
        select.hidden = (this.currentSkins?.length ?? 0) < 2;
        (this.currentSkins ?? []).forEach(([name], i) => {
            const opt = document.createElement('option');
            opt.textContent = name;
            opt.value = String(i);
            select.appendChild(opt);
        });
        this.applySkin(0); // скины: перекрасит; без скинов — модель напрямую
        if (!built.skins) this.viewer.setModel(built.model, { rig: built.rig, clip: built.clip, tiles: built.tiles });
        (document.getElementById('dev-play') as HTMLButtonElement).textContent = '⏸';
        (document.getElementById('dev-spin') as HTMLInputElement).checked = false;
        this.spinOn = false;
        (document.getElementById('dev-stats') as HTMLDivElement).textContent = this.statText();
        document.getElementById('devview')!.hidden = false;
        this.playing = true;
        this.last = performance.now();
        const step = (now: number): void => {
            if (!this.viewer) return; // вьювер закрыт — цикл закончен
            this.viewer.render(Math.min(0.1, (now - this.last) / 1000));
            this.last = now;
            this.raf = requestAnimationFrame(step);
        };
        this.raf = requestAnimationFrame(step);
    }

    /** Закрыть вьювер (панель списка остаётся). */
    private closeViewer(): void {
        if (!this.viewer) return;
        cancelAnimationFrame(this.raf);
        this.viewer.dispose();
        this.viewer = null;
        document.getElementById('devview')!.hidden = true;
    }

    /** Перекрасить модель скином i (пересборка модели + облака). */
    private applySkin(i: number): void {
        if (!this.viewer || !this.currentEntry) return;
        const built = this.currentEntry.build();
        const skins = built.skins ?? this.currentSkins;
        if (!skins || skins.length === 0) return;
        const palette = skins[Math.min(i, skins.length - 1)][1];
        const { model, rig } = mannequin(palette);
        this.viewer.setModel(model, { rig, clip: built.clip, tiles: resolveTiles(model) });
    }

    /** Статы для подписи под вьювером. */
    private statText(): string {
        const s = this.viewer?.stats();
        return s ? `${s.voxels} вокс · ${s.size.join('×')} · костей: ${s.bones} · тайлей: ${s.tiles}` : '';
    }
}