/**
 * Esp32C3Simulator — Browser-side ESP32-C3 emulator.
 *
 * Wraps RiscVCore (RV32IMC) with:
 * - ESP32-C3 memory map: Flash IROM/DROM @ 0x42000000/0x3C000000,
 *   DRAM @ 0x3FC80000, IRAM @ 0x4037C000
 * - UART0 MMIO @ 0x60000000 (serial I/O)
 * - GPIO MMIO @ 0x60004000 (pin output via OUT/W1TS/W1TC registers)
 * - 160 MHz clock, requestAnimationFrame execution loop
 * - Same public interface as AVRSimulator / RiscVSimulator
 */

import { RiscVCore } from './RiscVCore';
import type { PinManager } from './PinManager';
import { hexToUint8Array } from '../utils/hexParser';
import { parseMergedFlashImage } from '../utils/esp32ImageParser';

// ── ESP32-C3 Memory Map ──────────────────────────────────────────────────────
const IROM_BASE = 0x42000000;   // Flash instruction region (mapped via MMU)
const DROM_BASE = 0x3C000000;   // Flash data region (read-only alias of same flash)
const DRAM_BASE = 0x3FC80000;   // Data RAM
const IRAM_BASE = 0x4037C000;   // Instruction RAM

const IROM_SIZE = 4 * 1024 * 1024;   // 4 MB flash buffer
const DRAM_SIZE = 384 * 1024;         // 384 KB DRAM
const IRAM_SIZE = 384 * 1024;         // 384 KB IRAM

// ── UART0 @ 0x60000000 ──────────────────────────────────────────────────────
const UART0_BASE   = 0x60000000;
const UART0_SIZE   = 0x400;
const UART0_FIFO   = 0x00;   // write TX byte / read RX byte
const UART0_STATUS = 0x1C;   // TXFIFO_CNT in bits [19:16] (0 = empty = ready)

// ── GPIO @ 0x60004000 ───────────────────────────────────────────────────────
const GPIO_BASE   = 0x60004000;
const GPIO_SIZE   = 0x200;
const GPIO_OUT    = 0x04;   // GPIO_OUT_REG   — output value (read/write)
const GPIO_W1TS   = 0x08;   // GPIO_OUT_W1TS  — set bits (write-only)
const GPIO_W1TC   = 0x0C;   // GPIO_OUT_W1TC  — clear bits (write-only)
const GPIO_IN     = 0x3C;   // GPIO_IN_REG    — input value (read-only)
const GPIO_ENABLE = 0x20;   // GPIO_ENABLE_REG

// ── SYSTIMER @ 0x60023000 ────────────────────────────────────────────────────
// The SYSTIMER runs at 16 MHz (CPU_HZ / 10).  FreeRTOS programs TARGET0 to
// fire every 1 ms (16 000 SYSTIMER ticks = 160 000 CPU cycles) and routes the
// alarm interrupt to CPU interrupt 1 via the interrupt matrix.
const SYSTIMER_BASE = 0x60023000;
const SYSTIMER_SIZE = 0x100;
// Register offsets (ESP32-C3 TRM)
const ST_INT_ENA       = 0x04;  // TARGET0/1/2 enable bits
const ST_INT_RAW       = 0x08;  // raw interrupt status
const ST_INT_CLR       = 0x0C;  // write-1-to-clear
const ST_INT_ST        = 0x10;  // masked status (RAW & ENA)
const ST_UNIT0_OP      = 0x14;  // write bit30 to snapshot counter
const ST_UNIT0_VAL_LO  = 0x54;  // snapshot value low 32 bits
const ST_UNIT0_VAL_HI  = 0x58;  // snapshot value high 32 bits

// ── SPI Flash Controllers ────────────────────────────────────────────────────
// SPI1 @ 0x60002000 — direct flash controller (boot-time flash access)
// SPI0 @ 0x60003000 — cache SPI controller (transparent flash cache)
// SPI_MEM_CMD_REG offset 0x00 bits [17–31] are "write 1 to start, HW clears when done".
const SPI1_BASE  = 0x60002000;
const SPI0_BASE  = 0x60003000;
const SPI_SIZE   = 0x200;
const SPI_CMD    = 0x00;   // SPI_MEM_CMD_REG — command trigger / status

// ── EXTMEM (cache controller) @ 0x600C4000 ──────────────────────────────────
// Manages ICache enable, invalidation, preload, and MMU configuration.
const EXTMEM_BASE = 0x600C4000;
const EXTMEM_SIZE = 0x1000;
// Key register offsets with "done" status bits that must read as 1:
const EXTMEM_ICACHE_SYNC_CTRL   = 0x28;  // bit1=SYNC_DONE
const EXTMEM_ICACHE_PRELOAD_CTRL = 0x34; // bit1=PRELOAD_DONE
const EXTMEM_ICACHE_AUTOLOAD_CTRL = 0x40; // bit3=AUTOLOAD_DONE
const EXTMEM_ICACHE_LOCK_CTRL   = 0x1C;  // bit2=LOCK_DONE

// ── Interrupt Matrix @ 0x600C2000 ─────────────────────────────────────────
// The ESP32-C3 interrupt matrix routes 62 peripheral interrupt sources to
// up to 31 CPU interrupt lines (line 0 = disabled).
const INTMATRIX_BASE = 0x600C2000;
const INTMATRIX_SIZE = 0x800;
// Register layout (offsets from INTMATRIX_BASE):
//   0x000-0x0F4 : 62 SOURCE_MAP registers (5-bit mapping: source → CPU line)
//   0x104       : INTR_STATUS (pending lines bitmap, read-only)
//   0x108       : CLOCK_GATE (clock gating enable)
//   0x118-0x194 : PRIORITY for lines 1–31 (4-bit each)
//   0x198       : THRESH (interrupt threshold, 4-bit)

// ── SYSTEM/CLK registers @ 0x600C0000 ─────────────────────────────────────
// Contains FROM_CPU_INTR software interrupt triggers and misc system config.
const SYSCON_BASE = 0x600C0000;
const SYSCON_SIZE = 0x800;

// ── Interrupt source numbers (from ESP-IDF soc/esp32c3/interrupts.h) ─────
const ETS_SYSTIMER_TARGET0_SRC = 37;
const ETS_FROM_CPU_INTR0_SRC   = 28;  // FROM_CPU_INTR0..3 → sources 28-31

// ── ESP32-C3 ROM stub @ 0x40000000 ──────────────────────────────────────────
// ROM lives at 0x40000000-0x4001FFFF.  Without a ROM image every ROM call
// fetches 0x0000 → CPU executes reserved C.ADDI4SPN and loops at 0x0.
// Stub: return C.JR ra (0x8082) so any ROM call immediately returns.
//   Little-endian: even byte = 0x82, odd byte = 0x80.
const ROM_BASE  = 0x40000000;
const ROM_SIZE  = 0x60000;   // 0x40000000-0x4005FFFF (first ROM + margin)
const ROM2_BASE = 0x40800000;
const ROM2_SIZE = 0x20000;   // 0x40800000-0x4081FFFF (second ROM region)

// ── Clock ───────────────────────────────────────────────────────────────────
const CPU_HZ = 160_000_000;
const CYCLES_PER_FRAME = Math.round(CPU_HZ / 60);
/** CPU cycles per FreeRTOS tick (1 ms at 160 MHz). */
const CYCLES_PER_TICK  = 160_000;

export class Esp32C3Simulator {
  private core: RiscVCore;
  private flash: Uint8Array;
  private dram: Uint8Array;
  private iram: Uint8Array;
  private running = false;
  private animFrameId = 0;
  private rxFifo: number[] = [];
  private gpioOut = 0;
  private gpioIn  = 0;

  // SYSTIMER emulation state
  private _stIntEna = 0;  // ST_INT_ENA register
  private _stIntRaw = 0;  // ST_INT_RAW register (bit0 = TARGET0 fired)

  // ── ROM binary data (loaded asynchronously from /boards/esp32c3-rom.bin) ──
  private _romData: Uint8Array | null = null;

  // ── Interrupt matrix state ────────────────────────────────────────────────
  /** Source→CPU-line mapping (62 sources, each 5-bit → 0-31). */
  private _intSrcMap = new Uint8Array(62);
  /** CPU interrupt line enable bitmap (bit N = line N enabled). */
  private _intLineEnable = 0;
  /** Per-line priority (lines 1–31, 4-bit each). Index 0 unused. */
  private _intLinePrio = new Uint8Array(32);
  /** Interrupt threshold — only lines with priority > threshold can fire. */
  private _intThreshold = 0;
  /** Pending interrupt bitmap (set when source is active but can't fire). */
  private _intPending = 0;
  /** Current level of each interrupt source (1=asserted, 0=deasserted). */
  private _intSrcActive = new Uint8Array(62);

  /**
   * Shared peripheral register file — echo-back map.
   * Peripheral MMIO writes that aren't handled by specific logic are stored
   * here keyed by word-aligned address so that subsequent reads return the
   * last written value.  This makes common "write → read-back → verify"
   * patterns in the ESP-IDF boot succeed without dedicated stubs.
   */
  private _periRegs = new Map<number, number>();

  /** CPU ticks per µs — updated by ets_update_cpu_frequency(). */
  private _ticksPerUs = 160;

  public pinManager: PinManager;
  public onSerialData: ((ch: string) => void) | null = null;
  public onBaudRateChange: ((baud: number) => void) | null = null;
  public onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;

  constructor(pinManager: PinManager) {
    this.pinManager = pinManager;

    // Flash is the primary (fast-path) memory region
    this.flash = new Uint8Array(IROM_SIZE);
    this.dram  = new Uint8Array(DRAM_SIZE);
    this.iram  = new Uint8Array(IRAM_SIZE);

    this.core = new RiscVCore(this.flash, IROM_BASE);

    // DROM — read-only alias of the same flash buffer at a different virtual address
    const flash = this.flash;
    this.core.addMmio(DROM_BASE, IROM_SIZE,
      (addr) => flash[addr - DROM_BASE] ?? 0,
      () => {},
    );

    // DRAM (384 KB)
    const dram = this.dram;
    this.core.addMmio(DRAM_BASE, DRAM_SIZE,
      (addr) => dram[addr - DRAM_BASE],
      (addr, val) => { dram[addr - DRAM_BASE] = val; },
    );

    // IRAM (384 KB)
    const iram = this.iram;
    this.core.addMmio(IRAM_BASE, IRAM_SIZE,
      (addr) => iram[addr - IRAM_BASE],
      (addr, val) => { iram[addr - IRAM_BASE] = val; },
    );

    // Broad catch-all for all peripheral space must be registered FIRST (largest
    // region) so that narrower, more specific handlers registered afterwards win
    // via mmioFor's "smallest size wins" rule.
    this._registerPeripheralCatchAll();
    this._registerUart0();
    this._registerGpio();
    this._registerSysTimer();
    this._registerIntMatrix();
    this._registerSysCon();
    this._registerRtcCntl();
    // Timer Groups — stub RTCCALICFG1.cal_done for all known base addresses
    // so rtc_clk_cal_internal() poll loop exits immediately.
    this._registerTimerGroup(0x60026000);  // TIMG0 (ESP-IDF v5 / arduino-esp32 3.x)
    this._registerTimerGroup(0x60027000);  // TIMG1
    this._registerTimerGroup(0x6001F000);  // TIMG0 alternative (older ESP-IDF)
    this._registerTimerGroup(0x60020000);  // TIMG1 alternative
    this._registerSpiFlash(SPI1_BASE);   // SPI1 — direct flash controller
    this._registerSpiFlash(SPI0_BASE);   // SPI0 — cache SPI controller
    this._registerExtMem();
    this._registerRomStub();
    this._registerRomStub2();

    // Wire MIE transition callback — when firmware re-enables interrupts,
    // scan the interrupt matrix for pending sources and inject them.
    this.core.onMieEnabled = () => this._onMieEnabled();

    // NOTE: Real ROM binary loading disabled — the ROM code accesses many
    // peripherals we don't fully emulate, causing the CPU to jump to invalid
    // addresses.  C.RET stubs returning a0=0 are sufficient for now.
    // this._loadRom();

    this.core.reset(IROM_BASE);
    // Initialize SP to top of DRAM — MUST be after reset() which zeroes all regs
    this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
  }

  // ── MMIO registration ──────────────────────────────────────────────────────

  private _registerUart0(): void {
    this.core.addMmio(UART0_BASE, UART0_SIZE,
      (addr) => {
        const off = addr - UART0_BASE;
        if (off === UART0_FIFO)   return this.rxFifo.length > 0 ? (this.rxFifo.shift()! & 0xFF) : 0;
        if (off === UART0_STATUS) return 0;  // TXFIFO always empty = ready to accept data
        return 0;
      },
      (addr, val) => {
        if (addr - UART0_BASE === UART0_FIFO) {
          this.onSerialData?.(String.fromCharCode(val & 0xFF));
        }
      },
    );
  }

  private _registerGpio(): void {
    this.core.addMmio(GPIO_BASE, GPIO_SIZE,
      (addr) => {
        const off = (addr - GPIO_BASE) & ~3;  // word-align for register lookup
        const byteIdx = (addr - GPIO_BASE) & 3;
        if (off === GPIO_OUT)    return (this.gpioOut >> (byteIdx * 8)) & 0xFF;
        if (off === GPIO_IN)     return (this.gpioIn  >> (byteIdx * 8)) & 0xFF;
        if (off === GPIO_ENABLE) return 0xFF;
        return 0;
      },
      (addr, val) => {
        const off      = (addr - GPIO_BASE) & ~3;
        const byteIdx  = (addr - GPIO_BASE) & 3;
        const shift    = byteIdx * 8;
        const byteMask = 0xFF << shift;
        const prev     = this.gpioOut;

        if (off === GPIO_W1TS) {
          // Set bits — each byte write sets corresponding bits
          this.gpioOut |= (val & 0xFF) << shift;
        } else if (off === GPIO_W1TC) {
          // Clear bits
          this.gpioOut &= ~((val & 0xFF) << shift);
        } else if (off === GPIO_OUT) {
          // Direct write — reconstruct 32-bit value byte by byte
          this.gpioOut = (this.gpioOut & ~byteMask) | ((val & 0xFF) << shift);
        }

        const changed = prev ^ this.gpioOut;
        if (changed) {
          const timeMs = (this.core.cycles / CPU_HZ) * 1000;
          for (let bit = 0; bit < 22; bit++) {   // ESP32-C3 has GPIO0–GPIO21
            if (changed & (1 << bit)) {
              const state = !!(this.gpioOut & (1 << bit));
              console.log(`[ESP32-C3] GPIO${bit} → ${state ? 'HIGH' : 'LOW'} @ ${timeMs.toFixed(1)}ms`);
              this.onPinChangeWithTime?.(bit, state, timeMs);
              this.pinManager.setPinState(bit, state);
            }
          }
        }
      },
    );
  }

  private _registerSysTimer(): void {
    const peri = this._periRegs;
    this.core.addMmio(SYSTIMER_BASE, SYSTIMER_SIZE,
      (addr) => {
        const off      = addr - SYSTIMER_BASE;
        const wordOff  = off & ~3;
        const byteIdx  = off &  3;
        let word = 0;
        let handled = true;
        switch (wordOff) {
          case ST_INT_ENA:      word = this._stIntEna; break;
          case ST_INT_RAW:      word = this._stIntRaw; break;
          case ST_INT_ST:       word = this._stIntRaw & this._stIntEna; break;
          case ST_UNIT0_OP:     word = (1 << 29); break;  // VALID bit always set
          case ST_UNIT0_VAL_LO: word = (this.core.cycles / 10) >>> 0; break;
          case ST_UNIT0_VAL_HI: word = 0; break;
          default:              handled = false; break;
        }
        if (!handled) {
          // Echo last written value for unknown offsets
          const wordAddr = addr & ~3;
          word = peri.get(wordAddr) ?? 0;
        }
        return (word >> (byteIdx * 8)) & 0xFF;
      },
      (addr, val) => {
        const off     = addr - SYSTIMER_BASE;
        const wordOff = off & ~3;
        const shift   = (off & 3) * 8;
        switch (wordOff) {
          case ST_INT_ENA:
            this._stIntEna = (this._stIntEna & ~(0xFF << shift)) | ((val & 0xFF) << shift);
            break;
          case ST_INT_CLR:
            this._stIntRaw &= ~((val & 0xFF) << shift);
            // If TARGET0 was cleared, deassert the interrupt source
            if (!(this._stIntRaw & 1)) {
              this._lowerIntSource(ETS_SYSTIMER_TARGET0_SRC);
            }
            break;
          default: {
            // Echo-back: store the written value
            const wordAddr = addr & ~3;
            const prev = peri.get(wordAddr) ?? 0;
            peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
            break;
          }
        }
      },
    );
  }

  // ── Async ROM binary loader ──────────────────────────────────────────────

  // @ts-expect-error kept for future use when more peripherals are emulated
  private async _loadRom(): Promise<void> {
    try {
      const resp = await fetch('/boards/esp32c3-rom.bin');
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
      const buf = await resp.arrayBuffer();
      this._romData = new Uint8Array(buf);
      console.log(`[ESP32-C3] ROM binary loaded (${this._romData.length} bytes)`);
    } catch (e) {
      console.warn('[ESP32-C3] Failed to load ROM binary, using C.RET stub:', e);
    }
  }

  // ── Interrupt matrix ───────────────────────────────────────────────────────

  /**
   * Interrupt matrix (0x600C2000).
   *
   * 62 SOURCE_MAP registers route peripheral interrupt sources to CPU lines.
   * The ENABLE bitmap, per-line PRIORITY, and THRESHOLD control which
   * interrupts can fire.  Reads/writes are echo-backed via _periRegs and
   * internal state is updated on every write.
   */
  private _registerIntMatrix(): void {
    const peri = this._periRegs;
    const BASE = INTMATRIX_BASE;

    this.core.addMmio(BASE, INTMATRIX_SIZE,
      (addr) => {
        const off = (addr - BASE) & ~3;
        const byteIdx = (addr - BASE) & 3;
        let word = 0;

        if (off <= 0x0F8) {
          // SOURCE_MAP[0..62] (offsets 0x000-0x0F8)
          const src = off >> 2;
          word = src < 62 ? this._intSrcMap[src] & 0x1F : 0;
        } else if (off === 0x104) {
          // CPU_INT_ENABLE — which CPU interrupt lines are enabled (R/W)
          word = this._intLineEnable;
        } else if (off === 0x108) {
          // CPU_INT_TYPE — edge/level per line (echo-back)
          word = peri.get(addr & ~3) ?? 0;
        } else if (off === 0x10C) {
          // CPU_INT_EIP_STATUS — which lines have pending interrupts (read-only)
          word = this._intPending;
        } else if (off >= 0x114 && off <= 0x190) {
          // CPU_INT_PRI_0..31 (offsets 0x114 + line*4)
          const line = (off - 0x114) >> 2;
          word = line < 32 ? this._intLinePrio[line] : 0;
        } else if (off === 0x194) {
          // CPU_INT_THRESH
          word = this._intThreshold;
        } else {
          word = peri.get(addr & ~3) ?? 0;
        }
        return (word >>> (byteIdx * 8)) & 0xFF;
      },
      (addr, val) => {
        // Always store for echo-back
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        const newWord = (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift);
        peri.set(wordAddr, newWord);

        // Update internal state from accumulated word
        const off = (wordAddr - BASE);
        if (off <= 0x0F8) {
          const src = off >> 2;
          if (src < 62) {
            this._intSrcMap[src] = newWord & 0x1F;
          }
        } else if (off === 0x104) {
          this._intLineEnable = newWord;
        } else if (off >= 0x114 && off <= 0x190) {
          const line = (off - 0x114) >> 2;
          if (line < 32) this._intLinePrio[line] = newWord & 0xF;
        } else if (off === 0x194) {
          this._intThreshold = newWord & 0xF;
        }
      },
    );
  }

  /**
   * SYSTEM/CLK registers (0x600C0000).
   *
   * Provides FROM_CPU_INTR software interrupt triggers (FreeRTOS uses these
   * for cross-core signalling / context switch on single-core C3) and a
   * random-number register.
   */
  private _registerSysCon(): void {
    const peri = this._periRegs;
    const BASE = SYSCON_BASE;

    this.core.addMmio(BASE, SYSCON_SIZE,
      (addr) => {
        const wordAddr = addr & ~3;
        const byteIdx = (addr - BASE) & 3;
        const word = peri.get(wordAddr) ?? 0;
        return (word >>> (byteIdx * 8)) & 0xFF;
      },
      (addr, val) => {
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        const newWord = (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift);
        peri.set(wordAddr, newWord);

        // FROM_CPU_INTR: offsets 0x028, 0x02C, 0x030, 0x034
        const off = wordAddr - BASE;
        if (off >= 0x028 && off <= 0x034) {
          const idx = (off - 0x028) >> 2;           // 0–3
          const src = ETS_FROM_CPU_INTR0_SRC + idx;  // sources 28–31
          if (newWord & 1) this._raiseIntSource(src);
          else             this._lowerIntSource(src);
        }
      },
    );
  }

  // ── Interrupt matrix dispatch helpers ──────────────────────────────────────

  /**
   * Assert a peripheral interrupt source. Looks up its CPU line via the
   * source-map and either fires the interrupt (if MIE is set and priority
   * meets threshold) or marks it pending.
   */
  private _raiseIntSource(src: number): void {
    if (src >= 62) return;
    this._intSrcActive[src] = 1;
    const line = this._intSrcMap[src] & 0x1F;
    if (line === 0) return;  // line 0 = disabled / not routed

    // Mark pending for this line
    this._intPending |= (1 << line);

    // Can we deliver right now?
    if (!(this._intLineEnable & (1 << line))) return;
    const prio = this._intLinePrio[line];
    if (prio <= this._intThreshold) return;
    if (!(this.core.mstatusVal & 0x8)) return;  // MIE not set — stay pending

    this._intPending &= ~(1 << line);
    this.core.triggerInterrupt(0x80000000 | line);
  }

  /** Deassert a peripheral interrupt source and clear its pending state. */
  private _lowerIntSource(src: number): void {
    if (src >= 62) return;
    this._intSrcActive[src] = 0;
    const line = this._intSrcMap[src] & 0x1F;
    if (line === 0) return;

    // Check if any OTHER active source also maps to this line
    let stillActive = false;
    for (let s = 0; s < 62; s++) {
      if (s !== src && this._intSrcActive[s] && (this._intSrcMap[s] & 0x1F) === line) {
        stillActive = true;
        break;
      }
    }
    if (!stillActive) this._intPending &= ~(1 << line);
  }

  /**
   * Scan pending interrupts and deliver the highest-priority one.
   * Called when mstatus.MIE transitions 0→1 (MRET or CSR write).
   */
  private _onMieEnabled(): void {
    if (this._intPending === 0) return;
    let bestLine = 0;
    let bestPrio = 0;
    for (let line = 1; line < 32; line++) {
      if (!(this._intPending & (1 << line))) continue;
      if (!(this._intLineEnable & (1 << line))) continue;
      const prio = this._intLinePrio[line];
      if (prio > this._intThreshold && prio > bestPrio) {
        bestPrio = prio;
        bestLine = line;
      }
    }
    if (bestLine > 0) {
      this._intPending &= ~(1 << bestLine);
      this.core.triggerInterrupt(0x80000000 | bestLine);
    }
  }

  /**
   * ROM region (0x40000000-0x4005FFFF) — when the CPU fetches an instruction
   * from a known ROM function address, we emulate it natively in JavaScript
   * (memset, memcpy, __udivdi3, ets_delay_us, etc.), set the return value
   * in a0/a1, and serve C.RET (0x8082) so the CPU returns to the caller.
   * Unknown functions still get a0=0 + C.RET.
   */
  private _registerRomStub(): void {
    const core = this.core;
    this.core.addMmio(ROM_BASE, ROM_SIZE,
      (addr) => {
        // If we have the real ROM binary, serve it
        if (this._romData) {
          const off = (addr >>> 0) - ROM_BASE;
          if (off < this._romData.length) return this._romData[off];
        }
        // Detect instruction fetch and emulate known ROM functions
        if ((addr & 1) === 0 && (addr >>> 0) === (core.pc >>> 0)) {
          this._emulateRomFunction(addr >>> 0);
        }
        return (addr & 1) === 0 ? 0x82 : 0x80;
      },
      () => {},
    );
  }

  /** Second ROM region (0x40800000) — same emulation with fallback a0=0. */
  private _registerRomStub2(): void {
    const core = this.core;
    this.core.addMmio(ROM2_BASE, ROM2_SIZE,
      (addr) => {
        if ((addr & 1) === 0 && (addr >>> 0) === (core.pc >>> 0)) {
          this._emulateRomFunction(addr >>> 0);
        }
        return (addr & 1) === 0 ? 0x82 : 0x80;
      },
      () => {},
    );
  }

  // ── ROM function emulation ─────────────────────────────────────────────────
  //
  // The ESP32-C3 firmware links common C library functions (memset, memcpy,
  // __udivdi3, …) and helper functions (ets_delay_us, rtc_get_reset_reason, …)
  // to addresses in the ROM region (0x40000000+).  Since we don't run the real
  // ROM binary, we emulate these functions natively in JavaScript by reading
  // arguments from registers a0–a7, performing the operation on emulated
  // memory, and writing the result back to a0(/a1).  The caller then hits our
  // C.RET (0x8082) stub and returns normally.
  //
  // Address mappings from esp32c3.rom.ld / esp32c3.rom.libgcc.ld:

  private _emulateRomFunction(addr: number): void {
    const r = this.core.regs;
    const c = this.core;

    switch (addr) {
      // ── C library functions ──────────────────────────────────────────
      case 0x40000354: { // memset(dest, val, n) → dest
        const dest = r[10] >>> 0;
        const val  = r[11] & 0xFF;
        const n    = r[12] >>> 0;
        const safeN = Math.min(n, 0x100000); // cap at 1 MB safety
        for (let i = 0; i < safeN; i++) c.writeByte(dest + i, val);
        r[10] = dest | 0;
        break;
      }
      case 0x40000358: { // memcpy(dest, src, n) → dest
        const dest = r[10] >>> 0;
        const src  = r[11] >>> 0;
        const n    = r[12] >>> 0;
        const safeN = Math.min(n, 0x100000);
        // Copy forward (like real memcpy, undefined for overlap)
        for (let i = 0; i < safeN; i++) c.writeByte(dest + i, c.readByte(src + i));
        r[10] = dest | 0;
        break;
      }
      case 0x4000035c: { // memmove(dest, src, n) → dest
        const dest = r[10] >>> 0;
        const src  = r[11] >>> 0;
        const n    = r[12] >>> 0;
        const safeN = Math.min(n, 0x100000);
        if (dest < src) {
          for (let i = 0; i < safeN; i++) c.writeByte(dest + i, c.readByte(src + i));
        } else {
          for (let i = safeN - 1; i >= 0; i--) c.writeByte(dest + i, c.readByte(src + i));
        }
        r[10] = dest | 0;
        break;
      }
      case 0x40000360: { // memcmp(a, b, n) → int
        const a = r[10] >>> 0;
        const b = r[11] >>> 0;
        const n = r[12] >>> 0;
        let result = 0;
        for (let i = 0; i < n; i++) {
          const diff = c.readByte(a + i) - c.readByte(b + i);
          if (diff !== 0) { result = diff > 0 ? 1 : -1; break; }
        }
        r[10] = result;
        break;
      }
      case 0x40000364: { // strcpy(dest, src) → dest
        const dest = r[10] >>> 0;
        const src  = r[11] >>> 0;
        let i = 0;
        while (i < 0x100000) {
          const ch = c.readByte(src + i);
          c.writeByte(dest + i, ch);
          if (ch === 0) break;
          i++;
        }
        r[10] = dest | 0;
        break;
      }
      case 0x4000036c: { // strcmp(a, b) → int
        const a = r[10] >>> 0;
        const b = r[11] >>> 0;
        let result = 0;
        for (let i = 0; i < 0x100000; i++) {
          const ca = c.readByte(a + i);
          const cb = c.readByte(b + i);
          if (ca !== cb) { result = ca - cb; break; }
          if (ca === 0) break;
        }
        r[10] = result;
        break;
      }
      case 0x40000374: { // strlen(s) → size_t
        const s = r[10] >>> 0;
        let len = 0;
        while (len < 0x100000 && c.readByte(s + len) !== 0) len++;
        r[10] = len;
        break;
      }
      case 0x4000037c: { // bzero(dest, n)
        const dest = r[10] >>> 0;
        const n    = r[11] >>> 0;
        const safeN = Math.min(n, 0x100000);
        for (let i = 0; i < safeN; i++) c.writeByte(dest + i, 0);
        break;
      }

      // ── libgcc 64-bit integer math ───────────────────────────────────
      case 0x400008ac: { // __udivdi3(a, b) → a / b (unsigned 64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const bLo = r[12] >>> 0, bHi = r[13] >>> 0;
        const a = BigInt(aLo) | (BigInt(aHi) << 32n);
        const b = BigInt(bLo) | (BigInt(bHi) << 32n);
        const q = b !== 0n ? a / b : 0n;
        r[10] = Number(q & 0xFFFFFFFFn) | 0;
        r[11] = Number((q >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x400008b0: { // __udivmoddi4(a, b, *rem) → a / b, *rem = a % b
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const bLo = r[12] >>> 0, bHi = r[13] >>> 0;
        const remPtr = r[14] >>> 0; // a4
        const a = BigInt(aLo) | (BigInt(aHi) << 32n);
        const b = BigInt(bLo) | (BigInt(bHi) << 32n);
        const q = b !== 0n ? a / b : 0n;
        const rem = b !== 0n ? a % b : 0n;
        r[10] = Number(q & 0xFFFFFFFFn) | 0;
        r[11] = Number((q >> 32n) & 0xFFFFFFFFn) | 0;
        if (remPtr !== 0) {
          c.writeWord(remPtr,     Number(rem & 0xFFFFFFFFn));
          c.writeWord(remPtr + 4, Number((rem >> 32n) & 0xFFFFFFFFn));
        }
        break;
      }
      case 0x400008b4: { // __udivsi3(a, b) → a / b (unsigned 32-bit)
        const a = r[10] >>> 0, b = r[11] >>> 0;
        r[10] = b !== 0 ? (a / b) >>> 0 : 0;
        break;
      }
      case 0x400008bc: { // __umoddi3(a, b) → a % b (unsigned 64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const bLo = r[12] >>> 0, bHi = r[13] >>> 0;
        const a = BigInt(aLo) | (BigInt(aHi) << 32n);
        const b = BigInt(bLo) | (BigInt(bHi) << 32n);
        const rem = b !== 0n ? a % b : 0n;
        r[10] = Number(rem & 0xFFFFFFFFn) | 0;
        r[11] = Number((rem >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x400008c0: { // __umodsi3(a, b) → a % b (unsigned 32-bit)
        const a = r[10] >>> 0, b = r[11] >>> 0;
        r[10] = b !== 0 ? (a % b) >>> 0 : 0;
        break;
      }
      case 0x400007b4: { // __divdi3(a, b) → a / b (signed 64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] | 0;
        const bLo = r[12] >>> 0, bHi = r[13] | 0;
        const a = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
        const b = BigInt.asIntN(64, BigInt(bLo) | (BigInt(bHi) << 32n));
        const q = b !== 0n ? a / b : 0n;
        const qu = BigInt.asUintN(64, q);
        r[10] = Number(qu & 0xFFFFFFFFn) | 0;
        r[11] = Number((qu >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x400007c0: { // __divsi3(a, b) → a / b (signed 32-bit)
        const a = r[10] | 0, b = r[11] | 0;
        r[10] = b !== 0 ? (a / b) | 0 : 0;
        break;
      }
      case 0x4000083c: { // __moddi3(a, b) → a % b (signed 64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] | 0;
        const bLo = r[12] >>> 0, bHi = r[13] | 0;
        const a = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
        const b = BigInt.asIntN(64, BigInt(bLo) | (BigInt(bHi) << 32n));
        const rem = b !== 0n ? a % b : 0n;
        const remu = BigInt.asUintN(64, rem);
        r[10] = Number(remu & 0xFFFFFFFFn) | 0;
        r[11] = Number((remu >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x40000840: { // __modsi3(a, b) → a % b (signed 32-bit)
        const a = r[10] | 0, b = r[11] | 0;
        r[10] = b !== 0 ? (a % b) | 0 : 0;
        break;
      }
      case 0x4000084c: { // __muldi3(a, b) → a * b (64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const bLo = r[12] >>> 0, bHi = r[13] >>> 0;
        const a = BigInt(aLo) | (BigInt(aHi) << 32n);
        const b = BigInt(bLo) | (BigInt(bHi) << 32n);
        const p = BigInt.asUintN(64, a * b);
        r[10] = Number(p & 0xFFFFFFFFn) | 0;
        r[11] = Number((p >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x40000858: { // __mulsi3(a, b) → a * b (32-bit)
        r[10] = Math.imul(r[10], r[11]);
        break;
      }

      // ── libgcc shift / bit operations ────────────────────────────────
      case 0x4000077c: { // __ashldi3(a, shift) → a << shift (64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const shift = r[12] & 63;
        const val = BigInt(aLo) | (BigInt(aHi) << 32n);
        const res = BigInt.asUintN(64, val << BigInt(shift));
        r[10] = Number(res & 0xFFFFFFFFn) | 0;
        r[11] = Number((res >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x40000780: { // __ashrdi3(a, shift) → a >> shift (signed/arithmetic 64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const shift = r[12] & 63;
        const val = BigInt.asIntN(64, BigInt(aLo) | (BigInt(aHi) << 32n));
        const res = BigInt.asUintN(64, val >> BigInt(shift));
        r[10] = Number(res & 0xFFFFFFFFn) | 0;
        r[11] = Number((res >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x40000830: { // __lshrdi3(a, shift) → a >>> shift (unsigned/logical 64-bit)
        const aLo = r[10] >>> 0, aHi = r[11] >>> 0;
        const shift = r[12] & 63;
        const val = BigInt(aLo) | (BigInt(aHi) << 32n);
        const res = val >> BigInt(shift); // positive BigInt → logical shift
        r[10] = Number(res & 0xFFFFFFFFn) | 0;
        r[11] = Number((res >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x4000079c: { // __clzsi2(a) → count leading zeros (32-bit)
        r[10] = Math.clz32(r[10] >>> 0);
        break;
      }
      case 0x40000798: { // __clzdi2(a) → count leading zeros (64-bit)
        const lo = r[10] >>> 0, hi = r[11] >>> 0;
        r[10] = hi !== 0 ? Math.clz32(hi) : 32 + Math.clz32(lo);
        break;
      }
      case 0x400007a8: { // __ctzsi2(a) → count trailing zeros (32-bit)
        const v = r[10] >>> 0;
        r[10] = v === 0 ? 32 : 31 - Math.clz32(v & -v);
        break;
      }
      case 0x400007a4: { // __ctzdi2(a) → count trailing zeros (64-bit)
        const lo = r[10] >>> 0, hi = r[11] >>> 0;
        if (lo !== 0) {
          r[10] = 31 - Math.clz32(lo & -lo);
        } else if (hi !== 0) {
          r[10] = 32 + (31 - Math.clz32(hi & -hi));
        } else {
          r[10] = 64;
        }
        break;
      }
      case 0x4000086c: { // __negdi2(a) → -a (64-bit)
        const lo = r[10] >>> 0, hi = r[11] >>> 0;
        const val = BigInt(lo) | (BigInt(hi) << 32n);
        const neg = BigInt.asUintN(64, -val);
        r[10] = Number(neg & 0xFFFFFFFFn) | 0;
        r[11] = Number((neg >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }
      case 0x400007d0: { // __ffsdi2(a) → find first set bit (64-bit), 0 if none
        const lo = r[10] >>> 0, hi = r[11] >>> 0;
        if (lo !== 0) {
          r[10] = (31 - Math.clz32(lo & -lo)) + 1;
        } else if (hi !== 0) {
          r[10] = 32 + (31 - Math.clz32(hi & -hi)) + 1;
        } else {
          r[10] = 0;
        }
        break;
      }
      case 0x400007d4: { // __ffssi2(a) → find first set bit (32-bit), 0 if none
        const v = r[10] >>> 0;
        r[10] = v === 0 ? 0 : (31 - Math.clz32(v & -v)) + 1;
        break;
      }
      case 0x40000784: { // __bswapdi2(a) → byte-swap 64-bit
        const lo = r[10] >>> 0, hi = r[11] >>> 0;
        const swapLo = ((lo >>> 24) | ((lo >>> 8) & 0xFF00) | ((lo << 8) & 0xFF0000) | (lo << 24)) >>> 0;
        const swapHi = ((hi >>> 24) | ((hi >>> 8) & 0xFF00) | ((hi << 8) & 0xFF0000) | (hi << 24)) >>> 0;
        r[10] = swapHi | 0; // swapped hi becomes new lo
        r[11] = swapLo | 0; // swapped lo becomes new hi
        break;
      }
      case 0x40000788: { // __bswapsi2(a) → byte-swap 32-bit
        const v = r[10] >>> 0;
        r[10] = ((v >>> 24) | ((v >>> 8) & 0xFF00) | ((v << 8) & 0xFF0000) | (v << 24)) | 0;
        break;
      }
      case 0x400007a0: { // __cmpdi2(a, b) → 0 if a<b, 1 if a==b, 2 if a>b (signed 64-bit)
        const a = BigInt.asIntN(64, BigInt(r[10] >>> 0) | (BigInt(r[11]) << 32n));
        const b = BigInt.asIntN(64, BigInt(r[12] >>> 0) | (BigInt(r[13]) << 32n));
        r[10] = a < b ? 0 : a === b ? 1 : 2;
        break;
      }
      case 0x400008a8: { // __ucmpdi2(a, b) → 0 if a<b, 1 if a==b, 2 if a>b (unsigned 64-bit)
        const a = BigInt(r[10] >>> 0) | (BigInt(r[11] >>> 0) << 32n);
        const b = BigInt(r[12] >>> 0) | (BigInt(r[13] >>> 0) << 32n);
        r[10] = a < b ? 0 : a === b ? 1 : 2;
        break;
      }
      case 0x40000764: { // __absvdi2(a) → |a| (signed 64-bit, aborts on overflow)
        const val = BigInt.asIntN(64, BigInt(r[10] >>> 0) | (BigInt(r[11]) << 32n));
        const abs = val < 0n ? BigInt.asUintN(64, -val) : BigInt.asUintN(64, val);
        r[10] = Number(abs & 0xFFFFFFFFn) | 0;
        r[11] = Number((abs >> 32n) & 0xFFFFFFFFn) | 0;
        break;
      }

      // ── ESP-IDF ROM helpers ──────────────────────────────────────────
      case 0x40000018: { // rtc_get_reset_reason() → 1 (POWERON_RESET)
        r[10] = 1;
        break;
      }
      case 0x40000050: { // ets_delay_us(us) → void
        // Burn the equivalent number of CPU cycles so timers advance
        const us = r[10] >>> 0;
        const burnCycles = Math.min(us * this._ticksPerUs, 1_000_000);
        this.core.cycles += burnCycles;
        r[10] = 0;
        break;
      }
      case 0x40000548: { // Cache_Set_IDROM_MMU_Size(...) → 0 (success)
        r[10] = 0;
        break;
      }
      case 0x40001960: { // rom_i2c_writeReg_Mask(...) → 0 (success)
        r[10] = 0;
        break;
      }
      case 0x4000195c: { // rom_i2c_writeReg(...) → 0 (success)
        r[10] = 0;
        break;
      }
      case 0x40000588: { // ets_update_cpu_frequency(ticks_per_us)
        // Store value so our ets_delay_us can use the right multiplier.
        // Firmware calls this with e.g. 40 or 160.
        this._ticksPerUs = r[10] >>> 0;
        r[10] = 0;
        break;
      }
      case 0x40000084: { // uart_tx_wait_idle(uart_num) — no-op
        r[10] = 0;
        break;
      }
      case 0x400005f4: { // intr_matrix_set(cpu_no, model_num, intr_num)
        // Maps peripheral interrupt source a1 to CPU interrupt line a2.
        // This directly programs the interrupt matrix hardware.
        const source = r[11] >>> 0;
        const line   = r[12] & 0x1F;
        if (source < 62) {
          this._intSrcMap[source] = line;
          // Also store in the MMIO echo-back register so firmware reads work
          this._periRegs.set(INTMATRIX_BASE + source * 4, line);
        }
        break;
      }

      default: {
        // Unknown ROM function — return a0=0 (ESP_OK for esp_err_t functions)
        r[10] = 0;
        break;
      }
    }
  }

  /**
   * Timer Group stub (TIMG0 / TIMG1).
   *
   * Critical register: RTCCALICFG1 at offset 0x6C (confirmed from qemu-lcgamboa
   * esp32c3_timg.h — offset 0x48 is TIMG_WDTCONFIG0, not the calibration result).
   *   Bit 31 = TIMG_RTC_CALI_DONE — must read as 1 or rtc_clk_cal_internal()
   *   spins forever waiting for calibration to complete.
   *   Bits [30:7] = cal_value — must be non-zero or the outer retry loop
   *   in esp_rtc_clk_init() keeps calling rtc_clk_cal() forever.
   *
   * Called for all known TIMG0/TIMG1 base addresses across ESP-IDF versions.
   */
  private _registerTimerGroup(base: number): void {
    const peri = this._periRegs;
    this.core.addMmio(base, 0x100,
      (addr) => {
        const off  = addr - base;
        const wOff = off & ~3;
        if (wOff === 0x68) {
          // TIMG_RTCCALICFG: bit15=TIMG_RTC_CALI_RDY=1 — calibration instantly done
          // Also set bit31 (start bit echo) which some versions check
          const word = (1 << 15) | (1 << 31);
          return (word >>> ((off & 3) * 8)) & 0xFF;
        }
        if (wOff === 0x6C) {
          // TIMG_RTCCALICFG1: bits[31:7]=rtc_cali_value — non-zero so outer retry exits
          const word = (136533 << 7) >>> 0; // typical 150kHz RTC vs 40MHz XTAL
          return (word >>> ((off & 3) * 8)) & 0xFF;
        }
        if (wOff === 0x80) {
          // TIMG_RTCCALICFG2 (ESP-IDF v5): bit31=timeout(0), bits[24:7]=cali_value
          // ESP-IDF v5 reads result HERE instead of RTCCALICFG1.
          // Must be non-zero or rtc_clk_cal() retries forever.
          const word = (136533 << 7) >>> 0;
          return (word >>> ((off & 3) * 8)) & 0xFF;
        }
        // Echo last written value for all other offsets
        const wordAddr = addr & ~3;
        const word = peri.get(wordAddr) ?? 0;
        return (word >>> ((addr & 3) * 8)) & 0xFF;
      },
      (addr, val) => {
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
      },
    );
  }

  /**
   * SPI flash controller stub (SPI0 / SPI1).
   *
   * SPI_MEM_CMD_REG (offset 0x00) bits [17–31] are "write 1 to start operation,
   * hardware clears when done".  The firmware polls these bits after triggering
   * flash reads, writes, erases, etc.  We auto‑clear them so every flash
   * operation appears to complete instantly.
   *
   * Other registers use echo‑back so configuration writes can be read back.
   */
  private _registerSpiFlash(base: number): void {
    const peri = this._periRegs;
    this.core.addMmio(base, SPI_SIZE,
      (addr) => {
        const off     = addr - base;
        const wordOff = off & ~3;
        if (wordOff === SPI_CMD) {
          // Always return 0 for CMD register — all operations are "done"
          return 0;
        }
        // Echo last written value for all other offsets
        const wordAddr = addr & ~3;
        const word = peri.get(wordAddr) ?? 0;
        return (word >>> ((addr & 3) * 8)) & 0xFF;
      },
      (addr, val) => {
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
      },
    );
  }

  /**
   * EXTMEM cache controller stub (0x600C4000).
   *
   * The ESP-IDF boot enables ICache, then triggers cache invalidation / sync /
   * preload operations and polls "done" bits.  We return all "done" bits as 1
   * so these operations appear to complete instantly.
   */
  private _registerExtMem(): void {
    const peri = this._periRegs;
    this.core.addMmio(EXTMEM_BASE, EXTMEM_SIZE,
      (addr) => {
        const off     = addr - EXTMEM_BASE;
        const wordOff = off & ~3;
        // Return "done" bits for operations that the boot polls:
        let override: number | null = null;
        switch (wordOff) {
          case EXTMEM_ICACHE_SYNC_CTRL:    override = (1 << 1); break; // SYNC_DONE
          case EXTMEM_ICACHE_PRELOAD_CTRL: override = (1 << 1); break; // PRELOAD_DONE
          case EXTMEM_ICACHE_AUTOLOAD_CTRL: override = (1 << 3); break; // AUTOLOAD_DONE
          case EXTMEM_ICACHE_LOCK_CTRL:    override = (1 << 2); break; // LOCK_DONE
        }
        if (override !== null) {
          // Merge override bits with any written value so enable bits are preserved
          const wordAddr = addr & ~3;
          const word = (peri.get(wordAddr) ?? 0) | override;
          return (word >>> ((addr & 3) * 8)) & 0xFF;
        }
        // Echo last written value for all other offsets
        const wordAddr = addr & ~3;
        const word = peri.get(wordAddr) ?? 0;
        return (word >>> ((addr & 3) * 8)) & 0xFF;
      },
      (addr, val) => {
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
      },
    );
  }

  /**
   * Broad catch-all for the entire ESP32-C3 peripheral address space
   * (0x60000000–0x6FFFFFFF).
   *
   * Writes are stored in _periRegs so that the firmware's common
   * "write config → read back → verify" pattern works for any peripheral
   * register we haven't stubbed explicitly.  All narrower, more specific
   * handlers (UART0, GPIO, SYSTIMER, INTC, RTC_CNTL …) have smaller MMIO
   * sizes and therefore take priority via mmioFor's "smallest-size-wins" rule.
   */
  private _registerPeripheralCatchAll(): void {
    const peri = this._periRegs;
    this.core.addMmio(0x60000000, 0x10000000,
      (addr) => {
        const wordAddr = addr & ~3;
        const word = peri.get(wordAddr) ?? 0;
        return (word >>> ((addr & 3) * 8)) & 0xFF;
      },
      (addr, val) => {
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
      },
    );
  }

  /**
   * RTC_CNTL peripheral stub (0x60008000, 4 KB).
   *
   * Critical register: TIME_UPDATE_REG at offset 0x70 (address 0x60008070).
   *   Bit 30 = TIME_VALID — must read as 1 or the `rtc_clk_cal()` loop in
   *   esp-idf never exits and MIE is never enabled (FreeRTOS scheduler stalls).
   * Also covers the eFUSE block at 0x60008800 (offset 0x800) — returns 0 for
   * all eFuse words (chip-revision 0 / all features disabled = safe defaults).
   */
  private _registerRtcCntl(): void {
    const RTC_BASE = 0x60008000;
    const peri = this._periRegs;
    this.core.addMmio(RTC_BASE, 0x1000,
      (addr) => {
        const off     = addr - RTC_BASE;
        const wordOff = off & ~3;
        // offset 0x70 (RTC_CLK_CONF): TIME_VALID (bit 30) = 1 so rtc_clk_cal() exits.
        // offset 0x38 (RESET_STATE): return 1 = ESP32C3_POWERON_RESET (matches QEMU).
        if (wordOff === 0x70) {
          const word = (1 << 30);
          return (word >>> ((off & 3) * 8)) & 0xFF;
        }
        if (wordOff === 0x38) {
          return off === (wordOff) ? 1 : 0;  // byte 0 = 1, rest = 0
        }
        // Echo last written value for all other offsets
        const wordAddr = addr & ~3;
        const word = peri.get(wordAddr) ?? 0;
        return (word >>> ((addr & 3) * 8)) & 0xFF;
      },
      (addr, val) => {
        const wordAddr = addr & ~3;
        const prev = peri.get(wordAddr) ?? 0;
        const shift = (addr & 3) * 8;
        peri.set(wordAddr, (prev & ~(0xFF << shift)) | ((val & 0xFF) << shift));
      },
    );
  }

  // ── HEX loading ────────────────────────────────────────────────────────────

  /**
   * Load an Intel HEX file. The hex addresses must be relative to IROM_BASE
   * (0x42000000), or zero-based (the parser will treat them as flash offsets).
   */
  loadHex(hexContent: string): void {
    this.flash.fill(0);
    const bytes = hexToUint8Array(hexContent);

    // hexToUint8Array returns bytes indexed from address 0.
    // If the hex records used IROM_BASE-relative addressing, the byte array
    // will start at offset IROM_BASE within a huge buffer — we can't use that.
    // Support both:
    //   a) Small array (< IROM_SIZE) → direct flash offset mapping
    //   b) Large array → slice from IROM_BASE offset if present
    if (bytes.length <= IROM_SIZE) {
      const maxCopy = Math.min(bytes.length, IROM_SIZE);
      this.flash.set(bytes.subarray(0, maxCopy), 0);
    } else {
      // Try to extract data at IROM_BASE offset
      const iromOffset = IROM_BASE;
      if (bytes.length > iromOffset) {
        const maxCopy = Math.min(bytes.length - iromOffset, IROM_SIZE);
        this.flash.set(bytes.subarray(iromOffset, iromOffset + maxCopy), 0);
      }
    }

    this.dram.fill(0);
    this.iram.fill(0);
    this.core.reset(IROM_BASE);
    this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
  }

  /**
   * Load a raw binary image into flash at offset 0 (maps to IROM_BASE 0x42000000).
   * Use this with binaries produced by:
   *   riscv32-esp-elf-objcopy -O binary firmware.elf firmware.bin
   */
  loadBin(bin: Uint8Array): void {
    this.flash.fill(0);
    const maxCopy = Math.min(bin.length, IROM_SIZE);
    this.flash.set(bin.subarray(0, maxCopy), 0);
    this.dram.fill(0);
    this.iram.fill(0);
    this.rxFifo  = [];
    this.gpioOut = 0;
    this.gpioIn  = 0;
    this._periRegs.clear();
    this.core.reset(IROM_BASE);
    this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
  }

  /**
   * Load a merged ESP32 flash image from the backend (base64-encoded).
   *
   * The backend produces a 4 MB merged image:
   *   0x01000 — bootloader
   *   0x08000 — partition table
   *   0x10000 — application (ESP32 image format with segment headers)
   *
   * Each image segment is loaded at its virtual load address:
   *   IROM (0x42xxxxxx) → flash buffer  (executed code)
   *   DROM (0x3Cxxxxxx) → flash buffer  (read-only data alias)
   *   DRAM (0x3FCxxxxx) → dram buffer   (initialised .data)
   *   IRAM (0x4037xxxx) → iram buffer   (ISR / time-critical code)
   *
   * The CPU resets to the entry point declared in the image header.
   */
  loadFlashImage(base64: string): void {
    // Base64 decode
    const binStr = atob(base64);
    const data = new Uint8Array(binStr.length);
    for (let i = 0; i < binStr.length; i++) data[i] = binStr.charCodeAt(i);

    // Parse ESP32 image format
    const parsed = parseMergedFlashImage(data);

    // Clear all memory regions
    this.flash.fill(0);
    this.dram.fill(0);
    this.iram.fill(0);
    this.rxFifo  = [];
    this.gpioOut = 0;
    this.gpioIn  = 0;
    this._periRegs.clear();

    // Load each segment at its virtual address
    for (const { loadAddr, data: seg } of parsed.segments) {
      const uAddr = loadAddr >>> 0;

      if (uAddr >= IROM_BASE && uAddr + seg.length <= IROM_BASE + IROM_SIZE) {
        this.flash.set(seg, uAddr - IROM_BASE);
      } else if (uAddr >= DROM_BASE && uAddr + seg.length <= DROM_BASE + IROM_SIZE) {
        // DROM is a virtual alias of flash — store at same flash buffer
        this.flash.set(seg, uAddr - DROM_BASE);
      } else if (uAddr >= DRAM_BASE && uAddr + seg.length <= DRAM_BASE + DRAM_SIZE) {
        this.dram.set(seg, uAddr - DRAM_BASE);
      } else if (uAddr >= IRAM_BASE && uAddr + seg.length <= IRAM_BASE + IRAM_SIZE) {
        this.iram.set(seg, uAddr - IRAM_BASE);
      } else {
        console.warn(
          `[Esp32C3Simulator] Segment 0x${uAddr.toString(16)}` +
          ` (${seg.length} B) outside known regions — skipped`
        );
      }
    }

    // Boot CPU at image entry point
    this.core.reset(parsed.entryPoint);
    this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;

    console.log(
      `[Esp32C3Simulator] Loaded ${parsed.segments.length} segments,` +
      ` entry=0x${parsed.entryPoint.toString(16)}`
    );
  }

  // ── Lifecycle ──────────────────────────────────────────────────────────────

  start(): void {
    if (this.running) return;
    this._ticksPerUs = 160;
    console.log(`[ESP32-C3] Simulation started, entry=0x${this.core.pc.toString(16)}`);
    this.running = true;
    this._loop();
  }

  stop(): void {
    this.running = false;
    cancelAnimationFrame(this.animFrameId);
  }

  reset(): void {
    this.stop();
    this.rxFifo         = [];
    this.gpioOut        = 0;
    this.gpioIn         = 0;
    this._stIntEna      = 0;
    this._stIntRaw      = 0;
    this._periRegs.clear();
    this._ticksPerUs = 160;
    this.dram.fill(0);
    this.iram.fill(0);
    this.core.reset(IROM_BASE);
    this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
  }

  serialWrite(text: string): void {
    for (let i = 0; i < text.length; i++) {
      this.rxFifo.push(text.charCodeAt(i));
    }
  }

  setPinState(pin: number, state: boolean): void {
    if (state) this.gpioIn |=  (1 << pin);
    else        this.gpioIn &= ~(1 << pin);
  }

  isRunning(): boolean {
    return this.running;
  }

  // ── Execution loop ─────────────────────────────────────────────────────────

  private _loop(): void {
    if (!this.running) return;

    // Execute in 1 ms chunks so FreeRTOS tick interrupts fire at ~1 kHz.
    let rem = CYCLES_PER_FRAME;
    while (rem > 0) {
      const n = rem < CYCLES_PER_TICK ? rem : CYCLES_PER_TICK;
      for (let i = 0; i < n; i++) {
        this.core.step();
      }
      rem -= n;

      // Raise SYSTIMER TARGET0 alarm → routed through interrupt matrix.
      this._stIntRaw |= 1;
      if (this._stIntEna & 1) {
        this._raiseIntSource(ETS_SYSTIMER_TARGET0_SRC);
      }
    }

    this.animFrameId = requestAnimationFrame(() => this._loop());
  }
}
