/**
 * TEMF v1 binary format
 *
 * Header (64 bytes):
 *   [0-3]   u8[4]  MAGIC = "TEMF" (0x54 0x45 0x4D 0x46)
 *   [4]     u8     version = 0x01
 *   [5]     u8     channelNum
 *   [6-7]   u16 LE sampleDepth
 *   [8]     u8     sampleFreqCode
 *   [9-10]  u16 LE accNum
 *   [11]    u8     ampRatio
 *   [12]    u8     srcMode
 *   [13-20] f64 LE timestamp (ms, local)
 *   [21-28] f64 LE latitude  (degrees WGS-84)
 *   [29-36] f64 LE longitude (degrees WGS-84)
 *   [37-40] u32 LE utc       (seconds since Unix epoch)
 *   [41-44] f32 LE altitude  (meters)
 *   [45-46] u16 LE batteryVolt (raw ADC)
 *   [47-48] i16 LE temperature (raw ADC)
 *   [49-50] u16 LE current    (raw ADC)
 *   [51]    u8     gpsStatus
 *   [52]    u8     sdStatus
 *   [53-54] i16 LE roll  (degrees × 100)
 *   [55-56] i16 LE pitch (degrees × 100)
 *   [57-58] i16 LE yaw   (degrees × 10)
 *   [59-63] u8[5]  reserved (zero)
 *
 * Data (after header):
 *   int32 LE, row-major: channelNum rows × sampleDepth columns
 *   Values are raw ADC counts; calibrated μV = raw / accNum / AMP_GAIN[ampRatio]
 */

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

const MAGIC = [0x54, 0x45, 0x4d, 0x46] as const; // "TEMF"
const VERSION = 0x01;
export const HEADER_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 TEMF v1 bytes. No IO. */
export function encodeBin(frame: MeasurementFrame): Uint8Array {
  const m   = frame.meta;
  const ch  = frame.adcRaw.length;
  const spc = frame.adcRaw[0]?.length ?? 0;
  const buf = new ArrayBuffer(HEADER_SIZE + ch * spc * 4);
  const dv  = new DataView(buf);

  MAGIC.forEach((b, i) => dv.setUint8(i, b));
  dv.setUint8(4,  VERSION);
  dv.setUint8(5,  ch);
  dv.setUint16(6, spc,              true);
  dv.setUint8(8,  frame.sampleFreqCode);
  dv.setUint16(9, frame.accNum,     true);
  dv.setUint8(11, m.ampRatio);
  dv.setUint8(12, m.sourceMode);
  dv.setFloat64(13, frame.timestamp, true);
  dv.setFloat64(21, m.latitude,      true);
  dv.setFloat64(29, m.longitude,     true);
  dv.setUint32(37,  m.utc,           true);
  dv.setFloat32(41, m.altitude,      true);
  dv.setUint16(45,  m.batteryVolt,   true);
  dv.setInt16(47,   m.temperature,   true);
  dv.setUint16(49,  m.current,       true);
  dv.setUint8(51,   m.gpsStatus);
  dv.setUint8(52,   m.sdStatus);
  dv.setInt16(53,   Math.round(m.roll  * 100), true);
  dv.setInt16(55,   Math.round(m.pitch * 100), true);
  dv.setInt16(57,   Math.round(m.yaw   * 10),  true);
  // [59-63] reserved — zero by default

  let off = HEADER_SIZE;
  for (const c of frame.adcRaw) {
    for (let i = 0; i < c.length; i++) { dv.setInt32(off, c[i], true); off += 4; }
  }
  return new Uint8Array(buf);
}

/** Decode TEMF v1 bytes into a LoadedBin. Returns null if invalid. No IO. */
export function decodeBin(bytes: Uint8Array): LoadedBin | null {
  if (bytes.length < HEADER_SIZE) return null;
  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);

  if (MAGIC.some((b, i) => dv.getUint8(i) !== b)) return null;
  if (dv.getUint8(4) !== VERSION) return null;

  const channelNum     = dv.getUint8(5);
  const sampleDepth    = dv.getUint16(6,  true);
  const sampleFreqCode = dv.getUint8(8);
  const accNum         = dv.getUint16(9,  true);
  const ampRatio       = dv.getUint8(11);
  const srcMode        = dv.getUint8(12);
  const timestamp      = dv.getFloat64(13, true);
  const latitude       = dv.getFloat64(21, true);
  const longitude      = dv.getFloat64(29, true);
  const utc            = dv.getUint32(37, true);
  const altitude       = dv.getFloat32(41, true);
  const batteryVolt    = dv.getUint16(45, true);
  const temperature    = dv.getInt16(47,  true);
  const current        = dv.getUint16(49, true);
  const gpsStatus      = dv.getUint8(51);
  const sdStatus       = dv.getUint8(52);
  const roll           = dv.getInt16(53,  true) / 100;
  const pitch          = dv.getInt16(55,  true) / 100;
  const yaw            = dv.getInt16(57,  true) / 10;

  const expected = HEADER_SIZE + channelNum * sampleDepth * 4;
  if (bytes.length < expected) 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[] = [];
  let off = HEADER_SIZE;
  for (let ch = 0; ch < channelNum; ch++) {
    const channel = new Float64Array(sampleDepth);
    for (let i = 0; i < sampleDepth; i++) {
      channel[i] = dv.getInt32(off, true) * scale;
      off += 4;
    }
    adcUV.push(channel);
  }

  return {
    adcUV, sampleDepth, channelNum, sampleFreqCode,
    accNum, ampRatio, srcMode, timestamp,
    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;
}
