// ---------------------------------------------------------------------------
// U8G2 font binary format decoder
// Derived from: https://github.com/olikraus/u8g2/blob/master/csrc/u8g2_font.c
//               https://github.com/olikraus/u8g2/blob/master/tools/font/bdfconv/bdf_rle.c
// ---------------------------------------------------------------------------

const HDR_SIZE = 23;

// ---------------------------------------------------------------------------
// BitReader - LSB-first, matching u8g2_font_decode_get_unsigned_bits exactly:
//   val = *ptr >> bit_pos
//   if (bit_pos + cnt >= 8): val |= *(ptr+1) << (8 - bit_pos); ptr++
//   val &= (1 << cnt) - 1
// ---------------------------------------------------------------------------
class BitReader {
  private ptr: number;
  private bitPos: number;

  constructor(private data: Uint8Array, startByte: number) {
    this.ptr = startByte;
    this.bitPos = 0;
  }

  read(cnt: number): number {
    let val = (this.data[this.ptr]! >> this.bitPos) & 0xFF;
    const newPos = this.bitPos + cnt;
    if (newPos >= 8) {
      this.ptr++;
      val |= (this.data[this.ptr]! << (8 - this.bitPos));
      this.bitPos = newPos - 8;
    } else {
      this.bitPos = newPos;
    }
    return val & ((1 << cnt) - 1);
  }

  // Offset-binary signed: stored as unsigned + (1 << (cnt-1)), so decode = val - (1 << (cnt-1))
  // Matches: v = get_unsigned(cnt); d = 1 << (cnt-1); v -= d;
  readSigned(cnt: number): number {
    return this.read(cnt) - (1 << (cnt - 1));
  }
}

function readU16BE(data: Uint8Array, off: number): number {
  return ((data[off]! << 8) | data[off + 1]!) >>> 0;
}

// ---------------------------------------------------------------------------
// Font header (23 bytes)
// Offsets 17/18, 19/20, 21/22 are relative to end of header (byte 23)
// ---------------------------------------------------------------------------
function parseHeader(font: Uint8Array) {
  return {
    m0: font[2]!,
    m1: font[3]!,
    bitsPerW: font[4]!,
    bitsPerH: font[5]!,
    bitsPerX: font[6]!,
    bitsPerY: font[7]!,
    bitsPerDx: font[8]!,
    ascentA: font[13]! as number,
    // offsets stored as BE uint16, relative to end of 23-byte header
    startUpper: HDR_SIZE + readU16BE(font, 17),
    startLower: HDR_SIZE + readU16BE(font, 19),
    startUnicode: HDR_SIZE + readU16BE(font, 21),
  };
}

// ---------------------------------------------------------------------------
// RLE decoder - matches fd_decode / u8g2_font_decode_glyph in the C source.
//
// The bitstream is a sequence of (zeros-run, ones-run) pairs, repeated.
// Each pair:
//   [m0 bits]  count of zero pixels
//   [m1 bits]  count of one pixels
//   [unary]    repetition: read 1-bits until a 0-stop; reps = (number of 1s) + 1
//
// This matches bg_rle_compress which encodes pairs and then a unary repeat.
// ---------------------------------------------------------------------------
function decodeRLE(
  br: BitReader,
  w: number,
  h: number,
  m0: number,
  m1: number,
): Uint8Array {
  const total = w * h;
  const bits = new Uint8Array(total);
  let filled = 0;

  while (filled < total) {
    const zeros = br.read(m0);
    const ones = br.read(m1);
    let reps = 1;
    while (br.read(1) === 1) reps++;

    for (let r = 0; r < reps && filled < total; r++) {
      for (let p = 0; p < zeros && filled < total; p++) bits[filled++] = 0;
      for (let p = 0; p < ones && filled < total; p++) bits[filled++] = 1;
    }
  }
  return bits;
}

// ---------------------------------------------------------------------------
// Glyph result
// ---------------------------------------------------------------------------
export interface U8G2GlyphData {
  cols: Uint8Array | Uint32Array; // column words (bit 0 = top row), length = w
  w: number;
  h: number;
  dx: number;     // horizontal advance
  yOff: number;     // downward offset from top of cell to top of glyph
}

// ---------------------------------------------------------------------------
// Decode the glyph bitstream at byteOffset (after the record header bytes).
// Field order from C source (u8g2_font.c lines 590-625):
//   glyph_width, glyph_height, x (signed), y (signed), delta_x (signed)
// ---------------------------------------------------------------------------
function decodeGlyphAt(
  font: Uint8Array,
  byteOffset: number,
  hdr: ReturnType<typeof parseHeader>,
): U8G2GlyphData {
  const br = new BitReader(font, byteOffset);

  const w = br.read(hdr.bitsPerW);
  const h = br.read(hdr.bitsPerH);
  /*  x  */ br.readSigned(hdr.bitsPerX); // x bearing - consumed but not needed
  const yB = br.readSigned(hdr.bitsPerY);
  const dx = br.readSigned(hdr.bitsPerDx);

  if (w === 0 || h === 0) {
    // Blank glyph (e.g. space) - advance is stored in dx
    return { cols: new Uint8Array(1), w: 1, h: 0, dx: Math.max(dx, 1), yOff: 0 };
  }

  // Decode row-major horizontal bitmap
  const bits = decodeRLE(br, w, h, hdr.m0, hdr.m1);

  // Convert row-major → column words (bit 0 = top row) for rasterizeText
  // Must be Uint32Array - glyphs can be up to 32px tall; Uint8Array truncates rows 8+
  const cols = new Uint32Array(w);
  for (let row = 0; row < h; row++) {
    for (let col = 0; col < w; col++) {
      if (bits[row * w + col]) cols[col] |= (1 << row);
    }
  }

  // yB: signed distance from baseline upward to bottom of glyph bounding box.
  // ascentA: pixels above baseline for cap-height reference.
  // yOff: distance downward from top of the notional box to top of this glyph.
  const yOff = hdr.ascentA - yB - h;

  return { cols, w, h, dx, yOff };
}

// ---------------------------------------------------------------------------
// Glyph search
//
// Record layout (cp < 0x0100):
//   [1 byte]  encoding
//   [1 byte]  jump = byte distance from START of this record to next record
//   [bitstream ...]
//
// Record layout (cp >= 0x0100):
//   [2 bytes BE] encoding
//   [1 byte]     jump (same semantics)
//   [bitstream ...]
//
// Unicode section is preceded by a lookup table (added in v2.23):
//   pairs of [uint16 BE delta | uint16 BE last-encoding], terminated by last==0xFFFF
//   delta is cumulative byte offset from TABLE START to the glyph sub-block.
// ---------------------------------------------------------------------------
export function u8g2Glyph(cp: number, font: Uint8Array): U8G2GlyphData | null {
  if (font.length < HDR_SIZE) return null;
  const hdr = parseHeader(font);

  try {
    if (cp >= 0x0100) {
      // -- Unicode block ----------------------------------------------------
      const tableBase = hdr.startUnicode;
      if (tableBase + 4 > font.length) return null;

      // Walk the jump table (BE uint16 pairs) to find the right sub-block.
      // delta is offset from tableBase to start of sub-block.
      let glyphStart = tableBase;
      let pos = tableBase;
      while (pos + 4 <= font.length) {
        const delta = readU16BE(font, pos);
        const lastEncoding = readU16BE(font, pos + 2);
        pos += 4;
        if (lastEncoding === 0xFFFF) {
          glyphStart = tableBase + delta;
          break;
        }
        if (lastEncoding >= cp) {
          glyphStart = tableBase + delta;
          break;
        }
      }

      // Scan glyph records in the sub-block
      let recPos = glyphStart;
      while (recPos + 3 <= font.length) {
        const recStart = recPos;
        const encoding = readU16BE(font, recPos);
        const jump = font[recPos + 2]!;
        if (jump === 0) break;
        recPos += 3; // bitstream starts here
        if (encoding === cp) return decodeGlyphAt(font, recPos, hdr);
        recPos = recStart + jump;
      }
      return null;

    } else {
      // -- ASCII block (cp < 0x0100) ----------------------------------------
      // Jump-start from the nearest block offset to avoid scanning from byte 0.
      let recPos: number;
      if (cp >= 0x61) recPos = hdr.startLower;
      else if (cp >= 0x41) recPos = hdr.startUpper;
      else recPos = HDR_SIZE;

      while (recPos + 2 <= font.length) {
        const recStart = recPos;
        const encoding = font[recPos]!;
        const jump = font[recPos + 1]!;
        if (jump === 0) break;
        recPos += 2; // bitstream starts here
        if (encoding === cp) return decodeGlyphAt(font, recPos, hdr);
        recPos = recStart + jump;
      }
      return null;
    }
  } catch {
    return null;
  }
}

// ---------------------------------------------------------------------------
// Text → pixel-image rasteriser
// ---------------------------------------------------------------------------
export function rasterizeText(
  text: string,
  cellHeight: number,
  font: { FONT_DATA: Uint8Array },
): { pixels: Uint8Array; width: number; height: number } {
  const hdr = parseHeader(font.FONT_DATA);
  const glyphs: Array<U8G2GlyphData> = [];

  for (const char of text) {
    const cp = char.codePointAt(0) ?? 0x3F;
    const g = u8g2Glyph(cp, font.FONT_DATA);
    if (g) glyphs.push(g);
  }

  if (glyphs.length === 0) {
    return { pixels: new Uint8Array(cellHeight), width: 1, height: cellHeight };
  }

  // Total width: sum all advances, but last glyph uses its actual pixel width
  const totalW = glyphs.reduce((sum, g, i) =>
    sum + (i < glyphs.length - 1 ? g.dx : g.w), 0);

  const pixels = new Uint8Array(totalW * cellHeight);

  // Top-align the full font (ascent + |descent|) within the cell.
  // header byte 14 is descent of 'g' - stored as a negative signed value.
  const descent = font.FONT_DATA[14]! > 127 ? font.FONT_DATA[14]! - 256 : font.FONT_DATA[14]!;
  const fontH = hdr.ascentA - descent;      // total font height in pixels
  const topPad = 0;
  const baseline = topPad + hdr.ascentA;

  let x = 0;
  for (const g of glyphs) {
    // yOff = ascentA - yBearing - h  (may be negative if glyph taller than 'A')
    const top = baseline - hdr.ascentA + g.yOff;
    for (let col = 0; col < g.w; col++) {
      const colByte = g.cols[col] ?? 0;
      for (let row = 0; row < g.h; row++) {
        if ((colByte >> row) & 1) {
          const py = top + row;
          if (py >= 0 && py < cellHeight) {
            pixels[py * totalW + x + col] = 1;
          }
        }
      }
    }
    x += g.dx;
  }

  return { pixels, width: totalW, height: cellHeight };
}
