// ---------------------------------------------------------------------------
// MonoDisplayFile - JSON descriptor → binary .bin builder
// ---------------------------------------------------------------------------

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

/**
 * Encodes a MonoDisplayFileDescriptor to a valid .bin ArrayBuffer.
 *
 * elements_always -> Section type 1 (ElementsAlways).
 *   Pass a DrawElement[] or { flags, elements }.
 *   Default flags: drawFront=true, drawBack=false, clearBuffer=false.
 *
 * PLAN: encode ElementsTimespan sections, CustomFont sections.
 */
export class MonoDisplayFile implements MonoFormatFile {
  constructor(public sections: MonoFormatSection[], public version: number = 1) {

  }

  toBuffer(): Uint8Array<ArrayBufferLike> {
    const sections: Uint8Array[] = [];

    for (const section of this.sections) {
      switch (section.sectionType) {
        case SectionType.ElementsAlways:
        {
          sections.push(this.#encodeElementsAlwaysSection(section));
          break;
        }
        case SectionType.ElementsTimespan:
        {
          sections.push(this.#encodeElementsTimespanSection(section));
          break;
        }
        case SectionType.CustomFont: {
          sections.push(this.#encodeCustomFont(section));
          break;
        }
      }
    }


    // File header (12 bytes)
    const hdrBuf  = new ArrayBuffer(12);
    const hdrView = new DataView(hdrBuf);
    hdrView.setUint32(0, MONOFORMAT_MAGIC_HEADER, true);
    hdrView.setUint32(4, this.version,    true); // version
    hdrView.setUint16(8, sections.length, true);
    hdrView.setUint16(10, 0,    true); // reserved

    return this.#concat(new Uint8Array(hdrBuf), ...sections);
  }

  // --- section encoders ---

  #encodeElementsAlwaysSection(
    section: MonoFormatElementsAlways,
  ): Uint8Array {
    const flagBits =
      (section.flags.drawFront  ?? true  ? 0x01 : 0) |
      (section.flags.drawBack   ?? false ? 0x02 : 0) |
      (section.flags.clearBuffer ?? false ? 0x04 : 0);

    const encodedEls = section.elements.map(el => this.#encodeElement(el));
    const sectionDataSize = 4 + encodedEls.reduce((s, e) => s + e.byteLength, 0);
    // sectionSize (in header) = 4 (header) + sectionDataSize
    const sectionSize = 4 + sectionDataSize;

    const hdr = new Uint8Array(4 + 4); // section header (4) + section sub-header (4)
    const v   = new DataView(hdr.buffer);
    v.setUint8(0, section.sectionType);
    v.setUint8(1,  sectionSize        & 0xFF);
    v.setUint8(2, (sectionSize >> 8)  & 0xFF);
    v.setUint8(3, (sectionSize >> 16) & 0xFF);
    v.setUint16(4, flagBits,        true); // flags
    v.setUint16(6, section.elements.length, true); // numElements

    return this.#concat(hdr, ...encodedEls);
  }

  #encodeElementsTimespanSection(
    section: MonoFormatElementsTimespan,
  ): Uint8Array {
    const flagBits =
      (section.flags.drawFront  ?? true  ? 0x01 : 0) |
      (section.flags.drawBack   ?? false ? 0x02 : 0) |
      (section.flags.clearBuffer ?? false ? 0x04 : 0);

    const encodedEls = section.elements.map(el => this.#encodeElement(el));
    // sub-header: flags(2) + numElements(2) + startTimestamp(8) + endTimestamp(8) = 20 bytes
    const sectionDataSize = 20 + encodedEls.reduce((s, e) => s + e.byteLength, 0);
    // sectionSize (in header) = 4 (header) + sectionDataSize
    const sectionSize = 4 + sectionDataSize;

    const hdr = new Uint8Array(4 + 20); // section header (4) + sub-header (20)
    const v   = new DataView(hdr.buffer);
    v.setUint8(0, section.sectionType);
    v.setUint8(1,  sectionSize        & 0xFF);
    v.setUint8(2, (sectionSize >> 8)  & 0xFF);
    v.setUint8(3, (sectionSize >> 16) & 0xFF);
    v.setUint16(4, flagBits,                   true); // flags
    v.setUint16(6, section.elements.length,    true); // numElements

    const start = section.startTimestamp ?? 0n;
    const end   = section.endTimestamp   ?? 0n;
    v.setUint32( 8, Number(start & 0xFFFFFFFFn),        true);
    v.setUint32(12, Number((start >> 32n) & 0xFFFFFFFFn), true);
    v.setUint32(16, Number(end   & 0xFFFFFFFFn),        true);
    v.setUint32(20, Number((end   >> 32n) & 0xFFFFFFFFn), true);

    return this.#concat(hdr, ...encodedEls);
  }

  #encodeCustomFont(
    section: MonoFormatCustomFont,
  ): Uint8Array {

    const sectionSize = 4 + section.fontData.length;
    const hdr = new Uint8Array(4 + 4); // section header (4) + section sub-header (4)
    const v   = new DataView(hdr.buffer);
    v.setUint8(0, section.sectionType);
    v.setUint8(1,  sectionSize        & 0xFF);
    v.setUint8(2, (sectionSize >> 8)  & 0xFF);
    v.setUint8(3, (sectionSize >> 16) & 0xFF);
    v.setUint8(4,  section.fontData.length        & 0xFF);
    v.setUint8(5, (section.fontData.length >> 8)  & 0xFF);
    v.setUint8(6, (section.fontData.length >> 16) & 0xFF);
    v.setUint8(7, 0);

    return this.#concat(hdr, section.fontData);
  }

  // --- element encoders ---

  #encodeElement(el: MonoFormatElement): Uint8Array {
    switch (el.type) {
      case ElementType.Image2D:          return this.#encodeImage2D(el);
      case ElementType.Animation:        return this.#encodeAnimation(el);
      case ElementType.HorizontalScroll: return this.#encodeHScroll(el);
      case ElementType.VerticalScroll:   return this.#encodeVScroll(el);
      case ElementType.Line:             return this.#encodeLine(el);
      case ElementType.ClippedText:      return this.#encodeClippedText(el);
      case ElementType.HScrollText:      return this.#encodeHScrollText(el);
      case ElementType.CurrentTime:      return this.#encodeCurrentTime(el);
      default: return new Uint8Array();
    }
  }

  #encodeImage2D(el: MonoFormatImage2D): Uint8Array {
    const packed  = packPixels(el.image.pixels, el.image.width, el.image.height);
    const fixed   = 12; // bytes before pixel data
    const rawSize = fixed + packed.byteLength;
    const total   = rawSize + pad32(rawSize);
    const out = new Uint8Array(total);
    const v   = new DataView(out.buffer);
    v.setUint16(0, ElementType.Image2D, true);
    v.setUint16(2, el.xOffset ?? 0,     true);
    v.setUint16(4, el.yOffset ?? 0,     true);
    v.setUint16(6, el.image.width,      true);
    v.setUint16(8, el.image.height,     true);
    v.setUint16(10, 0,                  true); // reserved
    out.set(packed, 12);
    return out;
  }

  #encodeAnimation(el: MonoFormatAnimation): Uint8Array {
    const frameBytes = packedSize(el.width, el.height);
    const fixed   = 16;
    const rawSize = fixed + el.frames.length * frameBytes;
    const total   = rawSize + pad32(rawSize);
    const out = new Uint8Array(total);
    const v   = new DataView(out.buffer);
    v.setUint16(0,  ElementType.Animation,    true);
    v.setUint16(2,  el.xOffset ?? 0,          true);
    v.setUint16(4,  el.yOffset ?? 0,          true);
    v.setUint16(6,  el.width,                 true);
    v.setUint16(8,  el.height,                true);
    v.setUint16(10, el.frames.length,         true);
    v.setUint16(12, el.updateInterval ?? 0,   true);
    v.setUint16(14, 0,                        true); // reserved
    let off = 16;
    for (const f of el.frames) {
      out.set(packPixels(f.pixels, el.width, el.height), off);
      off += frameBytes;
    }
    return out;
  }

  #encodeScrollFlags(f: MonoFormatScrollElementFlags | undefined): number {
    return (
      (f?.endless         ? 0x01 : 0) |
      (f?.invertDirection ? 0x02 : 0) |
      (f?.padStart        ? 0x04 : 0) |
      (f?.padEnd          ? 0x08 : 0)
    );
  }

  #encodeHScroll(el: MonoFormatHScroll): Uint8Array {
    const packed  = packPixels(el.content.pixels, el.contentWidth, el.height);
    const fixed   = 16;
    const rawSize = fixed + packed.byteLength;
    const total   = rawSize + pad32(rawSize);
    const out = new Uint8Array(total);
    const v   = new DataView(out.buffer);
    v.setUint16(0,  ElementType.HorizontalScroll, true);
    v.setUint16(2,  el.xOffset ?? 0,              true);
    v.setUint16(4,  el.yOffset ?? 0,              true);
    v.setUint16(6,  el.width,                     true);
    v.setUint16(8,  el.height,                    true);
    v.setUint16(10, el.contentWidth,              true);
    v.setUint8( 12, this.#encodeScrollFlags(el.flags));
    v.setUint8( 13, el.scrollSpeed ?? 0);
    v.setUint16(14, 0,                            true); // reserved
    out.set(packed, 16);
    return out;
  }

  #encodeVScroll(el: MonoFormatVScroll): Uint8Array {
    const packed  = packPixels(el.content.pixels, el.width, el.contentHeight);
    const fixed   = 16;
    const rawSize = fixed + packed.byteLength;
    const total   = rawSize + pad32(rawSize);
    const out = new Uint8Array(total);
    const v   = new DataView(out.buffer);
    v.setUint16(0,  ElementType.VerticalScroll, true);
    v.setUint16(2,  el.xOffset ?? 0,            true);
    v.setUint16(4,  el.yOffset ?? 0,            true);
    v.setUint16(6,  el.width,                   true);
    v.setUint16(8,  el.height,                  true);
    v.setUint16(10, el.contentHeight,           true);
    v.setUint8( 12, this.#encodeScrollFlags(el.flags));
    v.setUint8( 13, el.scrollSpeed ?? 0);
    v.setUint16(14, 0,                          true);
    out.set(packed, 16);
    return out;
  }

  #encodeLine(el: MonoFormatLine): Uint8Array {
    // 12 bytes, already 32-bit aligned
    const out = new Uint8Array(12);
    const v   = new DataView(out.buffer);
    v.setUint16(0, ElementType.Line,   true);
    v.setUint16(2, el.xOrigin,         true);
    v.setUint16(4, el.yOrigin,         true);
    v.setUint16(6, el.xTarget,         true);
    v.setUint16(8, el.yTarget,         true);
    v.setUint8(10, el.lineStyle ?? 0);
    v.setUint8(11, el.invertPixels ? 1 : 0);
    return out;
  }

  #encodeClippedText(el: MonoFormatClippedText): Uint8Array {
    const enc     = new TextEncoder().encode(el.text);
    const fixed   = 14; // 2+2+2+2+2+2+2
    const rawSize = fixed + enc.byteLength;
    const total   = rawSize + pad32(rawSize);
    const out = new Uint8Array(total);
    const v   = new DataView(out.buffer);
    v.setUint16(0,  ElementType.ClippedText, true);
    v.setUint16(2,  el.xOffset ?? 0,         true);
    v.setUint16(4,  el.yOffset ?? 0,         true);
    v.setUint16(6,  el.width,                true);
    v.setUint16(8,  el.height,               true);
    v.setUint16(10, el.fontIndex ?? 0,       true);
    v.setUint16(12, enc.byteLength,          true);
    out.set(enc, 14);
    return out;
  }

  #encodeHScrollText(el: MonoFormatHScrollText): Uint8Array {
    const enc     = new TextEncoder().encode(el.text);
    const fixed   = 16; // 2+2+2+2+2+1+1+2+2
    const rawSize = fixed + enc.byteLength;
    const total   = rawSize + pad32(rawSize);
    const out = new Uint8Array(total);
    const v   = new DataView(out.buffer);
    v.setUint16(0,  ElementType.HScrollText, true);
    v.setUint16(2,  el.xOffset ?? 0,         true);
    v.setUint16(4,  el.yOffset ?? 0,         true);
    v.setUint16(6,  el.width,                true);
    v.setUint16(8,  el.height,               true);
    v.setUint8( 10, this.#encodeScrollFlags(el.flags));
    v.setUint8( 11, el.scrollSpeed ?? 0);
    v.setUint16(12, el.fontIndex ?? 0,       true);
    v.setUint16(14, enc.byteLength,          true); // PLAN: confirm with spec
    out.set(enc, 16);
    return out;
  }

  #encodeCurrentTime(el: MonoFormatCurrentTime): Uint8Array {
    // Fixed 16 bytes, already 32-bit aligned
    const out = new Uint8Array(16);
    const v   = new DataView(out.buffer);
    const f   = el.flags;
    const flagsByte =
      (f?.clock12h    ? 0x01 : 0) |
      (f?.showHours   ?? true  ? 0x02 : 0) |
      (f?.showMinutes ?? true  ? 0x04 : 0) |
      (f?.showSeconds ? 0x08 : 0);
    v.setUint16( 0, ElementType.CurrentTime,          true);
    v.setUint16( 2, el.xOffset ?? 0,                  true);
    v.setUint16( 4, el.yOffset ?? 0,                  true);
    v.setUint16( 6, el.width,                         true);
    v.setUint16( 8, el.height,                        true);
    v.setUint16(10, el.fontIndex ?? 0,                true);
    v.setInt16( 12, el.utcOffsetMinutes ?? 0,         true);
    v.setUint8( 14, flagsByte);
    v.setUint8( 15, 0); // reserved
    return out;
  }

  // --- utilities ---

  #concat(...parts: Uint8Array[]): Uint8Array {
    const total = parts.reduce((s, p) => s + p.byteLength, 0);
    const out   = new Uint8Array(total);
    let off = 0;
    for (const p of parts) { out.set(p, off); off += p.byteLength; }
    return out;
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

export async function loadBinFile(url: string): Promise<ArrayBuffer> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`loadBinFile: HTTP ${res.status} for ${url}`);
  return res.arrayBuffer();
}

/**
 * Build a single-element-always .bin buffer containing one Image2D element.
 * Convenience for tests; for production use MonoDisplayFile directly.
 */
export function buildBinBuffer(el: MonoFormatImage2D): Uint8Array {
  return new MonoDisplayFile([{
    sectionType: SectionType.ElementsAlways,
    elements: [el],
    flags: {
      drawFront: true,
      drawBack: true,
      clearBuffer: true,
    },
  }]).toBuffer();
}
