/**
 * Катсцены как данные: жанронезависимый рантайм (третий повтор паттерна
 * «стек шагов» в игре — поднят в ядро). Шаг {dur, fn?, cam?}: dur — секунды
 * ручного времени цикла (детерминизм гейтов), fn — разовое действие в
 * начале шага, cam — цель камеры (глайд от точки старта шага). Рантайм
 * про рендер не знает: камера — инъекция (get/set), мир снаружи замер
 * (update звать вместо тика мира); по окончании снаружи возвращает камеру
 * к герою (политика игры, не движка).
 */

/** Шаг катсцены: длительность, действие на старте, цель камеры. */
export interface CutsceneStep {
    /** Длительность шага (сек ручного времени цикла). */
    dur: number;
    /** Разовое действие в начале шага. */
    fn?: () => void;
    /** Цель камеры (мир): глайд от точки старта шага. */
    cam?: readonly [number, number, number];
}

/** Снапшот для моста: играет ли, шаг, таймер (числовой дебаг сцен). */
export interface CutsceneSnapshot {
    active: boolean;
    /** Индекс шага (0-базный) и общее число шагов (0 вне катсцены). */
    step: number;
    of: number;
    /** Время внутри шага (сек). */
    t: number;
}

/** Камера-носитель: точка старта глайда и установка цели. */
export interface CutsceneCamera {
    get(): readonly [number, number, number];
    set(p: readonly [number, number, number]): void;
}

export class CutsceneRunner {
    private steps: readonly CutsceneStep[] = [];
    private i = 0;
    private t = 0;
    /** Откуда глайдит камера (позиция на старте шага). */
    private from: readonly [number, number, number] | null = null;

    constructor(private readonly camera: CutsceneCamera) {}

    /** Играет ли катсцена (пустой стек = нет). */
    get active(): boolean {
        return this.i < this.steps.length;
    }

    /** Суммарная длительность текущей катсцены (гейтам: сколько тиков ждать). */
    get duration(): number {
        return this.steps.reduce((s, st) => s + st.dur, 0);
    }

    /** Начать катсцену: первый шаг стартует сразу; прерывает играющую. */
    start(steps: readonly CutsceneStep[]): void {
        this.abort();
        this.steps = steps;
        this.i = 0;
        this.t = 0;
        this.from = null;
        steps[0]?.fn?.(); // действие первого шага — сразу (посадка/тост)
    }

    /** Снять играющую катсцену (прерывание: переход области, новый диалог). */
    abort(): void {
        this.steps = [];
        this.i = 0;
        this.t = 0;
        this.from = null;
    }

    /** Тик: таймер шага + глайд камеры; true — ещё играет. Глайд по времени
     *  ПОСЛЕ инкремента — на последнем тике шага камера достигает цели
     *  точно (k = 1). Действие шага может перезапустить катсцену
     *  (start/abort) — тогда этот тик уже принадлежит новой сцене. */
    update(dt: number): boolean {
        if (!this.active) return false;
        const token = this.steps;
        const step = this.steps[this.i]!;
        this.t += dt;
        if (step.cam) {
            if (!this.from) this.from = this.camera.get();
            const f = this.from;
            const s = step.cam;
            const k = Math.min(1, this.t / step.dur);
            this.camera.set([f[0] + (s[0] - f[0]) * k, f[1] + (s[1] - f[1]) * k, f[2] + (s[2] - f[2]) * k]);
        }
        if (this.t < step.dur) return true;
        this.i += 1;
        this.t = 0;
        this.from = null;
        const next = this.steps[this.i];
        if (next) next.fn?.();
        else this.steps = []; // конец: active = false
        return this.steps === token && this.active;
    }

    /** Снапшот для моста/гейтов (числовой). */
    info(): CutsceneSnapshot {
        return this.active
            ? { active: true, step: this.i, of: this.steps.length, t: this.t }
            : { active: false, step: 0, of: 0, t: 0 };
    }
}

/** Валидация каталога катсцен агентом (как validateDialogue): пустых нет,
 *  dur конечен и > 0, cam — конечная тройка. Возвращает список ошибок. */
export function validateCutscenes(scenes: Record<string, readonly CutsceneStep[]>): string[] {
    const errs: string[] = [];
    for (const [id, steps] of Object.entries(scenes)) {
        if (!steps.length) errs.push(`${id}: катсцена пуста`);
        steps.forEach((s, i) => {
            if (!Number.isFinite(s.dur) || s.dur <= 0) errs.push(`${id}[${i}]: dur ${s.dur} — нужен конечный > 0`);
            if (s.cam && s.cam.some((v) => !Number.isFinite(v))) errs.push(`${id}[${i}]: cam не конечная тройка`);
        });
    }
    return errs;
}