import { gunzipSync } from 'fflate';

self.__streamLoaderStates = self.__streamLoaderStates || new Map();

const utf8Decoder = new TextDecoder('utf-8');

self.onmessage = function (e) {
  const { id, type, data } = e.data;
  if (type === 'parseBufferData') {
    try {
      const { buffer, prefixIdKey } = data;
      const result = parseBufferData(buffer, prefixIdKey);
      // Transfer the buffer back if possible, but we constructed new arrays.
      // We rely on structured cloning.
      self.postMessage({ id, type: 'success', result });
    } catch (error) {
      self.postMessage({ id, type: 'error', error: error.message });
    }
  } else if (type === 'streamInit') {
    try {
      const { streamId, prefixIdKey } = data || {};
      if (!streamId) throw new Error('Missing streamId');
      const state = createStreamState(prefixIdKey);
      self.__streamLoaderStates.set(streamId, state);
      self.postMessage({ id, type: 'success', result: true });
    } catch (error) {
      self.postMessage({ id, type: 'error', error: error.message });
    }
  } else if (type === 'streamPush') {
    try {
      const { streamId, chunk, byteOffset = 0, byteLength, prefixIdKey } = data || {};
      if (!streamId) throw new Error('Missing streamId');
      let state = self.__streamLoaderStates.get(streamId);
      if (!state) {
        state = createStreamState(prefixIdKey);
        self.__streamLoaderStates.set(streamId, state);
      }
      const view = new Uint8Array(chunk, byteOffset, byteLength ?? chunk.byteLength - byteOffset);
      const result = parseStreamChunk(state, view);
      self.postMessage({ id, type: 'success', result });
    } catch (error) {
      self.postMessage({ id, type: 'error', error: error.message });
    }
  } else if (type === 'streamFlush') {
    try {
      const { streamId } = data || {};
      if (!streamId) throw new Error('Missing streamId');
      const state = self.__streamLoaderStates.get(streamId);
      if (!state) {
        self.postMessage({ id, type: 'success', result: { meshes: [], primitives: [] } });
        return;
      }
      const result = parseStreamChunk(state, null, true);
      self.postMessage({ id, type: 'success', result });
    } catch (error) {
      self.postMessage({ id, type: 'error', error: error.message });
    }
  } else if (type === 'streamDispose') {
    try {
      const { streamId } = data || {};
      if (streamId) self.__streamLoaderStates.delete(streamId);
      self.postMessage({ id, type: 'success', result: true });
    } catch (error) {
      self.postMessage({ id, type: 'error', error: error.message });
    }
  } else if (type === 'buildInstancedMapping') {
    try {
      const { instances, drawObjs } = data || {};
      const result = buildInstancedMapping(instances || [], drawObjs || []);
      self.postMessage({ id, type: 'success', result });
    } catch (error) {
      self.postMessage({ id, type: 'error', error: error.message });
    }
  } else if (type === 'occlusionScan') {
    const transfer = data && data.buffer ? [data.buffer] : [];
    try {
      const { buffer, sw, sh, stride, maxIdx, minSampleCount } = data || {};
      const result = scanOcclusionBuffer(buffer, sw, sh, stride, maxIdx, minSampleCount);
      self.postMessage({ id, type: 'success', result, buffer }, transfer);
    } catch (error) {
      const buffer = data && data.buffer ? data.buffer : null;
      self.postMessage({ id, type: 'error', error: error.message, buffer }, transfer);
    }
  }
};

function createStreamState(prefixIdKey) {
  return {
    prefixIdKey: prefixIdKey || '',
    buffer: new Uint8Array(),
    pendingPrimitives: new Map(),
    expectingPrimitive: true,
    isFullProps: true,
  };
}

function parseStreamChunk(state, chunkView, flushing = false) {
  if (chunkView && chunkView.length > 0) {
    const merged = new Uint8Array(state.buffer.length + chunkView.length);
    merged.set(state.buffer);
    merged.set(chunkView, state.buffer.length);
    state.buffer = merged;
  }

  const meshes = [];
  const primitives = [];

  while (state.buffer.length > 0) {
    if (state.expectingPrimitive) {
      if (state.buffer.length < 12) break;

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

        _applyPrefixId(primitiveData, 'id', null, state.prefixIdKey);
        _applyPrefixId(primitiveData, 'material', null, state.prefixIdKey);

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

        state.pendingPrimitives.set(primitiveData.id, primitiveData);
        state.expectingPrimitive = false;
        state.buffer = state.buffer.slice(primitiveResult.offset);
      } catch (e) {
        break;
      }
    } else {
      if (state.buffer.length < 4) break;

      const length = new DataView(state.buffer.buffer, state.buffer.byteOffset).getUint32(0, false);
      if (length > 10 * 1024 * 1024) {
        state.expectingPrimitive = true;
        continue;
      }

      const totalLen = 4 + length;
      if (state.buffer.length < totalLen) break;

      const data = state.buffer.slice(4, totalLen);
      let mesh = null;

      try {
        const jsonStr = utf8Decoder.decode(data);
        mesh = JSON.parse(jsonStr);
      } catch (e) {
        state.expectingPrimitive = true;
        continue;
      }

      if (mesh) {
        state.isFullProps = true;

        _applyPrefixId(mesh, 'id', null, state.prefixIdKey);
        if (Array.isArray(mesh)) {
          mesh.forEach(item => {
            _applyPrefixId(item.primitives, 'prmid', item.documentId, state.prefixIdKey);
          });
        } else {
          _applyPrefixId(mesh.primitives, 'prmid', mesh.documentId, state.prefixIdKey);
        }

        const docId = mesh.id;
        const meshToPrimId = mesh.primitives.map(item => item.prmid);
        const hasPendingPrimitive = meshToPrimId.some(item => state.pendingPrimitives.has(item));
        const hasPendingMesh = state.pendingPrimitives.has(docId);

        if (hasPendingMesh || hasPendingPrimitive) {
          let primitiveData = state.pendingPrimitives.get(docId);
          if (!hasPendingMesh && hasPendingPrimitive) {
            primitiveData = state.pendingPrimitives.get(meshToPrimId[0]);
          }
          meshes.push(mesh);
          primitives.push(primitiveData);
        }

        state.expectingPrimitive = false;
        state.buffer = state.buffer.slice(totalLen);
      }
    }
  }

  if (flushing) {
    state.buffer = new Uint8Array();
  }

  return { meshes, primitives };
}

function parseBufferData(buffer, prefixIdKey) {
  let uint8Array = null;
  let dataView = null;
  let meshJsonBytes = null;
  let primitives = [];
  let mesh = null;

  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 meshJsonStr = utf8Decoder.decode(meshJsonBytes);
    meshJsonBytes = null;

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

    while (offset < uint8Array.length) {
      try {
        const result = parsePrimitive(dataView, uint8Array, offset);
        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];
      _applyPrefixId(primitive, 'id', null, prefixIdKey);
      _applyPrefixId(primitive, 'material', null, prefixIdKey);
      const { position, normal, posindex, nolindex, indices } = parsePrimitiveData(primitive);
      const formatted = 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;
  }
}

function _applyPrefixId(target, propKey, customPrefixId, prefixIdKey) {
  if (!prefixIdKey || !target) return;

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

function parsePrimitive(dataView, uint8Array, offset, isFullProps = true) {
  const primitive = {};
  if (dataView.byteLength < offset + 12) {
    throw new Error('Insufficient data for primitive header');
  }
  primitive.id = dataView.getInt32(offset, false);
  offset += 4;

  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 = utf8Decoder.decode(textBytes);
    offset += documentTextLen;
  } else {
    primitive.documentId = '';
  }

  primitive.material = dataView.getInt32(offset, false);
  offset += 4;

  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 = utf8Decoder.decode(textBytes);
    offset += geomTextLen;
  } else {
    primitive.geomText = '';
  }

  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 = utf8Decoder.decode(textBytes);
    offset += dataTypeLen;
  } else {
    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;

  return { primitive, offset };
}

function 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;
}

function 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 };
}

function buildInstancedMapping(instances, drawObjs) {
  const drawObjMapInstance = {};
  const instanceToDrawObject = [];

  const meshByPrimId = new Map();
  for (let i = 0; i < drawObjs.length; i++) {
    const item = drawObjs[i];
    if (!item || !Array.isArray(item.geoms)) continue;
    const geoms = item.geoms;
    for (let j = 0; j < geoms.length; j++) {
      const geom = geoms[j];
      if (geom && geom.prmid !== undefined && geom.prmid !== null) {
        meshByPrimId.set(geom.prmid, geoms);
      }
    }
  }

  const seenByDrawObject = new Map();
  for (let i = 0; i < instances.length; i++) {
    const instance = instances[i];
    if (!instance) continue;
    const drawObject = instance.drawObject;
    if (!drawObject) continue;

    instanceToDrawObject.push({
      instanceId: instance.instanceId,
      drawObjectId: drawObject,
    });

    let record = drawObjMapInstance[drawObject];
    if (!record) {
      record = {
        MapInstance: [],
        MapMesh: meshByPrimId.get(drawObject),
      };
      drawObjMapInstance[drawObject] = record;
      seenByDrawObject.set(drawObject, new Set());
    }

    const seen = seenByDrawObject.get(drawObject);
    if (!seen.has(instance.instanceId)) {
      record.MapInstance.push(instance);
      seen.add(instance.instanceId);
    }
  }

  return { drawObjMapInstance, instanceToDrawObject };
}

function scanOcclusionBuffer(buffer, sw, sh, stride, maxIdx, minSampleCount) {
  const indices = [];
  if (!buffer || !sw || !sh || !maxIdx) return { indices };
  const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
  const step = Math.max(1, stride || 1);
  const minCount = Math.max(1, minSampleCount || 1);
  const counts = new Uint32Array(maxIdx + 1);
  for (let y = 0; y < sh; y += step) {
    const row = y * sw * 4;
    for (let x = 0; x < sw; x += step) {
      const p = row + x * 4;
      const idxColor = view[p] + (view[p + 1] << 8) + (view[p + 2] << 16);
      if (idxColor > 0 && idxColor <= maxIdx) {
        const next = counts[idxColor] + 1;
        counts[idxColor] = next;
        if (next === minCount) {
          indices.push(idxColor);
        }
      }
    }
  }
  return { indices };
}
