/**
 * 设备原始帧格式 — 与设备 0x85/0x95 数据帧完全一致
 *
 * 帧头 (10 字节):
 *   [0-3]    u8[4]  MAGIC = 0x68 0x68 0xFF 0xFF
 *   [4]      u8     flag  (0xFE=4字节长度, 0xFF=2字节长度)
 *   [5]      u8     func  (0x85=数据帧, 0x95=组合源数据帧)
 *   [6-9]    u32 LE payloadLen (flag=0xFE时) 或 u16 LE + 0x68 0x68 (flag=0xFF时)
 *
 * 元数据 (54 字节, 从 offset 10 开始):
 *   [10]     u8     devId
 *   [11-14]  u32 LE utcSecond
 *   [15-22]  f64 LE longitude
 *   [23-30]  f64 LE latitude
 *   [31-34]  f32 LE altitude
 *   [35-38]  f32 LE height
 *   [39]     u8     sdGps  (高4位=gpsStatus, 低4位=sdStatus)
 *   [40]     u8     ampRatio
 *   [41-44]  f32 LE roll
 *   [45-48]  f32 LE pitch
 *   [49-52]  f32 LE yaw
 *   [53]     u8     channelNum
 *   [54-57]  u32 LE current
 *   [58-59]  u16 LE temperature
 *   [60-61]  u16 LE batteryVolt
 *   [62]     u8     sourceMode
 *   [63]     u8     reserved
 *
 * ADC 数据 (从 offset 64 开始):
 *   int32 LE, 交织排列 (sample-major):
 *   [S0_CH0][S0_CH1]...[S0_CHn][S1_CH0]...
 */

import * as FileSystem from 'expo-file-system/legacy';
import { AMP_GAIN } from '../protocol/constants';
import type { MeasurementFrame } from '../protocol/types';

const FRAME_HEADER_SIZE = 10;
const META_SIZE = 54;
const DATA_OFFSET = FRAME_HEADER_SIZE + META_SIZE; // 64

// ── Public types ────────────────────────────────────────────────────────────

export interface LoadedBin {
  adcUV:          Float64Array[];
  sampleDepth:    number;
  channelNum:     number;
  sampleFreqCode: number;
  accNum:         number;
  ampRatio:       number;
  srcMode:        number;
  timestamp:      number;
  latitude:       number;
  longitude:      number;
  utc:            number;
  altitude:       number;
  batteryVolt:    number;
  temperature:    number;
  current:        number;
  gpsStatus:      number;
  sdStatus:       number;
  roll:           number;
  pitch:          number;
  yaw:            number;
}

// ── Base64 ↔ Uint8Array helpers ─────────────────────────────────────────────

export function u8ToBase64(u8: Uint8Array): string {
  const chunk = 8192;
  let str = '';
  for (let i = 0; i < u8.length; i += chunk) {
    str += String.fromCharCode(...u8.subarray(i, Math.min(i + chunk, u8.length)));
  }
  return btoa(str);
}

export function base64ToU8(b64: string): Uint8Array {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes;
}

// ── Pure encode / decode ────────────────────────────────────────────────────

/** Encode a MeasurementFrame into a complete device-compatible frame (header + meta + ADC). */
export function encodeBin(frame: MeasurementFrame): Uint8Array {
  const m   = frame.meta;
  const ch  = frame.adcRaw.length;
  const spc = frame.adcRaw[0]?.length ?? 0;
  const payloadLen = META_SIZE + ch * spc * 4;
  const totalLen = FRAME_HEADER_SIZE + payloadLen;
  const buf = new ArrayBuffer(totalLen);
  const dv  = new DataView(buf);

  // 10-byte frame header (device format, 4-byte length mode)
  dv.setUint8(0, 0x68);
  dv.setUint8(1, 0x68);
  dv.setUint8(2, 0xff);
  dv.setUint8(3, 0xff);
  dv.setUint8(4, 0xfe); // flag: 4-byte length
  dv.setUint8(5, 0x85); // func: DATA_ACK
  dv.setUint32(6, payloadLen, true);

  // 54-byte metadata (offset 10-63)
  let o = FRAME_HEADER_SIZE;
  dv.setUint8(o, m.devId ?? 0);                                         o += 1;
  dv.setUint32(o, m.utc, true);                                         o += 4;
  dv.setFloat64(o, m.longitude, true);                                   o += 8;
  dv.setFloat64(o, m.latitude, true);                                    o += 8;
  dv.setFloat32(o, m.altitude, true);                                    o += 4;
  dv.setFloat32(o, m.height ?? 0, true);                                 o += 4;
  dv.setUint8(o, ((m.gpsStatus & 0x0f) << 4) | (m.sdStatus & 0x0f));   o += 1;
  dv.setUint8(o, m.ampRatio);                                           o += 1;
  dv.setFloat32(o, m.roll, true);                                        o += 4;
  dv.setFloat32(o, m.pitch, true);                                       o += 4;
  dv.setFloat32(o, m.yaw, true);                                         o += 4;
  dv.setUint8(o, m.channelNum);                                          o += 1;
  dv.setUint32(o, m.current, true);                                      o += 4;
  dv.setUint16(o, m.temperature, true);                                  o += 2;
  dv.setUint16(o, m.batteryVolt, true);                                  o += 2;
  dv.setUint8(o, m.sourceMode);                                          o += 1;
  dv.setUint8(o, 0);                                                     o += 1;

  // ADC data (offset 64+), interleaved (sample-major)
  let off = DATA_OFFSET;
  for (let i = 0; i < spc; i++) {
    for (let c = 0; c < ch; c++) {
      dv.setInt32(off, frame.adcRaw[c][i], true);
      off += 4;
    }
  }

  return new Uint8Array(buf);
}

/** Decode a device-compatible frame binary into a LoadedBin. */
export function decodeBin(bytes: Uint8Array): LoadedBin | null {
  if (bytes.length < DATA_OFFSET) return null;
  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);

  // Validate frame header magic
  if (dv.getUint8(0) !== 0x68 || dv.getUint8(1) !== 0x68 ||
      dv.getUint8(2) !== 0xff || dv.getUint8(3) !== 0xff) return null;

  // Parse metadata (offset 10-63)
  let o = FRAME_HEADER_SIZE;
  const devId       = dv.getUint8(o);                  o += 1;
  const utc         = dv.getUint32(o, true);           o += 4;
  const longitude   = dv.getFloat64(o, true);          o += 8;
  const latitude    = dv.getFloat64(o, true);          o += 8;
  const altitude    = dv.getFloat32(o, true);          o += 4;
  const height      = dv.getFloat32(o, true);          o += 4;
  const sdGps       = dv.getUint8(o);                  o += 1;
  const ampRatio    = dv.getUint8(o);                  o += 1;
  const roll        = dv.getFloat32(o, true);          o += 4;
  const pitch       = dv.getFloat32(o, true);          o += 4;
  const yaw         = dv.getFloat32(o, true);          o += 4;
  const channelNum  = dv.getUint8(o);                  o += 1;
  const current     = dv.getUint32(o, true);           o += 4;
  const temperature = dv.getUint16(o, true);           o += 2;
  const batteryVolt = dv.getUint16(o, true);           o += 2;
  const srcMode     = dv.getUint8(o);                  o += 1;

  const gpsStatus = (sdGps >> 4) & 0x0f;
  const sdStatus  = sdGps & 0x0f;

  // Parse ADC data (offset 64+), interleaved
  const dataLen = bytes.length - DATA_OFFSET;
  const totalSamples = dataLen / 4;
  const sampleDepth = channelNum > 0 ? Math.floor(totalSamples / channelNum) : 0;
  if (sampleDepth === 0) return null;

  const gain = AMP_GAIN[ampRatio] ?? 1;
  const ADC_UV_SCALE = (5.0 * 1e6) / 0x7fffffff;
  const scale = ADC_UV_SCALE / gain;
  const adcUV: Float64Array[] = [];

  for (let c = 0; c < channelNum; c++) {
    adcUV.push(new Float64Array(sampleDepth));
  }

  let off = DATA_OFFSET;
  for (let i = 0; i < sampleDepth; i++) {
    for (let c = 0; c < channelNum; c++) {
      adcUV[c][i] = dv.getInt32(off, true) * scale;
      off += 4;
    }
  }

  return {
    adcUV, sampleDepth, channelNum, sampleFreqCode: 0,
    accNum: 0, ampRatio, srcMode, timestamp: 0,
    latitude, longitude, utc, altitude,
    batteryVolt, temperature, current,
    gpsStatus, sdStatus, roll, pitch, yaw,
  };
}

// ── File IO ─────────────────────────────────────────────────────────────────

export async function readFileBytes(path: string): Promise<Uint8Array | null> {
  try {
    const b64 = await FileSystem.readAsStringAsync(path, {
      encoding: FileSystem.EncodingType.Base64,
    });
    return base64ToU8(b64);
  } catch {
    return null;
  }
}

export async function writeFileBytes(path: string, data: Uint8Array): Promise<void> {
  await FileSystem.writeAsStringAsync(path, u8ToBase64(data), {
    encoding: FileSystem.EncodingType.Base64,
  });
}

export async function saveBinFile(path: string, frame: MeasurementFrame): Promise<void> {
  await writeFileBytes(path, encodeBin(frame));
}

export async function loadBinFile(path: string): Promise<LoadedBin | null> {
  const bytes = await readFileBytes(path);
  return bytes ? decodeBin(bytes) : null;
}
