/**
 * Кадровый аниматор спрайта с именованными клипами: play/pause, режимы
 * loop/once/pingpong, события onFrame/onFinish, рассинхрон толпы через
 * randomPhase (seed). Тонкий Pixi-адаптер: математика — в anim/clip.ts,
 * текстура пишется только при смене шага. Тикается вручную либо через
 * engine.fx (реестр Updatables).
 */
import type { Sprite, Texture } from 'pixi.js';
import type { Updatable } from '../core/Updatables';
import { createRng } from '../math/rng';
import { stepClip, type ClipLoop } from './clip';

export interface SpriteClip {
    frames: readonly Texture[];
    /** Кадров в секунду; без значения — дефолт аниматора. */
    fps?: number;
    loop?: ClipLoop;
    speedScale?: number;
}

export interface SpriteAnimatorOptions {
    /** Дефолт fps для клипов без своего (8, как у FrameAnimation). */
    fps?: number;
    /** Дефолтный режим для клипов без своего. */
    loop?: ClipLoop;
    speedScale?: number;
    /** Seed: детерминированная случайная фаза старта (рассинхрон толпы). */
    randomPhase?: number;
    /** Шаг, который показывать до первого play(). */
    startStep?: number;
}

export class SpriteAnimator implements Updatable {
    readonly sprite: Sprite;
    /** Вызывается при смене шага. */
    onFrame?: (step: number) => void;
    /** Вызывается один раз при finished у once/pingpong-прохода. */
    onFinish?: (clip: string) => void;

    /** Самозачистка из engine.fx: спрайт разрушен — реестр отпустит запись. */
    get destroyed(): boolean {
        return this.sprite.destroyed;
    }

    private clips: Record<string, SpriteClip>;
    private defaults: Required<Pick<SpriteAnimatorOptions, 'fps' | 'loop'>>;
    private speedScale_: number;
    private currentName: string | null = null;
    private time = 0;
    private step_ = 0;
    private paused_ = false;
    private finishedNotified = false;
    /** Случайная фаза старта в долях шага [0,1) — из seed (без Math.random). */
    private phase = 0;

    constructor(
        sprite: Sprite,
        clips: Record<string, SpriteClip>,
        current?: string,
        opts: SpriteAnimatorOptions = {}
    ) {
        this.sprite = sprite;
        this.clips = clips;
        this.defaults = { fps: opts.fps ?? 8, loop: opts.loop ?? 'loop' };
        this.speedScale_ = opts.speedScale ?? 1;
        if (opts.randomPhase !== undefined) this.applyRandomPhase(opts.randomPhase);
        if (current !== undefined) {
            this.play(current, { restart: true });
        } else {
            // Стартовый шаг первого клипа — просто отрисовать, не запуская время.
            const first = Object.keys(clips)[0];
            if (first !== undefined) {
                this.currentName = first;
                this.showStep(opts.startStep ?? 0);
            }
        }
    }

    /** Включить клип; тот же клип без restart продолжается с текущего шага. */
    play(name: string, opts: { restart?: boolean } = {}): void {
        const clip = this.clips[name];
        if (!clip) throw new Error(`клип «${name}» не найден`);
        if (name !== this.currentName || opts.restart) {
            this.currentName = name;
            this.time = this.phase;
            this.finishedNotified = false;
            this.setStep(this.currentName, 0);
        }
    }

    pause(): void {
        this.paused_ = true;
    }

    resume(): void {
        this.paused_ = false;
    }

    get paused(): boolean {
        return this.paused_;
    }

    get clip(): string | null {
        return this.currentName;
    }

    get step(): number {
        return this.step_;
    }

    get speedScale(): number {
        return this.speedScale_;
    }

    set speedScale(v: number) {
        this.speedScale_ = v;
    }

    /** Заморозить на конкретном шаге текущего клипа (idle-поза без клипа). */
    showStep(step: number): void {
        this.paused_ = true;
        this.setStep(this.currentName, step);
    }

    /** Заменить кадры клипа на лету (пересборка атласа, вариации окраса). */
    setClipFrames(name: string, frames: readonly Texture[]): void {
        const clip = this.clips[name];
        if (!clip) throw new Error(`клип «${name}» не найден`);
        clip.frames = frames;
        if (name === this.currentName) this.setStep(name, Math.min(this.step_, Math.max(0, frames.length - 1)));
    }

    update(dt: number): void {
        if (this.paused_ || this.currentName === null) return;
        const name = this.currentName;
        const clip = this.clips[name];
        if (clip.frames.length === 0) return;
        const def = { fps: clip.fps ?? this.defaults.fps, loop: clip.loop ?? this.defaults.loop, speedScale: clip.speedScale };
        const { time, tick } = stepClip(clip.frames.length, def, this.time, dt * this.speedScale_);
        this.time = time;
        if (tick.step !== this.step_) this.setStep(name, tick.step);
        if (tick.finished && !this.finishedNotified) {
            this.finishedNotified = true;
            this.onFinish?.(name);
        }
        // pingpong/once с конечным временем: после конца время не двигается.
        if (tick.finished && def.loop === 'once') this.paused_ = true;
    }

    /** Случайная фаза из seed: доля периода клипа, детерминированная. */
    private applyRandomPhase(seed: number): void {
        // Единый ГПСЧ движка (mulberry32): [0,1) из целого seed без Math.random.
        this.phase = createRng(seed | 0 || 0x9e3779b9).next();
    }

    private setStep(name: string | null, step: number): void {
        if (name === null) return;
        const frames = this.clips[name].frames;
        this.step_ = step;
        const tex = frames[step];
        if (tex && this.sprite.texture !== tex) this.sprite.texture = tex;
        this.onFrame?.(step);
    }
}