import { gunzipSync } from 'fflate';
import StreamLoaderParserWorker from './StreamLoaderParser.worker.js';

/**
 * StreamLoader 类
 * 封装流式加载、解析与渲染调度的核心逻辑
 */
export class StreamLoader {
  constructor(config = {}) {
    // 依赖注入
    this.modelApi = config.modelApi;
    this.projectId = config.projectId || '';

    this.createStreamRequestId =
      config.createStreamRequestId ||
      (() => `stream_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`);
    this.onCancelRequestId = config.onCancelRequestId;
    this.batchSize = config.batchSize || 20;

    // 钩子函数
    this.renderModelData = config.renderModelData || (async () => {});
    this.onRangeStreamComplete = config.onRangeStreamComplete;
    // 外部提供的交互状态检查函数（可选）
    this.externalEnsureNotInteracting = config.ensureNotInteracting;
    this.prefixIdKey = config.prefixIdKey || 'documentId';
    this.debug = config.debug || false;

    // 内部状态管理
    this.activeRequests = new Map();
    this.requestCounter = 0;

    this.currentAbortController = null;
    this.currentStreamReader = null;
    this.activeStreamReaders = new Set();
    this.activeStreamControllers = new Set();
    this.activeRequestIds = new Set();
    this.currentRequestId = null;

    this.isUserInteracting = false;
    this.parsePausePromise = null;
    this.parsePauseResolver = null;

    this.controlPressStartAt = 0;
    this.longPressAbortThresholdMs = 250;

    this.sceneBox = null;
    this.boxIndex = null;
    this.modelRegistry = new Map();

    // Initialize worker
    try {
      this.worker = new StreamLoaderParserWorker();
      // this.worker = null; // TODO
    } catch (e) {
      this.worker = null;
    }
    this.workerRequestMap = new Map();
    this.workerMessageHandler = null;
    if (this.worker) {
      this.workerMessageHandler = e => {
        const payload = e.data || {};
        const pending = this.workerRequestMap.get(payload.id);
        if (!pending) return;
        this.workerRequestMap.delete(payload.id);
        if (payload.type === 'success') {
          pending.resolve(payload.result);
        } else {
          pending.reject(new Error(payload.error));
        }
      };
      this.worker.addEventListener('message', this.workerMessageHandler);
    }
  }

  /**
   * 为对象或对象数组应用prefixId前缀
   * @param {Object|Array<Object>} target - 单个对象或对象数组
   * @param {string} propKey - 需要应用prefixId前缀的属性键名
   * @param {string} customPrefixId - 自定义prefixId，可选
   */
  _applyPrefixId(target, propKey, customPrefixId) {
    if (!this.prefixIdKey || !target) return;

    if (Array.isArray(target)) {
      target.forEach(item => {
        const prefixId = customPrefixId || item[this.prefixIdKey];
        if (item && prefixId && item[propKey] != null) {
          item[propKey] = `${item[propKey]}:${prefixId}`;
        }
      });
    } else if ((customPrefixId || target[this.prefixIdKey]) && target[propKey] != null) {
      const prefixId = customPrefixId || target[this.prefixIdKey];
      target[propKey] = `${target[propKey]}:${prefixId}`;
    }
  }

  /**
   * 将合并材质数据覆盖到原材质数据中（仅覆盖 merge 中明确提供的字段）
   * 规则：mergeMaterialData 中 id 在 materialData 出现过的，覆盖其 color/transp/visible
   * 注意：应在 _applyPrefixId(materialData, 'id') 之前调用（使用原始 id 对比）
   * @param {Array<Object>|Object|null} materialData
   * @param {Array<Object>|Object|null} mergeMaterialData
   * @returns {Array<Object>|Object|null} materialData（原地修改后返回）
   */
  _mergeMaterialDataOverrides(materialData, mergeMaterialData) {
    if (!materialData || !mergeMaterialData) return materialData;

    const materials = Array.isArray(materialData) ? materialData : [materialData];
    const merges = Array.isArray(mergeMaterialData) ? mergeMaterialData : [mergeMaterialData];
    if (materials.length === 0 || merges.length === 0) return materialData;

    const mergeById = new Map();
    for (let i = 0; i < merges.length; i++) {
      const m = merges[i];
      if (!m || m.id == null) continue;
      const id = String(m.id);
      mergeById.set(id, m);
      const baseId = id.split(':')[0];
      if (baseId && !mergeById.has(baseId)) {
        mergeById.set(baseId, m);
      }
    }
    if (mergeById.size === 0) return materialData;

    for (let i = 0; i < materials.length; i++) {
      const mat = materials[i];
      if (!mat || mat.id == null) continue;
      const merge = mergeById.get(String(mat.id));
      if (!merge) continue;

      if (merge.color !== undefined) {
        mat.color = merge.color;
      }
      if (merge.transp !== undefined) {
        mat.transp = merge.transp;
      }
      if (merge.visible !== undefined) {
        mat.visible = merge.visible;
      }
    }

    return materialData;
  }

  workerRequest(type, data, transferable = []) {
    if (!this.worker) {
      return Promise.reject(new Error('Worker is not initialized'));
    }
    return new Promise((resolve, reject) => {
      const id = this.requestCounter++;
      this.workerRequestMap.set(id, { resolve, reject });
      this.worker.postMessage({ id, type, data }, transferable);
    });
  }

  // ----------------------------------------------------------------
  // 核心解析逻辑
  // ----------------------------------------------------------------

  async parseStreamImmediate(reader, list, range, abortSignal = null, streamId = null) {
    const batchMeshes = [];
    const batchPrimitiveGroups = [];
    const batchSize = this.batchSize;
    let batchStart = 0;
    const streamStats = {
      totalBatches: 0,
      totalMeshes: 0,
      totalPrimitives: 0,
    };

    const flattenPrimitiveGroups = primitiveGroups => {
      if (!primitiveGroups || primitiveGroups.length === 0) return [];
      const primitivesToProcess = [];
      const usedIds = new Set();

      primitiveGroups.forEach(group => {
        const primitives = Array.isArray(group) ? group : [group];
        primitives.forEach(primitive => {
          if (!primitive) return;
          if (primitive.id == null) {
            primitivesToProcess.push(primitive);
            return;
          }
          if (usedIds.has(primitive.id)) return;
          usedIds.add(primitive.id);
          primitivesToProcess.push(primitive);
        });
      });

      return primitivesToProcess;
    };

    const processStreamBatch = async (meshesToProcess, primitiveGroupsToProcess) => {
      if (!meshesToProcess || meshesToProcess.length === 0) return;
      const primitivesToProcess = flattenPrimitiveGroups(primitiveGroupsToProcess);
      await this.processBatchData(meshesToProcess, primitivesToProcess, list, range, abortSignal);
      streamStats.totalBatches += 1;
      streamStats.totalMeshes += meshesToProcess.length;
      streamStats.totalPrimitives += primitivesToProcess ? primitivesToProcess.length : 0;
    };

    const ensureNotAborted = () => {
      if (abortSignal && abortSignal.aborted) {
        return reader.cancel().catch(() => {});
        // throw new DOMException('Request was aborted', 'AbortError');
      }
      return null;
    };

    if (this.worker) {
      const localStreamId = streamId || this.createStreamRequestId();
      const ensureNotInteracting = () => {
        if (!this.externalEnsureNotInteracting && !this.isUserInteracting) return null;
        return this.ensureNotInteracting(abortSignal);
      };

      await this.workerRequest('streamInit', {
        streamId: localStreamId,
        prefixIdKey: this.prefixIdKey,
      });

      try {
        if (this.debug) performance.mark('while-start');
        while (true) {
          const abortPromise = ensureNotAborted();
          if (abortPromise) await abortPromise;
          const interactionPromise = ensureNotInteracting();
          if (interactionPromise) await interactionPromise;
          let content;
          try {
            content = await reader.read();
          } catch (e) {
            if (e && e.name === 'AbortError') {
              throw e;
            }
            throw e;
          }

          const { done, value } = content;
          if (done) {
            const abortPromiseOnDone = ensureNotAborted();
            if (abortPromiseOnDone) await abortPromiseOnDone;
            if (this.debug) {
              performance.mark('while-end');
              performance.measure('while', 'while-start', 'while-end');
            }

            const flushed = await this.workerRequest('streamFlush', { streamId: localStreamId });
            if (flushed?.meshes?.length) {
              batchMeshes.push(...flushed.meshes);
              batchPrimitiveGroups.push(...flushed.primitives);
            }

            while (batchMeshes.length - batchStart >= batchSize) {
              const abortPromiseInBatch = ensureNotAborted();
              if (abortPromiseInBatch) await abortPromiseInBatch;
              const interactionPromiseInBatch = ensureNotInteracting();
              if (interactionPromiseInBatch) await interactionPromiseInBatch;
              const meshesToProcess = batchMeshes.slice(batchStart, batchStart + batchSize);
              const primitiveGroupsToProcess = batchPrimitiveGroups.slice(
                batchStart,
                batchStart + batchSize
              );
              batchStart += batchSize;
              await processStreamBatch(meshesToProcess, primitiveGroupsToProcess);
            }

            if (batchMeshes.length - batchStart > 0) {
              const abortPromiseInBatch = ensureNotAborted();
              if (abortPromiseInBatch) await abortPromiseInBatch;
              const interactionPromiseInBatch = ensureNotInteracting();
              if (interactionPromiseInBatch) await interactionPromiseInBatch;
              const meshesToProcess = batchMeshes.slice(batchStart);
              const primitiveGroupsToProcess = batchPrimitiveGroups.slice(batchStart);
              batchStart = batchMeshes.length;
              await processStreamBatch(meshesToProcess, primitiveGroupsToProcess);
            }
            break;
          }

          const transferable = value?.buffer instanceof ArrayBuffer ? [value.buffer] : [];
          const parsed = await this.workerRequest(
            'streamPush',
            {
              streamId: localStreamId,
              chunk: value.buffer,
              byteOffset: value.byteOffset,
              byteLength: value.byteLength,
            },
            transferable
          );

          if (parsed?.meshes?.length) {
            batchMeshes.push(...parsed.meshes);
            batchPrimitiveGroups.push(...parsed.primitives);
          }

          while (batchMeshes.length - batchStart >= batchSize) {
            const abortPromiseInBatch = ensureNotAborted();
            if (abortPromiseInBatch) await abortPromiseInBatch;
            const interactionPromiseInBatch = ensureNotInteracting();
            if (interactionPromiseInBatch) await interactionPromiseInBatch;
            const meshesToProcess = batchMeshes.slice(batchStart, batchStart + batchSize);
            const primitiveGroupsToProcess = batchPrimitiveGroups.slice(
              batchStart,
              batchStart + batchSize
            );
            batchStart += batchSize;
            await processStreamBatch(meshesToProcess, primitiveGroupsToProcess);
          }

          if (batchStart >= batchSize * 4) {
            batchMeshes.splice(0, batchStart);
            batchPrimitiveGroups.splice(0, batchStart);
            batchStart = 0;
          }
        }
      } finally {
        try {
          await this.workerRequest('streamDispose', { streamId: localStreamId });
        } catch (e) {}
      }
      return streamStats;
    }

    const decoder = new TextDecoder('utf-8');
    let buffer = new Uint8Array();

    let expectingMesh = true;
    let pendingMesh = null;
    let pendingPrimitiveIds = new Set();
    let pendingPrimitiveGroups = [];
    let pendingMatchedPrimitiveIds = new Set();
    let isFullProps = true;

    const normalizeMesh = mesh => {
      this._applyPrefixId(mesh, 'id');
      const meshList = Array.isArray(mesh) ? mesh : [mesh];
      meshList.forEach(item => {
        if (!item) return;
        if (!Array.isArray(item.primitives)) {
          item.primitives = [];
        }
        this._applyPrefixId(item.primitives, 'prmid', item.documentId);
      });
      return mesh;
    };

    const collectMeshPrimitiveIds = mesh => {
      const ids = [];
      const meshList = Array.isArray(mesh) ? mesh : [mesh];
      meshList.forEach(item => {
        if (!item || !Array.isArray(item.primitives)) return;
        item.primitives.forEach(primitive => {
          if (primitive && primitive.prmid != null) {
            ids.push(primitive.prmid);
          }
        });
      });
      return ids;
    };

    const isValidStreamMesh = mesh => {
      const meshList = Array.isArray(mesh) ? mesh : [mesh];
      if (meshList.length === 0) return false;
      return meshList.every(
        item => item && typeof item === 'object' && Array.isArray(item.primitives)
      );
    };

    const tryParseMeshFromBuffer = () => {
      if (buffer.length < 4) {
        return { status: 'incomplete' };
      }

      const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
      if (length <= 0 || length > 10 * 1024 * 1024) {
        return { status: 'invalid' };
      }

      const totalLen = 4 + length;
      if (buffer.length < totalLen) {
        return { status: 'incomplete' };
      }

      try {
        const data = buffer.slice(4, totalLen);
        const parsedMesh = JSON.parse(decoder.decode(data));
        if (!isValidStreamMesh(parsedMesh)) {
          return { status: 'invalid' };
        }
        const mesh = normalizeMesh(parsedMesh);
        return { status: 'success', mesh, totalLen };
      } catch (e) {
        return { status: 'invalid' };
      }
    };

    const parsePrimitiveFromBuffer = () => {
      const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
      const primitiveResult = this.parsePrimitive(dataView, buffer, 0, isFullProps);
      const primitiveData = primitiveResult.primitive;

      this._applyPrefixId(primitiveData, 'id');
      this._applyPrefixId(primitiveData, 'material');

      if (isFullProps) {
        const { position, normal, posindex, nolindex, indices } =
          this.parsePrimitiveData(primitiveData);
        const formatted = this.formatPrimitiveData({ position, normal, posindex, nolindex });

        delete primitiveData.nolindex;
        delete primitiveData.posindex;

        primitiveData.position = formatted.position;
        primitiveData.normal = formatted.normal;
        primitiveData.indices = indices;
      }

      return {
        primitive: primitiveData,
        consumedBytes: primitiveResult.offset,
      };
    };

    const commitPendingMesh = async () => {
      if (!pendingMesh) return;

      const meshList = Array.isArray(pendingMesh) ? pendingMesh : [pendingMesh];
      meshList.forEach(mesh => {
        batchMeshes.push(mesh);
        batchPrimitiveGroups.push(pendingPrimitiveGroups);
      });

      pendingMesh = null;
      pendingPrimitiveIds = new Set();
      pendingPrimitiveGroups = [];
      pendingMatchedPrimitiveIds = new Set();

      if (batchMeshes.length >= this.batchSize) {
        await ensureNotAborted();
        await this.ensureNotInteracting(abortSignal);
        await processStreamBatch(batchMeshes, batchPrimitiveGroups);

        batchMeshes.length = 0;
        batchPrimitiveGroups.length = 0;
      }
    };

    while (true) {
      await ensureNotAborted();

      await this.ensureNotInteracting(abortSignal);

      let content;
      try {
        content = await reader.read();
      } catch (e) {
        if (e && e.name === 'AbortError') {
          throw e;
        }
        throw e;
      }

      const { done, value } = content;
      if (done) {
        await ensureNotAborted();
        await commitPendingMesh();

        if (batchMeshes.length > 0) {
          await ensureNotAborted();
          await this.ensureNotInteracting(abortSignal);
          await processStreamBatch(batchMeshes, batchPrimitiveGroups);
        }
        break;
      }

      const newBuf = new Uint8Array(buffer.length + value.length);
      newBuf.set(buffer);
      newBuf.set(value, buffer.length);
      buffer = newBuf;

      while (buffer.length > 0) {
        await ensureNotAborted();
        if (expectingMesh) {
          const meshResult = tryParseMeshFromBuffer();
          if (meshResult.status === 'incomplete') break;
          if (meshResult.status === 'invalid') {
            break;
          }

          await commitPendingMesh();
          pendingMesh = meshResult.mesh;
          pendingPrimitiveIds = new Set(collectMeshPrimitiveIds(pendingMesh));
          pendingPrimitiveGroups = [];
          pendingMatchedPrimitiveIds = new Set();
          expectingMesh = false;
          buffer = buffer.slice(meshResult.totalLen);
        } else {
          const nextMeshResult = tryParseMeshFromBuffer();
          if (nextMeshResult.status === 'success') {
            await commitPendingMesh();
            pendingMesh = nextMeshResult.mesh;
            pendingPrimitiveIds = new Set(collectMeshPrimitiveIds(pendingMesh));
            expectingMesh = false;
            buffer = buffer.slice(nextMeshResult.totalLen);
            continue;
          }

          if (buffer.length < 12) break;

          let primitiveResult = null;
          try {
            primitiveResult = parsePrimitiveFromBuffer();
          } catch (e) {
            break;
          }

          const primitiveData = primitiveResult.primitive;

          if (pendingPrimitiveIds.has(primitiveData.id)) {
            pendingPrimitiveGroups.push(primitiveData);
            pendingMatchedPrimitiveIds.add(primitiveData.id);
          }

          buffer = buffer.slice(primitiveResult.consumedBytes);

          if (
            pendingPrimitiveIds.size > 0 &&
            pendingMatchedPrimitiveIds.size >= pendingPrimitiveIds.size
          ) {
            await commitPendingMesh();
            expectingMesh = true;
          }
        }
      }
    }
    return streamStats;
  }

  // ----------------------------------------------------------------
  // 数据处理与渲染
  // ----------------------------------------------------------------

  async processBatchData(meshes, primitives, list, range, abortSignal = null) {
    try {
      if (abortSignal && abortSignal.aborted) {
        throw new DOMException('Request was aborted', 'AbortError');
      }
      const renderResult = await this.renderModelData(
        meshes,
        primitives,
        list,
        range,
        null,
        undefined,
        {
          suppressLoadComplete: true,
          source: 'inRangeDis2',
        }
      );
      if (renderResult && renderResult.canceled) {
        throw new DOMException('Batch loading was canceled', 'AbortError');
      }
      if (abortSignal && abortSignal.aborted) {
        throw new DOMException('Request was aborted', 'AbortError');
      }
      return renderResult;
    } catch (error) {
      if (error && error.name === 'AbortError') {
        throw error;
      }
      console.error('Failed to render batch data:', error);
      throw error;
    }
  }

  // ----------------------------------------------------------------
  // 请求管理
  // ----------------------------------------------------------------

  async getPrimitivesByRangeStream(params, list, range, abortSignal = null) {
    await this.abortAllStreamRequests();

    const requestId = this.createStreamRequestId();
    let internalController = null;
    let reader = null;

    try {
      internalController = new AbortController();
      this.currentAbortController = internalController;
      this.activeStreamControllers.add(internalController);
      this.activeRequestIds.add(requestId);

      if (abortSignal) {
        if (abortSignal.aborted) {
          // internalController.abort(); // TODO
        } else {
          abortSignal.addEventListener('abort', () => internalController.abort(), { once: true });
        }
      }

      if (internalController.signal.aborted) {
        throw new DOMException('Request was aborted', 'AbortError');
      }
      params.requestId = requestId;
      params.projectId = this.projectId;
      let res;
      if (this.modelApi && typeof this.modelApi.getPrimitivesByRangeStream === 'function') {
        res = await this.modelApi.getPrimitivesByRangeStream(params, internalController.signal);
      } else {
        throw new Error('modelApi.getPrimitivesByRangeStream is not available');
      }

      reader = res.getReader();
      this.currentStreamReader = reader;
      this.activeStreamReaders.add(reader);

      const streamStats = await this.parseStreamImmediate(
        reader,
        list,
        range,
        internalController.signal,
        requestId
      );

      if (internalController.signal.aborted) {
        throw new DOMException('Request was aborted', 'AbortError');
      }

      if (res) {
        return {
          stream: res,
          requestId,
          ...(streamStats || {}),
        };
      }
      return null;
    } catch (error) {
      if (error.name === 'AbortError') {
        throw error;
      }
      console.error(error);
      throw error;
    } finally {
      if (reader) this.activeStreamReaders.delete(reader);
      if (internalController) this.activeStreamControllers.delete(internalController);
      if (requestId) this.activeRequestIds.delete(requestId);
      this.currentStreamReader = null;
      this.currentAbortController = null;
    }
  }

  async getPrimitivesByRange(params, abortSignal = null) {
    try {
      if (abortSignal && abortSignal.aborted) {
        throw new DOMException('Request was aborted', 'AbortError');
      }

      const res = await this.modelApi.getPrimitivesByRange(params, abortSignal);

      if (abortSignal && abortSignal.aborted) {
        throw new DOMException('Request was aborted', 'AbortError');
      }

      if (res) {
        return res;
      } else {
        throw new Error('Failed to get primitives');
      }
    } catch (error) {
      if (error.name === 'AbortError') {
        throw error;
      }
      throw error;
    }
  }

  async getPrimitivesByMaterial(params) {
    try {
      const res = await this.modelApi.getPrimitivesByMaterial(params);
      if (res) {
        return res;
      }
      throw new Error('Failed to get Material');
    } catch (error) {
      throw error;
    }
  }

  async getPrimitivesByMergeMaterial(params) {
    try {
      if (!this.modelApi || typeof this.modelApi.getPrimitivesByMergeMaterial !== 'function') {
        return null;
      }

      const res = await this.modelApi.getPrimitivesByMergeMaterial(params);
      return res || null;
    } catch (error) {
      // mergeMaterial 是可选能力：接口异常时降级为不合并
      return null;
    }
  }

  createAbortableRequest(requestKey = null) {
    const requestId = requestKey || `req_${++this.requestCounter}`;

    if (this.activeRequests.has(requestId)) {
      const existingController = this.activeRequests.get(requestId);
      existingController.abort();
    }

    const abortController = new AbortController();

    this.activeRequests.set(requestId, abortController);
    return {
      requestId,
      signal: abortController.signal,
      abort: () => {
        abortController.abort();
        this.activeRequests.delete(requestId);
      },
    };
  }

  cleanupRequest(requestId) {
    if (this.activeRequests.has(requestId)) {
      this.activeRequests.delete(requestId);
    }
  }

  async abortAllStreamRequests() {
    try {
      const requestIds = Array.from(this.activeRequestIds);
      for (const requestId of requestIds) {
        if (typeof this.onCancelRequestId === 'function') {
          try {
            await this.onCancelRequestId(requestId);
          } catch (e) {}
        }
      }
      this.activeRequestIds.clear();

      this.activeStreamControllers.forEach(controller => {
        try {
          controller.abort();
        } catch (e) {}
      });
      this.activeStreamControllers.clear();

      const readers = Array.from(this.activeStreamReaders);
      this.activeStreamReaders.clear();
      for (const reader of readers) {
        try {
          await reader.cancel();
        } catch (e) {}
      }
    } finally {
      if (this.currentAbortController) {
        try {
          this.currentAbortController.abort();
        } catch (e) {}
        this.currentAbortController = null;
      }
      if (this.currentStreamReader) {
        try {
          await this.currentStreamReader.cancel();
        } catch (e) {}
        this.currentStreamReader = null;
      }
    }
  }

  // ----------------------------------------------------------------
  // 辅助加载方法
  // ----------------------------------------------------------------

  async fetchPrimitiveBufferByStream(range, list, abortSignal = null, requestId = null) {
    try {
      await this.abortAllStreamRequests();
      const request = this.createAbortableRequest(requestId); // Use requestId if provided as key? Logic slightly different in utils

      // Adaptation: utils used getPrimitivesByRangeStreamWithAutoAbort which calls getPrimitivesByRangeStream
      // Here I simplify by calling getPrimitivesByRangeStream directly but managing request key

      const streamResult = await this.getPrimitivesByRangeStream(
        range,
        list,
        range,
        abortSignal || request.signal
      );

      this.cleanupRequest(request.requestId);
      if (!streamResult) return null;
      return streamResult;
    } catch (error) {
      if (error.name === 'AbortError') {
        // throw error;
      }
      if (requestId && this.currentRequestId !== requestId) {
        return null;
      }
      // throw error;
    }
  }

  async fetchPrimitiveBuffer(range, abortSignal = null, requestId = null) {
    try {
      const request = this.createAbortableRequest(requestId || 'default');
      const buffer = await this.getPrimitivesByRange(range, abortSignal || request.signal);
      this.cleanupRequest(request.requestId);

      if (!buffer || buffer.byteLength === 0) return null;
      if (buffer.byteLength < 4) return null;
      return buffer;
    } catch (error) {
      if (error.name === 'AbortError') {
        throw error;
      }
      if (requestId && this.currentRequestId !== requestId) {
        return null;
      }
      throw error;
    }
  }

  async parseBufferData(buffer) {
    if (this.worker) {
      let transferable = [];
      if (buffer instanceof ArrayBuffer) {
        transferable.push(buffer);
      } else if (buffer.buffer instanceof ArrayBuffer) {
        transferable.push(buffer.buffer);
      }

      return this.workerRequest(
        'parseBufferData',
        { buffer, prefixIdKey: this.prefixIdKey },
        transferable
      );
    } else {
      return this._parseBufferDataSync(buffer);
    }
  }

  async _parseBufferDataSync(buffer) {
    let uint8Array = null;
    let dataView = null;
    let meshJsonBytes = null;
    let primitives = [];

    try {
      uint8Array = new Uint8Array(buffer);
      dataView = new DataView(buffer);

      let offset = 0;
      const meshJsonLength = dataView.getInt32(offset, false);
      offset += 4;

      meshJsonBytes = new Uint8Array(uint8Array.buffer.slice(offset, offset + meshJsonLength));
      offset += meshJsonLength;

      const decoder = new TextDecoder('utf-8');
      const meshJsonStr = decoder.decode(meshJsonBytes);
      meshJsonBytes = null;

      let mesh;
      try {
        mesh = JSON.parse(meshJsonStr);
        this._applyPrefixId(mesh, 'id');
        if (Array.isArray(mesh)) {
          mesh.forEach(item => {
            this._applyPrefixId(item.primitives, 'prmid', item.documentId);
          });
        } else {
          this._applyPrefixId(mesh.primitives, 'prmid', mesh.documentId);
        }
      } catch (e) {
        console.error('JSON 解析失败:', e, meshJsonStr);
        throw new Error('JSON解析失败');
      }

      while (offset < uint8Array.length) {
        try {
          const result = this.parsePrimitive(dataView, uint8Array, offset);
          console.log('result', result);
          if (!result) break;
          primitives.push(result.primitive);
          offset = result.offset;
        } catch (e) {
          console.error('解析失败，偏移:', offset, e);
          break;
        }
      }

      for (let i = 0; i < primitives.length; i++) {
        const primitive = primitives[i];
        this._applyPrefixId(primitive, 'id');
        this._applyPrefixId(primitive, 'material');
        const { position, normal, posindex, nolindex, indices } =
          this.parsePrimitiveData(primitive);
        const formatted = this.formatPrimitiveData({ position, normal, posindex, nolindex });
        delete primitive.nolindex;
        delete primitive.posindex;
        primitive.position = formatted.position;
        primitive.normal = formatted.normal;
        primitive.indices = indices;
      }

      return { mesh, primitives };
    } catch (error) {
      throw error;
    } finally {
      uint8Array = null;
      dataView = null;
      meshJsonBytes = null;
    }
  }

  async fetchJsonSync(list, range, abortSignal = null, requestId = null) {
    let buffer = null;
    let primitives = [];
    try {
      const loadStartTime = Date.now();
      buffer = await this.fetchPrimitiveBuffer(range, abortSignal, requestId);
      if (!buffer) {
        return { primitives: [] };
      }
      const { mesh, primitives: parsed } = await this.parseBufferData(buffer);
      primitives = parsed;
      if (typeof this.renderModelData === 'function') {
        await this.renderModelData(mesh, primitives, list, range);
      }
      return { primitives, mesh };
    } catch (err) {
      if (err.name === 'AbortError') {
        throw err;
      }
      if (requestId && this.currentRequestId !== requestId) {
        return;
      }
      throw err;
    } finally {
      try {
        buffer = null;
        primitives = null;
        if (typeof window !== 'undefined' && window.gc) {
          window.gc();
        }
      } catch (cleanupErr) {
        console.warn('内存清理时发生错误:', cleanupErr);
      }
    }
  }

  async fetchJsonStream(list, range, abortSignal = null, requestId = null) {
    try {
      const loadStartTime = Date.now();
      const streamResult = await this.fetchPrimitiveBufferByStream(
        range,
        list,
        abortSignal,
        requestId
      );
      return {
        ...(streamResult || {}),
        duration: Date.now() - loadStartTime,
      };
    } catch (err) {
      if (err.name === 'AbortError') {
        throw err;
      }
      if (requestId && this.currentRequestId !== requestId) {
        return;
      }
      throw err;
    }
  }

  // ----------------------------------------------------------------
  // 数据格式化与解析工具
  // ----------------------------------------------------------------

  formatPrimitiveData(primitive) {
    const { position, normal, posindex, nolindex } = primitive;
    const restoredPosition = new Array(posindex.length);
    const restoredNormal = new Array(nolindex.length);
    for (let i = 0; i < posindex.length; i++) {
      const posIndex = posindex[i];
      restoredPosition[i] = position[posIndex]; // 索引和顶点一对一
    }
    for (let i = 0; i < nolindex.length; i++) {
      const normalIndex = nolindex[i];
      restoredNormal[i] = normal[normalIndex]; // 索引和法向量一对一
    }
    primitive.position = null;
    primitive.normal = null;
    primitive.posindex = null;
    primitive.nolindex = null;
    return { position: restoredPosition, normal: restoredNormal };
  }

  parsePrimitiveData(primitive) {
    const keyOrder = ['position', 'normal', 'indices', 'posindex', 'nolindex'];
    const typeMap = {
      0: { bytes: 4, getter: 'getFloat32' },
      1: { bytes: 4, getter: 'getInt32' },
      2: { bytes: 2, getter: 'getInt16' },
    };

    const parsedResult = {};
    const dataTypeString = primitive.dataType;

    for (let i = 0; i < keyOrder.length; i++) {
      const key = keyOrder[i];
      const typeCode = dataTypeString[i];
      const typeInfo = typeMap[typeCode];
      const uint8Array =
        primitive.isCompressed === 1 || primitive.isCompressed === undefined
          ? gunzipSync(primitive[key])
          : primitive[key];
      if (!typeInfo || !uint8Array) {
        console.warn(`无法找到键 "${key}" 或其类型定义，已跳过。`);
        parsedResult[key] = [];
        continue;
      }
      const result_array = [];
      const dataView = new DataView(
        uint8Array.buffer,
        uint8Array.byteOffset,
        uint8Array.byteLength
      );
      const littleEndian = true;
      for (let byteOffset = 0; byteOffset < dataView.byteLength; byteOffset += typeInfo.bytes) {
        if (byteOffset + typeInfo.bytes > dataView.byteLength) {
          console.warn(
            `键 "${key}" 的数据长度 (${dataView.byteLength}) 不是其类型 (${typeInfo.bytes}字节) 的整数倍，末尾数据可能不完整。`
          );
          break;
        }
        const value = dataView[typeInfo.getter](byteOffset, littleEndian);
        result_array.push(value);
      }
      parsedResult[key] = result_array;
    }
    return parsedResult;
  }

  parsePrimitive(dataView, uint8Array, offset, isFullProps = true) {
    const primitive = {};
    if (dataView.byteLength < offset + 12) {
      throw new Error('Insufficient data for primitive header');
    }
    const primitiveIdTextLen = dataView.getUint32(offset, false);
    offset += 4;
    if (primitiveIdTextLen === 0xffffffff) {
      primitive.id = null;
    } else if (primitiveIdTextLen > 0) {
      if (dataView.byteLength < offset + primitiveIdTextLen) {
        throw new Error('Insufficient data for primitive id content');
      }
      const textBytes = uint8Array.subarray(offset, offset + primitiveIdTextLen);
      primitive.id = new TextDecoder('utf-8').decode(textBytes);
      offset += primitiveIdTextLen;
    } else {
      primitive.id = '';
    }

    if (dataView.byteLength < offset + 4) {
      throw new Error('Insufficient data for GeomText length');
    }
    const documentTextLen = dataView.getUint32(offset, false);
    offset += 4;
    if (documentTextLen === 0xffffffff) {
      primitive.documentId = null;
    } else if (documentTextLen > 0) {
      if (dataView.byteLength < offset + documentTextLen) {
        throw new Error('Insufficient data for GeomText content');
      }
      const textBytes = uint8Array.subarray(offset, offset + documentTextLen);
      primitive.documentId = new TextDecoder('utf-8').decode(textBytes);
      offset += documentTextLen;
    } else {
      primitive.documentId = '';
    }

    if (dataView.byteLength < offset + 4) {
      throw new Error('Insufficient data for Material length');
    }
    const materialTextLen = dataView.getUint32(offset, false);
    offset += 4;
    if (materialTextLen === 0xffffffff) {
      primitive.material = null;
    } else if (materialTextLen > 0) {
      if (dataView.byteLength < offset + materialTextLen) {
        throw new Error('Insufficient data for Material content');
      }
      const textBytes = uint8Array.subarray(offset, offset + materialTextLen);
      primitive.material = new TextDecoder('utf-8').decode(textBytes);
      offset += materialTextLen;
    } else {
      primitive.material = '';
    }

    if (dataView.byteLength < offset + 4) {
      throw new Error('Insufficient data for GeomText length');
    }
    const geomTextLen = dataView.getUint32(offset, false);
    offset += 4;
    if (geomTextLen === 0xffffffff) {
      primitive.geomText = null;
    } else if (geomTextLen > 0) {
      if (dataView.byteLength < offset + geomTextLen) {
        throw new Error('Insufficient data for GeomText content');
      }
      const textBytes = uint8Array.subarray(offset, offset + geomTextLen);
      primitive.geomText = new TextDecoder('utf-8').decode(textBytes);
      offset += geomTextLen;
    } else {
      primitive.geomText = '';
    }

    // console.log('window.plat', window.plat)
    // if(window.plat != 1){
    //   primitive.isCompressed = dataView.getInt32(offset, false);
    //   offset += 4;
    // }
    primitive.isCompressed = dataView.getInt32(offset, false);
    offset += 4;

    if (isFullProps) {
      if (dataView.byteLength < offset + 4)
        throw new Error('Insufficient data for Position length');
      const positionLen = dataView.getUint32(offset, false);
      offset += 4;
      if (positionLen === 0xffffffff) {
        primitive.position = null;
      } else {
        if (dataView.byteLength < offset + positionLen)
          throw new Error('Insufficient data for Position content');
        primitive.position = uint8Array.subarray(offset, offset + positionLen);
        offset += positionLen;
      }
    }

    if (isFullProps) {
      if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Normal length');
      const normalLen = dataView.getUint32(offset, false);
      offset += 4;
      if (normalLen === 0xffffffff) {
        primitive.normal = null;
      } else {
        if (dataView.byteLength < offset + normalLen)
          throw new Error('Insufficient data for Normal content');
        primitive.normal = uint8Array.subarray(offset, offset + normalLen);
        offset += normalLen;
      }
    }

    if (isFullProps) {
      if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Indices length');
      const indicesLen = dataView.getUint32(offset, false);
      offset += 4;
      if (indicesLen === 0xffffffff) {
        primitive.indices = null;
      } else {
        if (dataView.byteLength < offset + indicesLen)
          throw new Error('Insufficient data for Indices content');
        primitive.indices = uint8Array.subarray(offset, offset + indicesLen);
        offset += indicesLen;
      }
    }

    if (isFullProps) {
      if (dataView.byteLength < offset + 4)
        throw new Error('Insufficient data for Posindex length');
      const posindexLen = dataView.getUint32(offset, false);
      offset += 4;
      if (posindexLen === 0xffffffff) {
        primitive.posindex = null;
      } else {
        if (dataView.byteLength < offset + posindexLen)
          throw new Error('Insufficient data for Posindex content');
        primitive.posindex = uint8Array.subarray(offset, offset + posindexLen);
        offset += posindexLen;
      }
    }

    if (isFullProps) {
      if (dataView.byteLength < offset + 4)
        throw new Error('Insufficient data for Nolindex length');
      const nolindexLen = dataView.getUint32(offset, false);
      offset += 4;
      if (nolindexLen === 0xffffffff) {
        primitive.nolindex = null;
      } else {
        if (dataView.byteLength < offset + nolindexLen)
          throw new Error('Insufficient data for Nolindex content');
        primitive.nolindex = uint8Array.subarray(offset, offset + nolindexLen);
        offset += nolindexLen;
      }
    }

    if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for DataType length');
    const dataTypeLen = dataView.getUint32(offset, false);
    offset += 4;
    if (dataTypeLen === 0xffffffff) {
      primitive.dataType = null;
    } else if (dataTypeLen > 0) {
      if (dataView.byteLength < offset + dataTypeLen)
        throw new Error('Insufficient data for DataType content');
      const textBytes = uint8Array.subarray(offset, offset + dataTypeLen);
      primitive.dataType = new TextDecoder('utf-8').decode(textBytes);
      offset += dataTypeLen;
    } else {
      primitive.dataType = '';
    }

    // console.log('primitive.dataType', primitive.dataType)

    if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Min count');
    const minCount = dataView.getUint32(offset, false);
    offset += 4;
    if (minCount === 0xffffffff) {
      primitive.min = null;
    } else {
      if (dataView.byteLength < offset + minCount * 8)
        throw new Error('Insufficient data for Min values');
      primitive.min = [];
      for (let i = 0; i < minCount; i++) {
        primitive.min.push(dataView.getFloat64(offset, false));
        offset += 8;
      }
    }

    if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Max count');
    const maxCount = dataView.getUint32(offset, false);
    offset += 4;
    if (maxCount === 0xffffffff) {
      primitive.max = null;
    } else {
      if (dataView.byteLength < offset + maxCount * 8)
        throw new Error('Insufficient data for Max values');
      primitive.max = [];
      for (let i = 0; i < maxCount; i++) {
        primitive.max.push(dataView.getFloat64(offset, false));
        offset += 8;
      }
    }

    if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for GeomType');
    primitive.geomType = dataView.getInt32(offset, false);
    offset += 4;

    // console.log('primitive offset', primitive, offset)
    return { primitive, offset };
  }

  // ----------------------------------------------------------------
  // 交互控制
  // ----------------------------------------------------------------

  async ensureNotInteracting(abortSignal) {
    if (abortSignal && abortSignal.aborted) return;

    // 如果外部提供了检查函数，优先使用
    if (this.externalEnsureNotInteracting) {
      await this.externalEnsureNotInteracting(abortSignal);
      return;
    }

    if (!this.isUserInteracting) return;
    if (!this.parsePausePromise) {
      this.parsePausePromise = new Promise(resolve => {
        this.parsePauseResolver = resolve;
      });
    }
    await this.parsePausePromise;
  }

  resumeParsing() {
    if (this.parsePauseResolver) {
      this.parsePauseResolver();
      this.parsePauseResolver = null;
      this.parsePausePromise = null;
    }
  }

  handleControlStart() {
    this.controlPressStartAt = Date.now();
    this.isUserInteracting = true;
  }

  async handleControlEnd() {
    const pressDuration = Date.now() - (this.controlPressStartAt || 0);
    if (pressDuration >= this.longPressAbortThresholdMs) {
      try {
        // await this.abortAllStreamRequests();
      } catch (_) {}
    }
    this.controlPressStartAt = 0;
    this.isUserInteracting = false;
    this.resumeParsing();
  }

  async handleWheelStart() {
    try {
      // await this.abortAllStreamRequests();
    } catch (_) {}
    this.isUserInteracting = true;
  }

  handleWheelEnd() {
    this.isUserInteracting = false;
    this.resumeParsing();
  }

  // ----------------------------------------------------------------
  // 批量加载与详情获取
  // ----------------------------------------------------------------

  async batchLoadRegions(item, divideData) {
    if (!divideData || divideData.length === 0) {
      return;
    }
    try {
      const priorityRegions = [];
      const level0Regions = divideData.filter(
        region => region.lodLevel && region.lodLevel.includes(0)
      );
      const level1Regions = divideData.filter(
        region => region.lodLevel && region.lodLevel.includes(1)
      );
      const level2Regions = divideData.filter(
        region => region.lodLevel && region.lodLevel.includes(2)
      );
      const level3Regions = divideData.filter(
        region => region.lodLevel && region.lodLevel.includes(3)
      );
      const level4Regions = divideData.filter(
        region => region.lodLevel && region.lodLevel.includes(4)
      );
      priorityRegions.push(
        ...level0Regions,
        ...level1Regions,
        ...level2Regions,
        ...level3Regions,
        ...level4Regions
      );
      if (priorityRegions.length === 0) {
        return;
      }
      for (let i = 0; i < priorityRegions.length && i < 1; i++) {
        const regionData = priorityRegions[i];
        const range = { regionIndex: regionData.index, documentId: item.id };
        try {
          await this.fetchJsonSync(item, range);
        } catch (error) {}
      }
    } catch (error) {
      throw error;
    }
  }

  async loadModelByIds(options) {
    let { params, onComplete } = options;
    let buffer = null;
    let primitives = [];
    try {
      const loadStartTime = Date.now();

      // 构建 documentIdToIds 映射 多个documentid->modelid
      const { ids } = options.params || {};
      const documentIdToIds = this.parseModelDocumentMappings(ids);
      delete params.ids;
      params.documentIdToIds = documentIdToIds;
      params.projectId = this.projectId;

      buffer = await this.getPrimitivesByRangeDetail(params);
      if (!buffer) {
        return { primitives: [] };
      }
      const { mesh, primitives: parsed } = await this.parseBufferData(buffer);
      primitives = parsed;

      await this.renderModelData(
        mesh,
        primitives,
        params.folderInfo,
        null,
        onComplete,
        params.immediateUpdate ?? true
      );

      return { primitives, mesh };
    } catch (err) {
      if (err.name === 'AbortError') {
        throw err;
      }
      throw err;
    } finally {
      try {
        buffer = null;
        if (primitives && primitives.length > 0) {
          primitives.forEach(primitive => {
            if (primitive && typeof primitive === 'object') {
              Object.keys(primitive).forEach(key => {
                if (Array.isArray(primitive[key]) && primitive[key].length > 100) {
                  primitive[key] = null;
                }
              });
            }
          });
        }
        primitives = null;
        if (typeof window !== 'undefined' && window.gc) {
          window.gc();
        }
      } catch (cleanupErr) {}
    }
  }

  // ----------------------------------------------------------------
  // API 包装
  // ----------------------------------------------------------------

  async getPrimitivesByDivide(params) {
    const res = await this.modelApi.getPrimitivesByDivide(params);
    if (res) return res;
    throw new Error('Failed to get primitives');
  }

  async getPrimitivesByRangeDetail(params) {
    const res = await this.modelApi.getPrimitivesByRangeDetail(params);
    if (res) return res;
    throw new Error('Failed to get batch model');
  }

  // ----------------------------------------------------------------
  // 其他
  // ----------------------------------------------------------------

  async getSceneBox({ id, projectId = this.projectId }) {
    const res = await this.modelApi.getSceneBox({ id, projectId });
    if (res && res[0]) {
      this.sceneBox = res[0];
      return res[0];
    } else {
      return null;
    }
  }

  async getBox({ id, projectId = this.projectId || 0, isDebug }) {
    // 1. 尝试从 IndexedDB 读取
    const cached = await this._getFromDB(id, projectId);
    if (cached && !isDebug) {
      return cached;
    }

    // 2. 正常请求
    const res = await this.modelApi.getBox({ id, projectId });
    if (res) {
      // 3. 存入 IndexedDB (存储原始数据)
      this._applyPrefixId(res, 'id', id);

      this._saveToDB(id, res, projectId);
      return res;
    } else {
      return null;
    }
  }

  async handleCameraControlForStream(args) {
    // 性能优化注释：禁用 range 被注释字段的解构声明与初始化
    const {
      chainRequest,
      loadedModels,
      // max,
      // min,
      // viewportHeight,
      // viewportWidth,
      // formula,
      // cameraWorldPosition,
      // lodLevel,
      // fovY,
      // vertices,
      // distance,
      // highSSE,
      // sseValue,
      // isAllType,
      // queryIds,
      // sceneBox,
    } = args;

    // 构建 documentIdToIds 映射 多个documentid->modelid
    // 性能优化：提前构建映射，避免后续重复遍历 loadedModels
    const documentIdToIds = this.parseModelDocumentMappings(loadedModels);

    // 获取当前模型资源（通常由外部注入或设置）
    const activeItems = [];
    // const activeMaterialData = [];
    // const loadedDocIds = new Set(); // 性能优化：不再需要单独的 Set

    // 从注册表中收集数据
    // 性能优化：直接使用 documentIdToIds 的 keys，避免重复解析 loadedModels
    Object.keys(documentIdToIds).forEach(docId => {
      const record = this.modelRegistry.get(docId);
      if (record) {
        activeItems.push(record.item);
        // if (record.materialData) {
        //   if (Array.isArray(record.materialData)) {
        //     activeMaterialData.push(...record.materialData);
        //   } else {
        //     activeMaterialData.push(record.materialData);
        //   }
        // }
      }
    });

    if (activeItems.length === 0) {
      return;
    }

    const primaryItem = activeItems[0]; // 使用第一个item作为主上下文

    const requestId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);

    if (!chainRequest && this.currentAbortController) {
      this.currentAbortController.abort();
      this.currentAbortController = null;
    }

    this.currentAbortController = new AbortController();
    this.currentRequestId = requestId;

    // 性能优化注释：禁用 range 被注释字段的坐标拆解计算
    // const { x: coordinateX, y: coordinateY, z: coordinateZ } = cameraWorldPosition;
    // const documentIdToIds = {
    //   "a8d4e49c-874c-4a11-9d72-13e8d0a6354e": [614]
    // }
    // 性能优化注释：禁用 range 中除 documentIdToIds 外的所有字段
    const range = {
      // minX: min.x,
      // maxX: max.x,
      // minY: min.y,
      // maxY: max.y,
      // maxZ: max.z,
      // minZ: min.z,
      // viewportHeight,
      // formula,
      // coordinateX,
      // coordinateY,
      // coordinateZ,
      // fovY,
      // viewportWidth,
      documentIdToIds,
      // distance,
      // highSSE,
      // sseValue,
      // isAllType,
      // queryIds,
      // sceneBox,
      // ...vertices,
    };

    // 性能优化注释：禁用基于 sceneBox 的 range 二次裁剪逻辑
    // const boxToUse = sceneBox || this.sceneBox;
    // if (boxToUse) {
    //   range.minX = Math.max(range.minX, boxToUse.min_x);
    //   range.maxX = Math.min(range.maxX, boxToUse.max_x);
    //   range.minY = Math.max(range.minY, boxToUse.min_y);
    //   range.maxY = Math.min(range.maxY, boxToUse.max_y);
    //   range.minZ = Math.max(range.minZ, boxToUse.min_z);
    //   range.maxZ = Math.min(range.maxZ, boxToUse.max_z);
    // }

    try {
      const streamResult = await this.fetchJsonStream(
        primaryItem,
        range,
        this.currentAbortController.signal,
        requestId
      );
      if (this.currentRequestId === requestId) {
        if (typeof this.onRangeStreamComplete === 'function') {
          await this.onRangeStreamComplete({
            source: 'inRangeDis2',
            requestId,
            item: primaryItem,
            range,
            ...(streamResult || {}),
          });
        }
        this.currentAbortController = null;
        this.currentRequestId = null;
      } else {
      }
    } catch (error) {
      if (error.name === 'AbortError') {
        return;
      }
      if (this.currentRequestId === requestId) {
        this.currentAbortController = null;
        this.currentRequestId = null;
      }
    }
  }

  updateModelRegistry(item, materialData, mergeMaterialData, sceneBox, boxIndex) {
    if (item && item.id) {
      this.modelRegistry.set(item.id, {
        item,
        materialData,
        mergeMaterialData,
        sceneBox,
        boxIndex,
      });
    }
  }

  /**
   * 将复合 ID 数组解析为 documentId 到 modelId 列表的映射
   * @param {Array<string>} loadedModels - 复合 ID 数组 (如 ["288:docId"])
   * @returns {Object} - 映射对象 (如 {"docId": [288]})
   */
  parseModelDocumentMappings(loadedModels) {
    const acc = {};
    if (!loadedModels || !Array.isArray(loadedModels)) return acc;

    // 性能优化：使用 for 循环代替 reduce，并优化 parseInt 调用
    for (let i = 0, len = loadedModels.length; i < len; i++) {
      const str = loadedModels[i];
      if (typeof str !== 'string') continue;

      const firstIndex = str.indexOf(':');
      if (firstIndex !== -1) {
        // 优化：直接解析整数，避免 substring 创建临时字符串
        const modelId = parseInt(str, 10);
        if (!isNaN(modelId)) {
          const docId = str.substring(firstIndex + 1);
          if (docId) {
            if (!acc[docId]) {
              acc[docId] = [];
            }
            // if(modelId == 613 || modelId == 614){
            //   acc[docId].push(modelId);
            // } // TODO 608 603 模型单独处理
            acc[docId].push(modelId);
          }
        }
      }
    }
    return acc;
  }

  async processModelItem(item, options = {}) {
    try {
      if (typeof item.id !== 'string') {
        item.id = '' + item.id;
      }

      // 获取材质数据
      const materialData = await this.getPrimitivesByMaterial({
        id: item.id,
        projectId: this.projectId,
      });

      // 获取合并材质数据
      const mergeMaterialData = await this.getPrimitivesByMergeMaterial({
        id: item.id,
        projectId: this.projectId,
      });
      this._applyPrefixId(mergeMaterialData, 'id');
      this._applyPrefixId(materialData, 'id');

      // 获取场景包围盒
      const sceneBox = await this.getSceneBox({ id: item.id, projectId: this.projectId });
      this.sceneBox = sceneBox;

      // 获取BoxIndex
      const boxIndex = await this.getBox({
        id: item.id,
        projectId: this.projectId,
        isDebug: this.debug,
      });
      this.boxIndex = boxIndex;

      // 将 mergeMaterialData 覆盖到 materialData（按 id 覆盖 color/transp）
      this._mergeMaterialDataOverrides(boxIndex, mergeMaterialData);

      const modelResourceMapObj = { materialData, mergeMaterialData, item };

      // 设置当前模型到注册表
      this.updateModelRegistry(item, materialData, mergeMaterialData, sceneBox, boxIndex);
      console.log('modelRegistry', this.modelRegistry);

      return { modelResourceMap: modelResourceMapObj, sceneBox, boxIndex, item };
    } catch (error) {
      return null;
    }
  }

  // ----------------------------------------------------------------
  // IndexedDB 缓存支持
  // ----------------------------------------------------------------

  _initDB() {
    if (this._dbReady) return this._dbReady;
    this._dbReady = new Promise((resolve, reject) => {
      if (typeof indexedDB === 'undefined') {
        resolve(null);
        return;
      }
      // 打开数据库
      const request = indexedDB.open('StreamLoaderDB', 1);
      request.onerror = () => {
        console.warn('IndexedDB open failed');
        resolve(null);
      };
      request.onsuccess = () => resolve(request.result);
      request.onupgradeneeded = event => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains('boxCache')) {
          db.createObjectStore('boxCache');
        }
      };
    });
    return this._dbReady;
  }

  async _getFromDB(id, projectId = this.projectId || 0) {
    try {
      const db = await this._initDB();
      if (!db) return null;
      return new Promise(resolve => {
        const transaction = db.transaction(['boxCache'], 'readonly');
        const store = transaction.objectStore('boxCache');
        const request = store.get(`${projectId}:${id}`);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => resolve(null);
      });
    } catch (e) {
      console.warn('Error reading from IndexedDB:', e);
      return null;
    }
  }

  async _saveToDB(id, data, projectId = this.projectId || 0) {
    try {
      const db = await this._initDB();
      if (!db) return;
      const transaction = db.transaction(['boxCache'], 'readwrite');
      const store = transaction.objectStore('boxCache');
      store.put(data, `${projectId}:${id}`);
    } catch (e) {
      console.warn('Error saving to IndexedDB:', e);
    }
  }
}
