import { PartSimulationRegistry } from './PartSimulationRegistry';
import type { AnySimulator } from './PartSimulationRegistry';
import { RP2040Simulator } from '../RP2040Simulator';
import { getADC, setAdcVoltage } from './partUtils';

// ─── Helpers ────────────────────────────────────────────────────────────────

// ─── RGB LED (PWM-aware) ─────────────────────────────────────────────────────

/**
 * RGB LED implementation — supports both digital and PWM (analogWrite) output.
 * Falls back to digital mode if no PWM is detected.
 */
PartSimulationRegistry.register('rgb-led', {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const pinManager = (avrSimulator as any).pinManager;
        if (!pinManager) return () => { };

        const el = element as any;
        const unsubscribers: (() => void)[] = [];

        const pinR = getArduinoPinHelper('R');
        const pinG = getArduinoPinHelper('G');
        const pinB = getArduinoPinHelper('B');

        // Digital fallback
        if (pinR !== null) {
            unsubscribers.push(pinManager.onPinChange(pinR, (_: number, state: boolean) => {
                el.ledRed = state ? 255 : 0;
            }));
        }
        if (pinG !== null) {
            unsubscribers.push(pinManager.onPinChange(pinG, (_: number, state: boolean) => {
                el.ledGreen = state ? 255 : 0;
            }));
        }
        if (pinB !== null) {
            unsubscribers.push(pinManager.onPinChange(pinB, (_: number, state: boolean) => {
                el.ledBlue = state ? 255 : 0;
            }));
        }

        // PWM override — when analogWrite() is used the OCR value supersedes digital
        const pwmPins = [
            { pin: pinR, prop: 'ledRed' },
            { pin: pinG, prop: 'ledGreen' },
            { pin: pinB, prop: 'ledBlue' },
        ];
        for (const { pin, prop } of pwmPins) {
            if (pin !== null) {
                unsubscribers.push(pinManager.onPwmChange(pin, (_: number, dc: number) => {
                    el[prop] = Math.round(dc * 255);
                }));
            }
        }

        return () => unsubscribers.forEach(u => u());
    },
});

// ─── Potentiometer (rotary) ──────────────────────────────────────────────────

PartSimulationRegistry.register('potentiometer', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pin = getArduinoPinHelper('SIG');
        console.log(`[Potentiometer] attachEvents called, SIG pin resolved to: ${pin}`);
        if (pin === null) {
            console.warn('[Potentiometer] No SIG pin found — skipping ADC attachment');
            return () => { };
        }

        // Determine reference voltage based on board type
        const isRP2040 = simulator instanceof RP2040Simulator;
        const refVoltage = isRP2040 ? 3.3 : 5.0;
        console.log(`[Potentiometer] Board type: ${isRP2040 ? 'RP2040' : 'AVR'}, refV: ${refVoltage}`);

        const onInput = () => {
            const raw = parseInt((element as any).value || '0', 10);
            const volts = (raw / 1023.0) * refVoltage;
            console.log(`[Potentiometer] pin=${pin}, raw=${raw}, volts=${volts.toFixed(3)}`);
            if (!setAdcVoltage(simulator, pin, volts)) {
                console.warn(`[Potentiometer] ADC not available for pin ${pin}`);
            }
        };

        // Fire once on attach to set initial value
        onInput();

        element.addEventListener('input', onInput);
        return () => element.removeEventListener('input', onInput);
    },
});

// ─── Slide Potentiometer ─────────────────────────────────────────────────────

PartSimulationRegistry.register('slide-potentiometer', {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const arduinoPin = getArduinoPinHelper('SIG') ?? getArduinoPinHelper('OUT');
        if (arduinoPin === null) return () => { };

        const el = element as any;
        const isRP2040 = avrSimulator instanceof RP2040Simulator;
        const refVoltage = isRP2040 ? 3.3 : 5.0;

        const onInput = () => {
            const min = el.min ?? 0;
            const max = el.max ?? 1023;
            const value = el.value ?? 0;
            const normalized = (value - min) / (max - min || 1);
            const volts = normalized * refVoltage;
            setAdcVoltage(avrSimulator, arduinoPin, volts);
        };

        // Fire once on attach to set initial value
        onInput();

        element.addEventListener('input', onInput);
        return () => element.removeEventListener('input', onInput);
    },
});

// ─── Photoresistor Sensor ────────────────────────────────────────────────────

/**
 * Photoresistor sensor — the wokwi element does not emit input events,
 * so we simulate light level with a slider drawn via the component's
 * luminance property when available, or simply set a mid-range voltage.
 *
 * The element exposes `ledDO` and `ledPower` for display only.
 * We inject a static mid-range voltage on the AO pin so analogRead()
 * returns a valid value. Users can modify the element's `value` attribute.
 */
PartSimulationRegistry.register('photoresistor-sensor', {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const pinAO = getArduinoPinHelper('AO') ?? getArduinoPinHelper('A0');
        const pinDO = getArduinoPinHelper('DO') ?? getArduinoPinHelper('D0');
        const pinManager = (avrSimulator as any).pinManager;

        const unsubscribers: (() => void)[] = [];

        // Inject initial mid-range voltage (simulate moderate light)
        if (pinAO !== null) {
            setAdcVoltage(avrSimulator, pinAO, 2.5);
        }

        // Watch element's 'input' events in case the element supports it
        const onInput = () => {
            const val = (element as any).value;
            if (val !== undefined && pinAO !== null) {
                const volts = (val / 1023.0) * 5.0;
                setAdcVoltage(avrSimulator, pinAO, volts);
            }
        };
        element.addEventListener('input', onInput);
        unsubscribers.push(() => element.removeEventListener('input', onInput));

        // DO (digital output) — if connected, update element's LED indicator
        if (pinDO !== null && pinManager) {
            unsubscribers.push(pinManager.onPinChange(pinDO, (_: number, state: boolean) => {
                (element as any).ledDO = state;
            }));
        }

        return () => unsubscribers.forEach(u => u());
    },
});

// ─── Analog Joystick ─────────────────────────────────────────────────────────

/**
 * Analog Joystick — two axes (xValue/yValue 0-1023) + button press
 * Wokwi pins: VRX (X axis), VRY (Y axis), SW (button)
 */
PartSimulationRegistry.register('analog-joystick', {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const pinX   = getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT');
        const pinY   = getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT');
        const pinSW  = getArduinoPinHelper('SW');
        const el = element as any;

        // Center position is mid-range (~2.5V)
        if (pinX !== null) setAdcVoltage(avrSimulator, pinX, 2.5);
        if (pinY !== null) setAdcVoltage(avrSimulator, pinY, 2.5);

        const onMove = () => {
            // xValue / yValue are 0-1023
            if (pinX !== null) {
                const vx = ((el.xValue ?? 512) / 1023.0) * 5.0;
                setAdcVoltage(avrSimulator, pinX, vx);
            }
            if (pinY !== null) {
                const vy = ((el.yValue ?? 512) / 1023.0) * 5.0;
                setAdcVoltage(avrSimulator, pinY, vy);
            }
        };

        const onPress = () => {
            if (pinSW !== null) avrSimulator.setPinState(pinSW, false); // Active LOW
            el.pressed = true;
        };
        const onRelease = () => {
            if (pinSW !== null) avrSimulator.setPinState(pinSW, true);
            el.pressed = false;
        };

        element.addEventListener('input', onMove);
        element.addEventListener('joystick-move', onMove);
        element.addEventListener('button-press', onPress);
        element.addEventListener('button-release', onRelease);

        return () => {
            element.removeEventListener('input', onMove);
            element.removeEventListener('joystick-move', onMove);
            element.removeEventListener('button-press', onPress);
            element.removeEventListener('button-release', onRelease);
        };
    },
});

// ─── Servo ───────────────────────────────────────────────────────────────────

/**
 * Servo motor — reads OCR1A and ICR1 to calculate pulse width and angle.
 *
 * Standard RC servo protocol:
 *   - 50 Hz signal (20 ms period)
 *   - Pulse width 1 ms → 0°, 1.5 ms → 90°, 2 ms → 180°
 *
 * With Timer1, prescaler=8, F_CPU=16MHz:
 *   - ICR1 = 20000 for 50Hz
 *   - OCR1A = 1000 → 0°, 1500 → 90°, 2000 → 180°
 *
 * We poll these registers every animation frame via a requestAnimationFrame loop.
 */
PartSimulationRegistry.register('servo', {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1');
        const el = element as any;

        // OCR1A low byte = 0x88, OCR1A high byte = 0x89
        // ICR1L = 0x86, ICR1H = 0x87
        const OCR1AL = 0x88;
        const OCR1AH = 0x89;
        const ICR1L  = 0x86;
        const ICR1H  = 0x87;

        let rafId: number | null = null;
        let lastOcr1a = -1;

        const poll = () => {
            const cpu = (avrSimulator as any).cpu;
            if (!cpu) { rafId = requestAnimationFrame(poll); return; }

            const ocr1a = cpu.data[OCR1AL] | (cpu.data[OCR1AH] << 8);
            if (ocr1a !== lastOcr1a) {
                lastOcr1a = ocr1a;
                const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8);

                // Calculate pulse width in microseconds
                // prescaler 8, F_CPU 16MHz → 1 tick = 0.5µs
                // pulse_us = ocr1a * 0.5
                // But also handle prescaler 64 (1 tick = 4µs) and default ICR1 detection
                let pulseUs: number;
                if (icr1 > 0) {
                    // Proportional to ICR1 period (assume 20ms period)
                    pulseUs = 1000 + (ocr1a / icr1) * 1000;
                } else {
                    // Fallback: prescaler 8
                    pulseUs = ocr1a * 0.5;
                }

                // Clamp to 1000-2000µs and map to 0-180°
                const clamped = Math.max(1000, Math.min(2000, pulseUs));
                const angle = Math.round(((clamped - 1000) / 1000) * 180);
                el.angle = angle;
            }

            // Also support PWM duty cycle approach via PinManager
            if (pinSIG !== null) {
                const pinManager = (avrSimulator as any).pinManager;
                // Only override angle if cpu-based approach doesn't work
                // (ICR1 = 0 means Timer1 not configured as servo)
                const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8);
                if (icr1 === 0 && pinManager) {
                    const dc = pinManager.getPwmValue(pinSIG);
                    if (dc > 0) {
                        el.angle = Math.round(dc * 180);
                    }
                }
            }

            rafId = requestAnimationFrame(poll);
        };

        rafId = requestAnimationFrame(poll);

        return () => {
            if (rafId !== null) cancelAnimationFrame(rafId);
        };
    },
});

// ─── Buzzer ──────────────────────────────────────────────────────────────────

/**
 * Buzzer — uses Web Audio API to generate a tone.
 *
 * Reads OCR2A (Timer2 CTC mode) to determine frequency:
 *   f = F_CPU / (2 × prescaler × (OCR2A + 1))
 *
 * Prescaler detected from TCCR2B[2:0] bits.
 * Activates when duty cycle > 0 (pin is driven HIGH).
 */
PartSimulationRegistry.register('buzzer', {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const pinSIG = getArduinoPinHelper('1') ?? getArduinoPinHelper('+') ?? getArduinoPinHelper('POS');
        const pinManager = (avrSimulator as any).pinManager;

        let audioCtx: AudioContext | null = null;
        let oscillator: OscillatorNode | null = null;
        let gainNode: GainNode | null = null;
        let isSounding = false;
        const el = element as any;

        // Timer2 register addresses
        const OCR2A  = 0xB3;
        const TCCR2B = 0xB1;
        const F_CPU  = 16_000_000;

        const prescalerTable: Record<number, number> = {
            1: 1, 2: 8, 3: 32, 4: 64, 5: 128, 6: 256, 7: 1024,
        };

        function getFrequency(cpu: any): number {
            const ocr2a   = cpu.data[OCR2A] ?? 0;
            const tccr2b  = cpu.data[TCCR2B] ?? 0;
            const csField = tccr2b & 0x07;
            const prescaler = prescalerTable[csField] ?? 64;
            // CTC mode: f = F_CPU / (2 × prescaler × (OCR2A + 1))
            return F_CPU / (2 * prescaler * (ocr2a + 1));
        }

        function startTone(freq: number) {
            if (!audioCtx) {
                audioCtx = new AudioContext();
                gainNode = audioCtx.createGain();
                gainNode.gain.value = 0.1;
                gainNode.connect(audioCtx.destination);
            }
            // Browser autoplay policy: AudioContext starts in 'suspended' state
            // until a user gesture has occurred. Resume it here so sound plays.
            if (audioCtx.state === 'suspended') {
                audioCtx.resume();
            }
            if (oscillator) {
                oscillator.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.01);
                return;
            }
            oscillator = audioCtx.createOscillator();
            oscillator.type = 'square';
            oscillator.frequency.value = freq;
            oscillator.connect(gainNode!);
            oscillator.start();
            isSounding = true;
            if (el.playing !== undefined) el.playing = true;
        }

        function stopTone() {
            if (oscillator) {
                oscillator.stop();
                oscillator.disconnect();
                oscillator = null;
            }
            isSounding = false;
            if (el.playing !== undefined) el.playing = false;
        }

        // Poll via PWM duty cycle on the buzzer pin
        const unsubscribers: (() => void)[] = [];

        if (pinSIG !== null && pinManager) {
            unsubscribers.push(pinManager.onPwmChange(pinSIG, (_: number, dc: number) => {
                const cpu = (avrSimulator as any).cpu;
                if (dc > 0) {
                    const freq = cpu ? getFrequency(cpu) : 440;
                    startTone(Math.max(20, Math.min(20000, freq)));
                } else {
                    stopTone();
                }
            }));

            // Also respond to digital HIGH/LOW (tone() toggles the pin)
            unsubscribers.push(pinManager.onPinChange(pinSIG, (_: number, state: boolean) => {
                if (!isSounding && state) {
                    const cpu = (avrSimulator as any).cpu;
                    const freq = cpu ? getFrequency(cpu) : 440;
                    startTone(Math.max(20, Math.min(20000, freq)));
                } else if (isSounding && !state) {
                    // Don't stop on every LOW — tone() generates a square wave
                    // We stop only when duty cycle drops to 0 via onPwmChange
                }
            }));
        }

        return () => {
            stopTone();
            if (audioCtx) { audioCtx.close(); audioCtx = null; }
            unsubscribers.forEach(u => u());
        };
    },
});

// ─── LCD 1602 / 2004 ─────────────────────────────────────────────────────────

function createLcdSimulation(cols: number, rows: number) {
    return {
        attachEvents: (element: HTMLElement, avrSimulator: AnySimulator, getArduinoPinHelper: (pin: string) => number | null) => {
            const el = element as any;

            const ddram = new Uint8Array(128).fill(0x20);
            let ddramAddress = 0;
            let entryIncrement = true;
            let displayOn = true;
            let cursorOn = false;
            let blinkOn = false;
            let nibbleState: 'high' | 'low' = 'high';
            let highNibble = 0;
            let initialized = false;
            let initCount = 0;

            let rsState = false;
            let eState = false;
            let d4State = false;
            let d5State = false;
            let d6State = false;
            let d7State = false;

            const lineOffsets = rows >= 4
                ? [0x00, 0x40, 0x14, 0x54]
                : [0x00, 0x40];

            function ddramToLinear(addr: number): number {
                for (let row = 0; row < rows; row++) {
                    const offset = lineOffsets[row];
                    if (addr >= offset && addr < offset + cols) {
                        return row * cols + (addr - offset);
                    }
                }
                return -1;
            }

            function refreshDisplay() {
                if (!displayOn) {
                    el.characters = new Uint8Array(cols * rows).fill(0x20);
                    return;
                }
                const chars = new Uint8Array(cols * rows);
                for (let row = 0; row < rows; row++) {
                    const offset = lineOffsets[row];
                    for (let col = 0; col < cols; col++) {
                        chars[row * cols + col] = ddram[offset + col];
                    }
                }
                el.characters = chars;
                el.cursor = cursorOn;
                el.blink = blinkOn;
                const cursorLinear = ddramToLinear(ddramAddress);
                if (cursorLinear >= 0) {
                    el.cursorX = cursorLinear % cols;
                    el.cursorY = Math.floor(cursorLinear / cols);
                }
            }

            function processByte(rs: boolean, data: number) {
                if (!rs) {
                    if (data & 0x80) {
                        ddramAddress = data & 0x7F;
                    } else if (data & 0x40) {
                        // CGRAM — not implemented
                    } else if (data & 0x20) {
                        initialized = true;
                    } else if (data & 0x10) {
                        const sc = (data >> 3) & 1;
                        const rl = (data >> 2) & 1;
                        if (!sc) { ddramAddress = (ddramAddress + (rl ? 1 : -1)) & 0x7F; }
                    } else if (data & 0x08) {
                        displayOn = !!(data & 0x04);
                        cursorOn  = !!(data & 0x02);
                        blinkOn   = !!(data & 0x01);
                    } else if (data & 0x04) {
                        entryIncrement = !!(data & 0x02);
                    } else if (data & 0x02) {
                        ddramAddress = 0;
                    } else if (data & 0x01) {
                        ddram.fill(0x20);
                        ddramAddress = 0;
                    }
                } else {
                    ddram[ddramAddress & 0x7F] = data;
                    ddramAddress = entryIncrement
                        ? (ddramAddress + 1) & 0x7F
                        : (ddramAddress - 1) & 0x7F;
                }
                refreshDisplay();
            }

            function onEnableFallingEdge() {
                const nibble =
                    (d4State ? 0x01 : 0) |
                    (d5State ? 0x02 : 0) |
                    (d6State ? 0x04 : 0) |
                    (d7State ? 0x08 : 0);

                if (!initialized) {
                    initCount++;
                    if (initCount >= 4) { initialized = true; nibbleState = 'high'; }
                    return;
                }

                if (nibbleState === 'high') {
                    highNibble = nibble << 4;
                    nibbleState = 'low';
                } else {
                    processByte(rsState, highNibble | nibble);
                    nibbleState = 'high';
                }
            }

            const pinRS = getArduinoPinHelper('RS');
            const pinE  = getArduinoPinHelper('E');
            const pinD4 = getArduinoPinHelper('D4');
            const pinD5 = getArduinoPinHelper('D5');
            const pinD6 = getArduinoPinHelper('D6');
            const pinD7 = getArduinoPinHelper('D7');

            const pinManager = (avrSimulator as any).pinManager;
            if (!pinManager) return () => { };

            const unsubscribers: (() => void)[] = [];

            if (pinRS !== null) unsubscribers.push(pinManager.onPinChange(pinRS, (_: number, s: boolean) => { rsState = s; }));
            if (pinD4 !== null) unsubscribers.push(pinManager.onPinChange(pinD4, (_: number, s: boolean) => { d4State = s; }));
            if (pinD5 !== null) unsubscribers.push(pinManager.onPinChange(pinD5, (_: number, s: boolean) => { d5State = s; }));
            if (pinD6 !== null) unsubscribers.push(pinManager.onPinChange(pinD6, (_: number, s: boolean) => { d6State = s; }));
            if (pinD7 !== null) unsubscribers.push(pinManager.onPinChange(pinD7, (_: number, s: boolean) => { d7State = s; }));

            if (pinE !== null) {
                unsubscribers.push(pinManager.onPinChange(pinE, (_: number, s: boolean) => {
                    const wasHigh = eState;
                    eState = s;
                    if (wasHigh && !s) onEnableFallingEdge();
                }));
            }

            refreshDisplay();
            console.log(`[LCD] ${cols}x${rows} simulation initialized`);

            return () => {
                unsubscribers.forEach(u => u());
            };
        },
    };
}

PartSimulationRegistry.register('lcd1602', createLcdSimulation(16, 2));
PartSimulationRegistry.register('lcd2004', createLcdSimulation(20, 4));
PartSimulationRegistry.register('lcd2002', createLcdSimulation(20, 2));

// ─── ILI9341 TFT Display (SPI) ───────────────────────────────────────────────

/**
 * ILI9341 TFT display simulation via hardware SPI.
 *
 * Intercepts writes to SPDR (via AVRSPI) and decodes ILI9341 commands:
 *   - 0x2A CASET  – set column address window
 *   - 0x2B PASET  – set page (row) address window
 *   - 0x2C RAMWR  – stream RGB-565 pixel data
 *   - 0x01 SWRESET – clear display
 *   - All others are silently accepted (init sequences, DISPON, MADCTL…)
 *
 * DC/RS pin: LOW = command byte, HIGH = data bytes.
 */
const ili9341Simulation = {
    attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
        const el = element as any;
        const pinManager = (avrSimulator as any).pinManager;
        const spi = (avrSimulator as any).spi;

        if (!pinManager || !spi) {
            console.warn('[ILI9341] pinManager or SPI peripheral not available');
            return () => {};
        }

        // ── Canvas setup ──────────────────────────────────────────────────
        const SCREEN_W = 240;
        const SCREEN_H = 320;

        const initCanvas = (): CanvasRenderingContext2D | null => {
            // el.canvas is the getter defined in ili9341-element.ts:
            //   get canvas() { return this.shadowRoot?.querySelector('canvas'); }
            // The element already sets width=240 height=320 in its LitElement template.
            const canvas = el.canvas as HTMLCanvasElement | null;
            if (!canvas) return null;
            return canvas.getContext('2d');
        };

        let ctx = initCanvas();

        const onCanvasReady = () => { ctx = initCanvas(); };
        el.addEventListener('canvas-ready', onCanvasReady);

        // ── Shared ImageData buffer ───────────────────────────────────────
        // Accumulate pixels here; flush to canvas once per animation frame.
        let imageData: ImageData | null = null;

        const getOrCreateImageData = (): ImageData => {
            if (!ctx) ctx = initCanvas();
            if (!imageData && ctx) imageData = ctx.createImageData(SCREEN_W, SCREEN_H);
            return imageData!;
        };

        let pendingFlush = false;
        let rafId: number | null = null;

        const scheduleFlush = () => {
            if (rafId !== null) return;
            rafId = requestAnimationFrame(() => {
                rafId = null;
                if (pendingFlush && ctx && imageData) {
                    ctx.putImageData(imageData, 0, 0);
                    pendingFlush = false;
                }
            });
        };

        // ── ILI9341 state ─────────────────────────────────────────────────
        let colStart = 0, colEnd = SCREEN_W - 1;
        let rowStart = 0, rowEnd = SCREEN_H - 1;
        let curX = 0, curY = 0;

        let currentCmd = -1;
        let dataBytes: number[] = [];
        let inRamWrite = false;
        let pixelHiByte = 0;
        let pixelByteCount = 0;

        // ── DC pin tracking ───────────────────────────────────────────────
        let dcState = false; // LOW = command, HIGH = data
        const pinDC = getArduinoPinHelper('D/C');

        const unsubscribers: (() => void)[] = [];

        if (pinDC !== null) {
            unsubscribers.push(
                pinManager.onPinChange(pinDC, (_: number, s: boolean) => { dcState = s; })
            );
        }

        // ── Pixel writer ──────────────────────────────────────────────────
        const writePixel = (hi: number, lo: number) => {
            if (curX > colEnd || curY > rowEnd || curY >= SCREEN_H || curX >= SCREEN_W) return;

            const id = getOrCreateImageData();
            const color = (hi << 8) | lo;
            const r = ((color >> 11) & 0x1F) * 8;
            const g = ((color >> 5)  & 0x3F) * 4;
            const b = ( color        & 0x1F) * 8;

            const idx = (curY * SCREEN_W + curX) * 4;
            id.data[idx]     = r;
            id.data[idx + 1] = g;
            id.data[idx + 2] = b;
            id.data[idx + 3] = 255;

            pendingFlush = true;
            curX++;
            if (curX > colEnd) {
                curX = colStart;
                curY++;
            }
        };

        // ── Command / data processing ─────────────────────────────────────
        const processCommand = (cmd: number) => {
            currentCmd = cmd;
            dataBytes   = [];
            inRamWrite  = (cmd === 0x2C);
            pixelByteCount = 0;

            if (cmd === 0x01) { // SWRESET – clear framebuffer
                colStart = 0; colEnd = SCREEN_W - 1;
                rowStart = 0; rowEnd = SCREEN_H - 1;
                curX = 0;     curY  = 0;
                imageData = null;
                if (ctx) ctx.clearRect(0, 0, SCREEN_W, SCREEN_H);
            }
        };

        const processData = (value: number) => {
            if (inRamWrite) {
                // RGB-565: two bytes per pixel
                if (pixelByteCount === 0) {
                    pixelHiByte = value;
                    pixelByteCount = 1;
                } else {
                    writePixel(pixelHiByte, value);
                    scheduleFlush();
                    pixelByteCount = 0;
                }
                return;
            }

            dataBytes.push(value);
            switch (currentCmd) {
                case 0x2A: // CASET – column address set
                    if (dataBytes.length === 2) colStart = (dataBytes[0] << 8) | dataBytes[1];
                    if (dataBytes.length === 4) { colEnd = (dataBytes[2] << 8) | dataBytes[3]; curX = colStart; }
                    break;
                case 0x2B: // PASET – page address set
                    if (dataBytes.length === 2) rowStart = (dataBytes[0] << 8) | dataBytes[1];
                    if (dataBytes.length === 4) { rowEnd = (dataBytes[2] << 8) | dataBytes[3]; curY = rowStart; }
                    break;
                // All other commands (DISPON, MADCTL, COLMOD…) just buffer data
            }
        };

        // ── Intercept SPI ─────────────────────────────────────────────────
        const prevOnByte = spi.onByte.bind(spi);

        spi.onByte = (value: number) => {
            if (!dcState) {
                processCommand(value);
            } else {
                processData(value);
            }
            spi.completeTransfer(0xFF); // Unblock CPU immediately
        };

        console.log(`[ILI9341] SPI simulation ready. DC→pin${pinDC}`);

        // ── Cleanup ───────────────────────────────────────────────────────
        return () => {
            spi.onByte = prevOnByte;
            if (rafId !== null) cancelAnimationFrame(rafId);
            el.removeEventListener('canvas-ready', onCanvasReady);
            unsubscribers.forEach(u => u());
        };
    },
};

PartSimulationRegistry.register('ili9341', ili9341Simulation);
// board-ili9341-cap-touch (Wokwi type) maps to 'ili9341-cap-touch' metadataId — same SPI simulation
PartSimulationRegistry.register('ili9341-cap-touch', ili9341Simulation);
