import { Application, Graphics } from 'pixi.js';
import { Renderer, RendererOptions } from '../render/Renderer';
import { Camera } from '../render/Camera';
import { computeScale } from '../render/scale';
import { GameLoop } from './GameLoop';
import { FpsMeter } from './FpsMeter';
import { EventBus } from './EventBus';
import { TweenManager } from './Tween';
import { Updatables } from './Updatables';
import { InputManager } from '../input/InputManager';
import { SceneManager } from '../scene/SceneManager';

/**
 * Собирает подсистемы движка. Приложение создаёт Engine, добавляет стартовую
 * сцену через scenes.replace() и вызывает start().
 */
export interface EngineOptions extends RendererOptions {
    /** FPS фиксированного шага. */
    fixedFps: number;
    /** Лимит отрисовки, кадров/сек (0 — без лимита; шаги логики копятся). */
    maxFps?: number;
    /** Пересчитывать масштаб при ресайзе окна (по умолчанию true). */
    autoResize?: boolean;
}

export class Engine {
    readonly renderer: Renderer;
    readonly camera: Camera;
    readonly events = new EventBus();
    readonly scenes = new SceneManager();
    /** Твины и таймеры: обновляются каждым фиксированным шагом. */
    readonly tweens = new TweenManager();
    /** Эффекты/аниматоры/флэши, тикаемые движком сами (фиксированный шаг). */
    readonly fx = new Updatables();
    input!: InputManager;
    /** Дебаг-счётчик FPS (F3); создаётся в init(). */
    fpsMeter!: FpsMeter;
    private loop: GameLoop;
    private started = false;
    private ticks = 0;
    private readonly autoResize: boolean;
    private onWindowResize = (): void => {
        this.renderer.resize(
            computeScale(
                this.renderer.virtualWidth,
                this.renderer.virtualHeight,
                window.innerWidth,
                window.innerHeight
            )
        );
    };

    constructor(options: EngineOptions) {
        this.renderer = new Renderer(options);
        this.camera = new Camera(options.virtualWidth, options.virtualHeight);
        this.autoResize = options.autoResize ?? true;
        this.loop = new GameLoop(
            options.fixedFps,
            {
                update: (dt) => this.tick(dt),
                render: () => this.render()
            },
            { maxFps: options.maxFps }
        );
    }

    /**
     * Инициализация WebGL и ввода. Вызвать до scenes.replace()/start().
     * Возвращает готовый Application (для прямого доступа к Pixi при необходимости).
     */
    async init(): Promise<Application> {
        await this.renderer.setup();
        this.input = new InputManager(
            this.renderer.app.canvas,
            this.renderer.virtualWidth,
            this.renderer.virtualHeight
        );

        // Оверлей затемнения для переходов между сценами — верхний слой UI.
        const fade = new Graphics();
        this.renderer.uiRoot.addChild(fade);
        this.scenes.setOverlay(fade, this.renderer.virtualWidth, this.renderer.virtualHeight);

        // Дебаг-счётчик FPS (F3) — поверх мира, под фейдом переходов.
        this.fpsMeter = new FpsMeter();
        this.renderer.uiRoot.addChild(this.fpsMeter.view);

        if (this.autoResize) {
            window.addEventListener('resize', this.onWindowResize);
        }
        return this.renderer.app;
    }

    async start(): Promise<void> {
        if (this.started) return;
        this.started = true;
        this.loop.start();
    }

    stop(): void {
        this.loop.stop();
    }

    /**
     * Полная зачистка (HMR/выгрузка страницы): цикл, текущая сцена (exit —
     * там сцены снимают свои подписки и вьюхи), эффекты, твины, ввод,
     * FPS-метр, слушатели окна и Pixi-приложение. После destroy движок не годится.
     */
    destroy(): void {
        this.stop();
        void this.scenes.current?.exit();
        this.fx.clear();
        this.tweens.clear();
        this.events.clear();
        this.input?.dispose();
        this.fpsMeter?.dispose();
        window.removeEventListener('resize', this.onWindowResize);
        this.renderer.app.destroy(true, { children: true });
    }

    /** Счётчик фиксированных шагов с запуска (для агента и отладки). */
    get tickCount(): number {
        return this.ticks;
    }

    /** Длительность фиксированного шага, сек. */
    get fixedStep(): number {
        return this.loop.step;
    }

    /** Реальный FPS отрисовки (окно ~0,5 с) — для моста и отладки. */
    get fps(): number {
        return this.fpsMeter.fps;
    }

    /** Виртуальное разрешение (для пересчёта виртуальных px в CSS). */
    get virtualWidth(): number {
        return this.renderer.virtualWidth;
    }

    get virtualHeight(): number {
        return this.renderer.virtualHeight;
    }

    /** Один фиксированный шаг (без рендера) — мост/тесты крутят шаги сами. */
    stepTick(): number {
        this.tick(this.loop.step);
        this.ticks++;
        this.events.emit('engine:tick', { tick: this.ticks });
        return this.ticks;
    }

    /** Один кадр рендера без шага логики. */
    renderFrame(): void {
        this.render();
    }

    /**
     * n фиксированных шагов детерминированно: цикл на время переводится в ручной
     * режим, после — тайминги сбрасываются, чтобы rAF не «догонял» пропущенное.
     * finally: исключение в шаге не оставляет цикл в ручном режиме навсегда.
     */
    stepTicks(n = 1, render = false): number {
        this.loop.setManual(true);
        try {
            for (let i = 0; i < n; i++) {
                this.stepTick();
                if (render) this.render();
            }
        } finally {
            this.loop.setManual(false);
            this.loop.resetTiming();
        }
        return this.ticks;
    }

    /** Один фиксированный шаг. */
    private tick(dt: number): void {
        this.input?.update(); // опрос геймпадов до логики
        this.camera.update(dt); // тряска и прочее «живое» поведение камеры
        this.tweens.update(dt);
        this.fx.update(dt); // эффекты/аниматоры поверх твин-движения, до логики сцены
        this.scenes.update(dt);
        this.input?.endTick();
    }

    private render(): void {
        this.fpsMeter?.frame();
        this.camera.apply(this.renderer.worldRoot);
        this.scenes.render();
        this.renderer.render();
    }
}