import { MAX_PAYLOAD_SIZE } from './constants';
import type { RawFrame, MeasurementMeta, MeasurementFrame, AckPacket } from './types';
import { FuncCode, AMP_GAIN } from './constants';

// Streaming TCP frame parser.
// Handles packet fragmentation (TCP splits large packets across multiple reads).
export class TemFrameParser {
  private buf: Uint8Array;
  private len = 0;
  private readonly capacity: number;
  onOverflow?: () => void;

  constructor(capacity = 2 * 1024 * 1024) {
    this.capacity = capacity;
    this.buf = new Uint8Array(capacity);
  }

  feed(chunk: Uint8Array | ArrayBuffer | { buffer: ArrayBuffer; byteOffset: number; byteLength: number }): RawFrame[] {
    let incoming: Uint8Array;
    if (chunk instanceof Uint8Array) {
      incoming = chunk;
    } else if (chunk instanceof ArrayBuffer) {
      incoming = new Uint8Array(chunk);
    } else {
      incoming = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
    }

    if (this.len + incoming.length > this.capacity) {
      if (__DEV__) console.warn(`[Parser] buffer overflow: ${this.len} + ${incoming.length} > ${this.capacity}, resetting`);
      this.len = 0;
      this.onOverflow?.();
    }
    this.buf.set(incoming, this.len);
    this.len += incoming.length;

    const results: RawFrame[] = [];
    let offset = 0;

    while (offset < this.len) {
      const magic = this.findMagic(offset);
      if (magic === -1) {
        // No header found — keep last 3 bytes in case they start a header
        const keep = Math.min(3, this.len - offset);
        this.buf.copyWithin(0, this.len - keep);
        this.len = keep;
        return results;
      }
      offset = magic;

      // Wait until we have the full 10-byte header
      if (this.len - offset < 10) break;

      const flag = this.buf[offset + 4];
      const func = this.buf[offset + 5];
      const is2Byte = !!(flag & 0x01);
      const payloadLen = is2Byte
        ? this.readUint16LE(offset + 6)
        : this.readUint32LE(offset + 6);

      if (__DEV__) console.log(
        `[Parser] header @ ${offset}: flag=0x${flag.toString(16)} func=0x${func.toString(16)} lenMode=${is2Byte ? '2B' : '4B'} payloadLen=${payloadLen} bufLen=${this.len}`,
        'hdr:', Array.from(this.buf.slice(offset, offset + 10)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '),
      );

      if (payloadLen > MAX_PAYLOAD_SIZE) {
        if (__DEV__) console.warn(`[Parser] bogus payloadLen ${payloadLen} > MAX ${MAX_PAYLOAD_SIZE}, skipping magic`);
        offset += 4;
        continue;
      }

      const totalLen = 10 + payloadLen;
      if (this.len - offset < totalLen) {
        if (__DEV__) console.log(`[Parser] incomplete frame: have ${this.len - offset}, need ${totalLen}`);
        break;
      }

      const payload = new Uint8Array(totalLen - 10);
      payload.set(this.buf.subarray(offset + 10, offset + totalLen));
      if (__DEV__) console.log(`[Parser] ✓ frame complete: func=0x${func.toString(16)} payload=${payloadLen}B`);
      results.push({ flag, func, payload });
      offset += totalLen;
    }

    // Compact — move remaining bytes to front
    if (offset > 0 && offset <= this.len) {
      this.buf.copyWithin(0, offset, this.len);
      this.len -= offset;
    }

    return results;
  }

  reset() {
    this.len = 0;
  }

  private findMagic(start: number): number {
    const b = this.buf;
    const end = this.len - 3;
    for (let i = start; i <= end; i++) {
      if (b[i] === 0x68 && b[i + 1] === 0x68 && b[i + 2] === 0xff && b[i + 3] === 0xff) {
        return i;
      }
    }
    return -1;
  }

  private readUint16LE(offset: number): number {
    return this.buf[offset] | (this.buf[offset + 1] << 8);
  }

  private readUint32LE(offset: number): number {
    const b = this.buf;
    return b[offset] | (b[offset + 1] << 8) | (b[offset + 2] << 16) | (b[offset + 3] * 0x1000000);
  }
}

// Parse the metadata header of a 0x85 or 0x95 DATA_ACK payload
export function parseMetadata(payload: Uint8Array): MeasurementMeta | null {
  if (payload.length < 54) return null;
  const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
  let o = 0;
  const devId = view.getUint8(o++);
  const utc = view.getUint32(o, true); o += 4;
  const longitude = view.getFloat64(o, true); o += 8;
  const latitude = view.getFloat64(o, true); o += 8;
  const altitude = view.getFloat32(o, true); o += 4;
  const height = view.getFloat32(o, true); o += 4;
  const sdGps = view.getUint8(o++);
  const ampRatio = view.getUint8(o++);
  const roll = view.getFloat32(o, true); o += 4;
  const pitch = view.getFloat32(o, true); o += 4;
  const yaw = view.getFloat32(o, true); o += 4;
  const channelNum = view.getUint8(o++);
  const current = view.getUint32(o, true); o += 4;
  const temperature = view.getUint16(o, true); o += 2;
  const batteryVolt = view.getUint16(o, true); o += 2;
  const sourceMode = view.getUint8(o++);
  return {
    devId, utc, longitude, latitude, altitude, height,
    sdStatus: sdGps & 0x0f,
    gpsStatus: (sdGps >> 4) & 0x0f,
    ampRatio, roll, pitch, yaw, channelNum,
    current, temperature, batteryVolt, sourceMode,
  };
}

// Parse ADC sample data from a 0x85/0x95 payload into per-channel arrays
const ADC_FULL_SCALE_V = 5.0;
const INT32_MAX = 0x7fffffff;
// raw → μV: (raw / 0x7FFFFFFF) * 5V / gain * 1e6
const ADC_UV_SCALE = (ADC_FULL_SCALE_V * 1e6) / INT32_MAX;

export function parseAdcData(
  payload: Uint8Array,
  channelNum: number,
  _accNum: number,
): { adcRaw: Int32Array[]; adcUV: Float64Array[] } {
  const META_SIZE = 54;
  const sampleDataLen = payload.length - META_SIZE;
  const totalSamples = sampleDataLen / 4;
  const samplesPerChannel = Math.floor(totalSamples / channelNum);

  const view = new DataView(payload.buffer, payload.byteOffset + META_SIZE, sampleDataLen);
  const gain = AMP_GAIN[payload[30]] ?? 1;
  const scale = ADC_UV_SCALE / gain;

  const adcRaw: Int32Array[] = [];
  const adcUV: Float64Array[] = [];

  for (let ch = 0; ch < channelNum; ch++) {
    const raw = new Int32Array(samplesPerChannel);
    const uv = new Float64Array(samplesPerChannel);
    for (let i = 0; i < samplesPerChannel; i++) {
      const idx = (i * channelNum + ch) * 4;
      const val = view.getInt32(idx, true);
      raw[i] = val;
      uv[i] = val * scale;
    }
    adcRaw.push(raw);
    adcUV.push(uv);
  }

  return { adcRaw, adcUV };
}

// Parse a generic ACK packet (0x81–0x84)
export function parseAck(payload: Uint8Array): AckPacket {
  const result = payload[0] ?? 0x02;
  let reason = '';
  const reasonOffset = 22;
  if (payload.length > reasonOffset) {
    for (let i = reasonOffset; i < Math.min(reasonOffset + 32, payload.length); i++) {
      if (payload[i] === 0) break;
      reason += String.fromCharCode(payload[i]);
    }
  }
  return { result, reason };
}
