/**
 * Рига воксельной модели — «анимация = данные» (docs/plan.md, направление v2).
 * Кость = имя + родитель + pivot (точка вращения в связочном пространстве
 * модели, где центр вокселя (x,y,z) — это (x+0.5, y+0.5, z+0.5)).
 * Привязка вокселей к костям — детерминированная: у генератора части известны
 * заранее (привязка передаётся явно), для чужих моделей — bindNearest
 * (ближайший pivot, при равенстве меньший индекс кости).
 * Деформация жёсткая на кость (кости не растягиваются — дистанции внутри
 * кости сохраняются по построению).
 */
import { decodeModel, texBytes } from '../models/format';
import type { VoxelModel } from '../models/format';
import { composeAffine, rotateAround, affinePoint } from './mat';
import type { Affine, Vec3 } from './mat';

export interface RigBone {
    name: string;
    /** Индекс кости-родителя; -1 у корня. Родитель всегда с меньшим индексом. */
    parent: number;
    pivot: Vec3;
}

export interface VoxelRig {
    bones: RigBone[];
    /** Воксель i (i = x + sx·(y + sy·z)) → индекс кости; -1 у пустых. */
    binding: number[];
}

/** Поза: по имени кости — поворот (рад) и сдвиг (воксельных единиц). */
export interface Pose {
    readonly [bone: string]: { rot?: Vec3; pos?: Vec3 } | undefined;
}

/** Деформированный воксель: позиция центра, цвет слота сцены и индекс в сетке. */
export interface PosedVoxel {
    pos: Vec3;
    color: string;
    /** Индекс вокселя в сетке модели (i = x + sx·(y + sy·z)) — для привязки к кости. */
    index: number;
    /** Id тайля текстур (0 = плоский цвет) — только у моделей со слоем tex. */
    tile?: number;
}

/** Привязка «ближайший pivot» для моделей без явной разметки частей. */
export function bindNearest(bones: RigBone[], model: VoxelModel): number[] {
    const grid = decodeModel(model);
    const binding = new Array<number>(grid.data.length).fill(-1);
    const centers = bones.map((b) => [b.pivot[0], b.pivot[1], b.pivot[2]]);
    for (let i = 0; i < grid.data.length; i++) {
        if (grid.data[i] === 0) continue;
        const x = i % grid.sx, y = Math.floor(i / grid.sx) % grid.sy, z = Math.floor(i / (grid.sx * grid.sy));
        const c = [x + 0.5, y + 0.5, z + 0.5];
        let best = 0, bestD = Infinity;
        for (let b = 0; b < centers.length; b++) {
            const d = (centers[b][0] - c[0]) ** 2 + (centers[b][1] - c[1]) ** 2 + (centers[b][2] - c[2]) ** 2;
            if (d < bestD) { bestD = d; best = b; } // при равенстве остаётся меньший индекс
        }
        binding[i] = best;
    }
    return binding;
}

/** Рига из костей: привязка передаётся явно (генератор знает части) или по ближайшему pivot. */
export function makeRig(bones: RigBone[], model: VoxelModel, binding?: number[]): VoxelRig {
    return { bones, binding: binding ?? bindNearest(bones, model) };
}

/** Список проблем риги (числовая валидация агентом); пустой — рига корректна. */
export function validateRig(rig: VoxelRig, model: VoxelModel): string[] {
    const problems: string[] = [];
    const { bones, binding } = rig;
    if (bones.length === 0) problems.push('нет костей');
    const seen = new Set<string>();
    bones.forEach((b, i) => {
        if (!b.name) problems.push(`кость ${i}: пустое имя`);
        if (seen.has(b.name)) problems.push(`кость ${i}: имя «${b.name}» повторяется`);
        seen.add(b.name);
        if (b.parent < -1 || b.parent >= bones.length) problems.push(`кость «${b.name}»: родитель ${b.parent} вне риги`);
        else if (b.parent >= i) problems.push(`кость «${b.name}»: родитель ${b.parent} не раньше самой кости (цикл)`);
    });
    const grid = decodeModel(model);
    if (binding.length !== grid.data.length) {
        problems.push(`привязка ${binding.length} байт, а вокселей ${grid.data.length}`);
        return problems;
    }
    for (let i = 0; i < binding.length; i++) {
        const empty = grid.data[i] === 0;
        if (empty && binding[i] !== -1) problems.push(`воксель ${i} пуст, но привязан к кости ${binding[i]}`);
        if (!empty && binding[i] === -1) problems.push(`воксель ${i} занят, но не привязан ни к какой кости`);
        if (!empty && (binding[i] < 0 || binding[i] >= bones.length)) {
            problems.push(`воксель ${i} привязан к кости ${binding[i]} вне риги`);
            break;
        }
    }
    return problems;
}

/** Мир-трансформы костей (связочное пространство → мир) по цепочкам родителей. */
export function boneWorlds(rig: VoxelRig, pose: Pose): Affine[] {
    // родители всегда раньше детей по индексу (validateRig) — считаем итеративно
    const worlds: Affine[] = [];
    rig.bones.forEach((bone, i) => {
        const p = pose[bone.name];
        const local = rotateAround(p?.rot ?? [0, 0, 0], bone.pivot, p?.pos ?? [0, 0, 0]);
        worlds[i] = bone.parent >= 0 && bone.parent < i
            ? composeAffine(worlds[bone.parent], local)
            : local;
    });
    return worlds;
}

/** Деформированные воксели модели в позе (центры в мировых координатах). */
export function poseVoxels(rig: VoxelRig, model: VoxelModel, pose: Pose): PosedVoxel[] {
    const grid = decodeModel(model);
    const worlds = boneWorlds(rig, pose);
    const tex = texBytes(model);
    const out: PosedVoxel[] = [];
    for (let i = 0; i < grid.data.length; i++) {
        const slot = grid.data[i];
        if (slot === 0) continue;
        const x = i % grid.sx, y = Math.floor(i / grid.sx) % grid.sy, z = Math.floor(i / (grid.sx * grid.sy));
        const pos = affinePoint(worlds[rig.binding[i]], [x + 0.5, y + 0.5, z + 0.5]);
        const tile = tex ? tex[i] : 0;
        out.push({ pos, color: model.palette[String(slot)], index: i, ...(tile ? { tile } : {}) });
    }
    return out;
}