import { makeObservable, Disposable, DRef, Vector4, Vector3, Ray, DWeakRef } from '@zephyr3d/base';
import { SceneNode } from './scene_node.js';
import { Octree } from './octree.js';
import { RaycastVisitor } from './raycast_visitor.js';
import { Environment } from './environment.js';
import { getDevice } from '../app/api.js';
import '../values.js';
import '../render/render_queue.js';
import { SceneRenderer } from '../render/renderer.js';
import '../material/shader/helper.js';
import '../render/primitive.js';
import '@zephyr3d/device';
import '../render/sky.js';
import '../render/clipmap.js';
import '../material/lambert.js';
import '../material/blinn.js';
import '../material/unlit.js';
import '../material/particle.js';
import '../material/meshmaterial.js';
import '../material/grassmaterial.js';
import '../material/terrain-cm.js';
import '../material/pbrmr.js';
import '../material/pbrsg.js';
import '../material/pbrblueprint.js';
import '../material/sprite.js';
import '../material/sprite_std.js';
import '../utility/blueprint/material/texture.js';
import '../shaders/atmosphere.js';
import '../render/rendergraph/forward_plus_builder.js';
import { ScriptAttachment, normalizeScriptAttachmentConfig, normalizeScriptAttachments } from './script_attachment.js';

/**
 * Represents a renderable world that manages scene graph, spatial indexing, and environment.
 *
 * Core responsibilities:
 * - Owns the root {@link SceneNode} and maintains a spatial {@link Octree} for culling/raycasting.
 * - Manages environment/lighting via {@link Environment}.
 * - Provides per-frame update queues (global and per-camera) for scene nodes.
 * - Offers utilities like ray construction and raycasting.
 * - Emits observable events: `update`, `startrender`, `endrender`.
 *
 * Performance notes:
 * - Octree placement is lazily synchronized on demand before reads (e.g., `octree`, `boundingBox`)
 *   and during per-frame updates.
 * - Update queues use weak references to avoid retaining disposed nodes.
 *
 * @public
 */ class Scene extends makeObservable(Disposable)() {
    /** @internal */ static _nextId = 0;
    /** @internal Scene name. */ _name;
    /** @internal Root node reference. */ _rootNode;
    /** @internal Spatial index for placement/culling. */ _octree;
    /**
   * @internal Pending nodes whose placement in the octree must be updated.
   * Populated by {@link Scene.invalidateNodePlacement} and drained by {@link Scene.updateNodePlacement}.
   */ _nodePlaceList;
    /** @internal Environment data (sky/light/IBL). */ _env;
    /** @internal Frame counter that last updated this scene. */ _updateFrame;
    /** @internal Unique scene ID. */ _id;
    /** @internal One-shot per-frame update queue (runs before render). */ _nodeUpdateQueue;
    /** @internal */ _drawableUpdateQueue;
    /** @internal One-shot per-frame-per-camera update queue. */ _perCameraUpdateQueue;
    /** @internal Main camera reference. */ _mainCamera;
    /** @internal Arbitrary metadata loaded with the scene (optional). */ _metaData;
    /** @internal User-attached script entries (engine-defined). */ _scripts;
    /**
   * Creates an instance of Scene.
   *
   * Initializes:
   * - Unique ID
   * - Empty environment
   * - Octree with default depth/leaf capacities
   * - Root scene node named "Root"
   *
   * @param name - Optional scene name for diagnostics/UI.
   */ constructor(name){
        super();
        this._id = ++Scene._nextId;
        this._name = name ?? '';
        this._octree = new Octree(this, 8, 8);
        this._nodePlaceList = new Set();
        this._nodeUpdateQueue = [];
        this._drawableUpdateQueue = [];
        this._perCameraUpdateQueue = [];
        this._env = new Environment();
        this._updateFrame = -1;
        this._rootNode = new DRef();
        this._rootNode.set(new SceneNode(this));
        this._rootNode.get().name = 'Root';
        this._metaData = null;
        this._scripts = [];
        this._mainCamera = new DRef();
    }
    /**
   * Gets the unique identifier of the scene.
   */ get id() {
        return this._id;
    }
    /**
   * Name of the scene
   */ get name() {
        return this._name;
    }
    set name(val) {
        this._name = val ?? '';
    }
    /**
   * Main camera used to render the scene by default.
   */ get mainCamera() {
        return this._mainCamera.get();
    }
    set mainCamera(camera) {
        this._mainCamera.set(camera);
    }
    /**
   * Gets the root scene node of the scene
   */ get rootNode() {
        return this._rootNode.get();
    }
    /**
   * Gets the octree used for spatial organization and queries.
   *
   * Ensures pending node placements are synchronized before returning.
   */ get octree() {
        // Make sure the octree state is up to date
        this.updateNodePlacement(this._octree, this._nodePlaceList);
        return this._octree;
    }
    /**
   * Gets the world-axis-aligned bounding box of the scene.
   *
   * Ensures pending node placements are synchronized before computing.
   */ get boundingBox() {
        this.updateNodePlacement(this._octree, this._nodePlaceList);
        // this._syncBVChangedList();
        return this._octree.getRootNode().getBoxLoosed();
    }
    /**
   * The environment (sky, lights, IBL) of the scene.
   */ get env() {
        return this._env;
    }
    /**
   * Arbitrary metadata associated with the scene (e.g., imported asset info).
   */ get metaData() {
        return this._metaData;
    }
    set metaData(val) {
        this._metaData = val;
    }
    /**
   * Attached script filename or identifier (engine-specific).
   *
   * @remarks
   * Integrates with the engine’s scripting system if available.
   */ get script() {
        return this._scripts[0]?.script ?? '';
    }
    set script(fileName) {
        const script = fileName ?? '';
        if (!script) {
            this._scripts.splice(0, 1);
        } else if (this._scripts[0]) {
            this._scripts[0].script = script;
        } else {
            this._scripts.unshift(new ScriptAttachment(script));
        }
    }
    /**
   * Script component configuration payload used by editor/runtime script components.
   */ get scriptConfig() {
        return this._scripts[0]?.config ?? null;
    }
    set scriptConfig(value) {
        const normalized = normalizeScriptAttachmentConfig(value);
        if (this._scripts[0]) {
            this._scripts[0].config = normalized;
        } else if (normalized != null) {
            this._scripts.unshift(new ScriptAttachment('', normalized));
        }
        if (this._scripts[0] && !this._scripts[0].script && this._scripts[0].config == null) {
            this._scripts.splice(0, 1);
        }
    }
    /**
   * All script attachments on this scene.
   */ get scripts() {
        return this._scripts;
    }
    set scripts(value) {
        this._scripts = normalizeScriptAttachments(value);
    }
    /**
   * Script configs for all script attachments.
   */ get scriptConfigs() {
        return this._scripts.map((item)=>item.config);
    }
    set scriptConfigs(value) {
        const next = Array.isArray(value) ? value : [];
        for(let i = 0; i < Math.max(this._scripts.length, next.length); i++){
            const config = normalizeScriptAttachmentConfig(next[i]);
            if (this._scripts[i]) {
                this._scripts[i].config = config;
            } else if (config != null) {
                this._scripts.push(new ScriptAttachment('', config));
            }
        }
        this._scripts = this._scripts.filter((item)=>item.script || item.config != null);
    }
    /**
   * Finds a scene node by its persistent ID.
   *
   * @typeParam T - Expected node type.
   * @param id - Persistent identifier to match against `SceneNode.persistentId`.
   * @returns The first matching node, or `null` if not found.
   */ findNodeById(id) {
        return this._rootNode.get().findNodeById(id);
    }
    /**
   * Finds a scene node by name.
   *
   * If multiple nodes share the same name, returns the first match encountered
   * during traversal.
   *
   * @typeParam T - Expected node type.
   * @param name - Node name to match.
   * @returns The first matching node, or `null` if not found.
   *
   * @remarks
   * Names are not guaranteed unique. Prefer IDs for stable references.
   */ findNodeByName(name) {
        return this._rootNode.get().findNodeByName(name);
    }
    /**
   * Casts a ray into the scene and returns the closest intersection, if any.
   *
   * @param ray - The ray in world space.
   * @param length - Maximum ray length. Defaults to `Infinity`.
   * @returns Intersection info `{ target, dist, point }`, or `null` if no hit.
   *
   */ raycast(ray, length = Infinity) {
        const raycastVisitor = new RaycastVisitor(ray, length);
        this.octree.getRootNode().traverse(raycastVisitor);
        return raycastVisitor.intersected ? {
            target: raycastVisitor.intersected,
            dist: raycastVisitor.intersectedDist,
            point: raycastVisitor.intersectedPoint
        } : null;
    }
    /**
   * Constructs a world-space ray from a camera and a screen position.
   *
   * @param camera - Camera whose projection/view defines the ray.
   * @param viewportWidth - Current viewport width in pixels.
   * @param viewportHeight - Current viewport height in pixels.
   * @param screenX - Screen-space x (pixels).
   * @param screenY - Screen-space y (pixels).
   * @param invModelMatrix - Optional inverse model matrix to transform the ray into local space.
   * @returns The constructed Ray.
   *
   * @remarks
   * Computes NDC from screen coordinates, unprojects using `invViewProjectionMatrix`,
   * then forms a ray from the camera world position to the unprojected point.
   * If `invModelMatrix` is provided, both origin and direction are transformed.
   */ constructRay(camera, viewportWidth, viewportHeight, screenX, screenY, invModelMatrix) {
        const vClip = new Vector4(2 * screenX / viewportWidth - 1, 1 - 2 * screenY / viewportHeight, 1, 1);
        const vWorld = camera.invViewProjectionMatrix.transform(vClip);
        vWorld.scaleBy(1 / vWorld.w);
        let vEye = camera.getWorldPosition();
        let vDir = Vector3.sub(vWorld.xyz(), vEye).inplaceNormalize();
        if (invModelMatrix) {
            vEye = invModelMatrix.transformPointAffine(vEye);
            vDir = invModelMatrix.transformVectorAffine(vDir);
        }
        return new Ray(vEye, vDir);
    }
    /**
   * Queues a node for a one-shot update before the next render.
   *
   * The node's `update(frame, elapsedSeconds, deltaSeconds)` will be called
   * during Scene.frameUpdate and then the node is removed from the queue.
   *
   * @param node - Node to schedule.
   *
   * @remarks
   * - To update continuously each frame, call `queueUpdateNode(this)` from within the node's `update` method.
   * - Duplicate scheduling within the same frame is prevented.
   */ queueUpdateNode(node) {
        if (node && this._nodeUpdateQueue.findIndex((val)=>val.get() === node) < 0) {
            this._nodeUpdateQueue.push(new DWeakRef(node));
        }
    }
    /**
   * Queues a drawable for a one-shot state update before the next render.
   *
   * The drawable's `updateState()` will be called during Scene.frameUpdate and then the drawable is removed from the queue.
   * @param drawable - Drawable to schedule.
   *
   * @remarks
   * - To update continuously each frame, call `queueUpdateDrawable(this)` from within the drawable's `updateState` method.
   * - Duplicate scheduling within the same frame is prevented.
   */ queueUpdateDrawable(drawable) {
        if (drawable && this._drawableUpdateQueue.indexOf(drawable) < 0) {
            this._drawableUpdateQueue.push(drawable);
        }
    }
    /**
   * Queues a node for a one-shot per-camera update before render.
   *
   * The node's `updatePerCamera(camera, elapsedSeconds, deltaSeconds)` will be
   * called once for each camera during {@link Scene.frameUpdatePerCamera}, then removed.
   *
   * @param node - Node to schedule.
   *
   * @remarks
   * - To run per-camera work continuously, re-queue from `updatePerCamera`.
   * - Duplicate scheduling within the same frame is prevented.
   */ queuePerCameraUpdateNode(node) {
        if (node && this._perCameraUpdateQueue.findIndex((val)=>val.get() === node) < 0) {
            this._perCameraUpdateQueue.push(new DWeakRef(node));
        }
    }
    /**
   * Renders this scene using the `mainCamera`, if present.
   *
   * Equivalent to `mainCamera.render(this)` when `mainCamera` is set.
   */ render() {
        if (this.mainCamera) {
            this.mainCamera.render(this);
        }
    }
    /**
   * Marks a graph node as needing placement update in the octree.
   *
   * Called by nodes when their transform/visibility/attachment changes in a way
   * that affects spatial placement.
   *
   * @internal
   */ invalidateNodePlacement(node) {
        this._nodePlaceList.add(node);
    }
    /**
   * Synchronizes environment light maps (IBL, SH/FB) with the sky as needed.
   *
   * Uses device capabilities to decide between Spherical Harmonics floating-point blending (SHFB)
   * and classic SH textures, and lazily fills missing environment fields from the sky.
   *
   * @internal
   */ updateEnvLight() {
        if (this.env.light.type === 'ibl') {
            this.env.light.radianceMap = this.env.sky.radianceMap;
            this.env.light.irradianceWindow = this.env.sky.shWindowWeights;
            this.env.light.irradianceSHFB = this.env.sky.irradianceSHFB;
            this.env.light.irradianceSH = null;
        }
    }
    /**
   * Returns the renderer class used to render the scene.
   *
   * Override in subclasses to supply a custom renderer.
   *
   * @internal
   */ getRenderer() {
        return SceneRenderer;
    }
    /**
   * Performs per-frame scene updates, once per device frame.
   *
   * Steps:
   * - Ensure this runs only once per frame (via `frameInfo.frameCounter`).
   * - Update environment light synchronization.
   * - Dispatch `update` event.
   * - Drain the one-shot node update queue and call `node.update(...)`.
   * - Apply pending octree placement updates.
   *
   */ frameUpdate() {
        const frameInfo = getDevice().frameInfo;
        if (frameInfo.frameCounter !== this._updateFrame) {
            this._updateFrame = frameInfo.frameCounter;
            this.updateEnvLight();
            this.dispatchEvent('update', this);
            this.mainCamera?.updateController();
            if (this._nodeUpdateQueue.length > 0) {
                const elapsedInSeconds = frameInfo.elapsedOverall * 0.001;
                const deltaInSeconds = frameInfo.elapsedFrame * 0.001;
                const queue = this._nodeUpdateQueue;
                this._nodeUpdateQueue = [];
                while(queue.length > 0){
                    const ref = queue.shift();
                    const node = ref.get();
                    if (node?.attached) {
                        node.update(frameInfo.frameCounter, elapsedInSeconds, deltaInSeconds);
                    }
                    ref.dispose();
                }
            }
            if (this._drawableUpdateQueue.length > 0) {
                for (const drawable of this._drawableUpdateQueue){
                    drawable.updateState();
                }
                this._drawableUpdateQueue = [];
            }
            this.updateNodePlacement(this._octree, this._nodePlaceList);
        }
    }
    /**
   * Performs per-camera scene updates for the current frame.
   *
   * Steps:
   * - Drain the per-camera update queue and call `node.updatePerCamera(camera, ...)`.
   * - Apply pending octree placement updates.
   *
   * @param camera - The camera being updated for.
   */ frameUpdatePerCamera(camera) {
        if (this._perCameraUpdateQueue.length > 0) {
            const frameInfo = getDevice().frameInfo;
            const elapsedInSeconds = frameInfo.elapsedOverall * 0.001;
            const deltaInSeconds = frameInfo.elapsedFrame * 0.001;
            const queue = this._perCameraUpdateQueue;
            this._perCameraUpdateQueue = [];
            while(queue.length > 0){
                const ref = queue.shift();
                ref.get()?.updatePerCamera(camera, elapsedInSeconds, deltaInSeconds);
                ref.dispose();
            }
        }
        this.updateNodePlacement(this._octree, this._nodePlaceList);
    }
    /**
   * Applies placement changes for nodes in `list` to the given `octree`.
   *
   * Rules:
   * - If node is not disposed, attached, not hidden, and `placeToOctree` is true,
   *   it is placed; otherwise removed.
   * - Drains `list` until empty.
   *
   * @param octree - Target spatial index.
   * @param list - Set of nodes awaiting placement evaluation.
   */ updateNodePlacement(octree, list) {
        function placeNode(node) {
            if (!node.disposed && node.attached && !node.hidden && node.placeToOctree) {
                octree.placeNode(node);
            } else {
                octree.removeNode(node);
            }
            list.delete(node);
        }
        if (list.size > 0) {
            while(list.size > 0){
                const node = list.keys().next().value;
                if (octree) {
                    placeNode(node);
                } else {
                    list.delete(node);
                }
            }
        }
    }
    /**
   * Disposes the scene and its owned resources.
   *
   * Disposes:
   * - Environment
   * - Root node (and, by extension, its hierarchy)
   * - Main camera reference wrapper
   *
   */ onDispose() {
        this._env.dispose();
        this._rootNode.dispose();
        this._mainCamera.dispose();
    }
}

export { Scene };
//# sourceMappingURL=scene.js.map
