/**
 * ModelViewer — просмотрщик воксельных моделей для дев-панели (трек 28).
 * Орбитальная камера поверх VoxelRenderer (тот же текстурный слой/тайли),
 * воспроизведение клипов риги, авто-поворот (turntable), статы модели.
 * Игровая дев-панель — надстройка игры: движок даёт класс, панель — UI.
 * Чистая математика орбиты и статов — отдельные функции (урок №3).
 */
import type { VoxelModel } from '../models/format';
import { decodeModel } from '../models/format';
import type { VoxelRig, Pose } from '../animation/rig';
import { poseVoxels } from '../animation/rig';
import type { AnimClip } from '../animation/clip';
import { samplePose } from '../animation/clip';
import type { Vec3 } from '../animation/mat';
import { VoxelRenderer, type CloudTiles } from './voxelRenderer';
import type { VoxelCloud } from './voxelRenderer';

/** Статы модели для панели (числовой снапшот агентом). */
export interface ModelStats {
    /** Размер сетки [sx, sy, sz]. */
    size: [number, number, number];
    /** Занятых вокселей. */
    voxels: number;
    /** Костей риги (0 — модель без риги). */
    bones: number;
    /** Число тайлей текстурного слоя (0 — без слоя tex). */
    tiles: number;
}

/** Сферическая орбита камеры: yaw/pitch вокруг target на дистанции. */
export function orbitPosition(
    yaw: number, pitch: number, dist: number, target: readonly [number, number, number],
): Vec3 {
    const cp = Math.cos(pitch);
    return [
        target[0] + dist * cp * Math.cos(yaw),
        target[1] + dist * Math.sin(pitch),
        target[2] + dist * cp * Math.sin(yaw),
    ];
}

/** Центр bbox занятых вокселей (ось вращения вьювера — модель крутится
 * вокруг своей середины, а не вокруг угла сетки). Пустая модель — центр
 * размера сетки. */
export function modelCenter(model: VoxelModel): [number, number, number] {
    const g = decodeModel(model);
    let min = [Infinity, Infinity, Infinity];
    let max = [-Infinity, -Infinity, -Infinity];
    for (let i = 0; i < g.data.length; i++) {
        if (g.data[i] === 0) continue;
        const x = i % g.sx, y = Math.floor(i / g.sx) % g.sy, z = Math.floor(i / (g.sx * g.sy));
        min = [Math.min(min[0], x), Math.min(min[1], y), Math.min(min[2], z)];
        max = [Math.max(max[0], x), Math.max(max[1], y), Math.max(max[2], z)];
    }
    if (min[0] === Infinity) return [model.size[0] / 2, model.size[1] / 2, model.size[2] / 2];
    return [(min[0] + max[0] + 1) / 2, (min[1] + max[1] + 1) / 2, (min[2] + max[2] + 1) / 2];
}

/** Числовые статы модели (занятые воксели — по декодированной сетке). */
export function modelStats(model: VoxelModel, rig?: VoxelRig): ModelStats {
    const grid = decodeModel(model);
    let voxels = 0;
    for (let i = 0; i < grid.data.length; i++) if (grid.data[i] !== 0) voxels++;
    return {
        size: [...model.size] as [number, number, number],
        voxels,
        bones: rig?.bones.length ?? 0,
        tiles: model.tiles ? Object.keys(model.tiles).length : 0,
    };
}

export interface ModelViewerOptions {
    /** размер бэкинг-стора (по умолчанию 480×360) */
    width?: number;
    height?: number;
    /** половина высоты орто-фрустума (зум), в воксельных единицах */
    viewSize?: number;
    /** скорость авто-поворота (рад/с); 0 — без вращения */
    spin?: number;
}

/** Просмотрщик: модель + клип риги, орбита мышью, статы. */
export class ModelViewer {
    readonly renderer: VoxelRenderer;
    private readonly canvas: HTMLCanvasElement;
    private readonly viewSize: number;
    private spinSpeed: number;
    private cloud: VoxelCloud | null = null;
    private model: VoxelModel | null = null;
    private rig: VoxelRig | null = null;
    private clip: AnimClip | null = null;
    private tiles: CloudTiles | null = null;
    private center: [number, number, number] = [0, 4, 0]; // ось орбиты
    private time = 0;
    private playing = true;
    private spin = 0; // накопленный авто-поворот (рад)
    // орбита: yaw/pitch — мышью или методами (гейт), zoom — множитель viewSize
    private yaw = Math.PI / 4;
    private pitch = 0.5;
    private zoom = 1;
    private drag: { x: number; y: number } | null = null;
    private disposed = false;

    constructor(canvas: HTMLCanvasElement, opts: ModelViewerOptions = {}) {
        this.canvas = canvas;
        const width = opts.width ?? 480;
        const height = opts.height ?? 360;
        this.viewSize = opts.viewSize ?? 16;
        this.spinSpeed = opts.spin ?? 0;
        this.renderer = new VoxelRenderer(canvas, { width, height, viewSize: this.viewSize });
        canvas.addEventListener('pointerdown', this.onDown);
        canvas.addEventListener('pointermove', this.onMove);
        canvas.addEventListener('pointerup', this.onUp);
        canvas.addEventListener('wheel', this.onWheel, { passive: false });
        this.placeCamera();
    }

    /**
     * Показать модель. rig+clip — анимация (поза семплится в render);
     * tiles — текстурный слой (`resolveTiles(model)`), палитра — в модели.
     */
    setModel(model: VoxelModel, opts: { rig?: VoxelRig; clip?: AnimClip; tiles?: CloudTiles } = {}): void {
        this.model = model;
        this.rig = opts.rig ?? null;
        this.clip = opts.clip ?? null;
        this.tiles = opts.tiles ?? null;
        this.center = modelCenter(model);
        this.time = 0;
        this.cloud?.dispose();
        const pose: Pose = {};
        this.cloud = this.renderer.addVoxelCloud(poseVoxels(this.rig ?? dummyRig(model), model, pose), this.tiles ?? undefined);
        this.refreshCloud();
        this.placeCamera();
    }

    /** Пауза/продолжение клипа. */
    play(on: boolean): void {
        this.playing = on;
    }

    /** Авто-поворот (turntable), рад/с; 0 — выключить. */
    setSpin(radPerSec: number): void {
        this.spinSpeed = radPerSec;
    }

    /** Прыжок клипа в момент t (сек). */
    setTime(t: number): void {
        this.time = t;
        this.refreshCloud();
    }

    /** Орбита программой (гейтом): угол поворота и возвышения (рад). */
    setOrbit(yaw: number, pitch: number): void {
        this.yaw = yaw;
        this.pitch = Math.min(Math.PI / 2 - 0.05, Math.max(0.05, pitch));
        this.placeCamera();
    }

    /** Зум: доля от базового viewSize (0.3..3). */
    setZoom(k: number): void {
        this.zoom = Math.min(3, Math.max(0.3, k));
        this.placeCamera();
    }

    /** Один кадр: авто-поворот, семпл позы клипа, рендер. */
    render(dt: number): void {
        if (this.disposed) return;
        if (this.spinSpeed) {
            this.spin += this.spinSpeed * dt;
            this.placeCamera();
        }
        if (this.clip) {
            if (this.playing) this.time = (this.time + dt) % this.clip.duration;
            this.refreshCloud();
        }
        this.renderer.render();
    }

    /** Статы текущей модели (панель/мост). */
    stats(): ModelStats | null {
        return this.model ? modelStats(this.model, this.rig ?? undefined) : null;
    }

    dispose(): void {
        this.disposed = true;
        this.cloud?.dispose();
        this.canvas.removeEventListener('pointerdown', this.onDown);
        this.canvas.removeEventListener('pointermove', this.onMove);
        this.canvas.removeEventListener('pointerup', this.onUp);
        this.canvas.removeEventListener('wheel', this.onWheel);
    }

    /** Пересемплить позу клипа в облако (облако есть — модель стоит). */
    private refreshCloud(): void {
        if (!this.cloud || !this.model) return;
        const pose = this.clip ? samplePose(this.clip, this.time) : {};
        this.cloud.update(poseVoxels(this.rig ?? dummyRig(this.model), this.model, pose));
    }

    /** Камера на сферической орбите вокруг центра модели. */
    private placeCamera(): void {
        const model = this.model;
        const target: [number, number, number] = model ? this.center : [0, 4, 0];
        const span = model ? Math.max(...model.size) : 10;
        const dist = Math.max(span * 1.6, 10) * this.zoom;
        const pos = orbitPosition(this.yaw + this.spin, this.pitch, dist, target);
        const cam = this.renderer.camera;
        cam.position.set(...pos);
        cam.lookAt(target[0], target[1], target[2]);
        cam.updateMatrixWorld();
    }

    private onDown = (e: PointerEvent): void => {
        this.drag = { x: e.clientX, y: e.clientY };
        this.canvas.setPointerCapture(e.pointerId);
    };

    private onMove = (e: PointerEvent): void => {
        if (!this.drag) return;
        this.yaw -= (e.clientX - this.drag.x) * 0.01;
        this.pitch = Math.min(Math.PI / 2 - 0.05, Math.max(0.05, this.pitch + (e.clientY - this.drag.y) * 0.01));
        this.drag = { x: e.clientX, y: e.clientY };
        this.placeCamera();
    };

    private onUp = (): void => {
        this.drag = null;
    };

    private onWheel = (e: WheelEvent): void => {
        e.preventDefault();
        this.setZoom(this.zoom * (e.deltaY > 0 ? 1.1 : 0.9));
    };
}

/** Рига-пустышка: модели без риги — один root, все воксели к нему. */
function dummyRig(model: VoxelModel): VoxelRig {
    const count = model.size[0] * model.size[1] * model.size[2];
    return { bones: [{ name: 'root', parent: -1, pivot: [0, 0, 0] }], binding: new Array(count).fill(0) };
}