/* HDLC framing helpers for AX.25 starter
 * - encodeHDLC(payload, { includeFcs }) -> Uint8Array
 * - HDLCDeframer: incremental push-based deframer producing unescaped frames
 * - framesFromAsyncIterable: async generator over framed chunks
 */

import { Byte } from './frame.types';

const FLAG = 0x7e;
const ESC = 0x7d;
const ESC_XOR = 0x20;

export const escapeBuffer: (buf: Uint8Array) => Uint8Array = (buf) => {
  const out: number[] = [];
  for (const b of buf) {
    if (b === FLAG || b === ESC) {
      out.push(ESC, b ^ ESC_XOR);
    } else {
      out.push(b);
    }
  }
  return Uint8Array.from(out);
};

export const unescapeBuffer: (buf: Uint8Array) => Uint8Array = (buf) => {
  const out: number[] = [];
  for (let i = 0; i < buf.length; i++) {
    const b = buf[i];
    if (b === ESC && i + 1 < buf.length) {
      out.push(buf[i + 1] ^ ESC_XOR);
      i++;
    } else {
      out.push(b);
    }
  }
  return Uint8Array.from(out);
};

export const encodeHDLC: (payload: Uint8Array, options?: { includeFcs?: boolean }) => Uint8Array = (payload, options) => {
  // simple framing: FLAG (0x7E) + escaped payload (+ FCS if requested) + FLAG
  const includeFcs = options?.includeFcs ?? false;
  const bodySource = includeFcs ? appendFcsLE(payload) : payload;
  const body = escapeBuffer(bodySource);
  const out = new Uint8Array(1 + body.length + 1);
  out[0] = FLAG;
  out.set(body, 1);
  out[out.length - 1] = FLAG;
  return out;
};

// CRC / FCS helpers (CRC-16-CCITT)
export const crc16Ccitt: (buf: Uint8Array, initial?: number) => number = (buf, initial = 0xffff) => {
  let crc = initial & 0xffff;
  for (let i = 0; i < buf.length; i++) {
    crc ^= (buf[i] << 8) & 0xffff;
    for (let j = 0; j < 8; j++) {
      if ((crc & 0x8000) !== 0) crc = ((crc << 1) ^ 0x1021) & 0xffff;
      else crc = (crc << 1) & 0xffff;
    }
  }
  return crc & 0xffff;
};

export const verifyAndStripFcs: (buf: Uint8Array) => { ok: boolean; payload?: Uint8Array; fcs?: number } = (buf) => {
  if (buf.length < 2) return { ok: false };
  const fcs = buf[buf.length - 2] | (buf[buf.length - 1] << 8);
  const payload = buf.slice(0, buf.length - 2);
  const crc = crc16Ccitt(payload);
  // AX.25 transmits inverted CRC (FCS = CRC ^ 0xFFFF)
  if ((crc ^ 0xffff) === fcs) return { ok: true, payload, fcs };
  return { ok: false };
};

export const computeFcs: (buf: Uint8Array) => number = (buf) => {
  const crc = crc16Ccitt(buf);
  return crc ^ 0xffff;
};

export const appendFcsLE: (buf: Uint8Array) => Uint8Array = (buf) => {
  const fcs = computeFcs(buf);
  const out = new Uint8Array(buf.length + 2);
  out.set(buf, 0);
  out[buf.length] = fcs & 0xff; // low byte first
  out[buf.length + 1] = (fcs >> 8) & 0xff;
  return out;
};

export const bitStuffBits: (bits: string) => string = (bits) => {
  let out = '';
  let ones = 0;
  for (const b of bits) {
    out += b;
    if (b === '1') {
      ones++;
      if (ones === 5) {
        out += '0';
        ones = 0;
      }
    } else {
      ones = 0;
    }
  }
  return out;
};

export const bitUnstuffBits: (bits: string) => string = (bits) => {
  let out = '';
  let ones = 0;
  for (let i = 0; i < bits.length; i++) {
    const b = bits[i];
    out += b;
    if (b === '1') {
      ones++;
      if (ones === 5) {
        // next bit should be stuffed '0' - skip it
        i++;
        ones = 0;
      }
    } else {
      ones = 0;
    }
  }
  return out;
};

export class HDLCDeframer {
  private buf: number[] = [];
  private verifyFcs: boolean;

  constructor(options?: { verifyFcs?: boolean }) {
    this.verifyFcs = options?.verifyFcs ?? true;
  }

  push(chunk: Uint8Array): Uint8Array[] {
    // returns zero or more complete raw frame payloads (unescaped, FCS stripped if verifyFcs)
    const frames: Uint8Array[] = [];
    for (const b of chunk) this.buf.push(b);

    let start = -1;
    for (let i = 0; i < this.buf.length; i++) {
      if (this.buf[i] === FLAG) {
        if (start === -1) {
          start = i;
        } else {
          // frame between start and i (exclusive)
          const body = this.buf.slice(start + 1, i);
          if (body.length > 0) {
            const unescaped = unescapeBuffer(Uint8Array.from(body));
            if (this.verifyFcs) {
              const res = verifyAndStripFcs(unescaped);
              if (res.ok && res.payload) frames.push(res.payload);
            } else {
              frames.push(unescaped);
            }
          }
          start = i; // new potential start
        }
      }
    }

    // drop consumed prefix up to last start (keep trailing partial)
    if (start > 0) {
      this.buf = this.buf.slice(start);
    }

    return frames;
  }
}

export const framesFromAsyncIterable = async function* (source: AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
  const def = new HDLCDeframer();
  for await (const chunk of source) {
    const frames = def.push(chunk);
    for (const f of frames) yield f;
  }
};
