/**
 * PCDLoader - 解析 Point Cloud Data 文件
 * 支持 ASCII 和 BINARY 格式
 * 优化：BINARY 不整体 decode 为字符串；支持大文件采样
 */
class PCDLoader {
  constructor() {
    this.littleEndian = true;
  }

  parse(buffer, options = {}) {
    const maxPoints = options.maxPoints || 5000000;
    const onProgress = options.onProgress || null;

    // 1. Find header end
    const headerInfo = this.findHeaderEnd(buffer);
    if (!headerInfo) {
      throw new Error('PCD file missing DATA line');
    }
    const { dataType, headerEndByte } = headerInfo;

    // 2. Parse header text
    const headerBuffer = new Uint8Array(buffer, 0, headerEndByte);
    const headerText = new TextDecoder('ascii').decode(headerBuffer);
    const header = this.parseHeaderText(headerText);

    const fields = header.FIELDS || [];
    const sizes = (header.SIZE || []).map(Number);
    const types = header.TYPE || [];
    const counts = (header.COUNT || []).map(Number);
    const width = Number(header.WIDTH?.[0] || 0);
    const height = Number(header.HEIGHT?.[0] || 0);
    const pointsCount = Number(header.POINTS?.[0] || width * height);

    if (pointsCount <= 0) {
      throw new Error('PCD file has no points');
    }

    // Determine xyz indices
    const xIdx = fields.indexOf('x');
    const yIdx = fields.indexOf('y');
    const zIdx = fields.indexOf('z');
    const hasColor = fields.includes('rgb') || fields.includes('rgba');
    const rgbIdx = fields.indexOf('rgb');
    const rgbaIdx = fields.indexOf('rgba');
    const colorFieldIdx = rgbIdx >= 0 ? rgbIdx : rgbaIdx;

    // 3. Sampling setup
    const targetCount = Math.min(pointsCount, maxPoints);
    const useSampling = pointsCount > maxPoints;
    const sampleStep = useSampling ? pointsCount / targetCount : 1;
    let nextSampleThreshold = 0;

    const positions = new Float32Array(targetCount * 3);
    const colors = hasColor ? new Float32Array(targetCount * 3) : null;
    let posIdx = 0;
    let colorIdx = 0;
    let kept = 0;

    if (dataType === 'ascii') {
      // ASCII: stream parse lines from headerEndByte
      let parsed = 0;
      for (const line of this.readLines(buffer, headerEndByte, pointsCount)) {
        if (parsed >= pointsCount) break;
        const trimmed = line.trim();
        if (trimmed === '') continue;
        const values = trimmed.split(/\s+/);
        if (values.length < fields.length) continue;

        // Sampling decision (deterministic)
        let shouldKeep = false;
        if (!useSampling) {
          shouldKeep = true;
        } else if (parsed >= nextSampleThreshold) {
          shouldKeep = true;
          nextSampleThreshold += sampleStep;
          if (kept >= targetCount) shouldKeep = false;
        }

        if (shouldKeep) {
          positions[posIdx++] = xIdx >= 0 ? Number(values[xIdx]) : 0;
          positions[posIdx++] = yIdx >= 0 ? Number(values[yIdx]) : 0;
          positions[posIdx++] = zIdx >= 0 ? Number(values[zIdx]) : 0;

          if (hasColor && colorFieldIdx >= 0) {
            const c = this.parseColor(Number(values[colorFieldIdx]), rgbIdx >= 0 ? 'rgb' : 'rgba');
            colors[colorIdx++] = c.r;
            colors[colorIdx++] = c.g;
            colors[colorIdx++] = c.b;
          }
          kept++;
        }
        parsed++;

        if (onProgress && parsed % 100000 === 0) {
          onProgress(parsed, pointsCount);
        }
      }
    } else {
      // BINARY: direct DataView from headerEndByte
      const view = new DataView(buffer);
      const pointStep = fields.reduce((sum, _, i) => sum + (sizes[i] || 4) * (counts[i] || 1), 0);
      let offset = headerEndByte;

      for (let i = 0; i < pointsCount; i++) {
        // Sampling decision (deterministic)
        let shouldKeep = false;
        if (!useSampling) {
          shouldKeep = true;
        } else if (i >= nextSampleThreshold) {
          shouldKeep = true;
          nextSampleThreshold += sampleStep;
          if (kept >= targetCount) shouldKeep = false;
        }

        if (shouldKeep) {
          let x = 0, y = 0, z = 0;
          let cr = 1, cg = 1, cb = 1;
          let pointOffset = offset;

          for (let f = 0; f < fields.length; f++) {
            const field = fields[f];
            const size = sizes[f] || 4;
            const type = types[f] || 'F';
            const count = counts[f] || 1;

            for (let c = 0; c < count; c++) {
              let val;
              if (type === 'F') {
                if (size === 4) val = view.getFloat32(pointOffset, this.littleEndian);
                else if (size === 8) val = view.getFloat64(pointOffset, this.littleEndian);
              } else if (type === 'I') {
                if (size === 1) val = view.getInt8(pointOffset);
                else if (size === 2) val = view.getInt16(pointOffset, this.littleEndian);
                else if (size === 4) val = view.getInt32(pointOffset, this.littleEndian);
              } else if (type === 'U') {
                if (size === 1) val = view.getUint8(pointOffset);
                else if (size === 2) val = view.getUint16(pointOffset, this.littleEndian);
                else if (size === 4) val = view.getUint32(pointOffset, this.littleEndian);
              }
              pointOffset += size;

              if (c === 0) {
                if (field === 'x') x = val;
                else if (field === 'y') y = val;
                else if (field === 'z') z = val;
                else if (field === 'rgb') {
                  const col = this.parseColor(val, 'rgb');
                  cr = col.r; cg = col.g; cb = col.b;
                } else if (field === 'rgba') {
                  const col = this.parseColor(val, 'rgba');
                  cr = col.r; cg = col.g; cb = col.b;
                }
              }
            }
          }
          positions[posIdx++] = x;
          positions[posIdx++] = y;
          positions[posIdx++] = z;
          if (hasColor) {
            colors[colorIdx++] = cr;
            colors[colorIdx++] = cg;
            colors[colorIdx++] = cb;
          }
          kept++;
        }

        offset += pointStep;

        if (onProgress && i % 100000 === 0) {
          onProgress(i, pointsCount);
        }
      }
    }

    const actualPositions = positions.subarray(0, posIdx);
    const actualColors = colors ? colors.subarray(0, colorIdx) : null;

    return {
      positions: actualPositions,
      colors: actualColors,
      count: Math.floor(posIdx / 3),
      originalCount: pointsCount,
      fields,
      header
    };
  }

  /**
   * Scan raw bytes to find the DATA line and compute header end offset.
   * Only scans first 64KB — more than enough for any PCD header.
   */
  findHeaderEnd(buffer) {
    const view = new Uint8Array(buffer);
    const maxSearch = Math.min(view.length, 65536);

    for (let i = 0; i < maxSearch - 10; i++) {
      // Look for "DATA " (0x44 0x41 0x54 0x41 0x20)
      if (view[i] === 68 && view[i + 1] === 65 && view[i + 2] === 84 && view[i + 3] === 65 && view[i + 4] === 32) {
        let j = i + 5;
        while (j < maxSearch && view[j] === 32) j++; // skip spaces

        let typeStr = '';
        while (j < maxSearch && view[j] !== 13 && view[j] !== 10) {
          typeStr += String.fromCharCode(view[j]);
          j++;
        }

        const dataType = typeStr.toLowerCase();
        if (dataType !== 'ascii' && dataType !== 'binary') {
          continue; // false match
        }

        // Skip past newline(s)
        while (j < maxSearch && (view[j] === 13 || view[j] === 10)) j++;

        return { dataType, headerEndByte: j };
      }
    }

    return null;
  }

  parseHeaderText(text) {
    const lines = text.split(/\r?\n/);
    const header = {};
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('#') || trimmed === '') continue;
      const parts = trimmed.split(/\s+/);
      const key = parts[0].toUpperCase();
      header[key] = parts.slice(1);
    }
    return header;
  }

  /**
   * Generator that yields lines from an ArrayBuffer starting at `offset`.
   * Processes in 1MB chunks to avoid decoding the entire file as a single string.
   */
  *readLines(buffer, offset, maxLines) {
    const chunkSize = 1024 * 1024; // 1MB chunks
    const uint8 = new Uint8Array(buffer);
    let leftover = new Uint8Array(0);
    let i = offset;
    let linesYielded = 0;

    while (i < uint8.length && linesYielded < maxLines) {
      const end = Math.min(i + chunkSize, uint8.length);
      const chunk = uint8.subarray(i, end);
      const combined = new Uint8Array(leftover.length + chunk.length);
      combined.set(leftover);
      combined.set(chunk, leftover.length);

      // Find the last newline in combined buffer
      let lastNewline = -1;
      for (let j = combined.length - 1; j >= 0; j--) {
        if (combined[j] === 10) { // '\n'
          lastNewline = j;
          break;
        }
      }

      // If no newline found and there's more data, buffer this chunk
      if (lastNewline === -1 && end < uint8.length) {
        leftover = combined;
        i = end;
        continue;
      }

      // Decode and yield complete lines
      const textPart = combined.subarray(0, lastNewline + 1);
      const text = new TextDecoder('ascii').decode(textPart);
      const lines = text.split(/\r?\n/);
      for (const line of lines) {
        if (linesYielded >= maxLines) break;
        yield line;
        linesYielded++;
      }

      // Carry over incomplete tail
      leftover = combined.subarray(lastNewline + 1);
      i = end;
    }

    // Yield any remaining bytes as final line(s)
    if (leftover.length > 0 && linesYielded < maxLines) {
      const text = new TextDecoder('ascii').decode(leftover);
      const lines = text.split(/\r?\n/);
      for (const line of lines) {
        if (linesYielded >= maxLines) break;
        yield line;
        linesYielded++;
      }
    }
  }

  parseColor(val, type) {
    if (type === 'rgb') {
      const r = ((val >> 16) & 0xFF) / 255;
      const g = ((val >> 8) & 0xFF) / 255;
      const b = (val & 0xFF) / 255;
      return { r, g, b };
    } else {
      const r = ((val >> 24) & 0xFF) / 255;
      const g = ((val >> 16) & 0xFF) / 255;
      const b = ((val >> 8) & 0xFF) / 255;
      return { r, g, b };
    }
  }
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = { PCDLoader };
}
