import { Observable, flushPendingDisposals } from '@zephyr3d/base';
import { InputManager } from './inputmgr.js';
import { Engine } from './engine.js';
import { getApp, setApp } from './api.js';

/**
 * Application
 *
 * Entry-point and lifecycle coordinator for the engine. Responsible for:
 * - Creating and owning the rendering device from a chosen backend.
 * - Managing the per-frame loop, including timing and viewport setup.
 * - Forwarding device/DOM input events via an observable event map.
 * - Hosting the runtime scripting system and input manager.
 *
 * Singleton:
 * - Only one instance may exist at a time. Access via `{@link getApp}`.
 *
 * Events:
 * - See `appEventMap` for all emitted events (resize, tick, pointer/keyboard/drag).
 *
 * Usage:
 * - Construct with `AppOptions`, await `ready()`, then call `run()` to start the loop.
 *
 * @public
 */ class Application extends Observable {
    _options;
    _device;
    _inputManager;
    _engine;
    _editorMode;
    _ready;
    /**
   * Construct the Application singleton with the provided options.
   *
   * Throws if an instance already exists.
   *
   * @param opt - Application creation options (canvas, backend, and optional runtime/device settings).
   */ constructor(opt){
        super();
        if (getApp()) {
            throw new Error('It is not allowed to have multiple Application instances');
        }
        setApp(this);
        this._options = {
            backend: opt.backend,
            enableMSAA: opt.enableMSAA ?? false,
            pixelRatio: opt.pixelRatio ?? window.devicePixelRatio ?? 1,
            canvas: opt.canvas
        };
        this._inputManager = new InputManager(this);
        this._engine = new Engine(opt.runtimeOptions?.VFS, opt.runtimeOptions?.scriptsRoot, opt.runtimeOptions?.enabled);
        if (opt.runtimeOptions?.screen) {
            this._engine.screen.config = opt.runtimeOptions.screen;
        }
        this._editorMode = opt.runtimeOptions?.editorMode ?? 'none';
        this._device = null;
        this._ready = false;
    }
    /**
   * Editor mode
   */ get editorMode() {
        return this._editorMode;
    }
    /**
   * The input manager instance handling pointer/keyboard event routing.
   */ get inputManager() {
        return this._inputManager;
    }
    /**
   * Get the instanceof {@link Engine}.
   */ get engine() {
        return this._engine;
    }
    /**
   * The (sanitized) options used to create this application.
   *
   * Note: Defaults are applied for `enableMSAA` and `pixelRatio` if omitted.
   */ get options() {
        return this._options;
    }
    /**
   * The initialized rendering device.
   *
   * Available after `await ready()`.
   */ get device() {
        return this._device;
    }
    /**
   * Convenience accessor for the device type name provided by the backend.
   */ get deviceType() {
        return this._options.backend.typeName();
    }
    /**
   * Set keyboard focus to the device's canvas element.
   */ focus() {
        this._device.canvas.focus();
    }
    /**
   * Initialize the rendering device and start input processing.
   *
   * @throws If device creation fails.
   */ async ready() {
        if (!this._ready) {
            this._device = await this._options.backend.createDevice(this._options.canvas, {
                dpr: this._options.pixelRatio,
                msaa: !!this._options.enableMSAA
            });
            if (!this._device) {
                throw new Error('App.init(): create device failed');
            }
            this._device.canvas.focus();
            await this._engine.init();
            this._inputManager.start();
            this._device.on('resize', (width, height)=>{
                this._engine.screen.resolveViewport();
                this.dispatchEvent('resize', width, height);
            });
            this._ready = true;
        }
    }
    /**
   * Render a single frame.
   *
   * Steps:
   * - Flushes pending disposals from the previous frame.
   * - Clears device state (framebuffer, viewport, scissor).
   * - Queries frame timing from the device (`elapsedFrame`, `elapsedOverall`).
   * - Updates the runtime manager (scripting/behaviors).
   * - Emits `tick` with delta/elapsed times.
   *
   * Safe to call manually; also used by the run loop.
   */ frame() {
        // Processes all pending disposals from the previous frame.
        flushPendingDisposals();
        if (this._ready) {
            this.device.setFramebuffer(null);
            this.device.setViewport(null);
            this.device.setScissor(null);
            const dt = this.device.frameInfo.elapsedFrame;
            const elapsed = this.device.frameInfo.elapsedOverall;
            this.dispatchEvent('tick', dt, elapsed);
            this._engine.update(dt * 0.001, elapsed * 0.001);
            this._engine.render();
        }
    }
    /**
   * Start the application's render loop.
   *
   * Uses the device's internal scheduling (`device.runLoop`) to repeatedly call `frame()`.
   */ run() {
        this.device.runLoop(()=>{
            this.frame();
        });
    }
    /**
   * Stop the application's render loop.
   *
   * Uses `device.exitLoop()` to end the scheduling started by `run()`.
   */ stop() {
        this.device.exitLoop();
    }
}

export { Application };
//# sourceMappingURL=app.js.map
