/**
 * Контроллер персонажа: тело физики + walk-анимация от скорости.
 * Фаза клипа растёт пропорционально горизонтальной скорости (шаги не
 * «скользят» по земле), в покое замирает; персонаж поворачивается по
 * направлению движения. Мир/клип/модель приходят снаружи — контроллер
 * только связывает физику и анимацию (чистая композиция, урок №3).
 */
import type { Body } from './body';
import { makeBody, stepBody } from './body';
import type { SolidWorld } from './body';
import type { Vec3 } from '../animation/mat';
import type { AnimClip } from '../animation/clip';
import { samplePose } from '../animation/clip';
import type { VoxelRig, PosedVoxel } from '../animation/rig';
import { poseVoxels } from '../animation/rig';
import type { VoxelModel } from '../models/format';

export interface CharacterOptions {
    /** AABB тела [w, h, d]. */
    size?: readonly [number, number, number];
    /** Скорость ходьбы, вокс/с. */
    speed?: number;
    /** Путь за полный цикл шагов, вокселей (фаза = путь/stride). */
    strideLength?: number;
    /** Высота спавна (по умолчанию 2 — грань земли демо; выше — спавн в воздухе). */
    spawnY?: number;
}

/** Деформированный воксель позы для рендера (облако движка). */
export type CharacterVoxel = Pick<PosedVoxel, 'pos' | 'color' | 'tile' | 'rotY'>;

export class Character {
    readonly body: Body;
    readonly rig: VoxelRig;
    readonly model: VoxelModel;
    readonly clip: AnimClip;
    readonly speed: number;
    readonly strideLength: number;
    /** Фаза walk-цикла в циклах (растёт без заворота — гейт читает прирост). */
    phase = 0;
    /** Текущий поворот вокруг оси Y (рад): куда смотрит модель. */
    yaw = 0;

    constructor(
        rig: VoxelRig,
        model: VoxelModel,
        clip: AnimClip,
        spawn: readonly [number, number],
        opts: CharacterOptions = {},
    ) {
        this.rig = rig;
        this.model = model;
        this.clip = clip;
        this.speed = opts.speed ?? 3;
        this.strideLength = opts.strideLength ?? 3.2;
        // спавн: y=2 — грань земли демо; выше — спавн в воздухе (гейт физики)
        this.body = makeBody([spawn[0], opts.spawnY ?? 2, spawn[1]], opts.size ?? [3, 10, 3]);
    }

    /** Желаемое движение в МИРОВЫХ осях (длина ≤ 1); [0,0] — покой. */
    setMove(dir: readonly [number, number]): void {
        this.body.vel[0] = dir[0] * this.speed;
        this.body.vel[2] = dir[1] * this.speed;
    }

    /** Один шаг: физика, фаза от пройденного пути, поворот по движению. */
    step(world: SolidWorld, dt: number): void {
        stepBody(world, this.body, dt);
        const speed = Math.hypot(this.body.vel[0], this.body.vel[2]);
        if (speed > 1e-6) {
            this.phase += (speed * dt) / this.strideLength;
            this.yaw = Math.atan2(this.body.vel[0], this.body.vel[2]);
        }
    }

    /**
     * Деформированные воксели позы для рендера (центр модели по XZ тела).
     * Модель повёрнута по yaw жёстко: позиции вращаются вокруг тела, и каждый
     * куб несёт rotY = yaw — иначе на диагональном ходе кубы остаются
     * осями-выровненными и силуэт «пилит» углами.
     */
    visual(): CharacterVoxel[] {
        const posed = poseVoxels(this.rig, this.model, samplePose(this.clip, this.loopPhase()));
        const b = this.body;
        const c = Math.cos(this.yaw), s = Math.sin(this.yaw);
        const halfW = this.model.size[0] / 2, halfD = this.model.size[2] / 2;
        return posed.map((v) => {
            const dx = v.pos[0] - halfW, dz = v.pos[2] - halfD;
            return {
                pos: [
                    b.pos[0] + dx * c + dz * s,
                    b.pos[1] + v.pos[1],
                    b.pos[2] - dx * s + dz * c,
                ] as Vec3,
                color: v.color,
                ...(v.tile ? { tile: v.tile } : {}),
                rotY: this.yaw,
            };
        });
    }

    /** Фаза в секундах клипа: циклы → длительность (для семплинга позы). */
    loopPhase(): number {
        const c = ((this.phase % 1) + 1) % 1;
        return c * this.clip.duration;
    }
}