/**
 * Пейзажная фауна (перенос v1 FaunaSystem, воксельная версия): безгласные
 * олени. Стоят, медленно бродят по соседним тайлам; на громкий звон рядом
 * подходят к источнику и стоят у него. В v1 звон — боевой удар героя
 * (колокольчик); боёвки в игре v2 пока нет — звон подаёт startleAt
 * (гейт зовёт мостом startleFauna, при переносе боя проводим от удара).
 */
import { Rng, encodeModel, VoxelGrid, type VoxelCloud, type VoxelModel } from '@rpg/engine';
import { UNIT, tileCenter } from './areas';
import { deerVoxels } from './deer';
import { registerObject } from './registry';

/** Радиус слуха на звон (v1: 7 юнитов = 7 тайлов → воксели). */
const HEAR_RADIUS = 7 * UNIT;
/** Скорость брожения / подхода (v1: тайлов/сек × UNIT + фидбек «живее»). */
const WANDER_SPEED = 0.45 * UNIT;
const APPROACH_SPEED = 1.0 * UNIT;
/** Пауза между шагами брожения (сек), как в v1: 1.5 + rng*3. */
const PAUSE_BASE = 1.5;
const PAUSE_RANGE = 3;

/** Состояние автомата оленя (снапшот моста). */
export type DeerState = 'idle' | 'wander' | 'approach';

export interface DeerInfo {
    /** Позиция ног (мир, воксели). */
    x: number;
    z: number;
    /** Тайл, на котором стоит (в тайлах v1). */
    tile: { x: number; y: number };
    state: DeerState;
}

interface Deer {
    pos: { x: number; z: number };
    dest: { x: number; z: number } | null;
    state: DeerState;
    /** Куда смотрит (четверти оборота от +X: 1 — +Z, 2 — −X, 3 — −Z). */
    dir: number;
    /** Пауза до следующего шага брожения (сек). */
    pause: number;
    /** Ключ последнего отрисованного вида (поза/позиция) — без лишних апдейтов. */
    drawn: string;
    cloud: VoxelCloud;
}

export interface FaunaDeps {
    /** Спавны в тайлах v1 (олень стоит в центре тайла). */
    spawns: readonly { x: number; y: number }[];
    /** Непроходимость тайла (вода/пропы) — туда олень не идёт. */
    blocked: (tx: number, ty: number) => boolean;
    /** Фабрика облаков вокселей (renderer.addVoxelCloud). */
    addCloud: (voxels: ReturnType<typeof deerVoxels>) => VoxelCloud;
}

/** Движение к точке с шагом не больше step; true — дошли (снап к точке). */
function stepTowards(
    pos: { x: number; z: number },
    to: { x: number; z: number },
    step: number,
): boolean {
    const dx = to.x - pos.x;
    const dz = to.z - pos.z;
    const d = Math.hypot(dx, dz);
    if (d <= step) {
        pos.x = to.x;
        pos.z = to.z;
        return true;
    }
    pos.x += (dx / d) * step;
    pos.z += (dz / d) * step;
    return false;
}

/** Модель из списка вокселей [x,y,z,цвет] (палитра — по уникальным цветам). */
function modelFromVoxels(list: Array<[number, number, number, string]>): VoxelModel {
    let max = [0, 0, 0] as [number, number, number];
    for (const [x, y, z] of list) max = [Math.max(max[0], x), Math.max(max[1], y), Math.max(max[2], z)];
    const g = new VoxelGrid(max[0] + 1, max[1] + 1, max[2] + 1);
    const palette: Record<number, string> = {};
    let next = 1;
    for (const [x, y, z, color] of list) {
        let slot = Object.entries(palette).find(([, c]) => c === color)?.[0];
        if (slot === undefined) {
            palette[next] = color;
            slot = String(next);
            next++;
        }
        g.set(x, y, z, Number(slot));
    }
    return encodeModel(g, palette);
}

/** Модель оленя для каталога: поза покоя у начала координат, сетка от
 *  минимальных границ (мировые координаты → локальные). */
function deerModel(): VoxelModel {
    const list = deerVoxels(0, 0, false).map((v) =>
        [Math.round(v.pos[0]), Math.round(v.pos[1]), Math.round(v.pos[2]), v.color] as [number, number, number, string]);
    const xs = list.map((v) => v[0]), ys = list.map((v) => v[1]), zs = list.map((v) => v[2]);
    const shift = [1 - Math.min(...xs), 1 - Math.min(...ys), 1 - Math.min(...zs)];
    return modelFromVoxels(list.map(([x, y, z, c]) => [x + shift[0], y + shift[1], z + shift[2], c] as [number, number, number, string]));
}

export class FaunaSystem {
    private deer: Deer[] = [];
    /** Детерминированный ГПСЧ — брожение одинаково между прогонами (v1). */
    private rng = new Rng(20260908);

    constructor(private deps: FaunaDeps) {
        registerObject({ id: 'deer', label: 'Олень', build: () => ({ model: deerModel() }) });
        for (const s of deps.spawns) this.spawn(s);
    }

    /** Заспавнить оленя в центре тайла (стоя, ноги на земле). */
    private spawn(s: { x: number; y: number }): void {
        const c = tileCenter(s.x, s.y);
        this.deer.push({
            pos: { x: c.x, z: c.z },
            dest: null,
            state: 'idle',
            dir: 0,
            pause: PAUSE_BASE + this.rng.next() * PAUSE_RANGE,
            drawn: '',
            cloud: this.deps.addCloud(deerVoxels(c.x, c.z, false)),
        });
    }

    /** Громкий звон рядом: олень подходит к источнику и стоит (v1). */
    startleAt(x: number, z: number): void {
        for (const d of this.deer) {
            if (Math.hypot(d.pos.x - x, d.pos.z - z) > HEAR_RADIUS) continue;
            d.dest = { x, z };
            d.state = 'approach';
        }
    }

    /** Шаг брожения: случайный соседний проходимый тайл (v1). */
    private startWander(d: Deer): void {
        const tx = Math.floor(d.pos.x / UNIT);
        const ty = Math.floor(d.pos.z / UNIT);
        const options = [[1, 0], [-1, 0], [0, 1], [0, -1]]
            .map(([dx, dy]) => ({ x: tx + dx, y: ty + dy }))
            .filter((t) => !this.deps.blocked(t.x, t.y));
        if (options.length === 0) {
            d.pause = 2;
            return;
        }
        const t = this.rng.pick(options);
        d.dest = tileCenter(t.x, t.y);
        d.state = 'wander';
    }

    /**
     * Тик фауны: автомат (idle→wander→idle, approach по звону) + отрисовка.
     * step — фаза шага (2 шага/сек на ходу, стоя — стоп-кадр), как в v1.
     */
    update(dt: number, t: number): void {
        for (const d of this.deer) {
            if (d.state === 'idle') {
                d.pause -= dt;
                if (d.pause <= 0) this.startWander(d);
            } else if (d.dest) {
                this.faceDest(d); // олень поворачивается мордой по ходу
                if (stepTowards(d.pos, d.dest, (d.state === 'approach' ? APPROACH_SPEED : WANDER_SPEED) * dt)) {
                    d.dest = null;
                    d.state = 'idle';
                    d.pause = PAUSE_BASE + this.rng.next() * PAUSE_RANGE;
                }
            }
            this.draw(d, t);
        }
    }

    /** Направить взгляд по вектору движения (доминирующая ось). */
    private faceDest(d: Deer): void {
        const dx = d.dest!.x - d.pos.x;
        const dz = d.dest!.z - d.pos.z;
        if (Math.abs(dx) >= Math.abs(dz)) d.dir = dx >= 0 ? 0 : 2;
        else d.dir = dz > 0 ? 1 : 3;
    }

    /** Перерисовать облако, если позиция, фаза шага или взгляд изменились. */
    private draw(d: Deer, t: number): void {
        const step = d.dest !== null && Math.floor(t * 2) % 2 === 0;
        const key = `${d.pos.x.toFixed(2)},${d.pos.z.toFixed(2)},${step ? 1 : 0},${d.dir}`;
        if (key === d.drawn) return;
        d.drawn = key;
        d.cloud.update(deerVoxels(d.pos.x, d.pos.z, step, d.dir));
    }

    /** Снапшот для моста: олени области. */
    info(): DeerInfo[] {
        return this.deer.map((d) => ({
            x: d.pos.x,
            z: d.pos.z,
            tile: { x: Math.floor(d.pos.x / UNIT), y: Math.floor(d.pos.z / UNIT) },
            state: d.state,
        }));
    }

    dispose(): void {
        for (const d of this.deer) d.cloud.dispose();
        this.deer.length = 0;
    }
}