import 'dart:collection';

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' hide Matrix4;
import 'package:flutter_scene/src/camera.dart';
import 'package:flutter_scene/src/components/component.dart';
import 'package:flutter_scene/src/components/instanced_mesh_component.dart';
import 'package:flutter_scene/src/components/mesh_component.dart';
import 'package:flutter_scene/src/runtime_importer/runtime_importer.dart';
import 'package:flutter_scene/src/scene.dart';
import 'package:flutter_scene/src/animation.dart';
import 'package:flutter_scene/src/mesh.dart';
import 'package:flutter_scene/src/render/render_layers.dart';
import 'package:flutter_scene/src/render/render_scene.dart';
import 'package:flutter_scene/src/skin.dart';
import 'package:vector_math/vector_math.dart';
import 'package:vector_math/vector_math.dart' as vm;

void _visitMutable<T>(List<T> items, void Function(T item) visit) {
  HashSet<T>? visited;
  var index = 0;
  while (index < items.length) {
    final item = items[index];
    final tracked = visited;
    if (tracked != null && tracked.contains(item)) {
      index++;
      continue;
    }
    visit(item);

    // Unmutated and detachment paths stay linear and allocation-free.
    // Insertion before the cursor creates identity tracking.
    if (index < items.length && identical(items[index], item)) {
      tracked?.add(item);
      index++;
      continue;
    }

    final detached =
        (item is Component && !item.isAttached) ||
        (item is Node && item.parent == null);
    if (detached) {
      tracked?.add(item);
      continue;
    }

    final currentIndex = items.indexWhere(
      (candidate) => identical(candidate, item),
    );
    if (currentIndex <= index) {
      tracked?.add(item);
      continue;
    }

    if (tracked == null) {
      visited = HashSet<T>.identity()
        ..addAll(items.take(currentIndex + 1))
        ..add(item);
    } else {
      tracked.add(item);
    }
    index = 0;
  }
}

/// A `Node` represents a single element in a 3D scene graph.
///
/// Each node can contain a transform (position, rotation, scale), a mesh (3D geometry and material),
/// and child nodes. Nodes are used to build complex scenes by establishing relationships
/// between different elements, allowing for transformations to propagate down the hierarchy.
/// {@category Scene graph}
base class Node implements SceneGraph {
  /// Creates a node with an optional [name], [localTransform], and [mesh].
  ///
  /// When omitted, [localTransform] defaults to the identity matrix and the
  /// node has no associated geometry. A non-null [mesh] is attached as a
  /// [MeshComponent].
  Node({this.name = '', Matrix4? localTransform, Mesh? mesh})
    : _localTransform = localTransform ?? Matrix4.identity() {
    if (mesh != null) {
      addComponent(MeshComponent(mesh));
    }
  }

  /// The name of this node, used for identification.
  String name;

  /// Whether this node is visible in the scene. If false, the node and its children will not be rendered.
  bool visible = true;

  /// A highlight/outline color (linear RGBA) for this node, or null for none.
  ///
  /// When set, the scene draws a selection-style outline around this node's
  /// geometry in that color (see `Scene.highlightStyle`). Highlighting is a
  /// per-node property, so different nodes can use different colors. Setting it
  /// does not affect the node's normal rendering.
  /// {@category Rendering}
  Vector4? highlightColor;

  /// Whether this node and its descendants should be tested against the
  /// camera frustum each frame. When `true` (the default), subtrees
  /// whose [combinedLocalBounds] don't intersect the frustum are
  /// skipped entirely. Set to `false` for procedural geometry, large
  /// terrain pieces, or anything else where the cached bound is
  /// known-stale or known-misleading.
  ///
  /// Subtrees that report no bound (skinned content, geometry without
  /// computable bounds) are treated as always visible regardless of
  /// this flag.
  bool frustumCulled = true;

  /// The render layers this node occupies, a 32-bit bitmask. A
  /// [RenderView] renders this node's mesh only when its
  /// [RenderView.layerMask] intersects these layers
  /// (`layers & layerMask != 0`). Defaults to [kRenderLayerDefault]
  /// (layer 0). Each node carries its own layers; the value is not
  /// inherited by children.
  int layers = kRenderLayerDefault;

  /// Marks this node's meshes as static shadow casters: their geometry,
  /// material coverage, and world transform are promised not to change while
  /// mounted, so the engine may render them into cached shadow-map tiles that
  /// are reused across frames instead of re-encoding every caster every
  /// frame. Large static worlds become dramatically cheaper to shadow; nodes
  /// left dynamic (the default) still cast per-frame shadows on top of the
  /// cache. A static node that does change (moves, remesh, material coverage
  /// edit) shows stale shadows until its render item re-registers, so flag
  /// only genuinely static content. Not inherited by children; set it on each
  /// mesh-bearing node. Materials with a `vertex { }` displacement stage
  /// should stay dynamic, since their cached shadows would not follow a
  /// camera-dependent displacement.
  bool shadowStatic = false;

  /// Whether this node's meshes cast shadows. Defaults to `true`.
  ///
  /// This does not affect whether the meshes receive shadows. The value is not
  /// inherited by children.
  bool castsShadows = true;

  /// Whether scene raycasts (`Scene.raycast`) test this node's meshes.
  ///
  /// Disable for geometry that renders but should be transparent to rays
  /// (effects, decals); it then neither blocks nor receives picks. Distinct
  /// from [visible]: invisible nodes are already skipped by default.
  // TODO(fscene): serialize this flag (NodeSpec field + json + diff).
  bool raycastable = true;

  Matrix4 _localTransform;
  DecomposedTransform? _localTransformTrs;

  /// The transform of this node relative to its parent: position,
  /// rotation, and scale.
  ///
  /// Assigning marks this node and its descendants' cached world
  /// transforms stale, and this node and its ancestors' cached bounds
  /// stale. Mutating the returned matrix in place does not, so call
  /// [markTransformDirty] after an in-place edit.
  Matrix4 get localTransform => _localTransform;
  set localTransform(Matrix4 value) {
    _localTransform = value;
    _localTransformTrs = null;
    markTransformDirty();
  }

  /// The authored decomposition of [localTransform], when known.
  ///
  /// A matrix decompose puts a mirror's negative sign on the X scale no
  /// matter which axis the source mirrored, so animation blends anchored
  /// to a re-decomposed bind pose fade mirrored bones through zero scale.
  /// Importers record the authored decomposition here and blending
  /// anchors to it. Cleared when [localTransform] is assigned a matrix.
  @internal
  DecomposedTransform? get localTransformTrs => _localTransformTrs;

  /// Sets [localTransform] from [trs], keeping the decomposition.
  @internal
  void setLocalTransformTrs(DecomposedTransform trs) {
    _localTransform = trs.toMatrix4();
    _localTransformTrs = trs;
    markTransformDirty();
  }

  // Cached world-space transform, valid while _worldTransformDirty is
  // false. Recomputed lazily by globalTransform and by the render walk.
  final Matrix4 _worldTransform = Matrix4.identity();
  bool _worldTransformDirty = true;
  int _worldTransformVersion = 0;

  // Whether this node's accumulated transform reverses triangle winding (an
  // odd number of negative-determinant transforms up the chain). Cached
  // alongside _worldTransform and invalidated by the same dirty flag.
  bool _windingFlipped = false;

  /// When true, this node's own transform is treated as a coordinate-system
  /// convention rather than a geometry mirror, and excluded from
  /// [windingFlipped]. The importers set this on the synthesized scene root
  /// that flips glTF's handedness (`scale(1, 1, -1)`), so that flip does not
  /// reverse cull winding the way a real mirror node should.
  bool excludeFromWindingParity = false;

  /// The skin attached to this node, used for skeletal animation. Set by
  /// importers (both the scene importer and the runtime glTF/GLB loader).
  Skin? skin;

  /// Assigns the world-space transform of this node, automatically computing
  /// the [localTransform] needed to place the node at [transform] given the
  /// current parent transform.
  ///
  /// If the node has no parent, this is equivalent to assigning
  /// [localTransform] directly.
  set globalTransform(Matrix4 transform) {
    final parent = _parent;
    if (parent == null) {
      localTransform = transform;
    } else {
      // Solve `transform == parent.globalTransform * localTransform` for
      // localTransform. (`Matrix4.invert` returns the determinant and mutates
      // the receiver — `copyInverse` is the non-destructive version.)
      final parentInverse = Matrix4.identity();
      parent.globalTransform.copyInverse(parentInverse);
      localTransform = parentInverse * transform;
    }
  }

  /// The world-space transform of this node, with every ancestor's
  /// transform applied.
  ///
  /// Cached: O(1) when the cache is current, recomputed up the parent
  /// chain only after a transform change.
  Matrix4 get globalTransform {
    if (!_worldTransformDirty) return _worldTransform;
    final parent = _parent;
    final selfFlip =
        !excludeFromWindingParity && _localTransform.determinant() < 0;
    if (parent == null) {
      _worldTransform.setFrom(_localTransform);
      _windingFlipped = selfFlip;
    } else {
      _worldTransform
        ..setFrom(parent.globalTransform)
        ..multiply(_localTransform);
      // parent.globalTransform above refreshed the parent's cache, so
      // parent._windingFlipped is current.
      _windingFlipped = selfFlip != parent._windingFlipped;
    }
    _worldTransformDirty = false;
    _worldTransformVersion++;
    return _worldTransform;
  }

  /// Changes whenever this node's world transform is recomputed.
  @internal
  int get worldTransformVersion {
    globalTransform;
    return _worldTransformVersion;
  }

  /// Whether this node's accumulated transform reverses triangle winding (a
  /// mirror / negative scale somewhere up the chain). The renderer flips cull
  /// winding for such nodes so their front faces are not culled.
  @internal
  bool get windingFlipped {
    // Touch globalTransform to refresh the cache (which sets _windingFlipped).
    globalTransform;
    return _windingFlipped;
  }

  Node? _parent;

  /// The parent node of this node in the scene graph.
  Node? get parent => _parent;
  bool _isSceneRoot = false;

  /// The collection of [MeshPrimitive] objects that represent the 3D
  /// geometry and material properties of this node.
  ///
  /// This is a convenience over the node's first [MeshComponent]. The
  /// getter returns that component's mesh, or `null` when the node has no
  /// `MeshComponent`. The setter replaces the first `MeshComponent`'s
  /// mesh (adding a `MeshComponent` when there is none), or, given
  /// `null`, removes every `MeshComponent`.
  Mesh? get mesh => _meshComponents.isEmpty ? null : _meshComponents.first.mesh;
  set mesh(Mesh? value) {
    if (value == null) {
      for (final meshComponent in _meshComponents.toList()) {
        removeComponent(meshComponent);
      }
    } else if (_meshComponents.isNotEmpty) {
      _meshComponents.first.mesh = value;
    } else {
      addComponent(MeshComponent(value));
    }
  }

  // The render scene this node is mounted into, or null when the node is
  // not part of a live scene graph.
  RenderScene? _renderScene;

  /// The render scene this node is mounted into, or `null` when the node
  /// is not part of a live scene graph. Used by engine components to
  /// register and unregister their render items.
  @internal
  RenderScene? get internalRenderScene => _renderScene;

  // Whether this node and every ancestor is visible, recomputed each
  // frame by [scenePrePass].
  bool _effectiveVisible = false;

  // The components attached to this node, in attach order.
  final List<Component> _components = [];

  // Typed fast paths: the subsets of [_components] that feed the render
  // layer, so the per-frame pre-pass refreshes their render items
  // without scanning the full component list.
  final List<MeshComponent> _meshComponents = [];
  final List<InstancedMeshComponent> _instancedMeshComponents = [];

  /// Attaches [component] to this node.
  ///
  /// The component must not already be attached to a node. This fires its
  /// [Component.onAttach] hook, and, if this node is already part of a
  /// live scene, its [Component.onMount] and [Component.onLoad] hooks.
  void addComponent(Component component) {
    if (component.isAttached) {
      throw Exception('Component is already attached to a node');
    }
    _components.add(component);
    if (component is MeshComponent) {
      _meshComponents.add(component);
      markBoundsDirty();
    } else if (component is InstancedMeshComponent) {
      _instancedMeshComponents.add(component);
    }
    component.attachTo(this);
    if (_renderScene != null) {
      component.mount();
    }
  }

  /// Detaches [component] from this node.
  ///
  /// Fires [Component.onUnmount] (when this node is in a live scene) and
  /// [Component.onDetach]. Throws if [component] is not attached here.
  void removeComponent(Component component) {
    if (!_components.contains(component)) {
      throw Exception('Component is not attached to this node');
    }
    if (_renderScene != null) {
      component.unmount();
    }
    component.detachFrom();
    _components.remove(component);
    if (component is MeshComponent) {
      _meshComponents.remove(component);
      markBoundsDirty();
    } else if (component is InstancedMeshComponent) {
      _instancedMeshComponents.remove(component);
    }
  }

  /// Returns the first attached component of type [T], or `null`.
  T? getComponent<T>() {
    for (final component in _components) {
      if (component is T) return component as T;
    }
    return null;
  }

  /// Returns every attached component of type [T], in attach order.
  Iterable<T> getComponents<T>() => _components.whereType<T>();

  void _mount(RenderScene renderScene) {
    _renderScene = renderScene;
    _visitMutable(_components, (component) => component.mount());
    _visitMutable(children, (child) => child._mount(renderScene));
  }

  void _unmount() {
    _visitMutable(children, (child) => child._unmount());
    _visitMutable(_components, (component) => component.unmount());
    _renderScene = null;
  }

  // Combined local-space AABB cache. Three states:
  //   * _combinedBoundsCached == false: not yet computed (fall through
  //     to the lazy compute path on first access).
  //   * _combinedBoundsCached == true, _combinedBoundsCache == null:
  //     subtree is unbounded (skinned content, or geometry without
  //     computable bounds); treat as always visible.
  //   * _combinedBoundsCached == true, _combinedBoundsCache != null:
  //     valid cached AABB.
  vm.Aabb3? _combinedBoundsCache;
  bool _combinedBoundsCached = false;

  /// Local-space AABB covering this node's mesh and every descendant's
  /// (transformed) bounds. Returns `null` when the subtree contains
  /// skinned content or geometry without computable bounds, signalling
  /// "treat as always visible." Cached; invalidated by [markBoundsDirty].
  ///
  /// Mutating a `localTransform` matrix in place (rather than
  /// reassigning it) does not automatically invalidate the cache. Call
  /// [markBoundsDirty] after any in-place transform mutation.
  vm.Aabb3? get combinedLocalBounds {
    if (_combinedBoundsCached) return _combinedBoundsCache;
    _computeAndCacheCombinedLocalBounds();
    return _combinedBoundsCache;
  }

  void _computeAndCacheCombinedLocalBounds() {
    vm.Aabb3? result;
    bool subtreeBounded = true;

    final m = mesh;
    if (m != null) {
      final mb = m.localBounds;
      if (mb != null) {
        result = vm.Aabb3.copy(mb);
      } else if (m.primitives.isNotEmpty) {
        // Mesh with primitives but no localBounds (caller-managed
        // buffers without an override, or skinned mesh imported from a
        // file with no animation data) acts as unbounded.
        subtreeBounded = false;
      }
    }

    for (final child in children) {
      final childBounds = child.combinedLocalBounds;
      if (childBounds == null) {
        subtreeBounded = false;
        continue;
      }
      final transformed = vm.Aabb3.copy(childBounds)
        ..transform(child.localTransform);
      if (result == null) {
        result = transformed;
      } else {
        result.hull(transformed);
      }
    }

    _combinedBoundsCache = subtreeBounded ? result : null;
    _combinedBoundsCached = true;
  }

  /// The subtree's axis-aligned bounds in world space, or `null` when the
  /// subtree is unbounded ([combinedLocalBounds] is `null`, e.g. skinned
  /// content or caller-managed geometry).
  ///
  /// This is [combinedLocalBounds] placed into world space with this node's
  /// [globalTransform], the same bound the renderer frustum-culls against.
  /// Handy for framing a camera on a loaded model (see
  /// [PerspectiveCamera.framing]) or sizing an effect to a model's extent,
  /// without walking vertices by hand.
  vm.Aabb3? get combinedWorldBounds {
    final bounds = combinedLocalBounds;
    if (bounds == null) return null;
    return vm.Aabb3.copy(bounds)..transform(globalTransform);
  }

  /// The nodes in this subtree (this node and its descendants, depth-first)
  /// that carry a mesh with at least one primitive.
  ///
  /// The usual entry point after loading a model, when you need the drawable
  /// nodes to read geometry back, swap materials, or attach effects. Includes
  /// this node when it has a mesh.
  Iterable<Node> get meshNodes sync* {
    final m = mesh;
    if (m != null && m.primitives.isNotEmpty) yield this;
    for (final child in children) {
      yield* child.meshNodes;
    }
  }

  /// Whether this node's subtree would survive frustum culling against
  /// [camera] for a render target of the given [dimensions].
  ///
  /// Returns `true` when the node is configured to opt out of culling
  /// ([frustumCulled] is `false`), when the subtree is unbounded
  /// (skinned content, or geometry without computable bounds, both of
  /// which the renderer conservatively treats as always visible), or
  /// when the world-space AABB intersects the camera frustum. Returns
  /// `false` only when there is a sound bound and it lies entirely
  /// outside the frustum.
  ///
  /// Uses [globalTransform] to place the subtree's local-space AABB
  /// into world space.
  bool isVisibleTo(Camera camera, Size dimensions) {
    if (!frustumCulled) return true;
    final bounds = combinedLocalBounds;
    if (bounds == null) return true;
    final worldAabb = vm.Aabb3.copy(bounds)..transform(globalTransform);
    return camera.getFrustum(dimensions).intersectsWithAabb3(worldAabb);
  }

  /// Mark this node's [combinedLocalBounds] cache (and every ancestor's)
  /// stale. Call after replacing a mesh, mutating a child's local
  /// transform in place, or any other change that affects the bound.
  void markBoundsDirty() {
    Node? current = this;
    while (current != null && current._combinedBoundsCached) {
      current._combinedBoundsCache = null;
      current._combinedBoundsCached = false;
      current = current._parent;
    }
  }

  /// Marks this node's transform changed: its own and its descendants'
  /// cached world transforms become stale, and its own and its
  /// ancestors' cached bounds become stale.
  ///
  /// Assigning [localTransform] does this automatically. Call it
  /// manually only after mutating the [localTransform] matrix in place.
  void markTransformDirty() {
    _markWorldTransformDirty();
    markBoundsDirty();
  }

  void _markWorldTransformDirty() {
    // An already-dirty node has an already-dirty subtree, so stop.
    if (_worldTransformDirty) return;
    _worldTransformDirty = true;
    for (final child in children) {
      child._markWorldTransformDirty();
    }
  }

  /// Whether this node is a joint in a skeleton for animation.
  bool isJoint = false;

  final List<Animation> _animations = [];

  /// Append to the parsed animation list. Used by importers (including the
  /// runtime glTF/GLB loader).
  void addParsedAnimation(Animation animation) {
    _animations.add(animation);
  }

  /// The list of animations parsed when this node was deserialized.
  ///
  /// To instantiate an animation on a node, use [createAnimationClip].
  /// To search for an animation by name, use [findAnimationByName].
  List<Animation> get parsedAnimations => _animations;

  AnimationPlayer? _animationPlayer;

  /// Searches this node's descendants for the first child whose [Node.name]
  /// matches [name].
  ///
  /// Performs a depth-first search of the subtree rooted at this node and
  /// returns the first match, or `null` if no descendant has the given name.
  ///
  /// When [excludeAnimationPlayers] is `true`, descendants that already
  /// have an animation player attached are skipped — primarily used
  /// internally to avoid recursing into clip-attached subtrees.
  Node? getChildByName(String name, {bool excludeAnimationPlayers = false}) {
    for (var child in children) {
      if (excludeAnimationPlayers && child._animationPlayer != null) {
        continue;
      }
      if (child.name == name) {
        return child;
      }
      var result = child.getChildByName(name);
      if (result != null) {
        return result;
      }
    }
    return null;
  }

  /// Searches for an [Animation] by name.
  ///
  /// Returns `null` if no animation with the specified name is found.
  ///
  /// To enumerate all animations on this node, use [parsedAnimations].
  /// Animations can be instantiated on a nodes using [createAnimationClip].
  Animation? findAnimationByName(String name) {
    return _animations.firstWhereOrNull((element) => element.name == name);
  }

  /// Instantiates [animation] as an [AnimationClip] bound to this node.
  ///
  /// The returned clip starts paused at time 0; call [AnimationClip.play] to
  /// begin playback. Multiple clips may be created on the same node and are
  /// blended together by an internal [AnimationPlayer] each frame.
  ///
  /// To enumerate animations parsed from a model, use [parsedAnimations] or
  /// [findAnimationByName].
  AnimationClip createAnimationClip(Animation animation) {
    _animationPlayer ??= AnimationPlayer();
    return _animationPlayer!.createAnimationClip(animation, this);
  }

  /// Unregisters [clip] from this node's animation player so it no longer
  /// contributes to the blend. No-op when [clip] is not registered here.
  void removeAnimationClip(AnimationClip clip) =>
      _animationPlayer?.removeClip(clip);

  /// Load a glTF binary (GLB) model directly from raw bytes.
  ///
  /// No offline conversion is required, useful for runtime use cases such
  /// as user-uploaded models, network-loaded assets, or model editors.
  ///
  /// Example:
  /// ```dart
  /// final bytes = await rootBundle.load('assets/dash.glb');
  /// final node = await Node.fromGlbBytes(bytes.buffer.asUint8List());
  /// ```
  static Future<Node> fromGlbBytes(Uint8List bytes) {
    return importGlb(bytes);
  }

  /// Convenience wrapper for [fromGlbBytes] that loads from the asset bundle.
  static Future<Node> fromGlbAsset(String assetPath) async {
    final byteData = await rootBundle.load(assetPath);
    return importGlb(
      byteData.buffer.asUint8List(
        byteData.offsetInBytes,
        byteData.lengthInBytes,
      ),
    );
  }

  /// Load a multi-file glTF model from the raw bytes of its `.gltf`
  /// (JSON) file.
  ///
  /// Multi-file glTF keeps its geometry buffer and images in separate
  /// files referenced by relative URI. [resolveUri] fetches each of
  /// those resources by URI — for example downloading them relative to
  /// the `.gltf`'s own URL, or reading sibling files from disk.
  /// `data:` URIs are decoded internally and never reach [resolveUri].
  ///
  /// For single-file `.glb` models use [fromGlbBytes] instead.
  ///
  /// Example:
  /// ```dart
  /// final node = await Node.fromGltfBytes(
  ///   gltfJsonBytes,
  ///   resolveUri: (uri) => fetchBytes('$baseUrl/$uri'),
  /// );
  /// ```
  static Future<Node> fromGltfBytes(
    Uint8List gltfJson, {
    required GltfResourceResolver resolveUri,
  }) {
    return importGltf(gltfJson, resolveUri: resolveUri);
  }

  /// This list allows the node to act as a parent in the scene graph hierarchy. Transformations
  /// applied to this node, such as translation, rotation, and scaling, will also affect all child nodes.
  final List<Node> children = [];

  /// Registers this node as the root node of the scene graph.
  ///
  /// Throws an exception if the node is already a root or has a parent.
  void registerAsRoot(Scene scene) {
    name = 'root';
    if (_isSceneRoot) {
      throw Exception('Node is already a root');
    }
    if (_parent != null) {
      throw Exception('Node already has a parent');
    }
    _isSceneRoot = true;
    _mount(scene.renderScene);
  }

  /// Mounts this subtree into [renderScene] without a full [Scene].
  ///
  /// [Scene] construction touches the GPU context, which is unavailable
  /// in unit tests, so this seam lets render-lifecycle tests exercise
  /// the mount / unmount path (and the [RenderItem] registration it
  /// drives) against a bare [RenderScene].
  @visibleForTesting
  void debugMountInto(RenderScene renderScene) => _mount(renderScene);

  @override
  void add(Node child) {
    if (child._parent != null) {
      throw Exception('Child already has a parent');
    }
    children.add(child);
    child._parent = this;
    child._markWorldTransformDirty();
    final renderScene = _renderScene;
    if (renderScene != null) {
      child._mount(renderScene);
    }
    markBoundsDirty();
  }

  @override
  void addAll(Iterable<Node> children) {
    for (var child in children) {
      add(child);
    }
  }

  @override
  void addMesh(Mesh mesh) {
    final node = Node(mesh: mesh);
    add(node);
  }

  @override
  void remove(Node child) {
    if (child._parent != this) {
      throw Exception('Child is not attached to this node');
    }
    children.remove(child);
    child._parent = null;
    child._markWorldTransformDirty();
    if (child._renderScene != null) {
      child._unmount();
    }
    markBoundsDirty();
  }

  @override
  void removeAll() {
    while (children.isNotEmpty) {
      remove(children.last);
    }
  }

  /// Returns the sequence of [Node.name] values that walks from [ancestor]
  /// down to [child] through the scene graph.
  ///
  /// Useful for serializing a stable reference to a descendant node that
  /// can later be resolved with [getChildByNamePath].
  ///
  /// Returns `null` (and prints a debug warning) if [ancestor] is not an
  /// actual ancestor of [child].
  static Iterable<String>? getNamePath(Node ancestor, Node child) {
    List<String> result = [];
    Node? current = child;
    while (current != null) {
      if (identical(current, ancestor)) {
        return result.reversed;
      }
      result.add(current.name);
      current = current._parent;
    }

    debugPrint(
      'Name path formation failed because the given ancestor was not an ancestor of the given child.',
    );
    return null;
  }

  /// Returns the sequence of child indices that walks from [ancestor] down
  /// to [child] through the scene graph.
  ///
  /// Each entry is the index into the corresponding parent's [children] at
  /// that level. Useful for re-resolving a node reference on a cloned
  /// subtree (see [clone]).
  ///
  /// Returns `null` (and prints a debug warning) if [ancestor] is not an
  /// actual ancestor of [child].
  static Iterable<int>? getIndexPath(Node ancestor, Node child) {
    List<int> result = [];
    Node? current = child;
    while (current != null) {
      if (identical(current, ancestor)) {
        return result.reversed;
      }
      if (current._parent == null) {
        break;
      }
      result.add(current._parent!.children.indexOf(current));
      current = current._parent;
    }

    debugPrint(
      'Index path formation failed because the given ancestor was not an ancestor of the given child.',
    );
    return null;
  }

  /// Resolves a [namePath] (as produced by [getNamePath]) to a descendant
  /// node, or `null` if any segment does not match.
  Node? getChildByNamePath(Iterable<String> namePath) {
    Node? current = this;
    for (var name in namePath) {
      current = current!.getChildByName(name);
      if (current == null) {
        return null;
      }
    }
    return current;
  }

  /// Resolves an [indexPath] (as produced by [getIndexPath]) to a descendant
  /// node, or `null` if any segment is out of range.
  Node? getChildByIndexPath(Iterable<int> indexPath) {
    Node? current = this;
    for (var index in indexPath) {
      if (index < 0 || index >= current!.children.length) {
        return null;
      }
      current = current.children[index];
    }
    return current;
  }

  /// Returns the root node of the graph that this node is a part of.
  Node getRoot() {
    Node? current = this;
    while (current!._parent != null) {
      current = current._parent;
    }
    return current;
  }

  /// Returns the depth of this node in the scene graph hierarchy.
  /// The root node has a depth of 0.
  int getDepth() {
    int depth = 0;
    Node? current = this;
    while (current!._parent != null) {
      current = current._parent;
      depth++;
    }
    return depth;
  }

  /// Prints the hierarchy of this node and all its children to the console.
  void debugPrintHierarchy({int depth = 0}) {
    String indent = '  ' * depth;
    debugPrint('$indent$name');
    for (var child in children) {
      child.debugPrintHierarchy(depth: depth + 1);
    }
  }

  /// Creates a copy of this node.
  ///
  /// If [recursive] is `true`, the copy will include all child nodes.
  Node clone({bool recursive = true}) {
    // First, clone the node tree and collect any skins that need to be re-bound.
    List<Skin> clonedSkins = [];
    Node result = _cloneAndCollectSkins(recursive, clonedSkins);

    // Then, re-bind the skins to the cloned node tree.

    // Each of the clonedSkins currently have joint references in the old tree.
    for (var clonedSkin in clonedSkins) {
      for (
        int jointIndex = 0;
        jointIndex < clonedSkin.joints.length;
        jointIndex++
      ) {
        Node? joint = clonedSkin.joints[jointIndex];
        if (joint == null) {
          clonedSkin.joints[jointIndex] = null;
          continue;
        }

        Node? newJoint;

        // Get the index path from this node to the joint.
        Iterable<int>? nodeIndexPath = Node.getIndexPath(this, joint);
        if (nodeIndexPath != null) {
          // Then, replay the path on the cloned node tree to find the cloned
          // joint reference.
          newJoint = result.getChildByIndexPath(nodeIndexPath);
        }

        // Inline replace the joint reference with the cloned joint.
        // If the joint isn't found, a null placeholder is added.
        clonedSkin.joints[jointIndex] = newJoint;
      }
    }

    return result;
  }

  Node _cloneAndCollectSkins(bool recursive, List<Skin> clonedSkins) {
    // Clone the mesh wrapper so each instance owns its primitives (and can be
    // reskinned independently); the geometry and materials stay shared.
    Node result = Node(
      name: name,
      localTransform: localTransform,
      mesh: mesh?.clone(),
    );
    result.isJoint = isJoint;
    result._localTransformTrs = _localTransformTrs?.clone();
    // Preserve the coordinate-convention flag so a cloned scene root's
    // handedness flip is still excluded from winding parity (otherwise the
    // clone renders with reversed cull winding: see-through, inverted geometry).
    result.excludeFromWindingParity = excludeFromWindingParity;
    result._animations.addAll(_animations);
    // Components opt into cloning through [Component.cloneFor]; mesh
    // components are excluded because the mesh is already cloned through the
    // constructor above.
    for (final component in _components) {
      if (component is MeshComponent) continue;
      final copy = component.cloneFor(result);
      if (copy != null) result.addComponent(copy);
    }
    if (recursive) {
      for (var child in children) {
        result.add(child._cloneAndCollectSkins(recursive, clonedSkins));
      }
    }

    if (skin != null) {
      result.skin = Skin();
      for (Matrix4 inverseBindMatrix in skin!.inverseBindMatrices) {
        result.skin!.inverseBindMatrices.add(Matrix4.copy(inverseBindMatrix));
      }
      // Initially copy all the original joints. All of these will be replaced
      // with the cloned joints in Node.clone().
      result.skin!.joints.addAll(skin!.joints);
      clonedSkins.add(result.skin!);
    }

    return result;
  }

  /// Replaces this node's parsed animations with [animations] and re-binds any
  /// [AnimationClip]s created from the old set (matched by name), keeping
  /// their playback state.
  ///
  /// Used by scene hot reload when a reloaded scene's animations changed. The
  /// re-bind recaptures each bound node's rest pose from its current local
  /// transform; pass [restPoseOf] to supply the authored rest transform
  /// instead, so a node frozen mid-playback does not have its animated pose
  /// captured as the rest pose.
  void reloadParsedAnimations(
    List<Animation> animations, {
    Matrix4? Function(Node node)? restPoseOf,
  }) {
    _animations
      ..clear()
      ..addAll(animations);
    final player = _animationPlayer;
    if (player == null) return;
    if (restPoseOf != null) {
      for (final animation in _animations) {
        for (final channel in animation.channels) {
          final nodeName = channel.bindTarget.nodeName;
          final target = nodeName == name ? this : getChildByName(nodeName);
          if (target == null) continue;
          final pose = restPoseOf(target);
          if (pose != null) target.localTransform = pose;
        }
      }
    }
    player.rebind(this, animations: _animations);
  }

  /// Detaches this node from its parent in the scene graph.
  ///
  /// Once detached, this node is removed from its parent's list of children, effectively
  /// disconnecting this node and its subtree (all child nodes) from the scene graph.
  /// This operation is useful for temporarily removing nodes from the scene without deleting them.
  ///
  /// Throws an exception if this is the root node of the scene graph.
  /// No action is taken if the node already has no parent.
  void detach() {
    if (_isSceneRoot) {
      throw Exception('Root node cannot be detached');
    }
    final parent = _parent;
    if (parent != null) {
      parent.remove(this);
    }
  }

  /// Walks this node's subtree once per frame to prepare it for
  /// rendering: ticks components and animation players and refreshes the
  /// [RenderItem]s the render passes iterate.
  ///
  /// Called by [Scene.update] and [Scene.render]; not normally called
  /// directly. [deltaSeconds] is the elapsed time since the previous
  /// tick. [ancestorsVisible] is whether every ancestor of this node is
  /// visible, and defaults to `true` for the root.
  void scenePrePass(double deltaSeconds, [bool ancestorsVisible = true]) {
    _effectiveVisible = ancestorsVisible && visible;

    // Components tick whenever the node is mounted, independent of visibility.
    _visitMutable(_components, (component) => component.tick(deltaSeconds));

    if (_effectiveVisible) {
      _animationPlayer?.update(deltaSeconds);
      for (final meshComponent in _meshComponents) {
        meshComponent.refreshRenderItems();
      }
      for (final instancedMeshComponent in _instancedMeshComponents) {
        instancedMeshComponent.refreshRenderItem();
      }
    } else {
      // Keep a hidden subtree's items out of the render passes.
      for (final meshComponent in _meshComponents) {
        meshComponent.hideRenderItems();
      }
      for (final instancedMeshComponent in _instancedMeshComponents) {
        instancedMeshComponent.hideRenderItem();
      }
    }
    _visitMutable(
      children,
      (child) => child.scenePrePass(deltaSeconds, _effectiveVisible),
    );
  }

  /// Walks this node's subtree once per physics substep and dispatches
  /// [Component.fixedTick] to every component.
  ///
  /// Called by [Scene]'s fixed-step driver inside its substepping loop,
  /// before the physics world's step. Traversal order is parent before
  /// children, matching [scenePrePass].
  void sceneFixedPass(double fixedDt) {
    _visitMutable(_components, (component) => component.fixedTick(fixedDt));
    _visitMutable(children, (child) => child.sceneFixedPass(fixedDt));
  }
}
