import { HttpFS, DRef, MemoryFS, objectEntries } from '@zephyr3d/base';
import { ScriptingSystem } from './scriptingsystem.js';
import { ResourceManager } from '../utility/serialization/manager.js';
import '../render/primitive.js';
import { BoxShape } from '../shapes/box.js';
import { CapsuleShape } from '../shapes/capsule.js';
import { CylinderShape } from '../shapes/cylinder.js';
import { TorusShape } from '../shapes/torus.js';
import { PlaneShape } from '../shapes/plane.js';
import { SphereShape } from '../shapes/sphere.js';
import { TetrahedronShape } from '../shapes/tetrahedron.js';
import '../material/shader/helper.js';
import { LambertMaterial } from '../material/lambert.js';
import { BlinnMaterial } from '../material/blinn.js';
import { UnlitMaterial } from '../material/unlit.js';
import '../material/particle.js';
import '@zephyr3d/device';
import '../values.js';
import '../material/meshmaterial.js';
import '../material/grassmaterial.js';
import '../material/terrain-cm.js';
import { PBRMetallicRoughnessMaterial } from '../material/pbrmr.js';
import { PBRSpecularGlossinessMaterial } from '../material/pbrsg.js';
import '../material/pbrblueprint.js';
import '../material/sprite.js';
import { StandardSpriteMaterial } from '../material/sprite_std.js';
import '../utility/blueprint/material/texture.js';
import { ScreenAdapter } from './screen.js';

/**
 * Core engine class managing scripting, serialization, and rendering.
 *
 * Responsibilities:
 * - Manages a {@link ScriptingSystem} for dynamic script attachment and lifecycle.
 * - Manages a {@link ResourceManager} for loading scenes and assets.
 * - Maintains a list of active renderable objects to be rendered each frame.
 * - Provides methods to attach/detach scripts, update scripts, load scenes, and read files.
 * - Supports enabling/disabling of runtime operations.
 *
 * @remarks
 * The engine can be configured with a virtual file system (VFS) and script root path.
 * It exposes methods to manage scripts on host objects, update scripts each frame,
 * load scenes from files, and render active objects.
 *
 * @public
 */ class Engine {
    _builtinsVFS;
    _scriptingSystem;
    _resourceManager;
    _enabled;
    _screen;
    _activeRenderables;
    _loadingScenes;
    /**
   * Creates a new runtime manager.
   *
   * @param VFS - Optional virtual file system passed to the internal {@link ScriptingSystem}.
   * @param scriptsRoot - Optional scripts root path within the VFS. Defaults as in `ScriptingSystem`.
   * @param enabled - Whether runtime operations are active. Defaults to `true`.
   */ constructor(VFS, scriptsRoot, enabled){
        VFS = VFS ?? new HttpFS('./');
        this._builtinsVFS = null;
        this._scriptingSystem = new ScriptingSystem({
            VFS,
            scriptsRoot
        });
        this._resourceManager = new ResourceManager(VFS);
        this._enabled = enabled ?? true;
        this._activeRenderables = [];
        this._loadingScenes = {};
        this._screen = new ScreenAdapter();
    }
    /**
   * Exposes the instance of {@link ScriptingSystem}.
   */ get scriptingSystem() {
        return this._scriptingSystem;
    }
    /**
   * Exposes the virtual file system used by the underlying {@link ScriptingSystem}'s registry.
   */ get VFS() {
        return this._scriptingSystem.registry.VFS;
    }
    set VFS(vfs) {
        if (vfs !== this._resourceManager.VFS) {
            this._resourceManager.VFS?.close();
            this._resourceManager.VFS = vfs;
            this._scriptingSystem.registry.VFS = vfs;
            this.ensureBuiltinVFS();
        }
    }
    /**
   * Exposes the instance of {@link ResourceManager}.
   */ get resourceManager() {
        return this._resourceManager;
    }
    /**
   * Exposes the active {@link ScreenAdapter}.
   */ get screen() {
        return this._screen;
    }
    /** @internal */ async init() {
        await this.ensureBuiltinVFS();
    }
    /**
   * Detaches all scripts from all hosts, if enabled.
   *
   * No-op when `enabled === false`.
   */ detachAllScripts() {
        if (this._enabled) {
            this._scriptingSystem.detachAllScripts();
        }
    }
    /**
   * Loads a runtime script class from file
   * @param module - file path
   * @returns The runtime script class or null
   */ async loadRuntimeScriptClass(module) {
        return await this._scriptingSystem.loadRuntimeScriptClass(module);
    }
    /**
   * Attaches a script module to the given host, if enabled.
   *
   * When disabled, this method resolves to `null` without side effects.
   *
   * @typeParam T - Host type.
   * @param host - Host object to attach the script to.
   * @param module - Module identifier to resolve and load.
   * @returns The `RuntimeScript<T>` instance, or `null` if disabled or on failure.
   */ async attachScript(host, module, config) {
        return this._enabled ? await this._scriptingSystem.attachScript(host, module, config) : null;
    }
    /**
   * Detaches a script from a host, by module ID or instance, if enabled.
   *
   * No-op when disabled.
   *
   * @typeParam T - Host type.
   * @param host - Host to detach from.
   * @param idOrInstance - Target script by module ID or instance reference.
   */ detachScript(host, idOrInstance) {
        if (this._enabled) {
            this._scriptingSystem.detachScript(host, idOrInstance);
        }
    }
    /**
   * Gets all scripts attached to a host.
   *
   * Delegates to {@link ScriptingSystem.getScriptObjects}.
   *
   * @typeParam T - Expected script type.
   * @param host - Host object to query.
   * @returns Script instances attached to the host, or an empty array.
   */ getScriptObjects(host) {
        return this._scriptingSystem.getScriptObjects(host);
    }
    /**
   * Ticks all attached scripts by calling their `onUpdate` hooks, if enabled.
   *
   * Delegates to {@link ScriptingSystem.update}. No-op when disabled.
   *
   * @param deltaTime - Time since last update in Seconds.
   * @param elapsedTime - Total elapsed time in Seconds.
   */ update(deltaTime, elapsedTime) {
        if (this._enabled) {
            this._scriptingSystem.update(deltaTime, elapsedTime);
        }
    }
    async loadSceneFromFile(path) {
        path = this.VFS.normalizePath(path);
        if (!this._loadingScenes[path]) {
            this._loadingScenes[path] = this._loadScene(path);
        }
        return this._loadingScenes[path];
    }
    setRenderable(renderable, layer = 0, hook) {
        if (!this._activeRenderables[layer]) {
            this._activeRenderables[layer] = {
                renderable: new DRef(null),
                hook: null
            };
        }
        this._activeRenderables[layer].hook = hook ?? null;
        this._activeRenderables[layer].renderable.set(renderable);
    }
    async readFile(path, encoding) {
        try {
            const content = await this.VFS.readFile(path, {
                encoding: encoding ?? 'binary'
            });
            return content;
        } catch (err) {
            console.error(`Read file '${path}' failed: ${err}`);
            return null;
        }
    }
    async startup(startupScene, splashScreen, startupScript) {
        const splashScreenLayer = 9999;
        if (splashScreen) {
            const splashScreenScene = await this.loadSceneFromFile(splashScreen);
            if (splashScreenScene) {
                this.setRenderable(splashScreenScene, splashScreenLayer);
            }
        }
        if (startupScript) {
            const path = startupScript.toLowerCase().endsWith('.ts') || startupScript.toLowerCase().endsWith('.js') ? startupScript.slice(0, -3) : startupScript;
            await this.attachScript(null, path);
        }
        if (startupScene) {
            const scene = await this.loadSceneFromFile(startupScene);
            this.setRenderable(scene, 0);
        }
        this.setRenderable(null, splashScreenLayer);
    }
    render() {
        this._activeRenderables.forEach((info)=>{
            const render = info.hook?.beforeRender ? info.hook.beforeRender(info.renderable.get() ?? null) ?? true : true;
            if (render) {
                info.renderable.get()?.render();
            }
            if (info.hook?.afterRender) {
                info.hook.afterRender(info.renderable.get() ?? null);
            }
        });
    }
    async ensureBuiltinVFS() {
        if (!this._builtinsVFS) {
            this._builtinsVFS = await this.createBuiltinVFS();
        }
        this.VFS.unmount('/assets/@builtins');
        this.VFS.mount('/assets/@builtins', this._builtinsVFS);
    }
    async createBuiltinVFS() {
        const fs = new MemoryFS();
        const shapeClsMap = {
            '/primitives/box.zmsh': BoxShape,
            '/primitives/sphere.zmsh': SphereShape,
            '/primitives/cylinder.zmsh': CylinderShape,
            '/primitives/plane.zmsh': PlaneShape,
            '/primitives/torus.zmsh': TorusShape,
            '/primitives/tetrahedron.zmsh': TetrahedronShape,
            '/primitives/capsule.zmsh': CapsuleShape,
            '/materials/unlit.zmtl': UnlitMaterial,
            '/materials/lambert.zmtl': LambertMaterial,
            '/materials/blinnphong.zmtl': BlinnMaterial,
            '/materials/pbr_metallic_roughness.zmtl': PBRMetallicRoughnessMaterial,
            '/materials/pbr_specular_glossiness.zmtl': PBRSpecularGlossinessMaterial,
            '/materials/sprite_std.zmtl': StandardSpriteMaterial
        };
        for (const [key] of objectEntries(shapeClsMap)){
            const obj = new shapeClsMap[key]();
            await this.writeSerializableObject(fs, 'Default', obj, key);
            obj.dispose();
        }
        fs.readOnly = true;
        return fs;
    }
    async writeSerializableObject(VFS, type, obj, path) {
        try {
            const data = await this.resourceManager.serializeObject(obj);
            const content = JSON.stringify({
                type,
                data
            }, null, 2);
            await VFS.writeFile(path, content, {
                encoding: 'utf8',
                create: true
            });
        } catch (err) {
            console.error(`Write file '${path}' failed: ${err}`);
        }
    }
    async _loadScene(path) {
        try {
            const scene = await this._resourceManager.loadScene(path);
            if (scene) {
                const sceneScripts = scene.scripts.length > 0 ? scene.scripts : scene.script ? [
                    {
                        script: scene.script,
                        config: scene.scriptConfig
                    }
                ] : [];
                for (const attachment of sceneScripts){
                    if (!attachment.script) {
                        continue;
                    }
                    try {
                        await this.attachScript(scene, attachment.script, attachment.config ?? null);
                    } catch (err) {
                        console.error(`Attach script failed: ${err}`);
                    }
                }
                const nodes = [];
                scene.rootNode.iterate((node)=>{
                    nodes.push(node);
                });
                const attachTasks = nodes.map(async (node)=>{
                    const attachments = node.scripts.length > 0 ? node.scripts : node.script ? [
                        {
                            script: node.script,
                            config: node.scriptConfig
                        }
                    ] : [];
                    for (const attachment of attachments){
                        if (!attachment.script) {
                            continue;
                        }
                        try {
                            await this.attachScript(node, attachment.script, attachment.config ?? null);
                        } catch (err) {
                            console.error(`Attach script failed: ${err}`);
                        }
                    }
                });
                if (attachTasks.length > 0) {
                    await Promise.allSettled(attachTasks);
                }
            }
            return scene;
        } catch (err) {
            console.error(`Load scene from '${path}' failed: ${err}`);
            return null;
        }
    }
}

export { Engine };
//# sourceMappingURL=engine.js.map
