import { makeObservable, Disposable, randomUUID, DRef, ObservableVector3, ObservableQuaternion, Matrix4x4, Vector3, Vector4 } from '@zephyr3d/base';
import { AnimationSet } from '../animation/animationset.js';
import { getEngine } from '../app/api.js';
import { ScriptAttachment, normalizeScriptAttachmentConfig, normalizeScriptAttachments } from './script_attachment.js';

/**
 * The base class of all scene graph objects.
 *
 * @remarks
 * Responsibilities:
 * - Defines hierarchical transform (position, rotation, scale) with lazily computed matrices.
 * - Integrates with the scene graph (parent/children, attachment notifications).
 * - Provides traversal utilities (`iterate`, `iterateBottomToTop`, `traverse`).
 * - Manages visibility state and CPU/GPU picking flags.
 * - Computes and caches local/world bounding volumes, notifies scene for spatial updates.
 * - Supports cloning and shared model instancing.
 * - Emits events on visibility, transform, bounding volume, attachment, and disposal.
 *
 * Performance:
 * - `localMatrix`, `worldMatrix`, and `invWorldMatrix` are cached until invalidated.
 * - Transform mutations and `invalidateBoundingVolume` update caches and spatial structures.
 *
 * Events:
 * - `nodeattached`, `noderemoved`, `visiblechanged`, `transformchanged`, `bvchanged`, `dispose`.
 *
 * @public
 */ class SceneNode extends makeObservable(Disposable)() {
    static _runTimeId = 1;
    /*
  static readonly PICK_INHERITED = -1;
  static readonly PICK_DISABLED = 0;
  static readonly PICK_ENABLED = 1;
  */ /** Bounding-box draw mode inherited from nearest graph ancestor. */ static BBOXDRAW_INHERITED = -1;
    /** Disable bounding-box visualization. */ static BBOXDRAW_DISABLED = 0;
    /** Draw local-space bounding box. */ static BBOXDRAW_LOCAL = 1;
    /** Draw world-space bounding box. */ static BBOXDRAW_WORLD = 2;
    /** @internal Runtime unique id. */ _runtimeId;
    /** @internal Unique persistent id. */ _id;
    /** @internal Prefab id */ _prefabId;
    /** @internal Animation set reference. */ _animationSet;
    /** @internal Optional shared model reference for instancing. */ _sharedModel;
    /** @internal */ _jointTypeT;
    /** @internal */ _jointTypeS;
    /** @internal */ _jointTypeR;
    /** @internal Clip test flag used by renderer. */ _clipMode;
    /** @internal Bounding-box visualization mode. */ _boxDrawMode;
    /** @internal Visibility state ('visible'|'hidden'|'inherit'). */ _visible;
    /** @internal CPU picking flag. */ _pickable;
    /** @internal GPU picking flag. */ _gpuPickable;
    /** @internal Friendly name for debugging/UI. */ _name;
    /** @internal Owning scene. */ _scene;
    /** @internal Local-space bounding volume cache. */ _bv;
    /** @internal True if local BV needs recomputing. */ _bvDirty;
    /** @internal World-space bounding volume cache. */ _bvWorld;
    /** @internal Whether this node participates in scene spatial structures (octree). */ _placeToOctree;
    /** @internal If true, this node cannot be cloned/attached as a child (engine policy). */ _sealed;
    /** @internal Parent node. */ _parent;
    /** @internal Children list (DRef for memory/resource mgmt). */ _children;
    /** @internal Local position (observable). */ _position;
    /** @internal Local scale (observable). */ _scaling;
    /** @internal Local rotation (observable quaternion). */ _rotation;
    /** @internal Local transform matrix cache. */ _localMatrix;
    /** @internal World transform matrix cache. */ _worldMatrix;
    /** @internal Determinant of world transform (cached). */ _worldMatrixDet;
    /** @internal Inverse world transform matrix cache. */ _invWorldMatrix;
    /** @internal Scratch local matrix to avoid allocations. */ _tmpLocalMatrix;
    /** @internal Scratch world matrix to avoid allocations. */ _tmpWorldMatrix;
    /** @internal Monotonically increasing tag for transform changes. */ _transformTag;
    /** @internal Shared callback used by observables on transform mutation. */ _transformChangeCallback;
    /** @internal Arbitrary metadata payload for this node. */ _metaData;
    /** @internal If greater than zero, suppress transform-change callbacks (during bulk updates). */ _disableCallback;
    /** @internal User-attached script entries (engine-defined). */ _scripts;
    /**
   * Construct a scene node.
   *
   * @remarks
   * If a `scene` is provided and this is not the root node, the node is reparented
   * under the scene's root immediately.
   *
   * @param scene - Scene that will own this node.
   */ constructor(scene){
        super();
        this._runtimeId = SceneNode._runTimeId++;
        this._id = randomUUID();
        this._prefabId = '';
        this._scene = scene;
        this._name = '';
        this._animationSet = new DRef();
        this._sharedModel = new DRef();
        this._jointTypeT = 'none';
        this._jointTypeS = 'none';
        this._jointTypeR = 'none';
        this._bv = null;
        this._bvWorld = null;
        this._bvDirty = true;
        this._clipMode = true;
        this._boxDrawMode = SceneNode.BBOXDRAW_DISABLED;
        this._visible = 'inherit';
        this._pickable = false;
        this._gpuPickable = true;
        this._placeToOctree = true;
        this._sealed = false;
        this._children = [];
        this._transformChangeCallback = ()=>this._onTransformChanged(true);
        this._position = new ObservableVector3(0, 0, 0).setCallback(this._transformChangeCallback);
        this._scaling = new ObservableVector3(1, 1, 1).setCallback(this._transformChangeCallback);
        this._rotation = new ObservableQuaternion().setCallback(this._transformChangeCallback);
        this._worldMatrix = null;
        this._worldMatrixDet = null;
        this._invWorldMatrix = null;
        this._localMatrix = null;
        this._transformTag = 0;
        this._disableCallback = 0;
        this._tmpLocalMatrix = Matrix4x4.identity();
        this._tmpWorldMatrix = Matrix4x4.identity();
        this._scripts = [];
        this._metaData = null;
        this._parent = null;
        this.reparent(scene?.rootNode ?? null);
    }
    /**
   * Whether the node should be inserted into the scene's spatial structure.
   *
   * @remarks
   * Toggling this hints the scene to (re)place the node in octree/acceleration structures.
   */ get placeToOctree() {
        return this._placeToOctree;
    }
    set placeToOctree(val) {
        if (!!val !== this._placeToOctree) {
            this._placeToOctree = !!val;
            if (this.isGraphNode()) {
                this.scene?.invalidateNodePlacement(this);
            }
        }
    }
    /**
   * Node's runtime unique identifier
   */ get runtimeId() {
        return this._runtimeId;
    }
    /**
   * Node's persistent identifier.
   *
   * @remarks
   * Changing this affects serialization and registry lookup; ensure uniqueness.
   */ get persistentId() {
        return this._id;
    }
    set persistentId(id) {
        this._id = id;
    }
    /**
   * If not empty, this node was loaded from a prefab
   *
   * @remarks
   * Internal used for serialization
   */ get prefabId() {
        return this._prefabId;
    }
    set prefabId(id) {
        this._prefabId = id;
    }
    /**
   * Get prefab node this node belongs to, or null if this node does not belongs to any prefab
   * @returns prefab node this node belongs to
   */ getPrefabNode() {
        return this._prefabId ? this : this.parent?.getPrefabNode() ?? null;
    }
    /**
   * Translation type if this is a joint node of any skeleton
   *
   * @remarks
   * Internal used for serialization
   */ get jointTypeT() {
        return this._jointTypeT;
    }
    set jointTypeT(val) {
        this._jointTypeT = val;
    }
    /**
   * Scale type if this is a joint node of any skeleton
   *
   * @remarks
   * Internal used for serialization
   */ get jointTypeS() {
        return this._jointTypeS;
    }
    set jointTypeS(val) {
        this._jointTypeS = val;
    }
    /**
   * Rotation type if this is a joint node of any skeleton
   *
   * @remarks
   * Internal used for serialization
   */ get jointTypeR() {
        return this._jointTypeR;
    }
    set jointTypeR(val) {
        this._jointTypeR = val;
    }
    /**
   * Arbitrary metadata associated with this node.
   *
   * @remarks
   * Stored and transported with the node; format is application-defined.
   */ 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 node.
   */ 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);
    }
    /**
   * Display name of the node (for UI/debugging).
   */ get name() {
        return this._name;
    }
    set name(val) {
        this._name = val || '';
    }
    /** The owning scene. */ get scene() {
        return this._scene;
    }
    /**
   * Whether this node is currently attached under the scene's root.
   */ get attached() {
        if (!this._scene) {
            return false;
        }
        let node = this;
        while(node && node !== this._scene.rootNode){
            node = node.parent;
        }
        return node === this._scene.rootNode;
    }
    /**
   * If true, the node is logically sealed; some operations (like cloning as child)
   * may be restricted by engine policies.
   */ get sealed() {
        return this._sealed;
    }
    set sealed(val) {
        this._sealed = val;
    }
    /**
   * Lazily created animation set for this node.
   *
   * @remarks
   * Accessing this schedules the node for update in the scene.
   */ get animationSet() {
        if (!this._animationSet.get()) {
            this._animationSet.set(new AnimationSet(this));
            this.scene?.queueUpdateNode(this);
        }
        return this._animationSet.get();
    }
    /**
   * Shared model reference for instancing/streaming systems.
   */ get sharedModel() {
        return this._sharedModel.get();
    }
    set sharedModel(model) {
        this._sharedModel.set(model);
    }
    /**
   * Clone this node.
   *
   * @remarks
   * If a shared model exists, it may create an instanced node. The clone is
   * attached under the same parent; children are cloned based on `method` and `recursive`.
   *
   * @param method - Clone method ('deep' or 'instance').
   * @param recursive - Whether children are cloned recursively.
   * @returns New node instance
   */ async clone() {
        this.iterate((node)=>{
            if (node.isClipmapTerrain()) {
                throw new Error('Cloning terrain node is not allowed');
            }
        });
        const parent = this.parent;
        const tmpParent = new SceneNode(this.scene);
        const data = await getEngine().resourceManager.serializeObject(this);
        const other = await getEngine().resourceManager.deserializeObject(tmpParent, data);
        other.persistentId = randomUUID();
        other.parent = parent;
        tmpParent.dispose();
        const postClonePromises = [];
        other.iterate((node)=>{
            const P = node.onPostClone();
            if (P instanceof Promise) {
                postClonePromises.push(P);
            }
        });
        if (postClonePromises.length > 0) {
            await Promise.all(postClonePromises);
        }
        return other;
    }
    /**
   * Whether the given node is a direct child of this node.
   * @param child - The node to be checked
   * @returns true if the given node is a direct child of this node, false otherwise
   */ hasChild(child) {
        return child && child.parent === this;
    }
    /**
   * Remove all children from this node.
   */ removeChildren() {
        while(this._children.length){
            this._children[0].get().remove();
        }
    }
    /**
   * Whether this node is an ancestor (direct or indirect) of the given node.
   */ isParentOf(child) {
        while(child && child !== this){
            child = child.parent;
        }
        return child === this;
    }
    /**
   * Detach this node from its parent.
   *
   * @returns this
   */ remove() {
        this.parent = null;
        return this;
    }
    /**
   * Depth-first traversal of this node's subtree (pre-order).
   *
   * @param v - Visitor invoked on each node.
   */ traverse(v) {
        v.visit(this);
        for (const child of this._children){
            child.get().traverse(v);
        }
    }
    /**
   * Iterate self and descendants in pre-order.
   *
   * Warning: Do not remove children during this iteration. To allow removal, use `iterateBottomToTop`.
   *
   * @param callback - Called for each node; if returns true, iteration stops.
   * @returns If returns true, the iteration will immediately stop.
   */ iterate(callback) {
        if (callback(this)) {
            return true;
        }
        for (const child of this._children){
            if (child.get().iterate(callback)) {
                return true;
            }
        }
        return false;
    }
    /**
   * Iterate self and descendants in reverse post-order (bottom-to-top).
   *
   * Child nodes can be safely removed during this iteration.
   *
   * @param callback - Called for each node; if returns true, iteration stops.
   * @returns If returns true, the iteration will immediately stop.
   */ iterateBottomToTop(callback) {
        for(let i = this._children.length - 1; i >= 0; i--){
            const child = this._children[i];
            if (child.get().iterateBottomToTop(callback)) {
                return true;
            }
        }
        if (callback(this)) {
            return true;
        }
        return false;
    }
    /** Type guard: true if this node is a graph node. */ isGraphNode() {
        return false;
    }
    /** Type guard: true if this node is a light. */ isLight() {
        return false;
    }
    /** Type guard: true if this node is a mesh. */ isMesh() {
        return false;
    }
    /** Type guard: true if this node is a sprite */ isSprite() {
        return false;
    }
    /** Type guard: true if this node is a water node. */ isWater() {
        return false;
    }
    /** Type guard: true if this node is a particle system. */ isParticleSystem() {
        return false;
    }
    /** Type guard: true if this node is a batch group. */ isBatchGroup() {
        return false;
    }
    /** Type guard: true if this node is a clipmap terrain. */ isClipmapTerrain() {
        return false;
    }
    /** true if this is a camera node, false otherwise */ isCamera() {
        return false;
    }
    /** true if this is a punctual light node, false otherwise */ isPunctualLight() {
        return false;
    }
    /**
   * Computes the bounding volume of the node
   * @returns The output bounding volume
   */ computeBoundingVolume() {
        return null;
    }
    /**
   * Gets the bounding volume of the node
   * @returns The bounding volume of the node
   */ getBoundingVolume() {
        if (this._bvDirty) {
            this._bv = this.computeBoundingVolume();
            this._bvDirty = false;
        }
        return this._bv;
    }
    /**
   * Sets the bounding volume of the node
   * @param bv - The bounding volume to set
   */ setBoundingVolume(bv) {
        if (bv !== this._bv) {
            this._bv = bv;
            this._bvDirty = !this._bv;
            this.invalidateWorldBoundingVolume(false);
        }
    }
    /**
   * Gets the world space bounding volume of the node
   * @returns The world space bounding volume of the node
   */ getWorldBoundingVolume() {
        if (!this._bvWorld) {
            this._bvWorld = this.computeWorldBoundingVolume(this.getBoundingVolume());
        }
        return this._bvWorld;
    }
    /**
   * Computes the world space bounding volume of the node
   * @returns The output bounding volume
   */ computeWorldBoundingVolume(localBV) {
        return localBV?.transform(this.worldMatrix) ?? null;
    }
    /**
   * Force the bounding volume to be recalculated
   */ invalidateBoundingVolume() {
        this._bvDirty = true;
        this.invalidateWorldBoundingVolume(false);
    }
    /** Force the world space bounding volume to be recalculated */ invalidateWorldBoundingVolume(transformChanged) {
        this._bvWorld = null;
        if (this._scene) {
            if (transformChanged) {
                this.iterate((node)=>{
                    if (node.isGraphNode()) {
                        this._scene.invalidateNodePlacement(node);
                    }
                });
            } else if (this.isGraphNode()) {
                this._scene.invalidateNodePlacement(this);
            }
            this.dispatchEvent('bvchanged', this);
        }
    }
    /** Clip mode */ get clipTestEnabled() {
        return this._clipMode;
    }
    set clipTestEnabled(val) {
        this._clipMode = val;
    }
    /** Computed value of show state */ get hidden() {
        let node = this;
        while(node && node._visible === 'inherit'){
            node = node.parent;
        }
        return node ? node._visible === 'hidden' : false;
    }
    /** Show state */ get showState() {
        return this._visible;
    }
    set showState(val) {
        if (val !== this._visible) {
            const prevHidden = this.hidden;
            this._visible = val;
            if (prevHidden !== this.hidden) {
                if (this.isGraphNode()) {
                    this._scene?.invalidateNodePlacement(this);
                }
                let parent = this;
                while(parent){
                    parent.dispatchEvent('visiblechanged', this);
                    parent = parent.parent;
                }
                this.notifyHiddenChanged();
            }
        }
    }
    /** Whether this node is enabled for CPU picking */ get pickable() {
        return this._pickable;
    }
    set pickable(val) {
        this._pickable = !!val;
    }
    /** Whether this node is enabled for GPU picking */ get gpuPickable() {
        return this._gpuPickable;
    }
    set gpuPickable(val) {
        this._gpuPickable = !!val;
    }
    /**
   * 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) {
        let node = null;
        this.iterate((child)=>{
            if (child.persistentId === id) {
                node = child;
                return true;
            }
        });
        return node;
    }
    /**
   * Finds a skeleton object by its persistent ID.
   * @param id - Persistent identifier to match against `Skeleton.persistentId`.
   * @returns The first matchign node, or `null` if not found.
   */ findSkeletonById(id) {
        const prefabNode = this.getPrefabNode() ?? this;
        let sk = null;
        prefabNode.iterate((node)=>{
            sk = node.animationSet.skeletons.find((s)=>s.get().persistentId === id) ?? null;
            return !!sk;
        });
        // avoid ts2339 compilation error (maybe a typescript bug?)
        return sk?.get() ?? null;
    }
    /**
   * 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) {
        let node = null;
        this.iterate((child)=>{
            if (child.name === name) {
                node = child;
                return true;
            }
        });
        return node;
    }
    /** Computed value for bounding box draw mode */ get computedBoundingBoxDrawMode() {
        if (this._boxDrawMode === SceneNode.BBOXDRAW_INHERITED) {
            let parent = this.parent;
            while(parent && !parent.isGraphNode()){
                parent = parent.parent;
            }
            return parent?.computedBoundingBoxDrawMode ?? SceneNode.BBOXDRAW_DISABLED;
        }
        return this._boxDrawMode;
    }
    /** Bounding box draw mode */ get boundingBoxDrawMode() {
        return this._boxDrawMode;
    }
    set boundingBoxDrawMode(mode) {
        this._boxDrawMode = mode;
    }
    invalidateTransform(invalidateLocal = true) {
        this._onTransformChanged(invalidateLocal);
    }
    /** Get called when the node was just created by cloning from other node */ onPostClone() {}
    /** Disposes the node */ onDispose() {
        super.onDispose();
        this.remove();
        this.removeChildren();
        this._animationSet.dispose();
        this._sharedModel.dispose();
    }
    /** @internal */ _setParent(p) {
        if (p && p._scene !== this._scene) {
            throw new Error('Parent node and child node must belongs to the same scene');
        }
        let lastParent = this._parent;
        let newParent = p;
        if (newParent !== lastParent) {
            const willDetach = (!p || !p.attached) && this.attached;
            const willAttach = !this.attached && p && p.attached;
            if (newParent) {
                newParent._children.push(new DRef(this));
            }
            if (this._parent) {
                const index = this._parent._children.findIndex((val)=>val.get() === this);
                this._parent._children[index].dispose();
                this._parent._children.splice(index, 1);
            }
            this._parent = p;
            this._onTransformChanged(false);
            if (willDetach) {
                this._detached();
            }
            if (willAttach) {
                this._attached();
            }
            while(lastParent){
                lastParent.dispatchEvent('noderemoved', this);
                lastParent = lastParent.parent;
            }
            while(newParent){
                newParent.dispatchEvent('nodeattached', this);
                newParent = newParent.parent;
            }
        }
    }
    /** @internal */ _onTransformChanged(invalidateLocal) {
        if (this._disableCallback > 0) {
            return;
        }
        if (invalidateLocal) {
            this._localMatrix = null;
        }
        this.invalidateWorldBoundingVolume(true);
        this.dispatchEvent('transformchanged', this);
        if (this._worldMatrix) {
            this._transformTag++;
            this._worldMatrix = null;
            this._invWorldMatrix = null;
            this._worldMatrixDet = null;
            for (const child of this._children){
                child.get()._onTransformChanged(false);
            }
        }
    }
    /**
   * Get called when this node is attached to scene
   */ _onAttached() {}
    /**
   * Get called when this node is detached from scene
   */ _onDetached() {}
    /** @internal */ _attached() {
        this.iterate((child)=>{
            this.scene.queueUpdateNode(child);
            child._onAttached();
        });
    }
    /** @internal */ _detached() {
        this.iterate((child)=>{
            child._onDetached();
        });
    }
    /** @internal */ notifyHiddenChanged() {
        this._visibleChanged();
        for (const child of this._children){
            if (child.get().showState === 'inherit') {
                child.get().notifyHiddenChanged();
            }
        }
    }
    /** @internal */ _visibleChanged() {}
    /** Parent of the xform */ get parent() {
        return this._parent;
    }
    set parent(p) {
        if (p !== this._parent) {
            this._setParent(p);
        }
    }
    /** Children of this xform */ get children() {
        return this._children.map((child)=>child.get());
    }
    /**
   * Position of the xform relative to it's parent
   */ get position() {
        return this._position;
    }
    set position(val) {
        this._position.setXYZ(val[0], val[1], val[2]);
    }
    /**
   * Scaling of the xform
   */ get scale() {
        return this._scaling;
    }
    set scale(val) {
        this._scaling.setXYZ(val[0], val[1], val[2]);
    }
    /**
   * Rotation of the xform
   */ get rotation() {
        return this._rotation;
    }
    set rotation(val) {
        this._rotation.setXYZW(val[0], val[1], val[2], val[3]);
    }
    worldToThis(v, result) {
        if (v instanceof Vector3) {
            result = result || new Vector3();
            this.invWorldMatrix.transformPointAffine(v, result);
            return result;
        } else {
            result = result || new Vector4();
            this.invWorldMatrix.transformAffine(v, result);
            return result;
        }
    }
    otherToThis(other, v, result) {
        return this.worldToThis(other.thisToWorld(v, result), result);
    }
    thisToWorld(v, result) {
        if (v instanceof Vector3) {
            result = result || new Vector3();
            this.worldMatrix.transformPointAffine(v, result);
            return result;
        } else {
            result = result || new Vector4();
            this.worldMatrix.transformAffine(v, result);
            return result;
        }
    }
    thisToOther(other, v, result) {
        return other.worldToThis(this.thisToWorld(v, result), result);
    }
    /**
   * Gets the position of the xform in world space
   * @returns position of the xform in world space
   */ getWorldPosition(outPos) {
        return outPos?.setXYZ(this.worldMatrix.m03, this.worldMatrix.m13, this.worldMatrix.m23) ?? new Vector3(this.worldMatrix.m03, this.worldMatrix.m13, this.worldMatrix.m23);
    }
    /**
   * Moves the xform by an offset vector
   * @param delta - The offset vector
   * @returns self
   */ moveBy(delta) {
        this._position.addBy(delta);
        return this;
    }
    /**
   * Scales the xform by a given scale factor
   * @param factor - The scale factor
   * @returns self
   */ scaleBy(factor) {
        this._scaling.mulBy(factor);
        return this;
    }
    /**
   * Sets the local transform matrix of the xform
   * @param matrix - The transform matrix to set
   * @returns self
   */ setLocalTransform(matrix) {
        this._disableCallback++;
        matrix.decompose(this._scaling, this._rotation, this._position);
        this._disableCallback--;
        this._onTransformChanged(true);
        return this;
    }
    /** Local transformation matrix of the xform */ get localMatrix() {
        if (!this._localMatrix) {
            this.calculateLocalTransform(this._tmpLocalMatrix);
            this._localMatrix = this._tmpLocalMatrix;
        }
        return this._localMatrix;
    }
    set localMatrix(matrix) {
        this.setLocalTransform(matrix);
    }
    /** World transformation matrix of the xform */ get worldMatrix() {
        this.syncTransform();
        return this._worldMatrix;
    }
    /** The determinant of world matrix */ get worldMatrixDet() {
        this.syncTransform();
        return this._worldMatrixDet;
    }
    /** Inverse of the world transformation matrix of the xform */ get invWorldMatrix() {
        this.syncTransform();
        return this._invWorldMatrix;
    }
    /**
   * Calculate local transform matrix
   * @param outMatrix - Matrix object that holds the result of calculation
   */ calculateLocalTransform(outMatrix) {
        outMatrix.compose(this._scaling, this._rotation, this._position);
    }
    /**
   * Calculate world transform matrix
   * @param outMatrix - Matrix object that holds the result of calculation
   */ calculateWorldTransform(outMatrix) {
        if (this._parent) {
            Matrix4x4.multiplyAffine(this._parent.worldMatrix, this.localMatrix, outMatrix);
        } else {
            outMatrix.set(this.localMatrix);
        }
    }
    /**
   * Sets the local tranformation matrix by a look-at matrix
   * @param eye - The eye position used to make the look-at matrix
   * @param target - The target position used to make the look-at matrix
   * @param up - The up vector used to make the look-at matrix
   * @returns self
   */ lookAt(eye, target, up) {
        this.setLocalTransform(Matrix4x4.lookAt(eye, target, up));
        return this;
    }
    /**
   * Update node state once per-frame
   * @param frameId - Current frame id
   * @param elapsedInSeconds - Elapsed time from game start in seconds
   * @param deltaInSeconds - Elapsed time since previous frame in seconds
   */ update(frameId, elapsedInSeconds, deltaInSeconds) {
        if (!this.attached) {
            return;
        }
        const animationSet = this._animationSet.get();
        if (animationSet) {
            if (animationSet.numAnimations > 0 || animationSet.skeletons.length > 0) {
                animationSet.update(deltaInSeconds);
                this.scene.queueUpdateNode(this);
            } else {
                this._animationSet.dispose();
            }
        }
    }
    /**
   * Update node state once per-camera
   * @param _camera - Updates according to which camera
   * @param _elapsedInSeconds - Elapsed time from game start in seconds
   * @param _deltaInSeconds - Elapsed time since previous frame in seconds
   */ updatePerCamera(_camera, _elapsedInSeconds, _deltaInSeconds) {}
    /**
   * Removes this node from it's parent and add this node to another parent node if required
   * @param p - The new parent node that this node should be added to or null
   * @returns self
   */ reparent(p) {
        this.parent = p ?? null;
        return this;
    }
    /** @internal */ get transformTag() {
        return this._transformTag;
    }
    /** @internal */ syncTransform() {
        if (!this._worldMatrix) {
            this._worldMatrix = this._tmpWorldMatrix;
            this.calculateWorldTransform(this._worldMatrix);
        }
        if (this._worldMatrixDet === null) {
            this._worldMatrixDet = this._worldMatrix.det();
        }
        if (!this._invWorldMatrix) {
            this._invWorldMatrix = Matrix4x4.invertAffine(this._worldMatrix);
        }
    }
}

export { SceneNode };
//# sourceMappingURL=scene_node.js.map
