import { RP2040, GPIOPinState, ConsoleLogger, LogLevel } from 'rp2040js';
import type { RPI2C } from 'rp2040js';
import { PinManager } from './PinManager';
import { bootromB1 } from './rp2040-bootrom';

/**
 * RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
 *
 * Features:
 * - ARM Cortex-M0+ dual-core Cortex-M0+ CPU at 125 MHz (single-core emulated)
 * - 30 GPIO pins (GPIO0-GPIO29)  xc fv       nn 
 * - 2× UART, 2× SPI, 2× I2C
 * - ADC on GPIO26-GPIO29 (A0-A3) + internal temp sensor (ch4)
 * - PWM on any GPIO
 * - LED_BUILTIN on GPIO25
 * - Full bootrom B1 for proper boot sequence
 *
 * Arduino-pico pin mapping (Earle Philhower's core):
 *   D0  = GPIO0   … D29 = GPIO29
 *   A0  = GPIO26  … A3  = GPIO29
 *   LED_BUILTIN = GPIO25
 *   Default Serial  → UART0 (GPIO0=TX, GPIO1=RX)
 *   Default I2C     → I2C0  (GPIO4=SDA, GPIO5=SCL)
 *   Default SPI     → SPI0  (GPIO16=MISO, GPIO19=MOSI, GPIO18=SCK, GPIO17=CS)
 */

const F_CPU = 125_000_000; // 125 MHz
const CYCLE_NANOS = 1e9 / F_CPU; // nanoseconds per cycle (~8 ns)
const FPS = 60;
const CYCLES_PER_FRAME = Math.floor(F_CPU / FPS); // ~2 083 333

/** Virtual I2C device interface for RP2040 */
export interface RP2040I2CDevice {
  /** 7-bit I2C address */
  address: number;
  /** Called when master writes a byte */
  writeByte(value: number): boolean;   // return true for ACK
  /** Called when master reads a byte */
  readByte(): number;
  /** Optional: called on STOP condition */
  stop?(): void;
}

export class RP2040Simulator {
  private rp2040: RP2040 | null = null;
  private running = false;
  private animationFrame: number | null = null;
  public pinManager: PinManager;
  private speed = 1.0;
  private gpioUnsubscribers: Array<() => void> = [];
  private flashCopy: Uint8Array | null = null;

  /** Serial output callback — fires for each byte the Pico sends on UART0 */
  public onSerialData: ((char: string) => void) | null = null;

  /**
   * Fires for every GPIO pin transition with a millisecond timestamp.
   * Used by the oscilloscope / logic analyzer.
   * timeMs is derived from the RP2040 cycle counter (cycles / F_CPU * 1000).
   */
  public onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;

  /** I2C virtual devices on each bus */
  private i2cDevices: [Map<number, RP2040I2CDevice>, Map<number, RP2040I2CDevice>] = [new Map(), new Map()];
  private activeI2CDevice: [RP2040I2CDevice | null, RP2040I2CDevice | null] = [null, null];

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

  /**
   * Load a compiled binary into the RP2040 flash memory.
   * Accepts a base64-encoded string of the raw .bin file output by arduino-cli.
   */
  loadBinary(base64: string): void {
    console.log('[RP2040] Loading binary...');

    const binaryStr = atob(base64);
    const bytes = new Uint8Array(binaryStr.length);
    for (let i = 0; i < binaryStr.length; i++) {
      bytes[i] = binaryStr.charCodeAt(i);
    }

    console.log(`[RP2040] Binary size: ${bytes.length} bytes`);
    this.flashCopy = bytes;

    this.initMCU(bytes);
    console.log('[RP2040] CPU initialized with bootrom, UART, I2C, SPI, GPIO');
  }

  /** Same interface as AVRSimulator for store compatibility */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  loadHex(_hexContent: string): void {
    console.warn('[RP2040] loadHex() called on RP2040Simulator — use loadBinary() instead');
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getADC(): any {
    return this.rp2040?.adc ?? null;
  }

  /** Get underlying RP2040 instance (for advanced usage / tests) */
  getMCU(): RP2040 | null {
    return this.rp2040;
  }

  // ── Private initialization ───────────────────────────────────────────────

  private initMCU(programBytes: Uint8Array): void {
    this.rp2040 = new RP2040();

    // Suppress noisy internal logs (only show errors)
    this.rp2040.logger = new ConsoleLogger(LogLevel.Error);

    // Load RP2040 B1 bootrom — needed for proper boot sequence
    this.rp2040.loadBootrom(bootromB1);

    // Load binary into flash starting at offset 0 (maps to 0x10000000)
    this.rp2040.flash.set(programBytes, 0);

    // Set PC to flash start (boot vector)
    this.rp2040.core.PC = 0x10000000;

    // ── Wire UART0 (default Serial port for Arduino-Pico) ────────────
    let serialBuffer = '';
    this.rp2040.uart[0].onByte = (value: number) => {
      const ch = String.fromCharCode(value);
      serialBuffer += ch;
      if (ch === '\n') {
        console.log('[RP2040 UART0]', serialBuffer.trimEnd());
        serialBuffer = '';
      }
      if (this.onSerialData) {
        this.onSerialData(ch);
      }
    };

    // ── Wire UART1 (Serial1) — also forward to onSerialData for now ──
    this.rp2040.uart[1].onByte = (value: number) => {
      if (this.onSerialData) {
        this.onSerialData(String.fromCharCode(value));
      }
    };

    // ── Wire I2C0 and I2C1 ───────────────────────────────────────────
    this.wireI2C(0);
    this.wireI2C(1);

    // ── Wire SPI0 and SPI1 — default loopback ────────────────────────
    this.rp2040.spi[0].onTransmit = (value: number) => {
      this.rp2040!.spi[0].completeTransmit(value); // loopback
    };
    this.rp2040.spi[1].onTransmit = (value: number) => {
      this.rp2040!.spi[1].completeTransmit(value); // loopback
    };

    // ── Set default ADC values ───────────────────────────────────────
    // Channel 0-3: GPIO26-29, channel 4: internal temp sensor
    // Default to mid-range (~1.65V on 3.3V ref, 12-bit)
    this.rp2040.adc.channelValues[0] = 2048;
    this.rp2040.adc.channelValues[1] = 2048;
    this.rp2040.adc.channelValues[2] = 2048;
    this.rp2040.adc.channelValues[3] = 2048;
    // Internal temp sensor: T = 27 - (V - 0.706) / 0.001721
    // For 27°C: V = 0.706V → ADC = 0.706/3.3 * 4095 ≈ 876
    this.rp2040.adc.channelValues[4] = 876;

    // ── Set up GPIO listeners ────────────────────────────────────────
    this.setupGpioListeners();
  }

  private wireI2C(bus: 0 | 1): void {
    if (!this.rp2040) return;
    const i2c: RPI2C = this.rp2040.i2c[bus];
    const devices = this.i2cDevices[bus];
    i2c.onStart = () => {
      i2c.completeStart();
    };

    i2c.onConnect = (address: number) => {
      const device = devices.get(address);
      if (device) {
        this.activeI2CDevice[bus] = device;
        i2c.completeConnect(true); // ACK
      } else {
        this.activeI2CDevice[bus] = null;
        i2c.completeConnect(false); // NACK
      }
    };

    i2c.onWriteByte = (value: number) => {
      const dev = this.activeI2CDevice[bus];
      if (dev) {
        const ack = dev.writeByte(value);
        i2c.completeWrite(ack);
      } else {
        i2c.completeWrite(false);
      }
    };

    i2c.onReadByte = () => {
      const dev = this.activeI2CDevice[bus];
      if (dev) {
        i2c.completeRead(dev.readByte());
      } else {
        i2c.completeRead(0xff);
      }
    };

    i2c.onStop = () => {
      const dev = this.activeI2CDevice[bus];
      if (dev?.stop) dev.stop();
      this.activeI2CDevice[bus] = null;
      i2c.completeStop();
    };
  }

  private setupGpioListeners(): void {
    this.gpioUnsubscribers.forEach(fn => fn());
    this.gpioUnsubscribers = [];

    if (!this.rp2040) return;

    for (let gpioIdx = 0; gpioIdx < 30; gpioIdx++) {
      const pin = gpioIdx;
      const gpio = this.rp2040.gpio[gpioIdx];
      if (!gpio) continue;

      const unsub = gpio.addListener((state: GPIOPinState) => {
        const isHigh = state === GPIOPinState.High;
        this.pinManager.triggerPinChange(pin, isHigh);
        if (this.onPinChangeWithTime && this.rp2040) {
          // Use clock cycles if available, otherwise 0
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          const clk = (this.rp2040 as any).clock;
          const timeMs = clk ? clk.timeUs / 1000 : 0;
          this.onPinChangeWithTime(pin, isHigh, timeMs);
        }
      });
      this.gpioUnsubscribers.push(unsub);
    }
  }

  // ── Public API ───────────────────────────────────────────────────────────

  start(): void {
    if (this.running || !this.rp2040) {
      console.warn('[RP2040] Already running or not initialized');
      return;
    }

    this.running = true;
    console.log('[RP2040] Starting simulation at 125 MHz...');

    let frameCount = 0;
    const execute = () => {
      if (!this.running || !this.rp2040) return;

      const cyclesTarget = Math.floor(CYCLES_PER_FRAME * this.speed);
      const { core } = this.rp2040;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const clock = (this.rp2040 as any).clock;

      try {
        let cyclesDone = 0;
        while (cyclesDone < cyclesTarget) {
          if (core.waiting) {
            if (clock) {
              const jump: number = clock.nanosToNextAlarm;
              if (jump <= 0) break; // no pending alarms
              clock.tick(jump);
              cyclesDone += Math.ceil(jump / CYCLE_NANOS);
            } else {
              break;
            }
          } else {
            const cycles: number = core.executeInstruction();
            if (clock) clock.tick(cycles * CYCLE_NANOS);
            cyclesDone += cycles;
          }
        }

        frameCount++;
        if (frameCount % 60 === 0) {
          console.log(`[RP2040] Frame ${frameCount}, PC: 0x${core.PC.toString(16)}`);
        }
      } catch (error) {
        console.error('[RP2040] Simulation error:', error);
        this.stop();
        return;
      }

      this.animationFrame = requestAnimationFrame(execute);
    };

    this.animationFrame = requestAnimationFrame(execute);
  }

  stop(): void {
    if (!this.running) return;
    this.running = false;
    if (this.animationFrame !== null) {
      cancelAnimationFrame(this.animationFrame);
      this.animationFrame = null;
    }
    console.log('[RP2040] Simulation stopped');
  }

  reset(): void {
    this.stop();
    if (this.rp2040 && this.flashCopy) {
      this.initMCU(this.flashCopy);
      // Re-register any previously added I2C devices
      // (devices are kept in i2cDevices maps which persist across reset)
      console.log('[RP2040] CPU reset');
    }
  }

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

  setSpeed(speed: number): void {
    this.speed = Math.max(0.1, Math.min(10.0, speed));
  }

  getSpeed(): number {
    return this.speed;
  }

  /**
   * Drive a GPIO pin externally (e.g. from a button or slider).
   * GPIO n = Arduino D(n) for Raspberry Pi Pico.
   */
  setPinState(arduinoPin: number, state: boolean): void {
    if (!this.rp2040) return;
    const gpio = this.rp2040.gpio[arduinoPin];
    if (gpio) {
      gpio.setInputValue(state);
    }
  }

  /**
   * Send text to UART0 RX (as if typed in Serial Monitor).
   */
  serialWrite(text: string): void {
    if (!this.rp2040) return;
    for (let i = 0; i < text.length; i++) {
      this.rp2040.uart[0].feedByte(text.charCodeAt(i));
    }
  }

  /**
   * Register a virtual I2C device on the specified bus (0 or 1).
   * Default bus 0 = Wire, bus 1 = Wire1.
   */
  addI2CDevice(device: RP2040I2CDevice, bus: 0 | 1 = 0): void {
    this.i2cDevices[bus].set(device.address, device);
  }

  /**
   * Remove an I2C device by address.
   */
  removeI2CDevice(address: number, bus: 0 | 1 = 0): void {
    this.i2cDevices[bus].delete(address);
  }

  /**
   * Set ADC channel value (0-4095 for 12-bit).
   * Channels 0-3 = GPIO26-29, channel 4 = internal temperature sensor.
   */
  setADCValue(channel: number, value: number): void {
    if (!this.rp2040) return;
    if (channel >= 0 && channel < 5) {
      this.rp2040.adc.channelValues[channel] = Math.max(0, Math.min(4095, value));
    }
  }

  /**
   * Set SPI onTransmit handler for a bus (0 or 1).
   * callback receives TX byte and must call completeTransmit on the SPI instance.
   */
  setSPIHandler(bus: 0 | 1, handler: (value: number) => number): void {
    if (!this.rp2040) return;
    const spi = this.rp2040.spi[bus];
    spi.onTransmit = (value: number) => {
      const response = handler(value);
      spi.completeTransmit(response);
    };
  }
}
