
import {
  ElementType,
  SectionType,
  type MonoFormatAnimation,
  type MonoFormatClippedText,
  type MonoFormatCurrentTime,
  type MonoFormatElement,
  type MonoFormatElementsAlways,
  type MonoFormatElementsTimespan,
  type MonoFormatFile,
  type MonoFormatHScroll,
  type MonoFormatHScrollText,
  type MonoFormatLine,
  type MonoFormatPixelImage,
  type MonoFormatVScroll
} from "./types.js";
import { type MonoDisplayDriverOptions } from "./driver";
import {
  rasterizeText, MonoDisplayFont,
  u8g2_font_5x7_tf,
  u8g2_font_HelvetiPixel_tr,
  u8g2_font_spleen12x24_me,
  u8g2_font_smolfont_tf,
  u8g2_font_micropixel_tf,
  u8g2_font_micropixel_tr,
  u8g2_font_NokiaSmallPlain_tf,
  u8g2_font_5x7_mf
} from "./font";
/**
 * Renders a MonoFormatFile onto an HTMLCanvasElement.
 * Uses setInterval at 1000/fps ms per tick. All element state (animation
 * frame counters, scroll positions) is keyed by element index within the file.
 *
 * PLAN: ClippedText and HScrollText currently render via canvas fillText (stub).
 * Replace with U8G2 bitmap font renderer once font loading is implemented.
 * PLAN: Line element uses Bresenham stub; lineStyle field is reserved and ignored.
 */
export class MonoDisplayRenderer {
  private ctx: CanvasRenderingContext2D;
  private opts: Required<MonoDisplayDriverOptions>;

  private tickTimer: number | null = null;
  private tickCount: number = 0;
  // Per-element state: keyed by stable element index
  private animState: Map<number, { frame: number; counter: number }> = new Map();
  private hScrollPos: Map<number, number> = new Map(); // pixel offset (may be fractional)
  private vScrollPos: Map<number, number> = new Map();
  private customFonts: MonoDisplayFont[] = [];
  private textCache: Map<string, MonoFormatPixelImage> = new Map();
  public static readonly builtinFonts: Record<string, MonoDisplayFont> = {
    "NokiaSmallPlain_tf": u8g2_font_NokiaSmallPlain_tf,
    "5x7_mf": u8g2_font_5x7_mf,
    // "micropixel_tf": u8g2_font_micropixel_tf,
    // "micropixel_tr": u8g2_font_micropixel_tr,
    // "smolfont_tf": u8g2_font_smolfont_tf,
    // "spleen12x24_me": u8g2_font_spleen12x24_me,
    // "spleen12x24_me": u8g2_font_spleen12x24_me,
    // "5x7_tf": u8g2_font_5x7_tf,
  };

  constructor(canvas: HTMLCanvasElement, opts: Required<MonoDisplayDriverOptions>) {
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("Cannot get 2D canvas context");
    this.ctx = ctx;
    this.opts = opts;
  }

  stop(): void {
    if (this.tickTimer !== null) {
      clearInterval(this.tickTimer);
      this.tickTimer = null;
    }
    this.tickCount = 0;
    this.animState.clear();
    this.hScrollPos.clear();
    this.vScrollPos.clear();
    this.textCache.clear();
  }

  render(file: MonoFormatFile): void {
    this.stop();

    const { displayWidth: dw, displayHeight: dh, scale: s } = this.opts;
    this.ctx.canvas.width = dw * s;
    this.ctx.canvas.height = dh * s;

    // Extract custom fonts from file sections (0x8000-indexed)
    this.customFonts = [];
    for (const section of file.sections) {
      if (section.sectionType === SectionType.CustomFont) {
        this.customFonts.push(section.fontData);
      }
    }

    const tick = () => {
      this.#renderFile(file);
      this.tickCount++;
    };

    tick(); // immediate first frame
    this.tickTimer = setInterval(tick, 1000 / this.opts.fps) as unknown as number;
  }

  #renderFile(file: MonoFormatFile): void {
    const now = BigInt(Math.floor(this.opts.now().getTime() / 1000));
    let cleared = false;

    let elementId = 0;
    // default off
    this.ctx.fillStyle = this.opts.offColor;
    this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    for (const section of file.sections) {
      if (section.sectionType === SectionType.ElementsTimespan) {
        if (now < section.startTimestamp || now >= section.endTimestamp) {
          // Count elements so IDs stay stable even for skipped sections
          elementId += section.elements.length;
          continue;
        }
      }

      if (section.sectionType !== SectionType.ElementsAlways &&
        section.sectionType !== SectionType.ElementsTimespan) {
        continue;
      }

      const elSection = section as MonoFormatElementsAlways | MonoFormatElementsTimespan;

      if (elSection.flags.clearBuffer && !cleared) {
        this.ctx.fillStyle = this.opts.offColor;
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
        cleared = true;
      }

      for (const el of elSection.elements) {
        this.#renderElement(el, elementId++);
      }
    }
  }

  #renderElement(el: MonoFormatElement, id: number): void {
    switch (el.type) {
      case ElementType.Image2D: this.#blitImage(el.image, el.xOffset, el.yOffset); break;
      case ElementType.Animation: this.#renderAnimation(el, id); break;
      case ElementType.HorizontalScroll: this.#renderHScroll(el, id); break;
      case ElementType.VerticalScroll: this.#renderVScroll(el, id); break;
      case ElementType.Line: this.#renderLine(el); break;
      case ElementType.ClippedText: this.#renderClippedText(el); break;
      case ElementType.HScrollText: this.#renderHScrollText(el, id); break;
      case ElementType.CurrentTime: this.#renderCurrentTime(el); break;
    }
  }

  /** Blit a pixel image onto the canvas at (ox, oy) in display coordinates */
  #blitImage(img: MonoFormatPixelImage, ox: number, oy: number): void {
    const { ctx, opts: { onColor, scale: s } } = this;
    ctx.fillStyle = onColor;
    for (let y = 0; y < img.height; y++) {
      for (let x = 0; x < img.width; x++) {
        if (img.pixels[y * img.width + x]) {
          ctx.fillRect((ox + x) * s, (oy + y) * s, s, s);
        }
      }
    }
  }

  /** Blit a clipped sub-region of a pixel image. srcX is fractional scroll offset. */
  #blitClipped(
    img: MonoFormatPixelImage, ox: number, oy: number,
    viewW: number, viewH: number, srcX: number, srcY: number,
  ): void {
    const { ctx, opts: { onColor, scale: s } } = this;
    ctx.fillStyle = onColor;
    for (let y = 0; y < viewH; y++) {
      for (let x = 0; x < viewW; x++) {
        const sx = Math.floor(srcX) + x;
        const sy = Math.floor(srcY) + y;
        if (sx < 0 || sx >= img.width || sy < 0 || sy >= img.height) continue;
        if (img.pixels[sy * img.width + sx]) {
          ctx.fillRect((ox + x) * s, (oy + y) * s, s, s);
        }
      }
    }
  }

  #renderAnimation(el: MonoFormatAnimation, id: number): void {
    if (el.frames.length === 0) return;
    let state = this.animState.get(id);
    if (!state) { state = { frame: 0, counter: 0 }; this.animState.set(id, state); }

    this.#blitImage(el.frames[state.frame]!, el.xOffset, el.yOffset);

    state.counter++;
    if (state.counter > el.updateInterval) {
      state.counter = 0;
      state.frame = (state.frame + 1) % el.frames.length;
      if (!this.opts.loop && state.frame === 0) state.frame = el.frames.length - 1;
    }
  }

  #renderHScroll(el: MonoFormatHScroll, id: number): void {
    let pos = this.hScrollPos.get(id) ?? 0;
    // Draw visible slice
    this.#blitClipped(el.content, el.xOffset, el.yOffset, el.width, el.height, pos, 0);
    // Advance scroll: (SS+1)/16 pixels per tick
    const speed = (el.scrollSpeed + 1) / 16;
    pos += el.flags.invertDirection ? -speed : speed;
    if (el.flags.endless) {
      if (pos >= el.contentWidth) pos -= el.contentWidth;
      if (pos < 0) pos += el.contentWidth;
    } else {
      pos = Math.max(0, Math.min(pos, el.contentWidth - el.width));
    }
    this.hScrollPos.set(id, pos);
  }

  #renderVScroll(el: MonoFormatVScroll, id: number): void {
    let pos = this.vScrollPos.get(id) ?? 0;
    this.#blitClipped(el.content, el.xOffset, el.yOffset, el.width, el.height, 0, pos);
    const speed = (el.scrollSpeed + 1) / 16;
    pos += el.flags.invertDirection ? -speed : speed;
    if (el.flags.endless) {
      if (pos >= el.contentHeight) pos -= el.contentHeight;
      if (pos < 0) pos += el.contentHeight;
    } else {
      pos = Math.max(0, Math.min(pos, el.contentHeight - el.height));
    }
    this.vScrollPos.set(id, pos);
  }

  #renderLine(el: MonoFormatLine): void {
    // Bresenham's line algorithm
    const { ctx, opts: { onColor, offColor, scale: s } } = this;
    ctx.fillStyle = el.invertPixels ? offColor : onColor;
    let [x0, y0, x1, y1] = [el.xOrigin, el.yOrigin, el.xTarget, el.yTarget];
    const dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    const dy = -Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    let err = dx + dy;
    while (true) {
      ctx.fillRect(x0 * s, y0 * s, s, s);
      if (x0 === x1 && y0 === y1) break;
      const e2 = 2 * err;
      if (e2 >= dy) { err += dy; x0 += sx; }
      if (e2 <= dx) { err += dx; y0 += sy; }
    }
  }

  #getTextImage(text: string, height: number, fontIndex: number): MonoFormatPixelImage {
    const key = `${text}|${fontIndex}|${height}`;
    const cached = this.textCache.get(key);
    if (cached) return cached;
    fontIndex = Math.min(fontIndex, Object.keys(MonoDisplayRenderer.builtinFonts).length - 1);
    const builtinFont: MonoDisplayFont = Object.values(MonoDisplayRenderer.builtinFonts).at(fontIndex) || u8g2_font_NokiaSmallPlain_tf;
    const customFont: MonoDisplayFont | undefined = this.customFonts[fontIndex - 0x8000];
    const selectedFont: MonoDisplayFont = fontIndex >= 0x8000 ? (customFont || builtinFont) : builtinFont;
    const img = rasterizeText(text, height, selectedFont);
    this.textCache.set(key, img);
    return img;
  }

  #renderClippedText(el: MonoFormatClippedText): void {
    const img = this.#getTextImage(el.text, el.height, el.fontIndex);
    this.#blitClipped(img, el.xOffset, el.yOffset, el.width, el.height, 0, 0);
  }

  #renderHScrollText(el: MonoFormatHScrollText, id: number): void {
    const img = this.#getTextImage(el.text, el.height, el.fontIndex);
    let pos = this.hScrollPos.get(id) ?? 0;

    this.#blitClipped(img, el.xOffset, el.yOffset, el.width, el.height, pos, 0);

    const speed = (el.scrollSpeed + 1) / 16;
    pos += el.flags.invertDirection ? -speed : speed;
    if (el.flags.endless) {
      if (pos >= img.width) pos -= img.width;
      if (pos < 0) pos += img.width;
    } else {
      pos = Math.max(0, Math.min(pos, img.width - el.width));
    }
    this.hScrollPos.set(id, pos);
  }

  #renderCurrentTime(el: MonoFormatCurrentTime): void {
    const d = new Date(this.opts.now().getTime() + el.utcOffsetMinutes * 60_000);
    const { clock12h, showHours, showMinutes, showSeconds } = el.flags;

    const parts: string[] = [];
    if (showHours) {
      const h = d.getUTCHours();
      parts.push(String(clock12h ? (h % 12 || 12) : h).padStart(2, '0'));
    }
    if (showMinutes) parts.push(String(d.getUTCMinutes()).padStart(2, '0'));
    if (showSeconds) parts.push(String(d.getUTCSeconds()).padStart(2, '0'));

    const text = parts.join(':');
    if (!text) return;

    const img = this.#getTextImage(text, el.height, el.fontIndex);
    this.#blitClipped(img, el.xOffset, el.yOffset, el.width, el.height, 0, 0);
  }
}
