import {
  BufferGeometry,
  PointsMaterial,
  Points,
  Mesh,
  TriangleStripDrawMode,
  TriangleFanDrawMode,
  LineSegments,
  Line,
  LineLoop,
  Group,
  Vector3,
  Vector2,
  Quaternion,
  Matrix4,
  Box3,
  MeshPhongMaterial,
  Color,
  MathUtils,
  PerspectiveCamera,
  OrthographicCamera,
  DoubleSide,
  NormalBlending,
  BufferAttribute,
  LineBasicMaterial,
} from "three";
import { GL_CONSTANTS } from "./GltfStructure.js";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";

const STRUCTURE_ID_SEPARATOR = ":";

//#AI-GENERATED using Gemini 2.5 Pro, Claude-4-sonnet
//#Reviewed and adapted by dborysov@opendesign.com

export class DynamicGltfLoader {
  constructor(camera, scene, renderer) {
    this.camera = camera;
    this.scene = scene;
    this.renderer = renderer;

    this.eventHandlers = {
      geometryprogress: [],
      databasechunk: [],
      geometryend: [],
      geometryerror: [],
      update: [],
      geometrymemory: [],
      optimizationprogress: [],
    };

    this.loadDistance = 100;
    this.unloadDistance = 150;
    this.checkInterval = 1000;

    this.nodes = new Map();
    this.loadedMeshes = new Map();
    this.nodesToLoad = [];
    this.edgeNodes = [];
    this.structures = [];
    this.structureRoots = new Map();

    this.memoryLimit = this.getAvailableMemory();
    this.loadedGeometrySize = 0;
    this.geometryCache = new Map();
    this.materialCache = new Map();
    this.textureCache = new Map();
    this.currentMemoryUsage = 0;

    this.updateMemoryIndicator();

    this.loadedMaterials = new Map();
    this.abortController = new AbortController();

    this.batchSize = 10000;
    this.frameDelay = 0;

    this.graphicsObjectLimit = 10000;
    this.totalLoadedObjects = 0;

    this.lastUpdateTime = 0;
    this.updateInterval = 1000;

    this.handleToObjects = new Map();

    this.originalObjects = new Set();
    this.originalObjectsToSelection = new Set();

    this.optimizedOriginalMap = new Map();
    this.mergedMesh = new Set();
    this.mergedLines = new Set();
    this.mergedLineSegments = new Set();
    this.mergedPoints = new Set();

    this.isolatedObjects = [];
    //!!window.WebGL2RenderingContext && this.renderer.getContext() instanceof WebGL2RenderingContext
    this.useVAO = false;
    this.visibleEdges = true;

    this.handleToOptimizedObjects = new Map();

    this.hiddenHandles = new Set();
    this.newOptimizedObjects = new Set();
    this.oldOptimizeObjects = new Set();

    this.activeChunkLoads = 0;
    this.chunkQueue = [];

    // GPU-accelerated visibility system
    this.objectIdToIndex = new Map(); // objectId -> index
    this.maxObjectId = 0; // Maximum object ID
    this.objectVisibility = new Float32Array(); // Array of visibility flags for each object

    // Chunk loading configuration
    this.maxConcurrentChunks = 6; // Default limit

    // Merged geometry tracking
    this.mergedObjectMap = new Map(); // objectId -> {mergedObject, startIndex, endIndex, vertexCount}
    this.mergedGeometryVisibility = new Map(); // mergedObject -> visibility array

    this._webglInfoCache = null;
  }

  setVisibleEdges(visible) {
    this.visibleEdges = visible;
  }

  getAvailableMemory() {
    let memoryLimit = 6 * 1024 * 1024 * 1024;
    try {
      if (navigator.deviceMemory) {
        memoryLimit = navigator.deviceMemory * 1024 * 1024 * 1024;
      } else if (performance.memory) {
        const jsHeapSizeLimit = performance.memory.jsHeapSizeLimit;
        if (jsHeapSizeLimit) {
          memoryLimit = Math.min(memoryLimit, jsHeapSizeLimit);
        }
      }

      memoryLimit = Math.min(memoryLimit, 16 * 1024 * 1024 * 1024);
      memoryLimit = Math.max(memoryLimit, 2 * 1024 * 1024 * 1024);

      console.log(`Available memory set to ${Math.round(memoryLimit / (1024 * 1024 * 1024))}GB`);
    } catch (error) {
      console.warn("Error detecting available memory:", error);
    }

    return memoryLimit / 3;
  }

  getAbortController() {
    return this.abortController;
  }

  abortLoading() {
    this.abortController.abort();
  }

  updateMemoryIndicator() {
    this.dispatchEvent("geometrymemory", {
      currentUsage: this.currentMemoryUsage,
      limit: this.memoryLimit,
    });
  }

  setMemoryLimit(bytesLimit) {
    // this.memoryLimit = bytesLimit;
    //this.updateMemoryIndicator();
    // console.log(`Memory limit set to ${Math.round(bytesLimit / (1024 * 1024))}MB`);
  }

  estimateGeometrySize(nodeGroup) {
    let totalSize = 0;
    nodeGroup.traverse((child) => {
      if (child.geometry) {
        if (this.abortController.signal.aborted) {
          throw new DOMException("Loading aborted", "AbortError");
        }
        const geometry = child.geometry;

        if (geometry.attributes) {
          Object.values(geometry.attributes).forEach((attribute) => {
            if (attribute && attribute.array) {
              totalSize += attribute.array.byteLength;
            }
          });
        }

        if (geometry.index && geometry.index.array) {
          totalSize += geometry.index.array.byteLength;
        }
      }
    });

    return totalSize;
  }

  recalculateScene() {
    const geometries = [];
    this.scene.traverse((object) => {
      if (this.abortController.signal.aborted) {
        throw new DOMException("Loading aborted", "AbortError");
      }

      if (object.geometry && !this.geometryCache.has(object.geometry.uuid)) {
        const size = this.estimateGeometrySize(object);
        this.geometryCache.set(object.geometry.uuid, size);
        geometries.push({
          object,
          size,
          distance: object.position.distanceTo(this.camera.position),
        });
      }
    });

    if (this.abortController.signal.aborted) {
      throw new DOMException("Loading aborted", "AbortError");
    }

    geometries.sort((a, b) => b.distance - a.distance);

    let currentMemoryUsage = 0;
    for (const geo of geometries) {
      currentMemoryUsage += geo.size;
    }

    if (currentMemoryUsage > this.memoryLimit) {
      console.log(`Memory usage (${Math.round(currentMemoryUsage / (1024 * 1024))}MB) exceeds limit`);

      for (const geo of geometries) {
        if (currentMemoryUsage <= this.memoryLimit) break;

        if (this.abortController.signal.aborted) {
          throw new DOMException("Loading aborted", "AbortError");
        }

        const object = geo.object;
        if (object.geometry) {
          currentMemoryUsage -= geo.size;
          this.geometryCache.delete(object.geometry.uuid);
          object.geometry.dispose();
          object.visible = false;
        }
      }
    }

    this.currentMemoryUsage = currentMemoryUsage;
    this.updateMemoryIndicator();

    console.log(`Final memory usage: ${Math.round(currentMemoryUsage / (1024 * 1024))}MB`);
  }

  getStats() {
    let totalObjects = 0;
    let renderedObjects = 0;
    let totalTriangles = 0;
    let renderedTriangles = 0;
    let totalLines = 0;
    let renderedLines = 0;
    let totalEdges = 0;
    let renderedEdges = 0;

    this.scene.traverse((object) => {
      totalObjects++;

      const geometry = object.geometry;
      if (!geometry) return;

      let triCount = 0;
      if (geometry.index) {
        triCount = Math.floor(geometry.index.count / 3);
      } else if (geometry.attributes && geometry.attributes.position) {
        triCount = Math.floor(geometry.attributes.position.count / 3);
      }
      totalTriangles += triCount;

      let lineCount = 0;
      if (geometry.index) {
        lineCount = Math.floor(geometry.index.count / 2);
      } else if (geometry.attributes && geometry.attributes.position) {
        lineCount = Math.floor(geometry.attributes.position.count / 2);
      }

      if (object.type === "Line" || object.type === "LineSegments" || object.type === "LineLoop") {
        if (object.userData.isEdge) {
          totalEdges += lineCount;
        } else {
          totalLines += lineCount;
        }
      }

      if (object.visible !== false) {
        if (object.isMesh || object.isLine || object.isPoints) {
          renderedObjects++;
          if (object.isMesh) {
            renderedTriangles += triCount;
          } else if (object.type === "Line" || object.type === "LineSegments" || object.type === "LineLoop") {
            if (object.userData.isEdge) {
              renderedEdges += lineCount;
            } else {
              renderedLines += lineCount;
            }
          }
        }
      }
    });

    const geometryCount = this.geometryCache ? this.geometryCache.size : 0;
    const geometryMemoryBytes = Array.from(this.geometryCache?.values?.() || []).reduce((a, b) => a + b, 0);

    const uniqueMaterialIds = new Set();
    const uniqueTextureIds = new Set();
    if (Array.isArray(this.structures)) {
      for (const structure of this.structures) {
        // console.log(structure.materialCache.values());
        try {
          for (const entry of structure.materialCache.values()) {
            if (entry?.mesh?.uuid) uniqueMaterialIds.add(entry.mesh.uuid);
            if (entry?.points?.uuid) uniqueMaterialIds.add(entry.points.uuid);
            if (entry?.lines?.uuid) uniqueMaterialIds.add(entry.lines.uuid);
          }
        } catch (exp) {
          console.error("Error adding material to uniqueMaterialIds", exp);
        }
      }
    }
    const materialCount = uniqueMaterialIds.size;
    const textureCount = uniqueTextureIds.size;
    const estimatedGpuMemoryBytes = geometryMemoryBytes;

    if (!this._webglInfoCache) {
      try {
        const gl = this.renderer.getContext();
        const dbgInfo = gl.getExtension("WEBGL_debug_renderer_info");
        if (dbgInfo) {
          const rendererStr = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL);
          const vendorStr = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL);
          this._webglInfoCache = { renderer: rendererStr, vendor: vendorStr };
        } else {
          this._webglInfoCache = { renderer: null, vendor: null };
        }
      } catch (e) {
        console.error("Error getting webgl info", e);
        this._webglInfoCache = { renderer: null, vendor: null };
      }
    }

    const size = new Vector2();
    if (this.renderer && this.renderer.getSize) {
      this.renderer.getSize(size);
    }

    return {
      scene: {
        beforeOptimization: {
          objects: totalObjects - renderedObjects,
          triangles: totalTriangles - renderedTriangles,
          lines: totalLines - renderedLines,
          edges: totalEdges - renderedEdges,
        },

        afterOptimization: {
          objects: renderedObjects,
          triangles: renderedTriangles,
          lines: renderedLines,
          edges: renderedEdges,
        },
      },
      memory: {
        geometries: { count: geometryCount, bytes: geometryMemoryBytes },
        textures: { count: textureCount },
        materials: { count: materialCount },
        totalEstimatedGpuBytes: estimatedGpuMemoryBytes,
      },
      system: {
        webglRenderer: this._webglInfoCache?.renderer || "",
        webglVendor: this._webglInfoCache?.vendor || "",
        viewport: { width: size.x || 0, height: size.y || 0 },
      },
    };
  }

  async loadNode(nodeId, onLoadFinishCb) {
    const node = this.nodes.get(nodeId);
    if (!node || node.loaded || node.loading) return;

    node.loading = true;
    const meshDef = node.structure.getJson().meshes[node.meshIndex];

    try {
      const bufferRequests = [];
      const primitiveReqMap = new Map();
      for (let primIdx = 0; primIdx < meshDef.primitives.length; primIdx++) {
        const primitive = meshDef.primitives[primIdx];
        const reqs = [];

        if (primitive.attributes.POSITION !== undefined) {
          const accessorIndex = primitive.attributes.POSITION;
          const accessor = node.structure.json.accessors[accessorIndex];
          const bufferView = node.structure.json.bufferViews[accessor.bufferView];
          const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
          const components = node.structure.getNumComponents(accessor.type);
          const count = accessor.count;
          const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
          reqs.push({
            offset: byteOffset,
            length: byteLength,
            componentType: accessor.componentType,
            accessorIndex,
            type: "position",
            primIdx,
          });
        }

        if (primitive.attributes.NORMAL !== undefined) {
          const accessorIndex = primitive.attributes.NORMAL;
          const accessor = node.structure.json.accessors[accessorIndex];
          const bufferView = node.structure.json.bufferViews[accessor.bufferView];
          const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
          const components = node.structure.getNumComponents(accessor.type);
          const count = accessor.count;
          const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
          reqs.push({
            offset: byteOffset,
            length: byteLength,
            componentType: accessor.componentType,
            accessorIndex,
            type: "normal",
            primIdx,
          });
        }

        if (primitive.attributes.TEXCOORD_0 !== undefined) {
          const accessorIndex = primitive.attributes.TEXCOORD_0;
          const accessor = node.structure.json.accessors[accessorIndex];
          const bufferView = node.structure.json.bufferViews[accessor.bufferView];
          const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
          const components = node.structure.getNumComponents(accessor.type);
          const count = accessor.count;
          const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
          reqs.push({
            offset: byteOffset,
            length: byteLength,
            componentType: accessor.componentType,
            accessorIndex,
            type: "uv",
            primIdx,
          });
        }

        if (primitive.indices !== undefined) {
          const accessorIndex = primitive.indices;
          const accessor = node.structure.json.accessors[accessorIndex];
          const bufferView = node.structure.json.bufferViews[accessor.bufferView];
          const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
          const components = node.structure.getNumComponents(accessor.type);
          const count = accessor.count;
          const byteLength = count * components * node.structure.getComponentSize(accessor.componentType);
          reqs.push({
            offset: byteOffset,
            length: byteLength,
            componentType: accessor.componentType,
            accessorIndex,
            type: "index",
            primIdx,
          });
        }
        primitiveReqMap.set(primIdx, reqs);
        bufferRequests.push(...reqs);
      }

      if (bufferRequests.length === 0) {
        node.loaded = true;
        node.loading = false;
        return;
      }
      bufferRequests.sort((a, b) => a.offset - b.offset);
      const minOffset = bufferRequests[0].offset;
      const maxOffset = Math.max(...bufferRequests.map((r) => r.offset + r.length));
      const totalLength = maxOffset - minOffset;

      const { buffer, relOffset: baseRelOffset } = await node.structure.scheduleRequest({
        offset: minOffset,
        length: totalLength,
        componentType: null,
      });

      for (const req of bufferRequests) {
        const relOffset = req.offset - minOffset;
        req.data = node.structure.createTypedArray(buffer, baseRelOffset + relOffset, req.length, req.componentType);
      }

      for (let primIdx = 0; primIdx < meshDef.primitives.length; primIdx++) {
        const primitive = meshDef.primitives[primIdx];
        const geometry = new BufferGeometry();
        const reqs = primitiveReqMap.get(primIdx);

        if (primitive.attributes.POSITION !== undefined) {
          const req = reqs.find((r) => r.type === "position" && r.accessorIndex === primitive.attributes.POSITION);
          const accessor = node.structure.json.accessors[primitive.attributes.POSITION];
          const components = node.structure.getNumComponents(accessor.type);
          geometry.setAttribute("position", new BufferAttribute(req.data, components));
        }

        if (primitive.attributes.NORMAL !== undefined) {
          const req = reqs.find((r) => r.type === "normal" && r.accessorIndex === primitive.attributes.NORMAL);
          const accessor = node.structure.json.accessors[primitive.attributes.NORMAL];
          const components = node.structure.getNumComponents(accessor.type);
          geometry.setAttribute("normal", new BufferAttribute(req.data, components));
        }

        if (primitive.attributes.TEXCOORD_0 !== undefined) {
          const req = reqs.find((r) => r.type === "uv" && r.accessorIndex === primitive.attributes.TEXCOORD_0);
          const accessor = node.structure.json.accessors[primitive.attributes.TEXCOORD_0];
          const components = node.structure.getNumComponents(accessor.type);
          geometry.setAttribute("uv", new BufferAttribute(req.data, components));
        }

        if (primitive.indices !== undefined) {
          const req = reqs.find((r) => r.type === "index" && r.accessorIndex === primitive.indices);
          geometry.setIndex(new BufferAttribute(req.data, 1));
        }

        let material;
        if (primitive.material !== undefined) {
          material = node.structure.getCachedMaterial(primitive.material, primitive.mode);

          if (!material) {
            const materialDef = node.structure.json.materials[primitive.material];
            material = node.structure.createMaterial(materialDef, primitive.mode);
          }
        } else {
          material = this.createDefaultMaterial(primitive.mode);
        }

        let mesh;
        if (primitive.mode === GL_CONSTANTS.POINTS) {
          mesh = new Points(geometry, material);
        } else if (
          primitive.mode === GL_CONSTANTS.TRIANGLES ||
          primitive.mode === GL_CONSTANTS.TRIANGLE_STRIP ||
          primitive.mode === GL_CONSTANTS.TRIANGLE_FAN ||
          primitive.mode === undefined
        ) {
          mesh = new Mesh(geometry, material);
          if (primitive.mode === GL_CONSTANTS.TRIANGLE_STRIP) {
            mesh.drawMode = TriangleStripDrawMode;
          } else if (primitive.mode === GL_CONSTANTS.TRIANGLE_FAN) {
            mesh.drawMode = TriangleFanDrawMode;
          }
        } else if (primitive.mode === GL_CONSTANTS.LINES) {
          mesh = new LineSegments(geometry, material);
        } else if (primitive.mode === GL_CONSTANTS.LINE_STRIP) {
          mesh = new Line(geometry, material);
        } else if (primitive.mode === GL_CONSTANTS.LINE_LOOP) {
          mesh = new LineLoop(geometry, material);
        }

        if (node.extras) {
          mesh.userData = { ...mesh.userData, ...node.extras };
        }
        if (meshDef.extras) {
          mesh.userData = { ...mesh.userData, ...meshDef.extras };
        }
        if (primitive.extras) {
          mesh.userData = { ...mesh.userData, ...primitive.extras };
        }

        if (node.handle) {
          mesh.userData.handle = node.handle;
        } else {
          mesh.userData.handle = this.getFullHandle(node.structure.id, mesh.userData.handle);
        }

        if (mesh.material.name === "edges") {
          mesh.userData.isEdge = true;
        } else {
          mesh.userData.isEdge = false;
        }

        this.registerObjectWithHandle(mesh, mesh.userData.handle);

        mesh.position.copy(node.position);
        if (!geometry.attributes.normal) {
          geometry.computeVertexNormals();
        }
        if (material.aoMap && geometry.attributes.uv) {
          geometry.setAttribute("uv2", geometry.attributes.uv);
        }
        if (node.group) {
          node.group.add(mesh);
        } else {
          this.scene.add(mesh);
        }
        node.object = mesh;
        this.totalLoadedObjects++;
        mesh.visible = this.totalLoadedObjects < this.graphicsObjectLimit;
      }
      node.loaded = true;
      node.loading = false;
      const geometrySize = this.estimateGeometrySize(node.object);
      this.geometryCache.set(node.object.uuid, geometrySize);
      this.currentMemoryUsage += geometrySize;
      if (onLoadFinishCb) {
        onLoadFinishCb();
      }
    } catch (error) {
      node.loading = false;

      if (error.name === "AbortError") {
        return;
      }

      if (node.structure && node.structure.loadingAborted) {
        return;
      }

      console.error(`Error loading node ${nodeId}:`, error);
    }
  }

  unloadNode(nodeId) {
    const node = this.nodes.get(nodeId);
    if (!node || !node.loaded) return;

    if (node.object) {
      if (node.object.parent) {
        node.object.parent.remove(node.object);
      } else {
        this.scene.remove(node.object);
      }

      node.object.traverse((child) => {
        if (child.geometry) {
          const geometrySize = this.geometryCache.get(child.geometry.uuid) || 0;
          this.currentMemoryUsage -= geometrySize;
          this.geometryCache.delete(child.geometry.uuid);
          child.geometry.dispose();
        }
      });

      node.object = null;
      node.loaded = false;
      this.updateMemoryIndicator();
      console.log(`Unloaded node: ${nodeId}`);
    }
  }

  checkDistances() {
    const cameraPosition = this.camera.position;

    this.nodes.forEach((node, nodeId) => {
      const distance = cameraPosition.distanceTo(node.position);

      if (node.loaded) {
        if (distance > this.unloadDistance) {
          this.unloadNode(nodeId);
        }
      } else if (!node.loading) {
        if (distance < this.loadDistance) {
          this.loadNode(nodeId);
        }
      }
    });
  }

  async loadStructure(structures) {
    this.clear();

    const structureArray = Array.isArray(structures) ? structures : [structures];

    for (const structure of structureArray) {
      await structure.initialize(this);
      this.structures.push(structure);
    }

    for (const structure of this.structures) {
      try {
        await structure.loadTextures();
        await structure.loadMaterials();
      } catch (error) {
        console.error("Error loading materials:", error);
        throw error;
      }
    }

    await this.processSceneHierarchy();
  }

  async processSceneHierarchy() {
    if (this.structures.length === 0) {
      throw new Error("No GLTF structures loaded");
    }

    this.nodesToLoad = [];

    let estimatedSize = 0;

    for (const structure of this.structures) {
      const gltf = structure.getJson();

      if (!gltf.scenes || !gltf.scenes.length) {
        console.warn("No scenes found in GLTF structure");
        continue;
      }

      estimatedSize += gltf.buffers[0].byteLength;

      const rootGroup = new Group();
      rootGroup.name = `structure_${structure.id}_root`;
      this.scene.add(rootGroup);
      this.structureRoots.set(structure.id, rootGroup);

      const scene = gltf.scenes[gltf.scene || 0];

      for (const nodeIndex of scene.nodes) {
        await this.processNodeHierarchy(structure, nodeIndex, rootGroup);
      }
    }

    const ignoreEdges = estimatedSize * 2 > this.memoryLimit;

    this.nodesToLoad.sort((a, b) => {
      const nodeA = this.nodes.get(a);
      const nodeB = this.nodes.get(b);

      if (!nodeA?.geometryExtents || !nodeB?.geometryExtents) {
        return 0;
      }

      const sizeA = nodeA.geometryExtents.getSize(new Vector3());
      const sizeB = nodeB.geometryExtents.getSize(new Vector3());
      const volumeA = sizeA.x * sizeA.y * sizeA.z;
      const volumeB = sizeB.x * sizeB.y * sizeB.z;

      return volumeB - volumeA;
    });

    if (!ignoreEdges && this.visibleEdges) {
      this.nodesToLoad.push(...this.edgeNodes);
    }

    this.dispatchEvent("databasechunk", {
      totalNodes: this.nodesToLoad.length,
      structures: this.structures.map((s) => ({
        id: s.id,
        nodeCount: this.nodesToLoad.filter((nodeId) => nodeId.startsWith(s.id)).length,
      })),
    });
  }

  getFullHandle(structureId, originalHandle) {
    return `${structureId}${STRUCTURE_ID_SEPARATOR}${originalHandle}`;
  }

  async processNodeHierarchy(structure, nodeId, parentGroup) {
    const nodeDef = structure.json.nodes[nodeId];
    let nodeGroup = null;

    let handle = null;
    if (nodeDef.extras?.handle) {
      handle = this.getFullHandle(structure.id, nodeDef.extras.handle);
    }

    if (nodeDef.camera !== undefined) {
      const camera = this.loadCamera(structure, nodeDef.camera, nodeDef);
      if (nodeDef.extras) {
        camera.userData = { ...camera.userData, ...nodeDef.extras };
      }
      this.scene.add(camera);
      return;
    }

    const needsGroup = this.needsGroupForNode(structure, nodeDef);

    if (needsGroup) {
      nodeGroup = new Group();
      nodeGroup.name = nodeDef.name || `node_${nodeId}`;

      if (nodeDef.extras) {
        nodeGroup.userData = { ...nodeDef.extras };
        if (nodeGroup.userData.handle) {
          nodeGroup.userData.handle = this.getFullHandle(structure.id, nodeGroup.userData.handle);
        }
      }

      if (nodeDef.matrix) {
        nodeGroup.matrix.fromArray(nodeDef.matrix);
        nodeGroup.matrixAutoUpdate = false;
      } else if (nodeDef.translation || nodeDef.rotation || nodeDef.scale) {
        const position = nodeDef.translation ? new Vector3().fromArray(nodeDef.translation) : new Vector3();
        const quaternion = nodeDef.rotation ? new Quaternion().fromArray(nodeDef.rotation) : new Quaternion();
        const scale = nodeDef.scale ? new Vector3().fromArray(nodeDef.scale) : new Vector3(1, 1, 1);
        nodeGroup.matrix.compose(position, quaternion, scale);
        nodeGroup.matrixAutoUpdate = false;
      }

      if (parentGroup) {
        parentGroup.add(nodeGroup);
      }
    }

    if (nodeDef.mesh !== undefined) {
      const nodeMatrix = new Matrix4();
      const uniqueNodeId = `${structure.id}_${nodeId}`;
      const meshDef = structure.json.meshes[nodeDef.mesh];
      const geometryExtents = new Box3();

      for (const primitive of meshDef.primitives) {
        const positionAccessor = structure.json.accessors[primitive.attributes.POSITION];
        if (positionAccessor && positionAccessor.min && positionAccessor.max) {
          const primitiveBox = new Box3(
            new Vector3().fromArray(positionAccessor.min),
            new Vector3().fromArray(positionAccessor.max)
          );
          geometryExtents.union(primitiveBox);
        }
      }

      let isEdge = false;
      if (meshDef.primitives[0].material !== undefined) {
        const material = structure.json.materials[meshDef.primitives[0].material];
        if (material?.name === "edges") {
          isEdge = true;
        }
      }

      if (!isEdge) {
        this.nodesToLoad.push(uniqueNodeId);
      } else {
        this.edgeNodes.push(uniqueNodeId);
      }

      if (meshDef.extras && meshDef.extras.handle) {
        handle = this.getFullHandle(structure.id, meshDef.extras.handle);
      }

      this.nodes.set(uniqueNodeId, {
        position: nodeGroup ? nodeGroup.position.clone() : new Vector3().setFromMatrixPosition(nodeMatrix),
        nodeIndex: nodeId,
        meshIndex: nodeDef.mesh,
        loaded: false,
        loading: false,
        object: null,
        group: nodeGroup || parentGroup,
        structure,
        extras: nodeDef.extras,
        geometryExtents,
        handle: handle || this.getFullHandle(structure.id, structure._nextObjectId++),
      });
    }

    if (nodeDef.children) {
      for (const childId of nodeDef.children) {
        await this.processNodeHierarchy(structure, childId, nodeGroup || parentGroup);
      }
    }

    return nodeGroup;
  }

  needsGroupForNode(structure, nodeDef) {
    const hasTransforms = nodeDef.matrix || nodeDef.translation || nodeDef.rotation || nodeDef.scale;

    const hasMultiplePrimitives =
      nodeDef.mesh !== undefined && structure.json.meshes[nodeDef.mesh].primitives.length > 1;

    return hasTransforms !== undefined || hasMultiplePrimitives;
  }

  async processNodes() {
    const nodesToLoad = this.nodesToLoad;
    let loadedCount = 0;
    let lastLoadedCount = 0;
    const totalNodes = nodesToLoad.length;

    const loadProgress = async () => {
      loadedCount++;
      if (loadedCount - lastLoadedCount > 1000) {
        lastLoadedCount = loadedCount;
        this.updateMemoryIndicator();
        this.dispatchEvent("geometryprogress", {
          percentage: Math.round((loadedCount / totalNodes) * 100),
          loaded: loadedCount,
          total: totalNodes,
        });

        const currentTime = Date.now();
        if (currentTime - this.lastUpdateTime >= this.updateInterval) {
          this.dispatchEvent("update");
          this.lastUpdateTime = currentTime;
        }

        await new Promise((resolve) => {
          setTimeout(resolve, 0);
        });
      }
    };

    try {
      const loadOperations = [];
      for (const nodeId of nodesToLoad) {
        if (this.abortController.signal.aborted) {
          throw new DOMException("Loading aborted", "AbortError");
        }

        const estimatedSize = await this.estimateNodeSize(nodeId);

        if (this.currentMemoryUsage + estimatedSize > this.memoryLimit) {
          console.log(`Memory limit reached after loading ${loadedCount} nodes`);
          this.dispatchEvent("geometryerror", {
            message: "Memory limit reached",
          });
          this.dispatchEvent("update");
          return loadedCount;
        }

        loadOperations.push(this.loadNode(nodeId, loadProgress));
      }

      for (const structure of this.structures) {
        loadOperations.push(structure.flushBufferRequests());
      }

      await Promise.all(loadOperations);

      this.dispatchEvent("geometryend", {
        totalLoaded: loadedCount,
        totalNodes,
      });

      return loadedCount;
    } catch (error) {
      this.dispatchEvent("geometryerror", { error });
      throw error;
    }
  }

  async loadNodes() {
    console.time("Process nodes");
    await this.processNodes();
    console.timeEnd("Process nodes");

    console.time("Optimize scene");
    await this.optimizeScene();
    console.timeEnd("Optimize scene");
  }

  cleanupPartialLoad() {
    this.nodesToLoad.forEach((nodeId) => {
      const node = this.nodes.get(nodeId);
      if (node && node.loading) {
        this.unloadNode(nodeId);
      }
    });
  }

  createDefaultMaterial(primitiveMode = undefined) {
    if (primitiveMode === GL_CONSTANTS.POINTS) {
      return new PointsMaterial({
        color: new Color(0x808080),
        size: 0.05,
        sizeAttenuation: false,
        alphaTest: 0.5,
        transparent: true,
        vertexColors: false,
        blending: NormalBlending,
        depthWrite: false,
        depthTest: true,
      });
    } else if (
      primitiveMode === GL_CONSTANTS.LINES ||
      primitiveMode === GL_CONSTANTS.LINE_STRIP ||
      primitiveMode === GL_CONSTANTS.LINE_LOOP
    ) {
      return new LineBasicMaterial({
        color: 0x808080,
        linewidth: 1.0,
        alphaTest: 0.1,
        depthTest: true,
        depthWrite: true,
        transparent: true,
        opacity: 1.0,
      });
    } else {
      return new MeshPhongMaterial({
        color: 0x808080,
        specular: 0x222222,
        shininess: 10,
        side: DoubleSide,
      });
    }
  }

  async estimateNodeSize(nodeId) {
    const node = this.nodes.get(nodeId);
    if (!node) return 0;
    return await node.structure.estimateNodeSize(node.meshIndex);
  }

  getTotalGeometryExtent() {
    const totalExtent = new Box3();

    for (const node of this.nodes.values()) {
      if (!node.geometryExtents) continue;

      if (node.object && this.hiddenHandles.has(node.object.userData.handle)) continue;

      const transformedBox = node.geometryExtents.clone();

      if (node.group && node.group.matrix) {
        transformedBox.applyMatrix4(node.group.matrix);

        if (node.group.parent && node.group.parent.matrix) {
          transformedBox.applyMatrix4(node.group.parent.matrix);
        }
      }
      totalExtent.union(transformedBox);
    }

    return totalExtent;
  }

  loadCamera(structure, cameraIndex, nodeDef) {
    const cameraDef = structure.getJson().cameras[cameraIndex];
    const params = cameraDef[cameraDef.type];

    let camera;
    if (cameraDef.type === "perspective") {
      camera = new PerspectiveCamera(
        MathUtils.radToDeg(params.yfov),
        params.aspectRatio || 1,
        params.znear || 1,
        params.zfar || 2e6
      );
    } else if (cameraDef.type === "orthographic") {
      camera = new OrthographicCamera(
        params.xmag / -2,
        params.xmag / 2,
        params.ymag / 2,
        params.ymag / -2,
        params.znear,
        params.zfar
      );
    }

    if (nodeDef.matrix) {
      camera.matrix.fromArray(nodeDef.matrix);
      camera.matrix.decompose(camera.position, camera.quaternion, camera.scale);
    } else {
      if (nodeDef.translation) {
        camera.position.fromArray(nodeDef.translation);
      }
      if (nodeDef.rotation) {
        camera.quaternion.fromArray(nodeDef.rotation);
      }
      if (nodeDef.scale) {
        camera.scale.fromArray(nodeDef.scale);
      }
    }

    return camera;
  }

  clearNodesToLoad() {
    this.nodesToLoad = [];
  }

  removeOptimization() {
    this.originalObjects.forEach((obj) => (obj.visible = true));

    const disposeMerged = (obj) => {
      if (obj.parent) {
        obj.parent.remove(obj);
      }
      if (obj.geometry) {
        obj.geometry.dispose();
      }
    };

    if (this.structureGroups) {
      for (const group of this.structureGroups.values()) {
        group.meshes.forEach(disposeMerged);
        group.lines.forEach(disposeMerged);
        group.lineSegments.forEach(disposeMerged);
        group.meshes.clear();
        group.lines.clear();
        group.lineSegments.clear();
      }
    }
    this.optimizedOriginalMap.clear();
    this.mergedMesh.clear();
    this.mergedLines.clear();
    this.mergedLineSegments.clear();
    this.originalObjects.clear();
    this.originalObjectsToSelection.clear();
  }

  initializeObjectVisibility() {
    if (this.maxObjectId > 0) {
      this.objectVisibility = new Float32Array(this.maxObjectId);
      for (let i = 0; i < this.maxObjectId; i++) {
        this.objectVisibility[i] = 1.0;
      }
      console.log(`Initialized object visibility array: ${this.maxObjectId} objects`);
    }
  }

  createVisibilityMaterial(material) {
    material.onBeforeCompile = (shader) => {
      shader.vertexShader = shader.vertexShader.replace(
        "#include <common>",
        `
        #include <common>
        attribute float visibility;
        varying float vVisibility;
        `
      );

      shader.fragmentShader = shader.fragmentShader.replace(
        "#include <common>",
        `
        #include <common>
        varying float vVisibility;
        `
      );

      shader.vertexShader = shader.vertexShader.replace(
        "void main() {",
        `
        void main() {
          vVisibility = visibility;
        `
      );

      shader.fragmentShader = shader.fragmentShader.replace(
        "void main() {",
        `
        void main() {
          if (vVisibility < 0.5) discard;
        `
      );
    };
    material.needsUpdate = true;

    return material;
  }

  clear() {
    this.chunkQueue = [];

    this.structures.forEach((structure) => {
      if (structure) {
        structure.clear();
      }
    });
    this.structures = [];

    this.nodes.forEach((node) => {
      if (node.object) {
        if (node.object.parent) {
          node.object.parent.remove(node.object);
        }
        if (node.object.geometry) {
          node.object.geometry.dispose();
        }
        if (node.object.material) {
          if (Array.isArray(node.object.material)) {
            node.object.material.forEach((material) => material.dispose());
          } else {
            node.object.material.dispose();
          }
        }
      }
    });
    this.nodes.clear();

    this.loadedMeshes.forEach((mesh) => {
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((material) => material.dispose());
        } else {
          mesh.material.dispose();
        }
      }
    });
    this.loadedMeshes.clear();

    this.structureRoots.forEach((rootGroup) => {
      if (rootGroup) {
        rootGroup.traverse((child) => {
          if (child.geometry) child.geometry.dispose();
          if (child.material) {
            if (Array.isArray(child.material)) {
              child.material.forEach((material) => material.dispose());
            } else {
              child.material.dispose();
            }
          }
        });
        if (rootGroup.parent) {
          rootGroup.parent.remove(rootGroup);
        }
      }
    });
    this.structureRoots.clear();

    this.mergedMesh.forEach((mesh) => {
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((material) => material.dispose());
        } else {
          mesh.material.dispose();
        }
      }
      if (mesh.parent) mesh.parent.remove(mesh);
    });
    this.mergedMesh.clear();

    this.mergedLines.forEach((line) => {
      if (line.geometry) line.geometry.dispose();
      if (line.material) line.material.dispose();
      if (line.parent) line.parent.remove(line);
    });
    this.mergedLines.clear();

    this.mergedLineSegments.forEach((lineSegment) => {
      if (lineSegment.geometry) lineSegment.geometry.dispose();
      if (lineSegment.material) lineSegment.material.dispose();
      if (lineSegment.parent) lineSegment.parent.remove(lineSegment);
    });
    this.mergedLineSegments.clear();

    this.mergedPoints.forEach((points) => {
      if (points.geometry) points.geometry.dispose();
      if (points.material) points.material.dispose();
      if (points.parent) points.parent.remove(points);
    });
    this.mergedPoints.clear();

    this.geometryCache.clear();
    this.materialCache.clear();
    this.textureCache.clear();
    this.loadedMaterials.clear();

    this.nodesToLoad = [];
    this.handleToObjects.clear();
    this.originalObjects.clear();
    this.originalObjectsToSelection.clear();
    this.optimizedOriginalMap.clear();
    this.handleToOptimizedObjects.clear();
    this.hiddenHandles.clear();
    this.newOptimizedObjects.clear();
    this.oldOptimizeObjects.clear();
    this.isolatedObjects = [];

    this.totalLoadedObjects = 0;
    this.lastUpdateTime = 0;
    this.currentMemoryUsage = 0;
    this.loadedGeometrySize = 0;

    this.abortController = new AbortController();
    this.updateMemoryIndicator();

    this.objectIdToIndex.clear();
    this.maxObjectId = 0;
    this.objectVisibility = new Float32Array();
  }

  setStructureTransform(structureId, matrix) {
    const rootGroup = this.structureRoots.get(structureId);
    if (rootGroup) {
      rootGroup.matrix.copy(matrix);
      rootGroup.matrix.decompose(rootGroup.position, rootGroup.quaternion, rootGroup.scale);
      return true;
    }
    return false;
  }

  getStructureRootGroup(structureId) {
    return this.structureRoots.get(structureId);
  }

  addEventListener(event, handler) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].push(handler);
    }
  }

  removeEventListener(event, handler) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event] = this.eventHandlers[event].filter((h) => h !== handler);
    }
  }

  dispatchEvent(event, data) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].forEach((handler) => handler(data));
    }
  }

  registerObjectWithHandle(object, handle) {
    if (!handle) return;

    const fullHandle = object.userData.handle;
    if (!this.handleToObjects.has(fullHandle)) {
      this.handleToObjects.set(fullHandle, new Set());
    }
    this.handleToObjects.get(fullHandle).add(object);

    object.userData.structureId = object.userData.handle.split(STRUCTURE_ID_SEPARATOR)[0];
  }

  getObjectsByHandle(handle) {
    if (!handle) return [];
    return Array.from(this.handleToObjects.get(handle) || []);
  }

  getHandlesByObjects(objects) {
    if (!objects.length) return [];
    const handles = new Set();
    objects.forEach((obj) => {
      if (this.originalObjects.has(obj)) handles.add(obj.userData.handle);
    });
    return Array.from(handles);
  }

  getMaterialId(material, index) {
    const props = {
      type: material.type,
      color: material.color?.getHex(),
      map: material.map?.uuid,
      transparent: material.transparent,
      opacity: material.opacity,
      side: material.side,
      index: index ? 1 : 0,
    };
    return JSON.stringify(props);
  }

  addToMaterialGroup(object, groupsMap, optimizeGroupList) {
    const VERTEX_LIMIT = 100_000;
    const INDEX_LIMIT = 100_000;

    const objectGeometryVertexCount = object.geometry.attributes.position.count;
    const objectGeometryIndexCount = object.geometry.index ? object.geometry.index.count : 0;

    const material = object.material;
    let materialId = this.getMaterialId(material, object.geometry.index !== null);

    let group;
    if (!groupsMap.has(materialId)) {
      group = {
        material,
        objects: [object],
        totalVertices: objectGeometryVertexCount,
        totalIndices: objectGeometryIndexCount,
      };
      groupsMap.set(materialId, group);
      optimizeGroupList.push(group);
    } else {
      group = groupsMap.get(materialId);
      if (
        group.totalVertices + objectGeometryVertexCount > VERTEX_LIMIT ||
        group.totalIndices + objectGeometryIndexCount > INDEX_LIMIT
      ) {
        const newGroup = {
          material,
          objects: [object],
          totalVertices: objectGeometryVertexCount,
          totalIndices: objectGeometryIndexCount,
        };
        materialId = this.getMaterialId(material, object.geometry.index !== null);
        groupsMap.set(materialId, newGroup);
        optimizeGroupList.push(newGroup);
      } else {
        group.objects.push(object);
        group.totalVertices += objectGeometryVertexCount;
        group.totalIndices += objectGeometryIndexCount;
      }
    }

    this.originalObjects.add(object);
  }

  yieldToUI() {
    return new Promise((resolve) => {
      requestAnimationFrame(() => {
        setTimeout(resolve, 0);
      });
    });
  }

  async optimizeScene() {
    console.log("Starting scene optimization...");
    this.dispatchEvent("optimizationprogress", {
      phase: "start",
      progress: 0,
      message: "Starting optimization...",
    });

    this.originalObjects.clear();
    this.originalObjectsToSelection.clear();
    const structureGroups = new Map();

    this.dispatchEvent("optimizationprogress", {
      phase: "collecting",
      progress: 5,
      message: "Collecting scene objects...",
    });

    this.scene.traverse((object) => {
      if (object.userData.structureId) {
        const structureId = object.userData.structureId;
        if (!structureGroups.has(structureId)) {
          structureGroups.set(structureId, {
            mapMeshes: new Map(),
            mapLines: new Map(),
            mapLineSegments: new Map(),
            mapPoints: new Map(),

            meshes: [],
            lines: [],
            lineSegments: [],
            points: [],
            rootGroup: this.structureRoots.get(structureId),
          });
        }

        const group = structureGroups.get(structureId);

        if (object instanceof Mesh) {
          this.addToMaterialGroup(object, group.mapMeshes, group.meshes);
        } else if (object instanceof LineSegments) {
          this.addToMaterialGroup(object, group.mapLineSegments, group.lineSegments);
        } else if (object instanceof Line) {
          this.addToMaterialGroup(object, group.mapLines, group.lines);
        } else if (object instanceof Points) {
          this.addToMaterialGroup(object, group.mapPoints, group.points);
        }
      }
    });

    let processedGroups = 0;
    const totalGroups = structureGroups.size;

    this.dispatchEvent("optimizationprogress", {
      phase: "merging",
      progress: 10,
      message: `Merging ${totalGroups} structure groups...`,
      current: 0,
      total: totalGroups,
    });

    for (const group of structureGroups.values()) {
      group.mapMeshes.clear();
      group.mapLines.clear();
      group.mapLineSegments.clear();
      group.mapPoints.clear();

      await this.mergeMeshGroups(group.meshes, group.rootGroup);
      await this.yieldToUI();

      await this.mergeLineGroups(group.lines, group.rootGroup);
      await this.yieldToUI();

      await this.mergeLineSegmentGroups(group.lineSegments, group.rootGroup);
      await this.yieldToUI();

      await this.mergePointsGroups(group.points, group.rootGroup);

      processedGroups++;
      const progress = 10 + Math.round((processedGroups / totalGroups) * 80);

      this.dispatchEvent("optimizationprogress", {
        phase: "merging",
        progress,
        message: `Processing structure ${processedGroups}/${totalGroups}...`,
        current: processedGroups,
        total: totalGroups,
      });

      console.log(`Optimization progress: ${processedGroups}/${totalGroups} structure groups processed (${progress}%)`);

      await this.yieldToUI();
    }

    this.dispatchEvent("optimizationprogress", {
      phase: "finalizing",
      progress: 95,
      message: "Finalizing optimization...",
    });

    this.originalObjects.forEach((obj) => {
      obj.visible = false;
      if (!(obj instanceof Points) && !obj.userData.isEdge) {
        this.originalObjectsToSelection.add(obj);
      }
    });

    this.initializeObjectVisibility();

    console.log(`Optimization complete. Total objects: ${this.maxObjectId}`);

    this.dispatchEvent("optimizationprogress", {
      phase: "complete",
      progress: 100,
      message: `Optimization complete! ${this.maxObjectId} objects processed.`,
    });

    this.dispatchEvent("update");
  }

  async mergeMeshGroups(materialGroups, rootGroup) {
    let processedGroups = 0;
    for (const group of materialGroups) {
      if (!group.material) {
        console.warn("Skipping mesh group with null material");
        continue;
      }

      try {
        const geometries = [];
        const handles = new Set();
        const optimizedObjects = [];
        const objectMapping = new Map();
        let currentVertexOffset = 0;

        for (const mesh of group.objects) {
          const geometry = mesh.geometry.clone();

          const handle = mesh.userData.handle;
          if (!this.objectIdToIndex.has(handle)) {
            this.objectIdToIndex.set(handle, this.maxObjectId++);
          }
          const objectId = this.objectIdToIndex.get(handle);

          const vertexCount = geometry.attributes.position.count;
          const objectIds = new Float32Array(vertexCount);
          for (let i = 0; i < vertexCount; i++) {
            objectIds[i] = objectId;
          }
          geometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));

          objectMapping.set(mesh, {
            geometry,
            startVertexIndex: currentVertexOffset,
            vertexCount: geometry.attributes.position.count,
          });

          currentVertexOffset += geometry.attributes.position.count;
          geometries.push(geometry);

          optimizedObjects.push(mesh);
          handles.add(mesh.userData.handle);
        }

        const mergedObjects = [];

        if (geometries.length > 0) {
          const mergedGeometry = mergeGeometries(geometries);

          // Create visibility attribute
          const totalVertices = mergedGeometry.attributes.position.count;
          const visibilityArray = new Float32Array(totalVertices);

          // Initialize all vertices as visible (1.0)
          for (let i = 0; i < totalVertices; i++) {
            visibilityArray[i] = 1.0;
          }

          // Add visibility attribute to geometry
          mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));

          if (this.useVAO) {
            this.createVAO(mergedGeometry);
          }

          // Create visibility material
          const visibilityMaterial = this.createVisibilityMaterial(group.material);

          const mergedMesh = new Mesh(mergedGeometry, visibilityMaterial);
          mergedMesh.userData.isOptimized = true;
          rootGroup.add(mergedMesh);

          this.mergedMesh.add(mergedMesh);
          this.optimizedOriginalMap.set(mergedMesh, optimizedObjects);

          // Store object mappings with visibility tracking
          this.mergedObjectMap.set(mergedMesh.uuid, {
            objectMapping,
            visibilityArray,
            totalVertices,
          });

          this.mergedGeometryVisibility.set(mergedMesh, visibilityArray);

          mergedObjects.push(mergedMesh);

          geometries.forEach((geometry) => {
            geometry.dispose();
          });
        }

        handles.forEach((handle) => {
          if (this.handleToOptimizedObjects.has(handle)) {
            const existingObjects = this.handleToOptimizedObjects.get(handle);
            existingObjects.push(...mergedObjects);
            this.handleToOptimizedObjects.set(handle, existingObjects);
          } else {
            this.handleToOptimizedObjects.set(handle, mergedObjects);
          }
        });

        processedGroups++;
        if (processedGroups % 5 === 0) {
          await this.yieldToUI();
        }
      } catch (error) {
        console.error("Failed to merge meshes for material:", error);
        group.objects.forEach((mesh) => {
          mesh.visible = true;
        });
      }
    }
  }

  async mergeLineGroups(materialGroups, rootGroup) {
    let processedGroups = 0;
    for (const group of materialGroups) {
      if (group.objects.length === 0) continue;

      if (!group.material) {
        console.warn("Skipping line group with null material");
        continue;
      }

      const handles = new Set();
      let totalVertices = 0;
      const objectMapping = new Map();
      let currentVertexOffset = 0;

      group.objects.map((line) => {
        handles.add(line.userData.handle);
        totalVertices += line.geometry.attributes.position.count;
      });

      const positions = new Float32Array(totalVertices * 3);
      let posOffset = 0;

      const indices = [];
      let vertexOffset = 0;

      let isEdge = false;
      group.objects.forEach((line) => {
        isEdge = line.userData.isEdge;
        const geometry = line.geometry;
        const positionAttr = geometry.attributes.position;
        const vertexCount = positionAttr.count;

        const handle = line.userData.handle;
        if (!this.objectIdToIndex.has(handle)) {
          this.objectIdToIndex.set(handle, this.maxObjectId++);
        }

        objectMapping.set(line, {
          startVertexIndex: currentVertexOffset,
          vertexCount,
        });
        currentVertexOffset += vertexCount;
        const vector = new Vector3();

        for (let i = 0; i < vertexCount; i++) {
          vector.fromBufferAttribute(positionAttr, i);
          positions[posOffset++] = vector.x;
          positions[posOffset++] = vector.y;
          positions[posOffset++] = vector.z;
        }

        for (let i = 0; i < vertexCount - 1; i++) {
          indices.push(vertexOffset + i, vertexOffset + i + 1);
        }

        vertexOffset += vertexCount;
      });

      const geometry = new BufferGeometry();
      geometry.setAttribute("position", new BufferAttribute(positions, 3));
      geometry.setIndex(indices);
      geometry.computeBoundingSphere();
      geometry.computeBoundingBox();

      const objectIds = new Float32Array(totalVertices);
      let vertexIndex = 0;
      group.objects.forEach((line) => {
        const vertexCount = line.geometry.attributes.position.count;
        const handle = line.userData.handle;
        const objectId = this.objectIdToIndex.get(handle);

        for (let i = 0; i < vertexCount; i++) {
          objectIds[vertexIndex++] = objectId;
        }
      });
      geometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));

      const visibilityArray = new Float32Array(totalVertices);
      for (let i = 0; i < totalVertices; i++) {
        visibilityArray[i] = 1.0;
      }
      geometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));

      const visibilityMaterial = this.createVisibilityMaterial(group.material);

      const mergedLine = new LineSegments(geometry, visibilityMaterial);
      mergedLine.userData.isEdge = isEdge;
      mergedLine.userData.isOptimized = true;

      const mergedObjects = [mergedLine];
      if (this.useVAO) {
        this.createVAO(mergedLine);
      }
      rootGroup.add(mergedLine);
      this.mergedLines.add(mergedLine);
      this.optimizedOriginalMap.set(mergedLine, group.objects);

      this.mergedObjectMap.set(mergedLine.uuid, {
        objectMapping,
        visibilityArray,
        totalVertices,
      });

      this.mergedGeometryVisibility.set(mergedLine, visibilityArray);

      handles.forEach((handle) => {
        if (this.handleToOptimizedObjects.has(handle)) {
          const existingObjects = this.handleToOptimizedObjects.get(handle);
          existingObjects.push(...mergedObjects);
          this.handleToOptimizedObjects.set(handle, existingObjects);
        } else {
          this.handleToOptimizedObjects.set(handle, mergedObjects);
        }
      });

      processedGroups++;
      if (processedGroups % 5 === 0) {
        await this.yieldToUI();
      }
    }
  }

  async mergeLineSegmentGroups(materialGroups, rootGroup) {
    let processedGroups = 0;
    for (const group of materialGroups) {
      if (!group.material) {
        console.warn("Skipping line segment group with null material");
        continue;
      }

      try {
        const geometries = [];
        const optimizedObjects = [];
        const handles = new Set();
        const objectMapping = new Map();
        let currentVertexOffset = 0;
        let isEdge = false;

        for (const line of group.objects) {
          isEdge = line.userData.isEdge;
          const geometry = line.geometry.clone();

          const handle = line.userData.handle;
          if (!this.objectIdToIndex.has(handle)) {
            this.objectIdToIndex.set(handle, this.maxObjectId++);
          }
          const objectId = this.objectIdToIndex.get(handle);
          const vertexCount = geometry.attributes.position.count;
          const objectIds = new Float32Array(vertexCount);
          for (let i = 0; i < vertexCount; i++) {
            objectIds[i] = objectId;
          }
          geometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));

          objectMapping.set(line, {
            geometry,
            startVertexIndex: currentVertexOffset,
            vertexCount: geometry.attributes.position.count,
          });

          currentVertexOffset += geometry.attributes.position.count;
          geometries.push(geometry);
          optimizedObjects.push(line);
          handles.add(line.userData.handle);
        }

        const mergedObjects = [];

        if (geometries.length > 0) {
          const mergedGeometry = mergeGeometries(geometries, false);

          const totalVertices = mergedGeometry.attributes.position.count;
          const visibilityArray = new Float32Array(totalVertices);

          for (let i = 0; i < totalVertices; i++) {
            visibilityArray[i] = 1.0;
          }

          mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));

          const visibilityMaterial = this.createVisibilityMaterial(group.material);

          const mergedLine = new LineSegments(mergedGeometry, visibilityMaterial);
          mergedLine.userData.isEdge = isEdge;
          mergedLine.userData.isOptimized = true;

          if (this.useVAO) {
            this.createVAO(mergedLine);
          }

          rootGroup.add(mergedLine);
          this.mergedLineSegments.add(mergedLine);
          this.optimizedOriginalMap.set(mergedLine, optimizedObjects);

          this.mergedObjectMap.set(mergedLine.uuid, {
            objectMapping,
            visibilityArray,
            totalVertices,
          });

          this.mergedGeometryVisibility.set(mergedLine, visibilityArray);

          mergedObjects.push(mergedLine);

          geometries.forEach((geometry) => {
            geometry.dispose();
          });
        }

        handles.forEach((handle) => {
          if (this.handleToOptimizedObjects.has(handle)) {
            const existingObjects = this.handleToOptimizedObjects.get(handle);
            existingObjects.push(...mergedObjects);
            this.handleToOptimizedObjects.set(handle, existingObjects);
          } else {
            this.handleToOptimizedObjects.set(handle, mergedObjects);
          }
        });

        processedGroups++;
        if (processedGroups % 5 === 0) {
          await this.yieldToUI();
        }
      } catch (error) {
        console.warn("Failed to merge line segments for material:", error);
        group.objects.forEach((line) => {
          line.visible = true;
        });
      }
    }
  }

  async mergePointsGroups(materialGroups, rootGroup) {
    let processedGroups = 0;
    for (const group of materialGroups) {
      if (!group.material) {
        console.warn("Skipping points group with null material");
        continue;
      }

      try {
        const geometries = [];
        const optimizedObjects = [];
        const handles = new Set();

        for (const points of group.objects) {
          const geometry = points.geometry.clone();
          geometries.push(geometry);
          optimizedObjects.push(points);
          handles.add(points.userData.handle);
        }

        const mergedObjects = [];

        if (geometries.length > 0) {
          const mergedGeometry = mergeGeometries(geometries, false);
          const mergedPoints = new Points(mergedGeometry, group.material);
          mergedPoints.userData.isOptimized = true;

          if (this.useVAO) {
            this.createVAO(mergedPoints);
          }

          rootGroup.add(mergedPoints);

          this.mergedPoints.add(mergedPoints);
          this.optimizedOriginalMap.set(mergedPoints, optimizedObjects);
          mergedObjects.push(mergedPoints);

          geometries.forEach((geometry) => {
            geometry.dispose();
          });
        }

        handles.forEach((handle) => {
          if (this.handleToOptimizedObjects.has(handle)) {
            const existingObjects = this.handleToOptimizedObjects.get(handle);
            existingObjects.push(...mergedObjects);
            this.handleToOptimizedObjects.set(handle, existingObjects);
          } else {
            this.handleToOptimizedObjects.set(handle, mergedObjects);
          }
        });

        processedGroups++;
        if (processedGroups % 5 === 0) {
          await this.yieldToUI();
        }
      } catch (error) {
        console.warn("Failed to merge points for material:", error);
        group.objects.forEach((points) => {
          points.visible = true;
        });
      }
    }
  }

  mergeInSingleSegment(structureId, rootGroup) {
    const lineSegmentsArray = [...this.mergedLineSegments, ...this.mergedLines].filter(
      (obj) => obj.userData.structureId === structureId
    );

    if (lineSegmentsArray.length === 0) return;

    try {
      const geometriesWithIndex = [];
      const hasNormals = lineSegmentsArray.some((segment) => segment.geometry.attributes.normal !== undefined);

      lineSegmentsArray.forEach((segment) => {
        const clonedGeometry = segment.geometry.clone();
        clonedGeometry.applyMatrix4(segment.matrixWorld);

        if (hasNormals && !clonedGeometry.attributes.normal) {
          clonedGeometry.computeVertexNormals();
        }
        if (!hasNormals && clonedGeometry.attributes.normal) {
          clonedGeometry.deleteAttribute("normal");
        }

        const colorArray = new Float32Array(clonedGeometry.attributes.position.count * 3);
        for (let i = 0; i < colorArray.length; i += 3) {
          colorArray[i] = segment.material.color.r;
          colorArray[i + 1] = segment.material.color.g;
          colorArray[i + 2] = segment.material.color.b;
        }
        clonedGeometry.setAttribute("color", new BufferAttribute(colorArray, 3));

        if (!clonedGeometry.index) {
          const indices = [];
          const posCount = clonedGeometry.attributes.position.count;
          for (let i = 0; i < posCount - 1; i += 2) {
            indices.push(i, i + 1);
          }
          clonedGeometry.setIndex(indices);
        }

        geometriesWithIndex.push(clonedGeometry);
      });

      const finalGeometry = mergeGeometries(geometriesWithIndex, false);
      const material = new LineBasicMaterial({
        vertexColors: true,
      });

      if (this.useVAO) {
        this.createVAO(finalGeometry);
      }

      const mergedLine = new LineSegments(finalGeometry, material);
      mergedLine.userData.structureId = structureId;
      mergedLine.userData.isOptimized = true;
      rootGroup.add(mergedLine);
      this.mergedLineSegments.add(mergedLine);

      lineSegmentsArray.forEach((obj) => {
        if (obj.parent) {
          obj.parent.remove(obj);
        }
        obj.geometry.dispose();
      });
    } catch (error) {
      console.error("Failed to merge geometries:", error);
      lineSegmentsArray.forEach((obj) => {
        obj.visible = true;
        rootGroup.add(obj);
      });
    }
  }

  showOriginalObjects(objects) {
    objects.forEach((obj) => {
      if (this.originalObjects.has(obj)) {
        obj.visible = true;
      }
    });
  }

  hideOriginalObjects(objects) {
    objects.forEach((obj) => {
      if (this.originalObjects.has(obj)) {
        obj.visible = false;
      }
    });
  }

  createVAO(geometry) {
    if (!this.useVAO) {
      return;
    }

    if (geometry.attributes?.position?.count < 1000) {
      return;
    }

    const gl = this.renderer.getContext();
    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);

    for (const name in geometry.attributes) {
      const attribute = geometry.attributes[name];
      const buffer = this.renderer.properties.get(attribute).buffer;

      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.enableVertexAttribArray(attribute.itemSize);
      gl.vertexAttribPointer(attribute.itemSize, attribute.itemSize, gl.FLOAT, false, 0, 0);
    }

    if (geometry.index) {
      const indexBuffer = this.renderer.properties.get(geometry.index).buffer;
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    }

    gl.bindVertexArray(null);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    geometry.vao = vao;
  }

  getOriginalObjectForSelect() {
    const optimizedOriginals = [];

    for (const obj of this.originalObjectsToSelection) {
      if (this.hiddenHandles.has(obj.userData.handle)) {
        continue;
      }

      optimizedOriginals.push(obj);
    }

    return optimizedOriginals;
  }

  isolateObjects(handles) {
    if (this.hiddenHandles.size !== 0) {
      this.hiddenHandles.clear();
      this.syncHiddenObjects();
    }

    for (const handle of this.handleToOptimizedObjects.keys()) {
      if (!handles.has(handle)) {
        this.hiddenHandles.add(handle);
      }
    }

    this.syncHiddenObjects();
  }

  showAllHiddenObjects() {
    this.hiddenHandles.clear();
    this.syncHiddenObjects();
  }

  hideObjects(handles) {
    handles.forEach((handle) => {
      this.hiddenHandles.add(handle);
    });
    this.syncHiddenObjects();
  }

  showObjects(handles) {
    handles.forEach((handle) => {
      this.hiddenHandles.delete(handle);
    });
    this.syncHiddenObjects();
  }

  _updateVisibilityAttribute(mergedObject) {
    if (
      mergedObject.geometry &&
      mergedObject.geometry.attributes.visibility &&
      mergedObject.geometry.attributes.objectId
    ) {
      const visibilityArray = mergedObject.geometry.attributes.visibility.array;
      const objectIdArray = mergedObject.geometry.attributes.objectId.array;

      for (let i = 0; i < visibilityArray.length; i++) {
        const objectId = objectIdArray[i];
        if (objectId < this.objectVisibility.length) {
          visibilityArray[i] = this.objectVisibility[objectId];
        }
      }

      mergedObject.geometry.attributes.visibility.needsUpdate = true;
    }
  }

  syncHiddenObjects() {
    if (this.mergedObjectMap.size === 0) {
      console.log("No merged objects to sync");
      return;
    }

    if (this.objectVisibility.length > 0) {
      for (let i = 0; i < this.objectVisibility.length; i++) {
        this.objectVisibility[i] = 1.0;
      }

      this.hiddenHandles.forEach((handle) => {
        const index = this.objectIdToIndex.get(handle);
        if (index !== undefined && index < this.objectVisibility.length) {
          this.objectVisibility[index] = 0.0;
        }
      });
    }

    for (const mesh of this.mergedMesh) {
      this._updateVisibilityAttribute(mesh);
    }
    for (const line of this.mergedLines) {
      this._updateVisibilityAttribute(line);
    }
    for (const lineSegment of this.mergedLineSegments) {
      this._updateVisibilityAttribute(lineSegment);
    }
    for (const point of this.mergedPoints) {
      this._updateVisibilityAttribute(point);
    }
  }

  getStructureGeometryExtent(structureId) {
    const extent = new Box3();
    for (const [nodeId, node] of this.nodes.entries()) {
      if (!node.geometryExtents) continue;
      if (!nodeId.startsWith(structureId + "_")) continue;
      if (node.object && this.hiddenHandles && this.hiddenHandles.has(node.object.userData.handle)) continue;
      const transformedBox = node.geometryExtents.clone();
      if (node.group && node.group.matrix) {
        transformedBox.applyMatrix4(node.group.matrix);
        if (node.group.parent && node.group.parent.matrix) {
          transformedBox.applyMatrix4(node.group.parent.matrix);
        }
      }
      extent.union(transformedBox);
    }
    return extent;
  }

  setMaxConcurrentChunks(maxChunks) {
    if (maxChunks < 1) {
      console.warn("Max concurrent chunks must be at least 1");
      return;
    }
    this.maxConcurrentChunks = maxChunks;
  }

  waitForChunkSlot() {
    if (this.activeChunkLoads < this.maxConcurrentChunks) {
      this.activeChunkLoads++;
      return Promise.resolve();
    }

    return new Promise((resolve) => {
      this.chunkQueue.push(resolve);
    });
  }

  releaseChunkSlot() {
    this.activeChunkLoads--;

    if (this.chunkQueue.length > 0) {
      const nextResolve = this.chunkQueue.shift();
      this.activeChunkLoads++;
      nextResolve();
    }
  }
}
