/**
 * SensorParts.ts — Simulation logic for sensors, stepper motor, and NeoPixel devices.
 *
 * Implements:
 *  - tilt-switch
 *  - ntc-temperature-sensor
 *  - gas-sensor (MQ-series)
 *  - flame-sensor
 *  - heart-beat-sensor
 *  - big-sound-sensor
 *  - small-sound-sensor
 *  - stepper-motor (NEMA full-step decode)
 *  - led-ring (WS2812B NeoPixel ring)
 *  - neopixel-matrix (WS2812B NeoPixel matrix)
 */

import { PartSimulationRegistry } from './PartSimulationRegistry';
import { setAdcVoltage } from './partUtils';

// ─── Tilt Switch ─────────────────────────────────────────────────────────────

/**
 * Tilt switch — click the element to toggle between tilted (OUT HIGH) and
 * upright (OUT LOW). Starts upright (LOW).
 */
PartSimulationRegistry.register('tilt-switch', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pin = getArduinoPinHelper('OUT');
        if (pin === null) return () => {};

        let tilted = false;

        const onClick = () => {
            tilted = !tilted;
            simulator.setPinState(pin, tilted);
            console.log(`[TiltSwitch] pin ${pin} → ${tilted ? 'HIGH' : 'LOW'}`);
        };

        // Start LOW (upright)
        simulator.setPinState(pin, false);
        element.addEventListener('click', onClick);
        return () => element.removeEventListener('click', onClick);
    },
});

// ─── NTC Temperature Sensor ──────────────────────────────────────────────────

/**
 * NTC thermistor sensor — injects a mid-range analog voltage on the OUT pin
 * representing room temperature (~25°C, ~2.5V on a 5V divider).
 * Listens to `input` events in case the element ever gains a drag slider.
 */
PartSimulationRegistry.register('ntc-temperature-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pin = getArduinoPinHelper('OUT');
        if (pin === null) return () => {};

        // Room temperature default (2.5V = mid-range)
        setAdcVoltage(simulator, pin, 2.5);

        const onInput = () => {
            const val = (element as any).value;
            if (val !== undefined) {
                setAdcVoltage(simulator, pin, (val / 1023.0) * 5.0);
            }
        };
        element.addEventListener('input', onInput);
        return () => element.removeEventListener('input', onInput);
    },
});

// ─── Gas Sensor (MQ-series) ──────────────────────────────────────────────────

/**
 * Gas sensor — injects a low baseline voltage on AOUT (clean air),
 * shows power LED. When Arduino drives DOUT → updates threshold LED D0.
 */
PartSimulationRegistry.register('gas-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinAOUT = getArduinoPinHelper('AOUT');
        const pinDOUT = getArduinoPinHelper('DOUT');
        const pinManager = (simulator as any).pinManager;

        const el = element as any;
        el.ledPower = true;

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

        // Inject baseline analog voltage (1.5V ≈ clean air / low gas)
        if (pinAOUT !== null) {
            setAdcVoltage(simulator, pinAOUT, 1.5);
        }

        // DOUT from Arduino → threshold LED indicator
        if (pinDOUT !== null && pinManager) {
            unsubscribers.push(
                pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
                    el.ledD0 = state;
                })
            );
        }

        // Allow element to update analog value if it fires input events
        const onInput = () => {
            const val = (el as any).value;
            if (val !== undefined && pinAOUT !== null) {
                setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
            }
        };
        element.addEventListener('input', onInput);
        unsubscribers.push(() => element.removeEventListener('input', onInput));

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

// ─── Flame Sensor ────────────────────────────────────────────────────────────

/**
 * Flame sensor — injects a low baseline voltage on AOUT (no flame),
 * shows power LED. Arduino driving DOUT → updates signal LED.
 */
PartSimulationRegistry.register('flame-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinAOUT = getArduinoPinHelper('AOUT');
        const pinDOUT = getArduinoPinHelper('DOUT');
        const pinManager = (simulator as any).pinManager;

        const el = element as any;
        el.ledPower = true;

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

        if (pinAOUT !== null) {
            setAdcVoltage(simulator, pinAOUT, 1.5);
        }

        if (pinDOUT !== null && pinManager) {
            unsubscribers.push(
                pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
                    el.ledSignal = state;
                })
            );
        }

        const onInput = () => {
            const val = (el as any).value;
            if (val !== undefined && pinAOUT !== null) {
                setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
            }
        };
        element.addEventListener('input', onInput);
        unsubscribers.push(() => element.removeEventListener('input', onInput));

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

// ─── Heart Beat Sensor ───────────────────────────────────────────────────────

/**
 * Heart beat sensor — simulates a 60 BPM signal on OUT pin.
 * Every 1000ms: briefly pulls OUT HIGH for 100ms, then LOW again.
 */
PartSimulationRegistry.register('heart-beat-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pin = getArduinoPinHelper('OUT');
        if (pin === null) return () => {};

        simulator.setPinState(pin, false);

        const intervalId = setInterval(() => {
            simulator.setPinState(pin, true); // pulse HIGH
            setTimeout(() => simulator.setPinState(pin, false), 100);
        }, 1000);

        return () => clearInterval(intervalId);
    },
});

// ─── Big Sound Sensor ────────────────────────────────────────────────────────

/**
 * Big sound sensor (FC-04) — injects mid-range analog on AOUT,
 * shows power LED (led2). Arduino driving DOUT → signal LED (led1).
 */
PartSimulationRegistry.register('big-sound-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinAOUT = getArduinoPinHelper('AOUT');
        const pinDOUT = getArduinoPinHelper('DOUT');
        const pinManager = (simulator as any).pinManager;

        const el = element as any;
        el.led2 = true; // Power LED

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

        if (pinAOUT !== null) {
            setAdcVoltage(simulator, pinAOUT, 2.5);
        }

        if (pinDOUT !== null && pinManager) {
            unsubscribers.push(
                pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
                    el.led1 = state;
                })
            );
        }

        const onInput = () => {
            const val = (el as any).value;
            if (val !== undefined && pinAOUT !== null) {
                setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
            }
        };
        element.addEventListener('input', onInput);
        unsubscribers.push(() => element.removeEventListener('input', onInput));

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

// ─── Small Sound Sensor ──────────────────────────────────────────────────────

/**
 * Small sound sensor (KY-038) — injects mid-range analog on AOUT,
 * shows power LED. Arduino driving DOUT → signal LED.
 */
PartSimulationRegistry.register('small-sound-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinAOUT = getArduinoPinHelper('AOUT');
        const pinDOUT = getArduinoPinHelper('DOUT');
        const pinManager = (simulator as any).pinManager;

        const el = element as any;
        el.ledPower = true;

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

        if (pinAOUT !== null) {
            setAdcVoltage(simulator, pinAOUT, 2.5);
        }

        if (pinDOUT !== null && pinManager) {
            unsubscribers.push(
                pinManager.onPinChange(pinDOUT, (_: number, state: boolean) => {
                    el.ledSignal = state;
                })
            );
        }

        const onInput = () => {
            const val = (el as any).value;
            if (val !== undefined && pinAOUT !== null) {
                setAdcVoltage(simulator, pinAOUT, (val / 1023.0) * 5.0);
            }
        };
        element.addEventListener('input', onInput);
        unsubscribers.push(() => element.removeEventListener('input', onInput));

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

// ─── Stepper Motor (NEMA full-step decode) ───────────────────────────────────

/**
 * Stepper motor — monitors the 4 coil pins (A-, A+, B+, B-).
 * Uses a full-step lookup table to detect direction of rotation and
 * accumulates the shaft angle (1.8° per step = 200 steps per revolution).
 *
 * Full-step sequence (active-HIGH per coil):
 *   Step 0:  A+ = 1, B+ = 0, A- = 0, B- = 0
 *   Step 1:  A+ = 0, B+ = 1, A- = 0, B- = 0
 *   Step 2:  A+ = 0, B+ = 0, A- = 1, B- = 0
 *   Step 3:  A+ = 0, B+ = 0, A- = 0, B- = 1
 */
PartSimulationRegistry.register('stepper-motor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinManager = (simulator as any).pinManager;
        if (!pinManager) return () => {};

        const el = element as any;
        const STEP_ANGLE = 1.8; // degrees per step

        const pinAMinus = getArduinoPinHelper('A-');
        const pinAPlus  = getArduinoPinHelper('A+');
        const pinBPlus  = getArduinoPinHelper('B+');
        const pinBMinus = getArduinoPinHelper('B-');

        const coils = { aMinus: false, aPlus: false, bPlus: false, bMinus: false };
        let cumAngle = el.angle ?? 0;
        let prevStepIndex = -1;

        // Full-step table: index → [A+, B+, A-, B-]
        const stepTable: [boolean, boolean, boolean, boolean][] = [
            [true,  false, false, false], // step 0
            [false, true,  false, false], // step 1
            [false, false, true,  false], // step 2
            [false, false, false, true],  // step 3
        ];

        function coilToStepIndex(): number {
            for (let i = 0; i < stepTable.length; i++) {
                const [ap, bp, am, bm] = stepTable[i];
                if (coils.aPlus === ap && coils.bPlus === bp &&
                    coils.aMinus === am && coils.bMinus === bm) {
                    return i;
                }
            }
            return -1; // energized coil pattern not in full-step table
        }

        function onCoilChange() {
            const idx = coilToStepIndex();
            if (idx < 0) return; // half-step or off state — ignore
            if (prevStepIndex < 0) { prevStepIndex = idx; return; }

            const diff = (idx - prevStepIndex + 4) % 4;
            if (diff === 1) {
                cumAngle += STEP_ANGLE;
            } else if (diff === 3) {
                cumAngle -= STEP_ANGLE;
            }
            prevStepIndex = idx;
            el.angle = ((cumAngle % 360) + 360) % 360;
        }

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

        if (pinAMinus !== null) {
            unsubscribers.push(pinManager.onPinChange(pinAMinus, (_: number, s: boolean) => {
                coils.aMinus = s; onCoilChange();
            }));
        }
        if (pinAPlus !== null) {
            unsubscribers.push(pinManager.onPinChange(pinAPlus, (_: number, s: boolean) => {
                coils.aPlus = s; onCoilChange();
            }));
        }
        if (pinBPlus !== null) {
            unsubscribers.push(pinManager.onPinChange(pinBPlus, (_: number, s: boolean) => {
                coils.bPlus = s; onCoilChange();
            }));
        }
        if (pinBMinus !== null) {
            unsubscribers.push(pinManager.onPinChange(pinBMinus, (_: number, s: boolean) => {
                coils.bMinus = s; onCoilChange();
            }));
        }

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

// ─── WS2812B NeoPixel decode helper ──────────────────────────────────────────

/**
 * Decode WS2812B bit-stream from DIN pin changes for NeoPixel devices.
 *
 * Protocol (800 kHz, 16 MHz AVR: 1 tick = 62.5 ns):
 *   - bit 0: HIGH for ~0.35µs (≤8 cycles); LOW for ~0.80µs
 *   - bit 1: HIGH for ~0.70µs (>8 cycles); LOW for ~0.40µs
 *   - RESET: LOW for >50µs (≥800 cycles)
 *
 * We measure HIGH pulse_width via cpu.cycles difference.
 * 8 bits (GRB order from WS2812B) → 1 byte; 3 bytes → 1 pixel.
 */
function createNeopixelDecoder(
    simulator: any,
    pinDIN: number,
    onPixel: (index: number, r: number, g: number, b: number) => void,
): () => void {
    const pinManager = simulator.pinManager;
    if (!pinManager) return () => {};

    const CPU_CYCLES_PER_US = 16; // 16 MHz
    const RESET_CYCLES = 800;     // 50µs × 16 cycles/µs
    const BIT1_THRESHOLD = 8;     // ~0.5µs threshold between bit-0 and bit-1

    let lastRisingCycle = 0;
    let lastFallingCycle = 0;
    let lastHigh = false;

    let bitBuf = 0;
    let bitsCollected = 0;
    let byteBuf: number[] = [];
    let pixelIndex = 0;

    const unsub = pinManager.onPinChange(pinDIN, (_: number, high: boolean) => {
        const cpu = simulator.cpu ?? (simulator as any).cpu;
        const now: number = cpu?.cycles ?? 0;

        if (high) {
            // Rising edge — check if preceding LOW was a RESET
            const lowDur = now - lastFallingCycle;
            if (lowDur > RESET_CYCLES) {
                // RESET pulse received — flush and restart
                pixelIndex = 0;
                byteBuf = [];
                bitBuf = 0;
                bitsCollected = 0;
            }
            lastRisingCycle = now;
            lastHigh = true;
        } else {
            // Falling edge — measure HIGH pulse width
            if (lastHigh) {
                const highDur = now - lastRisingCycle;
                const bit = highDur > BIT1_THRESHOLD ? 1 : 0;

                // WS2812B transmits MSB first
                bitBuf = (bitBuf << 1) | bit;
                bitsCollected++;

                if (bitsCollected === 8) {
                    byteBuf.push(bitBuf & 0xFF);
                    bitBuf = 0;
                    bitsCollected = 0;

                    if (byteBuf.length === 3) {
                        // WS2812B byte order is GRB
                        const g = byteBuf[0];
                        const r = byteBuf[1];
                        const b = byteBuf[2];
                        onPixel(pixelIndex++, r, g, b);
                        byteBuf = [];
                    }
                }
            }
            lastFallingCycle = now;
            lastHigh = false;
        }
    });

    return unsub;
}

// ─── LED Ring (WS2812B NeoPixel ring) ────────────────────────────────────────

PartSimulationRegistry.register('led-ring', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinDIN = getArduinoPinHelper('DIN');
        if (pinDIN === null) return () => {};

        const el = element as any;

        const unsub = createNeopixelDecoder(
            (simulator as any),
            pinDIN,
            (index, r, g, b) => {
                try {
                    el.setPixel(index, { r, g, b });
                } catch (_) {
                    // setPixel not yet available (element not upgraded) — ignore
                }
            },
        );

        return unsub;
    },
});

// ─── NeoPixel Matrix (WS2812B matrix grid) ────────────────────────────────────

PartSimulationRegistry.register('neopixel-matrix', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinDIN = getArduinoPinHelper('DIN');
        if (pinDIN === null) return () => {};

        const el = element as any;

        const unsub = createNeopixelDecoder(
            (simulator as any),
            pinDIN,
            (index, r, g, b) => {
                // cols is set by the element property (default 8)
                const cols: number = el.cols ?? 8;
                const row = Math.floor(index / cols);
                const col = index % cols;
                try {
                    el.setPixel(row, col, { r, g, b });
                } catch (_) {
                    // ignore
                }
            },
        );

        return unsub;
    },
});

// ─── Single NeoPixel (WS2812B) ───────────────────────────────────────────────

/**
 * Single addressable RGB LED — decodes the WS2812B data stream on DIN
 * and updates the element's r/g/b properties (0–1 range).
 */
PartSimulationRegistry.register('neopixel', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pinDIN = getArduinoPinHelper('DIN');
        if (pinDIN === null) return () => {};

        const el = element as any;

        const unsub = createNeopixelDecoder(
            (simulator as any),
            pinDIN,
            (_index, r, g, b) => {
                el.r = r / 255;
                el.g = g / 255;
                el.b = b / 255;
            },
        );

        return unsub;
    },
});

// ─── PIR Motion Sensor ───────────────────────────────────────────────────────

/**
 * PIR motion sensor — click the element to simulate a motion event.
 * OUT pin goes HIGH for 3 seconds then returns LOW.
 */
PartSimulationRegistry.register('pir-motion-sensor', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const pin = getArduinoPinHelper('OUT');
        if (pin === null) return () => {};

        simulator.setPinState(pin, false); // idle LOW

        let timer: ReturnType<typeof setTimeout> | null = null;

        const onClick = () => {
            if (timer !== null) clearTimeout(timer);
            simulator.setPinState(pin, true); // motion detected → HIGH
            console.log('[PIR] Motion detected → OUT HIGH');
            timer = setTimeout(() => {
                simulator.setPinState(pin, false);
                timer = null;
                console.log('[PIR] Motion ended → OUT LOW');
            }, 3000);
        };

        element.addEventListener('click', onClick);
        return () => {
            element.removeEventListener('click', onClick);
            if (timer !== null) clearTimeout(timer);
        };
    },
});

// ─── KS2E-M-DC5 Relay ────────────────────────────────────────────────────────

/**
 * Dual-coil relay — listens for COIL1/COIL2 pin state changes.
 * In a typical Arduino circuit the Arduino drives the coil and the relay
 * switches a separate load circuit; no electrical feedback is needed.
 */
PartSimulationRegistry.register('ks2e-m-dc5', {
    onPinStateChange: (pinName, state, _element) => {
        if (pinName === 'COIL1' || pinName === 'COIL2') {
            console.log(`[Relay KS2E] ${pinName} → ${state ? 'ACTIVATED' : 'RELEASED'}`);
        }
    },
});

// ─── HC-SR04 Ultrasonic Distance Sensor ──────────────────────────────────────

/**
 * Ultrasonic sensor — monitors the TRIG pin.
 * When TRIG goes HIGH the sensor responds with an ECHO HIGH pulse
 * simulating an object at ~10 cm (≈582 µs echo width → 1 ms real-time).
 */
PartSimulationRegistry.register('hc-sr04', {
    attachEvents: (element, simulator, getArduinoPinHelper) => {
        const trigPin = getArduinoPinHelper('TRIG');
        const echoPin = getArduinoPinHelper('ECHO');
        if (trigPin === null || echoPin === null) return () => {};

        simulator.setPinState(echoPin, false); // ECHO LOW initially

        let echoTimer: ReturnType<typeof setTimeout> | null = null;

        const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => {
            if (state) {
                // TRIG HIGH — fire ECHO pulse after ~1 ms
                if (echoTimer !== null) clearTimeout(echoTimer);
                echoTimer = setTimeout(() => {
                    simulator.setPinState(echoPin, true); // ECHO HIGH
                    console.log('[HC-SR04] ECHO HIGH (10 cm)');
                    echoTimer = setTimeout(() => {
                        simulator.setPinState(echoPin, false); // ECHO LOW
                        echoTimer = null;
                    }, 1); // 1 ms ≈ 582 µs → ~10 cm
                }, 1);
            }
        });

        return () => {
            cleanup();
            if (echoTimer !== null) clearTimeout(echoTimer);
        };
    },
});
