import 'dart:typed_data';

import 'package:flutter/foundation.dart' show internal;
import 'package:flutter_scene/src/gpu/gpu.dart' as gpu;
import 'package:vector_math/vector_math.dart';

import 'package:flutter_scene/src/geometry/geometry.dart';
import 'package:flutter_scene/src/geometry/interleaved_layout.dart';
import 'package:flutter_scene/src/geometry/mesh_data.dart';

/// How a [MeshGeometry] manages its GPU buffers over its lifetime.
/// {@category Geometry}
enum GeometryStorage {
  /// The vertex and index buffers are uploaded once at construction and
  /// never change. This is the right choice for imported or generated
  /// meshes that stay still.
  fixed,

  /// The vertex and index buffers are retained so the mesh can be
  /// updated in place every frame without reallocating GPU memory. Use
  /// this for geometry that is regenerated continuously, such as a route
  /// line that follows a moving vehicle.
  ///
  /// An updatable geometry keeps a CPU-side copy of each attribute and
  /// allocates its buffers with spare capacity, so topology-stable
  /// updates ([MeshGeometry.updatePositions] and friends) and bounded
  /// growth ([MeshGeometry.rebuild]) avoid reallocation.
  updatable,
}

/// A triangle mesh built at runtime from vertex attribute arrays.
///
/// `MeshGeometry` is the general-purpose [Geometry] for procedurally
/// generated content. Build one directly from attribute arrays with
/// [MeshGeometry.fromArrays], or assemble it incrementally with a
/// [GeometryBuilder].
///
/// Callers supply attributes as independent typed arrays (positions,
/// normals, texture coordinates, colors, tangents) and never pack vertex bytes by
/// hand; the arrays are interleaved into the engine vertex layout
/// internally.
///
/// Pass [GeometryStorage.updatable] as the storage mode to create a
/// geometry that can be mutated in place. [updatePositions],
/// [updateNormals], [updateTexCoords], [updateTexCoords1], [updateColors], and
/// [updateTangents] replace one
/// attribute when the vertex count is unchanged; [rebuild] replaces
/// everything and reallocates only when the data outgrows the spare
/// capacity.
/// {@category Geometry}
class MeshGeometry extends UnskinnedGeometry {
  /// Builds a mesh from structure-of-arrays vertex attributes.
  ///
  /// [positions] is required and holds three floats per vertex. The
  /// optional attributes hold, per vertex, three floats for [normals],
  /// two for [texCoords] and [texCoords1], and four for [colors] and [tangents];
  /// each must match the vertex count implied by [positions] when supplied. Absent
  /// attributes fall back to defaults: texture coordinate `(0, 0)` and
  /// color opaque white.
  ///
  /// When [normals] is omitted and [primitiveType] is a triangle list,
  /// area-weighted vertex normals are generated from the faces; for line
  /// and point primitives, absent normals keep their default. [indices],
  /// when supplied, is an index list; when omitted a triangle mesh must
  /// have a vertex count that is a multiple of three.
  ///
  /// [primitiveType] selects how the vertex/index data is assembled into
  /// primitives when drawn, and defaults to a triangle list.
  ///
  /// [storage] selects whether the mesh can later be updated in place.
  /// Pass [bufferArena] to share immutable GPU buffer blocks with other fixed
  /// meshes. Set [retainCpuData] to false when the mesh does not need
  /// raycasting or [extractMeshData], avoiding a retained CPU copy after the
  /// upload. Updatable geometry requires retained CPU data and cannot use an
  /// arena.
  /// An updatable mesh fixes its indexed-or-not state at construction:
  /// supplying [indices] makes [rebuild] require indices thereafter, and
  /// omitting them makes it reject them. To start empty, pass a
  /// zero-length [positions] array with [GeometryStorage.updatable].
  ///
  /// [bounds], when supplied, becomes the local-space culling AABB and skips
  /// the construction-time position scan; a caller that assembled the
  /// vertices off the UI thread (a worker isolate) can compute it there. It
  /// must cover every position or the mesh over-culls.
  MeshGeometry.fromArrays({
    required Float32List positions,
    Float32List? normals,
    Float32List? texCoords,
    Float32List? texCoords1,
    Float32List? colors,
    Float32List? tangents,
    List<int>? indices,
    gpu.PrimitiveType primitiveType = gpu.PrimitiveType.triangle,
    Aabb3? bounds,
    this.storage = GeometryStorage.fixed,
    GeometryBufferArena? bufferArena,
    this.retainCpuData = true,
  }) {
    if (storage == GeometryStorage.updatable && bufferArena != null) {
      throw ArgumentError('Updatable geometry cannot use a buffer arena');
    }
    if (storage == GeometryStorage.updatable && !retainCpuData) {
      throw ArgumentError('Updatable geometry must retain CPU data');
    }
    if (positions.length % 3 != 0) {
      throw ArgumentError(
        'positions has ${positions.length} floats; expected a multiple of '
        'three (one vec3 per vertex)',
      );
    }
    final vertexCount = positions.length ~/ 3;
    this.primitiveType = primitiveType;

    if (bounds != null) {
      final center = (bounds.min + bounds.max) * 0.5;
      setLocalBounds(
        bounds,
        Sphere.centerRadius(center, (bounds.max - center).length),
      );
    }

    // Normals are generated from triangle faces; line and point
    // geometry has none, so absent normals are left at their default.
    final resolvedNormals =
        normals ??
        (vertexCount > 0 && primitiveType == gpu.PrimitiveType.triangle
            ? InterleavedLayoutAdapter.generateNormals(
                positions: positions,
                vertexCount: vertexCount,
                indices: indices,
              )
            : null);

    if (storage == GeometryStorage.fixed) {
      _uploadFixed(
        positions,
        vertexCount,
        resolvedNormals,
        texCoords,
        texCoords1,
        colors,
        tangents,
        indices,
        bufferArena,
      );
    } else {
      _indexed = indices != null;
      _setCpuStreams(
        positions,
        vertexCount,
        resolvedNormals,
        texCoords,
        texCoords1,
        colors,
        tangents,
      );
      _positionRing = _RingBufferStream(
        InterleavedLayoutAdapter.positionStreamBytes,
      );
      _normalRing = _RingBufferStream(
        InterleavedLayoutAdapter.normalStreamBytes,
      );
      _texCoordRing = _RingBufferStream(
        InterleavedLayoutAdapter.texCoordStreamBytes,
      );
      _texCoord1Ring = _RingBufferStream(
        InterleavedLayoutAdapter.texCoord1StreamBytes,
      );
      _colorRing = _RingBufferStream(InterleavedLayoutAdapter.colorStreamBytes);
      _tangentRing = _RingBufferStream(
        InterleavedLayoutAdapter.tangentStreamBytes,
      );
      _liveVertexCount = vertexCount;
      // Indices first, so the attribute upload can retain them for raycasting.
      if (_indexed) _uploadIndices(indices!);
      _uploadAllStreams();
      _recomputeBounds();
    }
  }

  /// Uploads a mesh from a [MeshData] snapshot built off the render
  /// isolate.
  ///
  /// The snapshot's CPU work (vertex assembly, normal generation) can run
  /// on a background isolate; this constructor performs only the GPU
  /// upload. See [MeshData] for the recipe. [storage] selects whether the
  /// mesh can later be updated in place. [bufferArena] and [retainCpuData]
  /// match [MeshGeometry.fromArrays].
  factory MeshGeometry.fromMeshData(
    MeshData data, {
    GeometryStorage storage = GeometryStorage.fixed,
    GeometryBufferArena? bufferArena,
    bool retainCpuData = true,
  }) {
    final geometry = MeshGeometry.fromArrays(
      positions: data.positions,
      normals: data.normals,
      texCoords: data.texCoords,
      texCoords1: data.texCoords1,
      colors: data.colors,
      tangents: data.tangents,
      indices: data.indices,
      primitiveType: data.primitiveType,
      storage: storage,
      bufferArena: bufferArena,
      retainCpuData: retainCpuData,
    );
    for (final entry in data.customAttributes.entries) {
      geometry.setCustomAttribute(
        entry.key,
        entry.value.data,
        components: entry.value.components,
      );
    }
    return geometry;
  }

  /// Replaces this updatable geometry's data from a [MeshData] snapshot.
  ///
  /// Equivalent to passing the snapshot's arrays to [rebuild], so the same
  /// indexed-or-not contract applies. Throws a [StateError] unless this
  /// geometry is [GeometryStorage.updatable].
  void applyMeshData(MeshData data) {
    rebuild(
      positions: data.positions,
      normals: data.normals,
      texCoords: data.texCoords,
      texCoords1: data.texCoords1,
      colors: data.colors,
      tangents: data.tangents,
      indices: data.indices,
    );
    for (final entry in data.customAttributes.entries) {
      setCustomAttribute(
        entry.key,
        entry.value.data,
        components: entry.value.components,
      );
    }
  }

  /// How this geometry's GPU buffers are managed; see [GeometryStorage].
  final GeometryStorage storage;

  /// Whether this mesh keeps CPU attributes for raycasting and extraction.
  final bool retainCpuData;

  // --- Updatable-storage state. Unused while [storage] is fixed. ---

  bool _indexed = false;
  int _liveVertexCount = 0;
  int _indexCapacity = 0;
  gpu.DeviceBuffer? _indexBuffer;
  gpu.IndexType _indexType = gpu.IndexType.int16;

  // One ring of host-visible buffers per attribute, so an in-place update
  // writes to a buffer the GPU is not currently reading (avoiding the
  // write-vs-read race). The index buffer stays single-buffered, since
  // topology-stable updates leave it untouched and only the slow rebuild path
  // rewrites it. [_streamViews] holds the currently bound view of each ring,
  // in slot order (position, normal, texcoord, color, tangent).
  late final _RingBufferStream _positionRing;
  late final _RingBufferStream _normalRing;
  late final _RingBufferStream _texCoordRing;
  late final _RingBufferStream _texCoord1Ring;
  late final _RingBufferStream _colorRing;
  late final _RingBufferStream _tangentRing;
  List<gpu.BufferView> _streamViews = const [];

  // The interleaved vertex bytes, built lazily from the structure-of-arrays
  // CPU streams only when the scene serializer asks for them, and invalidated
  // on every update. Nothing interleaves on the upload path anymore.
  Uint8List? _packedVertexBytes;
  Uint8List? _packedIndexBytes;
  bool _packedIndices32Bit = false;

  /// The packed interleaved vertex bytes (and packed index bytes with their
  /// width), in the engine's interleaved unskinned layout. Interleaved lazily
  /// from the per-attribute CPU streams on first access. The scene serializer
  /// emits the de-interleaved [soaData] instead; this stays as a convenience
  /// for callers wanting the interleaved form.
  ({Uint8List vertexBytes, Uint8List? indexBytes, bool indices32Bit})
  get packedData {
    _requireReadable('packedData');
    return (
      vertexBytes: _packedVertexBytes ??=
          InterleavedLayoutAdapter.packUnskinned(
            positions: _cpuPositions,
            vertexCount: _liveVertexCount,
            normals: _cpuNormals,
            texCoords: _cpuTexCoords,
            texCoords1: _cpuTexCoords1,
            colors: _cpuColors,
            tangents: _cpuTangents,
          ),
      indexBytes: _packedIndexBytes,
      indices32Bit: _packedIndices32Bit,
    );
  }

  /// The de-interleaved (structure-of-arrays) vertex bytes
  /// streams concatenated) plus the packed index bytes, for re-emitting this
  /// geometry as a `.fscene` payload with the
  /// [InterleavedLayoutAdapter.unskinnedSoaLayout] layout. Built directly from
  /// the per-attribute CPU streams, no interleave.
  @internal
  ({Uint8List vertexBytes, Uint8List? indexBytes, bool indices32Bit})
  get soaData {
    _requireReadable('soaData');
    return (
      vertexBytes: InterleavedLayoutAdapter.concatUnskinnedStreams(
        UnskinnedAttributeStreams(
          position: _bytesOf(_cpuPositions),
          normal: _bytesOf(_cpuNormals),
          texCoord: _bytesOf(_cpuTexCoords),
          texCoord1: _bytesOf(_cpuTexCoords1),
          color: _bytesOf(_cpuColors),
          tangent: _bytesOf(_cpuTangents),
        ),
      ),
      indexBytes: _packedIndexBytes,
      indices32Bit: _packedIndices32Bit,
    );
  }

  Float32List _cpuPositions = Float32List(0);
  Float32List _cpuNormals = Float32List(0);
  Float32List _cpuTexCoords = Float32List(0);
  Float32List _cpuTexCoords1 = Float32List(0);
  Float32List _cpuColors = Float32List(0);
  Float32List _cpuTangents = Float32List(0);

  /// The number of vertices currently drawn.
  int get vertexCount => _liveVertexCount;

  /// Whether this geometry can be updated in place.
  bool get isUpdatable => storage == GeometryStorage.updatable;

  /// Replaces every vertex position, keeping the vertex count unchanged.
  ///
  /// [positions] holds three floats per vertex and must match the
  /// current [vertexCount]. To change the vertex count, use [rebuild].
  /// Throws a [StateError] unless this geometry is
  /// [GeometryStorage.updatable].
  ///
  /// When only a contiguous span of vertices changed, pass [dirtyStart]
  /// and [dirtyCount] (in vertices) to upload just that span. [positions]
  /// must still hold every vertex (the full array backs raycasting and
  /// bounds); the hint only narrows the GPU upload. Omit both to upload
  /// the whole stream.
  void updatePositions(
    Float32List positions, {
    int? dirtyStart,
    int? dirtyCount,
  }) {
    _ensureUpdatable('updatePositions');
    _checkAttributeLength('positions', positions.length, 3);
    _cpuPositions = Float32List.fromList(positions);
    _writeStream(
      0,
      _positionRing,
      _cpuPositions,
      dirtyStart: dirtyStart,
      dirtyCount: dirtyCount,
    );
    _recomputeBounds();
  }

  /// Replaces every vertex normal, keeping the vertex count unchanged.
  ///
  /// Pass [dirtyStart]/[dirtyCount] to upload only a contiguous span; see
  /// [updatePositions].
  void updateNormals(Float32List normals, {int? dirtyStart, int? dirtyCount}) {
    _ensureUpdatable('updateNormals');
    _checkAttributeLength('normals', normals.length, 3);
    _cpuNormals = Float32List.fromList(normals);
    _writeStream(
      1,
      _normalRing,
      _cpuNormals,
      dirtyStart: dirtyStart,
      dirtyCount: dirtyCount,
    );
  }

  /// Replaces every texture coordinate, keeping the vertex count
  /// unchanged.
  ///
  /// Pass [dirtyStart]/[dirtyCount] to upload only a contiguous span; see
  /// [updatePositions].
  void updateTexCoords(
    Float32List texCoords, {
    int? dirtyStart,
    int? dirtyCount,
  }) {
    _ensureUpdatable('updateTexCoords');
    _checkAttributeLength('texCoords', texCoords.length, 2);
    _cpuTexCoords = Float32List.fromList(texCoords);
    _writeStream(
      2,
      _texCoordRing,
      _cpuTexCoords,
      dirtyStart: dirtyStart,
      dirtyCount: dirtyCount,
    );
  }

  /// Replaces every secondary texture coordinate.
  void updateTexCoords1(
    Float32List texCoords1, {
    int? dirtyStart,
    int? dirtyCount,
  }) {
    _ensureUpdatable('updateTexCoords1');
    _checkAttributeLength('texCoords1', texCoords1.length, 2);
    _cpuTexCoords1 = Float32List.fromList(texCoords1);
    _writeStream(
      3,
      _texCoord1Ring,
      _cpuTexCoords1,
      dirtyStart: dirtyStart,
      dirtyCount: dirtyCount,
    );
  }

  /// Replaces every vertex color, keeping the vertex count unchanged.
  ///
  /// Pass [dirtyStart]/[dirtyCount] to upload only a contiguous span; see
  /// [updatePositions].
  void updateColors(Float32List colors, {int? dirtyStart, int? dirtyCount}) {
    _ensureUpdatable('updateColors');
    _checkAttributeLength('colors', colors.length, 4);
    _cpuColors = Float32List.fromList(colors);
    _writeStream(
      4,
      _colorRing,
      _cpuColors,
      dirtyStart: dirtyStart,
      dirtyCount: dirtyCount,
    );
  }

  /// Replaces every vertex tangent, keeping the vertex count unchanged.
  ///
  /// Each tangent contains its xyz direction and bitangent sign in w.
  void updateTangents(
    Float32List tangents, {
    int? dirtyStart,
    int? dirtyCount,
  }) {
    _ensureUpdatable('updateTangents');
    _checkAttributeLength('tangents', tangents.length, 4);
    _cpuTangents = Float32List.fromList(tangents);
    _writeStream(
      5,
      _tangentRing,
      _cpuTangents,
      dirtyStart: dirtyStart,
      dirtyCount: dirtyCount,
    );
  }

  /// Replaces all of this geometry's data, allowing the vertex and index
  /// counts to change.
  ///
  /// Reuses the existing GPU buffers when the new data fits within their
  /// spare capacity, and reallocates with headroom only when it does
  /// not. The indexed-or-not state is fixed at construction: a geometry
  /// created with indices requires [indices] here, and one created
  /// without must omit them. Throws a [StateError] unless this geometry
  /// is [GeometryStorage.updatable].
  void rebuild({
    required Float32List positions,
    Float32List? normals,
    Float32List? texCoords,
    Float32List? texCoords1,
    Float32List? colors,
    Float32List? tangents,
    List<int>? indices,
  }) {
    _ensureUpdatable('rebuild');
    if (positions.length % 3 != 0) {
      throw ArgumentError(
        'positions has ${positions.length} floats; expected a multiple of '
        'three (one vec3 per vertex)',
      );
    }
    if (_indexed && indices == null) {
      throw ArgumentError(
        'This geometry was created with indices; rebuild requires them',
      );
    }
    if (!_indexed && indices != null) {
      throw ArgumentError(
        'This geometry was created without indices; rebuild must not '
        'supply them',
      );
    }
    final vertexCount = positions.length ~/ 3;
    final resolvedNormals =
        normals ??
        (vertexCount > 0 && primitiveType == gpu.PrimitiveType.triangle
            ? InterleavedLayoutAdapter.generateNormals(
                positions: positions,
                vertexCount: vertexCount,
                indices: indices,
              )
            : null);

    _setCpuStreams(
      positions,
      vertexCount,
      resolvedNormals,
      texCoords,
      texCoords1,
      colors,
      tangents,
    );
    _liveVertexCount = vertexCount;
    // The rings reallocate themselves when the count outgrows their capacity.
    if (_indexed) _uploadIndices(indices!);
    _uploadAllStreams();
    _recomputeBounds();
  }

  void _uploadFixed(
    Float32List positions,
    int vertexCount,
    Float32List? normals,
    Float32List? texCoords,
    Float32List? texCoords1,
    Float32List? colors,
    Float32List? tangents,
    List<int>? indices,
    GeometryBufferArena? bufferArena,
  ) {
    _liveVertexCount = vertexCount;
    // Retain the structure-of-arrays attributes (defaults filled). These back
    // both lazy serialization (interleaved on demand) and raycasting, and they
    // are uploaded straight to per-attribute GPU streams with no interleave.
    if (retainCpuData) {
      _setCpuStreams(
        positions,
        vertexCount,
        normals,
        texCoords,
        texCoords1,
        colors,
        tangents,
      );
    }
    ByteData? indexBytes;
    var indexType = gpu.IndexType.int16;
    if (indices != null) {
      final packed = InterleavedLayoutAdapter.packIndices(indices);
      indexBytes = ByteData.sublistView(packed.bytes);
      indexType = packed.is32Bit ? gpu.IndexType.int32 : gpu.IndexType.int16;
      if (retainCpuData) _packedIndexBytes = packed.bytes;
      _packedIndices32Bit = packed.is32Bit;
    }
    uploadUnskinnedAttributes(
      positions: retainCpuData ? _cpuPositions : positions,
      vertexCount: vertexCount,
      normals: retainCpuData ? _cpuNormals : normals,
      texCoords: retainCpuData ? _cpuTexCoords : texCoords,
      texCoords1: retainCpuData ? _cpuTexCoords1 : texCoords1,
      colors: retainCpuData ? _cpuColors : colors,
      tangents: retainCpuData ? _cpuTangents : tangents,
      indices: indexBytes,
      indexType: indexType,
      bufferArena: bufferArena,
      retainCpuData: retainCpuData,
    );
    // Raycast off this geometry's own attribute arrays instead of the
    // transient upload copies, so no extra position/texcoord copy lingers.
    // Keep the index bytes so an indexed mesh raycasts as indexed.
    if (retainCpuData) {
      setRaycastAttributes(
        positions: _cpuPositions,
        texCoords: _cpuTexCoords,
        texCoords1: _cpuTexCoords1,
        normals: _cpuNormals,
        colors: _cpuColors,
        tangents: _cpuTangents,
        indices: indexBytes,
      );
    }
  }

  void _requireReadable(String operation) {
    if (!retainCpuData) {
      throw StateError('$operation requires retainCpuData');
    }
  }

  // Writes all attribute streams into their rings and binds them.
  // Used at construction and on rebuild.
  void _uploadAllStreams() {
    _streamViews = [
      _positionRing.write(_bytesOf(_cpuPositions), _liveVertexCount),
      _normalRing.write(_bytesOf(_cpuNormals), _liveVertexCount),
      _texCoordRing.write(_bytesOf(_cpuTexCoords), _liveVertexCount),
      _texCoord1Ring.write(_bytesOf(_cpuTexCoords1), _liveVertexCount),
      _colorRing.write(_bytesOf(_cpuColors), _liveVertexCount),
      _tangentRing.write(_bytesOf(_cpuTangents), _liveVertexCount),
    ];
    setVertexStreams(_streamViews, _liveVertexCount);
    _refreshRaycastData();
    // Invalidate the lazily interleaved serialization bytes.
    _packedVertexBytes = null;
  }

  // Writes one changed attribute into its ring's next buffer and rebinds the
  // streams. The other attributes keep their current ring buffers, so only
  // the dirty stream's bytes are re-uploaded.
  void _writeStream(
    int slot,
    _RingBufferStream ring,
    Float32List attribute, {
    int? dirtyStart,
    int? dirtyCount,
  }) {
    final (start, end) = _resolveDirtyRange(dirtyStart, dirtyCount);
    _streamViews = List<gpu.BufferView>.of(_streamViews);
    _streamViews[slot] = ring.writeRange(
      _bytesOf(attribute),
      _liveVertexCount,
      start,
      end,
    );
    setVertexStreams(_streamViews, _liveVertexCount);
    _refreshRaycastData();
    _packedVertexBytes = null;
  }

  // Resolves an optional caller dirty range (in vertices) to a half-open
  // span, defaulting to the whole stream when unspecified. Validates the
  // span lies within the current vertex count.
  (int, int) _resolveDirtyRange(int? dirtyStart, int? dirtyCount) {
    if (dirtyStart == null && dirtyCount == null) {
      return (0, _liveVertexCount);
    }
    final start = dirtyStart ?? 0;
    final count = dirtyCount ?? (_liveVertexCount - start);
    if (start < 0 || count < 0 || start + count > _liveVertexCount) {
      throw RangeError(
        'dirty range [$start, ${start + count}) is outside the '
        '$_liveVertexCount vertices',
      );
    }
    return (start, start + count);
  }

  void _refreshRaycastData() {
    setRaycastAttributes(
      positions: _cpuPositions,
      texCoords: _cpuTexCoords,
      texCoords1: _cpuTexCoords1,
      normals: _cpuNormals,
      colors: _cpuColors,
      tangents: _cpuTangents,
      indices: _packedIndexBytes == null
          ? null
          : ByteData.sublistView(_packedIndexBytes!),
    );
  }

  static Uint8List _bytesOf(Float32List attribute) => attribute.buffer
      .asUint8List(attribute.offsetInBytes, attribute.lengthInBytes);

  void _uploadIndices(List<int> indices) {
    final packed = InterleavedLayoutAdapter.packIndices(indices);
    _packedIndexBytes = packed.bytes;
    _packedIndices32Bit = packed.is32Bit;
    // Raycast index data is retained alongside the attribute streams by
    // [_refreshRaycastData]; this method only manages the GPU index buffer.
    final indexType = packed.is32Bit
        ? gpu.IndexType.int32
        : gpu.IndexType.int16;
    final elementBytes = packed.is32Bit ? 4 : 2;
    if (_indexBuffer == null ||
        indices.length > _indexCapacity ||
        indexType != _indexType) {
      _indexCapacity = nextBufferCapacity(indices.length);
      _indexType = indexType;
      // Host-visible for the same reason as the attribute streams (see
      // [_RingBufferStream]); a rebuild may rewrite the index buffer.
      _indexBuffer = gpu.gpuContext.createDeviceBuffer(
        gpu.StorageMode.hostVisible,
        _indexCapacity * elementBytes,
      );
    }
    final buffer = _indexBuffer!;
    if (packed.bytes.isNotEmpty) {
      buffer.overwrite(ByteData.sublistView(packed.bytes));
      buffer.flush(offsetInBytes: 0, lengthInBytes: packed.bytes.length);
    }
    setIndices(
      gpu.BufferView(
        buffer,
        offsetInBytes: 0,
        lengthInBytes: packed.bytes.length,
      ),
      indexType,
    );
  }

  void _setCpuStreams(
    Float32List positions,
    int vertexCount,
    Float32List? normals,
    Float32List? texCoords,
    Float32List? texCoords1,
    Float32List? colors,
    Float32List? tangents,
  ) {
    if (normals != null && normals.length != vertexCount * 3) {
      throw ArgumentError(
        'normals has ${normals.length} floats; expected ${vertexCount * 3}',
      );
    }
    if (texCoords != null && texCoords.length != vertexCount * 2) {
      throw ArgumentError(
        'texCoords has ${texCoords.length} floats; expected '
        '${vertexCount * 2}',
      );
    }
    if (texCoords1 != null && texCoords1.length != vertexCount * 2) {
      throw ArgumentError(
        'texCoords1 has ${texCoords1.length} floats; expected '
        '${vertexCount * 2}',
      );
    }
    if (colors != null && colors.length != vertexCount * 4) {
      throw ArgumentError(
        'colors has ${colors.length} floats; expected ${vertexCount * 4}',
      );
    }
    if (tangents != null && tangents.length != vertexCount * 4) {
      throw ArgumentError(
        'tangents has ${tangents.length} floats; expected ${vertexCount * 4}',
      );
    }
    _cpuPositions = Float32List.fromList(positions);
    _cpuNormals = normals != null
        ? Float32List.fromList(normals)
        : _filledStream(vertexCount, 3, const [0.0, 0.0, 1.0]);
    _cpuTexCoords = texCoords != null
        ? Float32List.fromList(texCoords)
        : Float32List(vertexCount * 2);
    _cpuTexCoords1 = texCoords1 != null
        ? Float32List.fromList(texCoords1)
        : Float32List(vertexCount * 2);
    _cpuColors = colors != null
        ? Float32List.fromList(colors)
        : _filledStream(vertexCount, 4, const [1.0, 1.0, 1.0, 1.0]);
    _cpuTangents = tangents != null
        ? Float32List.fromList(tangents)
        : Float32List(vertexCount * 4);
  }

  void _recomputeBounds() {
    if (_liveVertexCount == 0) {
      setLocalBounds(null, null);
      return;
    }
    var minX = double.infinity, minY = double.infinity, minZ = double.infinity;
    var maxX = double.negativeInfinity,
        maxY = double.negativeInfinity,
        maxZ = double.negativeInfinity;
    for (var v = 0; v < _liveVertexCount; v++) {
      final x = _cpuPositions[v * 3];
      final y = _cpuPositions[v * 3 + 1];
      final z = _cpuPositions[v * 3 + 2];
      if (x < minX) minX = x;
      if (y < minY) minY = y;
      if (z < minZ) minZ = z;
      if (x > maxX) maxX = x;
      if (y > maxY) maxY = y;
      if (z > maxZ) maxZ = z;
    }
    final min = Vector3(minX, minY, minZ);
    final max = Vector3(maxX, maxY, maxZ);
    final center = (min + max) * 0.5;
    final radius = ((max - min) * 0.5).length;
    setLocalBounds(Aabb3.minMax(min, max), Sphere.centerRadius(center, radius));
  }

  void _ensureUpdatable(String operation) {
    if (storage != GeometryStorage.updatable) {
      throw StateError(
        '$operation requires GeometryStorage.updatable; this geometry is '
        'fixed',
      );
    }
  }

  void _checkAttributeLength(String name, int length, int componentsPerVertex) {
    final expected = _liveVertexCount * componentsPerVertex;
    if (length != expected) {
      throw ArgumentError(
        '$name has $length floats; a topology-stable update expects '
        '$expected for $_liveVertexCount vertices. Use rebuild to change '
        'the vertex count.',
      );
    }
  }

  static Float32List _filledStream(
    int vertexCount,
    int componentsPerVertex,
    List<double> value,
  ) {
    final stream = Float32List(vertexCount * componentsPerVertex);
    for (var v = 0; v < vertexCount; v++) {
      for (var c = 0; c < componentsPerVertex; c++) {
        stream[v * componentsPerVertex + c] = value[c];
      }
    }
    return stream;
  }
}

/// Rounds [needed] up to a buffer capacity with spare headroom.
///
/// Updatable geometry allocates its GPU buffers at the returned size so
/// that a buffer which grows gradually reallocates only a logarithmic
/// number of times rather than on every change. The result is the
/// smallest power of two that is at least [needed] and at least
/// [minimum].
int nextBufferCapacity(int needed, {int minimum = 16}) {
  if (needed <= minimum) return minimum;
  var capacity = minimum;
  while (capacity < needed) {
    capacity <<= 1;
  }
  return capacity;
}

/// A small ring of host-visible device buffers for one updatable attribute
/// stream.
///
/// Each [write] advances to the next buffer in the ring before overwriting,
/// so the GPU keeps reading the previously bound buffer while the CPU writes
/// the next one. The ring depth matches the frame count the engine's
/// transient `HostBuffer` cycles through, which bounds the GPU's in-flight
/// reads. The ring reallocates (at a power-of-two capacity) only when the
/// vertex count outgrows it, so steady-state updates never allocate.
/// The bounding vertex range that covers both [a] and [b]. An empty input
/// (`start >= end`) contributes nothing. Ranges are half-open
/// `[start, end)`. Used to accumulate which span of a ring buffer is stale
/// relative to the CPU copy, so a partial update uploads only what a given
/// buffer is missing.
({int start, int end}) unionVertexRange(
  ({int start, int end}) a,
  ({int start, int end}) b,
) {
  if (a.start >= a.end) return b;
  if (b.start >= b.end) return a;
  return (
    start: a.start < b.start ? a.start : b.start,
    end: a.end > b.end ? a.end : b.end,
  );
}

/// [range] with both ends pulled back to [vertexCount].
///
/// A mesh that shrank leaves a stale span reaching past the current
/// snapshot's byte array. Those vertices are no longer rendered, and a later
/// growth marks them stale again through its full-stream change range.
({int start, int end}) clampVertexRange(
  ({int start, int end}) range,
  int vertexCount,
) => (
  start: range.start > vertexCount ? vertexCount : range.start,
  end: range.end > vertexCount ? vertexCount : range.end,
);

// A ring of host-visible device buffers for one updatable attribute stream.
//
// Vertex data must be CPU-written every update, and `hostVisible` is the
// only storage mode that allows that: flutter_gpu has no buffer-to-buffer
// blit or read-back, so a `devicePrivate` buffer cannot be filled after
// allocation. On the coherent and unified-memory backends this is the
// shared CPU/GPU region, so the `flush` after a write is a no-op; on
// non-coherent backends `flush` uploads only the written range. The ring
// hands each update a buffer the GPU is not currently reading, avoiding the
// write-vs-read race.
class _RingBufferStream {
  _RingBufferStream(this.bytesPerVertex);

  /// Tightly packed bytes for one vertex of this attribute.
  final int bytesPerVertex;

  // Matches `TransientWriter`'s frame count, the number of frames the engine
  // cycles transient buffers through.
  static const int _ringDepth = 4;

  final List<gpu.DeviceBuffer> _buffers = [];
  // Per-buffer span (in vertices) that is stale relative to the current CPU
  // copy. Because the ring rotates, a buffer last written `_ringDepth`
  // writes ago is missing every change made since, so a partial write must
  // refresh that accumulated span, not just the caller's dirty span.
  final List<({int start, int end})> _stale = [];
  int _capacityVertices = 0;
  int _cursor = 0;

  /// Replaces every vertex (the full-stream write used at construction and
  /// on rebuild).
  gpu.BufferView write(Uint8List bytes, int vertexCount) =>
      writeRange(bytes, vertexCount, 0, vertexCount);

  /// Writes [bytes] (the full attribute array) into the ring's next buffer,
  /// uploading only the span that buffer is missing. [dirtyStart] and
  /// [dirtyEnd] mark the half-open vertex range the caller changed since the
  /// last write; the accumulated stale span of the chosen buffer is unioned
  /// in so a rotated, several-writes-old buffer is still brought current.
  gpu.BufferView writeRange(
    Uint8List bytes,
    int vertexCount,
    int dirtyStart,
    int dirtyEnd,
  ) {
    if (_buffers.isEmpty || vertexCount > _capacityVertices) {
      _capacityVertices = nextBufferCapacity(vertexCount);
      _buffers
        ..clear()
        ..addAll(
          List.generate(
            _ringDepth,
            (_) => gpu.gpuContext.createDeviceBuffer(
              gpu.StorageMode.hostVisible,
              _capacityVertices * bytesPerVertex,
            ),
          ),
        );
      // Freshly allocated buffers hold no data, so each is fully stale until
      // it is first written.
      _stale
        ..clear()
        ..addAll(List.filled(_ringDepth, (start: 0, end: vertexCount)));
      _cursor = 0;
    } else {
      _cursor = (_cursor + 1) % _ringDepth;
    }

    // The caller's change makes every buffer stale over that span. A mesh that
    // shrank leaves spans reaching past the new snapshot, so clamp them back.
    final change = (start: dirtyStart, end: dirtyEnd);
    for (var i = 0; i < _stale.length; i++) {
      _stale[i] = clampVertexRange(
        unionVertexRange(_stale[i], change),
        vertexCount,
      );
    }

    final buffer = _buffers[_cursor];
    final upload = _stale[_cursor];
    if (upload.end > upload.start) {
      final offset = upload.start * bytesPerVertex;
      final length = (upload.end - upload.start) * bytesPerVertex;
      buffer.overwrite(
        ByteData.sublistView(bytes, offset, offset + length),
        destinationOffsetInBytes: offset,
      );
      buffer.flush(offsetInBytes: offset, lengthInBytes: length);
    }
    // This buffer now matches the CPU copy.
    _stale[_cursor] = (start: 0, end: 0);
    return gpu.BufferView(
      buffer,
      offsetInBytes: 0,
      lengthInBytes: vertexCount * bytesPerVertex,
    );
  }
}

/// Assembles a [MeshGeometry] one vertex and triangle at a time.
///
/// The attribute setters ([normal], [texCoord], [texCoord1], [color], and
/// [tangent]) are sticky. Each value applies to every [addVertex] call that
/// follows until it is changed. [addVertex] returns the index of the added
/// vertex. When [deduplicate] is set, a vertex equal to one already added is
/// merged and the existing index is returned instead.
///
/// ```dart
/// final geometry = (GeometryBuilder()
///       ..color(Vector4(1, 0, 0, 1))
///       ..addVertex(Vector3(0, 0, 0))
///       ..addVertex(Vector3(1, 0, 0))
///       ..addVertex(Vector3(0, 1, 0))
///       ..addTriangle(0, 1, 2))
///     .build();
/// ```
/// {@category Geometry}
class GeometryBuilder {
  /// Creates an empty builder.
  ///
  /// When [deduplicate] is `true` (the default), [addVertex] merges a
  /// vertex equal to one already added.
  GeometryBuilder({this.deduplicate = true});

  /// Whether [addVertex] merges a vertex equal to one already added.
  final bool deduplicate;

  final List<double> _positions = [];
  final List<double> _normals = [];
  final List<double> _texCoords = [];
  final List<double> _texCoords1 = [];
  final List<double> _colors = [];
  final List<double> _tangents = [];
  final List<int> _indices = [];
  final Map<String, int> _vertexLookup = {};

  Vector3 _normal = Vector3(0.0, 0.0, 1.0);
  Vector2 _texCoord = Vector2.zero();
  Vector2 _texCoord1 = Vector2.zero();
  Vector4 _color = Vector4(1.0, 1.0, 1.0, 1.0);
  Vector4 _tangent = Vector4.zero();
  bool _normalsAuthored = false;

  /// The number of vertices added so far.
  int get vertexCount => _positions.length ~/ 3;

  /// The number of triangles added so far.
  int get triangleCount => _indices.length ~/ 3;

  /// Sets the normal applied to vertices added after this call.
  ///
  /// Authoring any normal opts the whole mesh out of generated normals;
  /// vertices added without an explicit normal keep the default
  /// `(0, 0, 1)`.
  GeometryBuilder normal(Vector3 value) {
    _normal = value.clone();
    _normalsAuthored = true;
    return this;
  }

  /// Sets the texture coordinate applied to vertices added after this
  /// call.
  GeometryBuilder texCoord(Vector2 value) {
    _texCoord = value.clone();
    return this;
  }

  /// Sets the secondary texture coordinate for subsequent vertices.
  GeometryBuilder texCoord1(Vector2 value) {
    _texCoord1 = value.clone();
    return this;
  }

  /// Sets the color applied to vertices added after this call.
  GeometryBuilder color(Vector4 value) {
    _color = value.clone();
    return this;
  }

  /// Sets the tangent applied to vertices added after this call.
  GeometryBuilder tangent(Vector4 value) {
    _tangent = value.clone();
    return this;
  }

  /// Adds a vertex at [position] carrying the current sticky attributes
  /// and returns its index.
  int addVertex(Vector3 position) {
    if (deduplicate) {
      final key = _vertexKey(position);
      final existing = _vertexLookup[key];
      if (existing != null) return existing;
      final index = vertexCount;
      _vertexLookup[key] = index;
      _appendVertex(position);
      return index;
    }
    final index = vertexCount;
    _appendVertex(position);
    return index;
  }

  /// Adds a triangle referencing three previously added vertex indices.
  GeometryBuilder addTriangle(int a, int b, int c) {
    final count = vertexCount;
    for (final index in [a, b, c]) {
      if (index < 0 || index >= count) {
        throw RangeError.range(index, 0, count - 1, 'vertex index');
      }
    }
    _indices
      ..add(a)
      ..add(b)
      ..add(c);
    return this;
  }

  /// Packs the accumulated vertices into the interleaved layout.
  ///
  /// Pure and free of GPU resources; [build] uses it, and it can be
  /// exercised directly without a render context.
  Uint8List packVertices() {
    return InterleavedLayoutAdapter.packUnskinned(
      positions: Float32List.fromList(_positions),
      vertexCount: vertexCount,
      normals: _resolveNormals(),
      texCoords: Float32List.fromList(_texCoords),
      texCoords1: Float32List.fromList(_texCoords1),
      colors: Float32List.fromList(_colors),
      tangents: Float32List.fromList(_tangents),
    );
  }

  /// Builds and GPU-uploads the accumulated mesh.
  ///
  /// Pass [GeometryStorage.updatable] for [storage] to build a mesh that
  /// can be mutated in place afterwards. [bufferArena] and [retainCpuData]
  /// match [MeshGeometry.fromArrays].
  MeshGeometry build({
    GeometryStorage storage = GeometryStorage.fixed,
    GeometryBufferArena? bufferArena,
    bool retainCpuData = true,
  }) {
    return MeshGeometry.fromArrays(
      positions: Float32List.fromList(_positions),
      normals: _resolveNormals(),
      texCoords: Float32List.fromList(_texCoords),
      texCoords1: Float32List.fromList(_texCoords1),
      colors: Float32List.fromList(_colors),
      tangents: Float32List.fromList(_tangents),
      indices: _indices.isEmpty ? null : List.of(_indices),
      storage: storage,
      bufferArena: bufferArena,
      retainCpuData: retainCpuData,
    );
  }

  Float32List _resolveNormals() {
    if (_normalsAuthored) return Float32List.fromList(_normals);
    return InterleavedLayoutAdapter.generateNormals(
      positions: Float32List.fromList(_positions),
      vertexCount: vertexCount,
      indices: _indices,
    );
  }

  void _appendVertex(Vector3 position) {
    _positions
      ..add(position.x)
      ..add(position.y)
      ..add(position.z);
    _normals
      ..add(_normal.x)
      ..add(_normal.y)
      ..add(_normal.z);
    _texCoords
      ..add(_texCoord.x)
      ..add(_texCoord.y);
    _texCoords1
      ..add(_texCoord1.x)
      ..add(_texCoord1.y);
    _colors
      ..add(_color.x)
      ..add(_color.y)
      ..add(_color.z)
      ..add(_color.w);
    _tangents
      ..add(_tangent.x)
      ..add(_tangent.y)
      ..add(_tangent.z)
      ..add(_tangent.w);
  }

  String _vertexKey(Vector3 position) {
    return '${position.x},${position.y},${position.z},'
        '${_normal.x},${_normal.y},${_normal.z},'
        '${_texCoord.x},${_texCoord.y},'
        '${_texCoord1.x},${_texCoord1.y},'
        '${_color.x},${_color.y},${_color.z},${_color.w},'
        '${_tangent.x},${_tangent.y},${_tangent.z},${_tangent.w}';
  }
}
