import { EaseFn, linear } from './easing';

/**
 * Твины и таймеры: плавное изменение числовых свойств объекта и отложенные вызовы.
 * Менеджер обновляется игровым циклом (движок делает это сам в Engine.tick).
 */

export interface TweenOptions {
    /** Длительность в секундах. */
    duration: number;
    /** Функция сглаживания (по умолчанию linear). */
    ease?: EaseFn;
    /** Задержка до старта в секундах. */
    delay?: number;
    /** Вызывается по завершении. */
    onDone?: () => void;
}

/** Управление конкретным твином/таймером: можно отменить. */
export interface TweenHandle {
    cancel(): void;
}

interface ActiveTween {
    target: Record<string, number>;
    props: Record<string, number>;
    from: Record<string, number> | null; // снимается при старте (после delay)
    elapsed: number;
    options: TweenOptions;
    cancelled: boolean;
}

interface ActiveDelay {
    remaining: number;
    fn: () => void;
    cancelled: boolean;
}

export class TweenManager {
    private tweens: ActiveTween[] = [];
    private delays: ActiveDelay[] = [];

    /**
     * Плавно привести числовые свойства target к заданным значениям.
     * Начальные значения снимаются в момент старта (после delay).
     */
    to(target: object, props: Record<string, number>, options: TweenOptions): TweenHandle {
        const tween: ActiveTween = {
            target: target as Record<string, number>,
            props,
            from: null,
            elapsed: 0,
            options: { ease: linear, ...options },
            cancelled: false
        };
        this.tweens.push(tween);
        return { cancel: () => (tween.cancelled = true) };
    }

    /** Вызвать fn через seconds секунд. */
    delay(seconds: number, fn: () => void): TweenHandle {
        const d: ActiveDelay = { remaining: seconds, fn, cancelled: false };
        this.delays.push(d);
        return { cancel: () => (d.cancelled = true) };
    }

    /** Отменить все твины, изменяющие target (таймеры не привязаны к target). */
    cancelFor(target: object): void {
        for (const t of this.tweens) {
            if (t.target === target) t.cancelled = true;
        }
        this.tweens = this.tweens.filter((t) => !t.cancelled);
    }

    /** Отменить всё. */
    clear(): void {
        for (const t of this.tweens) t.cancelled = true;
        for (const d of this.delays) d.cancelled = true;
        this.tweens = [];
        this.delays = [];
    }

    /** dt в секундах (фиксированный шаг). */
    update(dt: number): void {
        // Твины: компакт на месте (без filter — тик не должен аллоцировать).
        let w = 0;
        for (const t of this.tweens) {
            if (t.cancelled) continue;
            const delay = t.options.delay ?? 0;
            let dtLeft = dt;
            if (t.elapsed < delay) {
                const skipped = Math.min(dtLeft, delay - t.elapsed);
                t.elapsed += skipped;
                dtLeft -= skipped;
                if (dtLeft <= 0) {
                    this.tweens[w++] = t; // dt целиком ушёл в delay — твин остаётся
                    continue;
                }
            }
            if (!t.from) {
                t.from = {};
                for (const key of Object.keys(t.props)) {
                    t.from[key] = t.target[key] ?? 0;
                }
            }
            t.elapsed += dtLeft;
            const raw = Math.min(1, (t.elapsed - (t.options.delay ?? 0)) / t.options.duration);
            const k = t.options.ease!(raw);
            for (const key of Object.keys(t.props)) {
                t.target[key] = t.from[key] + (t.props[key] - t.from[key]) * k;
            }
            if (raw >= 1) {
                t.cancelled = true;
                t.options.onDone?.();
            }
            if (!t.cancelled) this.tweens[w++] = t;
        }
        this.tweens.length = w;

        // Таймеры
        w = 0;
        for (const d of this.delays) {
            if (d.cancelled) continue;
            d.remaining -= dt;
            if (d.remaining <= 0) {
                d.cancelled = true;
                d.fn();
            }
            if (!d.cancelled) this.delays[w++] = d;
        }
        this.delays.length = w;
    }

    get activeCount(): number {
        return this.tweens.filter((t) => !t.cancelled).length + this.delays.filter((d) => !d.cancelled).length;
    }
}