
// ---------------------------------------------------------------------------
// File format constants
// ---------------------------------------------------------------------------

import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper";
import { type MonoFormatFile } from "./types";
import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCurrentTime, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types";

// Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE
export const MONOFORMAT_MAGIC_HEADER = 0x632B7EAF;

// ---------------------------------------------------------------------------
// MonoDisplayParser
// ---------------------------------------------------------------------------

export class MonoDisplayParser {
  parse(buffer: ArrayBuffer | ArrayBufferView): MonoFormatFile {
    const r = new BinaryReader(buffer);

    // File header (12 bytes)
    const magic = r.readUint32();
    if (magic !== MONOFORMAT_MAGIC_HEADER) throw new Error(`Bad magic 0x${magic.toString(16)} - expected 0x632B7EAF`);

    const version = r.readUint32();
    if (version === 0 || version > 1) throw new Error(`Unsupported version ${version}`);

    const numSections = r.readUint16();
    r.readUint16(); // reserved

    const sections: MonoFormatSection[] = [];

    for (let i = 0; i < numSections; i++) {
      const sectionType  = r.readByte();
      const sectionSize  = r.readUint24(); // INCLUDES 4-byte header
      const sectionStart = r.offset;       // start of section DATA
      const dataSize     = sectionSize - 4;

      const sectionEnd = sectionStart + dataSize;
      switch (sectionType) {
        case SectionType.ElementsAlways:
          sections.push(this.#parseElementsSection(r, SectionType.ElementsAlways, false, sectionEnd));
          break;
        case SectionType.ElementsTimespan:
          sections.push(this.#parseElementsSection(r, SectionType.ElementsTimespan, true, sectionEnd));
          break;
        case SectionType.CustomFont:
          sections.push(this.#parseCustomFont(r, dataSize));
          break;
        default:
          // Unknown - skip
          break;
      }

      // Seek past full section data (handles padding, unknown fields, future extensions)
      r.seek(sectionStart + dataSize);
    }

    return { sections };
  }

  #parseFlags(raw: number): MonoFormatSectionFlags {
    return {
      drawFront:   !!(raw & 0x01),
      drawBack:    !!(raw & 0x02),
      clearBuffer: !!(raw & 0x04),
    };
  }

  #parseElementsSection(
    r: BinaryReader,
    type: SectionType.ElementsAlways | SectionType.ElementsTimespan,
    hasTimestamp: boolean,
    sectionEnd: number,
  ): MonoFormatElementsAlways | MonoFormatElementsTimespan {
    const flags       = this.#parseFlags(r.readUint16());
    const numElements = r.readUint16();

    let startTimestamp = 0n, endTimestamp = 0n;
    if (hasTimestamp) {
      startTimestamp = r.readUint64();
      endTimestamp   = r.readUint64();
    }

    const elements: MonoFormatElement[] = [];
    for (let i = 0; i < numElements; i++) {
      // Stop if fewer than 2 bytes remain in section (can't read element type)
      if (r.offset + 2 > sectionEnd) break;
      const el = this.#parseElement(r);
      if (el === null) break; // unknown type - can't determine size, stop
      elements.push(el);
    }

    if (hasTimestamp) {
      return { sectionType: SectionType.ElementsTimespan, flags, startTimestamp, endTimestamp, elements };
    }
    return { sectionType: SectionType.ElementsAlways, flags, elements };
  }

  #parseCustomFont(r: BinaryReader, dataSize: number): MonoFormatCustomFont {
    const actualSize = r.readUint24();
    r.readByte(); // reserved
    const fontData = new Uint8Array(r.readBytes(Math.min(actualSize, dataSize - 4)));
    return { sectionType: SectionType.CustomFont, fontData };
  }

  #parseElement(r: BinaryReader): MonoFormatElement | null {
    const elementStart = r.offset;
    const type = r.readUint16();

    switch (type) {
      case ElementType.Image2D:          return this.#parseImage2D(r, elementStart);
      case ElementType.Animation:        return this.#parseAnimation(r, elementStart);
      case ElementType.HorizontalScroll: return this.#parseHScroll(r, elementStart);
      case ElementType.VerticalScroll:   return this.#parseVScroll(r, elementStart);
      case ElementType.Line:             return this.#parseLine(r);
      case ElementType.ClippedText:      return this.#parseClippedText(r, elementStart);
      case ElementType.HScrollText:      return this.#parseHScrollText(r, elementStart);
      case ElementType.CurrentTime:      return this.#parseCurrentTime(r);
      default:
        // Unknown element type - cannot safely skip without knowing size; stop parsing section elements
        return null;
    }
  }

  #alignTo4(start: number, current: number): void {
    // elements are 32-bit aligned; seek past padding if any
    const end = start + (((current - start) + 3) & ~3);
    // no-op if already aligned (handled by caller via section seek)
  }

  #parseImage2D(r: BinaryReader, start: number): MonoFormatImage2D {
    const xOffset = r.readUint16();
    const yOffset = r.readUint16();
    const width   = r.readUint16();
    const height  = r.readUint16();
    r.readUint16(); // reserved
    // Fixed header: 2(type)+2+2+2+2+2 = 12 bytes
    const packed = r.readBytes(packedSize(width, height));
    const pixels = unpackPixels(new Uint8Array(packed), width, height);
    // Skip padding to 32-bit boundary for the element
    const dataRead = 12 + packed.byteLength;
    r.seek(start + dataRead + pad32(dataRead));
    return { type: ElementType.Image2D, xOffset, yOffset, image: { pixels, width, height } };
  }

  #parseAnimation(r: BinaryReader, start: number): MonoFormatAnimation {
    const xOffset        = r.readUint16();
    const yOffset        = r.readUint16();
    const width          = r.readUint16();
    const height         = r.readUint16();
    const numFrames      = r.readUint16();
    const updateInterval = r.readUint16();
    r.readUint16(); // reserved
    // Fixed header: 2+2+2+2+2+2+2+2 = 16 bytes
    const frameBytes = packedSize(width, height);
    const frames: MonoFormatPixelImage[] = [];
    for (let f = 0; f < numFrames; f++) {
      const packed = r.readBytes(frameBytes);
      frames.push({ pixels: unpackPixels(new Uint8Array(packed), width, height), width, height });
    }
    const dataRead = 16 + numFrames * frameBytes;
    r.seek(start + dataRead + pad32(dataRead));
    return { type: ElementType.Animation, xOffset, yOffset, width, height, updateInterval, frames };
  }

  #parseScrollFlags(raw: number) {
    return {
      endless:         !!(raw & 0x01),
      invertDirection: !!(raw & 0x02),
      padStart:        !!(raw & 0x04),
      padEnd:          !!(raw & 0x08),
    };
  }

  #parseHScroll(r: BinaryReader, start: number): MonoFormatHScroll {
    const xOffset      = r.readUint16();
    const yOffset      = r.readUint16();
    const width        = r.readUint16();
    const height       = r.readUint16();
    const contentWidth = r.readUint16();
    const flagsByte    = r.readByte();
    const scrollSpeed  = r.readByte();
    r.readUint16(); // reserved
    // Fixed header: 2+2+2+2+2+2+1+1+2 = 16 bytes
    const packed = r.readBytes(packedSize(contentWidth, height));
    const pixels = unpackPixels(new Uint8Array(packed), contentWidth, height);
    const dataRead = 16 + packed.byteLength;
    r.seek(start + dataRead + pad32(dataRead));
    return {
      type: ElementType.HorizontalScroll, xOffset, yOffset, width, height, contentWidth,
      scrollSpeed, flags: this.#parseScrollFlags(flagsByte),
      content: { pixels, width: contentWidth, height },
    };
  }

  #parseVScroll(r: BinaryReader, start: number): MonoFormatVScroll {
    const xOffset       = r.readUint16();
    const yOffset       = r.readUint16();
    const width         = r.readUint16();
    const height        = r.readUint16();
    const contentHeight = r.readUint16();
    const flagsByte     = r.readByte();
    const scrollSpeed   = r.readByte();
    r.readUint16(); // reserved
    const packed = r.readBytes(packedSize(width, contentHeight));
    const pixels = unpackPixels(new Uint8Array(packed), width, contentHeight);
    const dataRead = 16 + packed.byteLength;
    r.seek(start + dataRead + pad32(dataRead));
    return {
      type: ElementType.VerticalScroll, xOffset, yOffset, width, height, contentHeight,
      scrollSpeed, flags: this.#parseScrollFlags(flagsByte),
      content: { pixels, width, height: contentHeight },
    };
  }

  #parseLine(r: BinaryReader): MonoFormatLine {
    const xOrigin     = r.readUint16();
    const yOrigin     = r.readUint16();
    const xTarget     = r.readUint16();
    const yTarget     = r.readUint16();
    const lineStyle   = r.readByte();
    const flagsByte   = r.readByte();
    // 2+2+2+2+2+1+1 = 12 bytes, already 32-bit aligned
    return { type: ElementType.Line, xOrigin, yOrigin, xTarget, yTarget, lineStyle, invertPixels: !!(flagsByte & 1) };
  }

  #parseClippedText(r: BinaryReader, start: number): MonoFormatClippedText {
    const xOffset   = r.readUint16();
    const yOffset   = r.readUint16();
    const width     = r.readUint16();
    const height    = r.readUint16();
    const fontIndex = r.readUint16();
    const textLen   = r.readUint16();
    // Fixed header: 2+2+2+2+2+2+2 = 14 bytes
    const text = r.readUtf8(textLen);
    const dataRead = 14 + textLen;
    r.seek(start + dataRead + pad32(dataRead));
    return { type: ElementType.ClippedText, xOffset, yOffset, width, height, fontIndex, text };
  }

  #parseCurrentTime(r: BinaryReader): MonoFormatCurrentTime {
    // Layout after type field (already consumed): 14 bytes, total element = 16 bytes (aligned)
    const xOffset          = r.readUint16();
    const yOffset          = r.readUint16();
    const width            = r.readUint16();
    const height           = r.readUint16();
    const fontIndex        = r.readUint16();
    const utcOffsetMinutes = r.readInt16();
    const flagsByte        = r.readByte();
    r.readByte(); // reserved
    return {
      type: ElementType.CurrentTime,
      xOffset, yOffset, width, height, fontIndex, utcOffsetMinutes,
      flags: {
        clock12h:    !!(flagsByte & 0x01),
        showHours:   !!(flagsByte & 0x02),
        showMinutes: !!(flagsByte & 0x04),
        showSeconds: !!(flagsByte & 0x08),
      },
    };
  }

  #parseHScrollText(r: BinaryReader, start: number): MonoFormatHScrollText {
    const xOffset     = r.readUint16();
    const yOffset     = r.readUint16();
    const width       = r.readUint16();
    const height      = r.readUint16();
    const flagsByte   = r.readByte();
    const scrollSpeed = r.readByte();
    const fontIndex   = r.readUint16();
    const textLen     = r.readUint16(); // PLAN: confirm with spec (assumed, not explicit in diagram)
    // Fixed header: 2+2+2+2+2+1+1+2+2 = 16 bytes
    const text = r.readUtf8(textLen);
    const dataRead = 16 + textLen;
    r.seek(start + dataRead + pad32(dataRead));
    return {
      type: ElementType.HScrollText, xOffset, yOffset, width, height,
      scrollSpeed, fontIndex, flags: this.#parseScrollFlags(flagsByte), text,
    };
  }
}
