import { pythonGenerator, Order } from 'blockly/python';
import { getDeviceId } from '../devices/registry.js';

const DEVICE = () => getDeviceId();

// micro:bit pin name: pin0..pin20 (clamp to valid range)
function mbPin(pin) {
  const n = Math.max(0, Math.min(20, parseInt(pin, 10) || 0));
  return `pin${n}`;
}

// ─── Pin I/O ──────────────────────────────────────────────

function ensurePinVar(pin, mode) {
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const varName = `pin_${pin}`;
  const modeStr = mode === 'IN' ? 'Pin.IN' : 'Pin.OUT';
  pythonGenerator.definitions_[`pin_init_${pin}`] = `${varName} = Pin(${pin}, ${modeStr})`;
  return varName;
}

pythonGenerator.forBlock['pin_set_mode'] = function (block) {
  const pin = block.getFieldValue('PIN');
  if (DEVICE() === 'microbit') {
    return ''; // micro:bit has no explicit set_mode
  }
  const mode = block.getFieldValue('MODE');
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const varName = `pin_${pin}`;
  const pyMode = `Pin.${mode}`;
  pythonGenerator.definitions_[`pin_init_${pin}`] = `${varName} = Pin(${pin}, ${pyMode})`;
  return '';
};

pythonGenerator.forBlock['pin_digital_write'] = function (block) {
  const pin = block.getFieldValue('PIN');
  const value = block.getFieldValue('VALUE');
  if (DEVICE() === 'microbit') {
    return `${mbPin(pin)}.write_digital(${value})\n`;
  }
  const varName = ensurePinVar(pin, 'OUT');
  return `${varName}.value(${value})\n`;
};

pythonGenerator.forBlock['pin_digital_read'] = function (block) {
  const pin = block.getFieldValue('PIN');
  if (DEVICE() === 'microbit') {
    return [`${mbPin(pin)}.read_digital()`, Order.FUNCTION_CALL];
  }
  const varName = ensurePinVar(pin, 'IN');
  return [`${varName}.value()`, Order.FUNCTION_CALL];
};

// ─── PWM ──────────────────────────────────────────────────

pythonGenerator.forBlock['pwm_init'] = function (block) {
  if (DEVICE() === 'microbit') return ''; // no separate init on micro:bit
  const pin = block.getFieldValue('PIN');
  const freq = block.getFieldValue('FREQ');
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  return `pwm_${pin} = PWM(Pin(${pin}), freq=${freq})\n`;
};

pythonGenerator.forBlock['pwm_set_duty'] = function (block) {
  const pin = block.getFieldValue('PIN');
  const duty = pythonGenerator.valueToCode(block, 'DUTY', Order.NONE) || '0';
  if (DEVICE() === 'microbit') {
    return `${mbPin(pin)}.write_analog(${duty})\n`;
  }
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  return `pwm_${pin}.duty(${duty})\n`;
};

pythonGenerator.forBlock['pwm_set_freq'] = function (block) {
  if (DEVICE() === 'microbit') return ''; // micro:bit doesn't support changing PWM freq per-pin
  const pin = block.getFieldValue('PIN');
  const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '1000';
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  return `pwm_${pin}.freq(${freq})\n`;
};

// ─── ADC ──────────────────────────────────────────────────

pythonGenerator.forBlock['adc_read'] = function (block) {
  const pin = block.getFieldValue('PIN');
  if (DEVICE() === 'microbit') {
    return [`${mbPin(pin)}.read_analog()`, Order.FUNCTION_CALL];
  }
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  pythonGenerator.definitions_[`adc_init_${pin}`] = `adc_${pin} = ADC(Pin(${pin}))`;
  return [`adc_${pin}.read()`, Order.FUNCTION_CALL];
};

// ─── Sensors (sonar / HC-SR04) ────────────────────────────

const SONAR_HELPER_MACHINE = `def _sonar_cm(trig, echo):
    from machine import Pin, time_pulse_us
    import time
    t = Pin(int(trig), Pin.OUT)
    e = Pin(int(echo), Pin.IN)
    t.off()
    time.sleep_us(2)
    t.on()
    time.sleep_us(10)
    t.off()
    d = time_pulse_us(e, 1, 30000)
    return round(d / 58.0, 1) if d >= 0 else -1
`;

const SONAR_HELPER_MICROBIT = `def _sonar_cm(trig, echo):
    import utime
    import microbit
    trig, echo = int(trig), int(echo)
    t = getattr(microbit, 'pin' + str(trig), microbit.pin0)
    e = getattr(microbit, 'pin' + str(echo), microbit.pin0)
    t.write_digital(0)
    utime.sleep_us(2)
    t.write_digital(1)
    utime.sleep_us(10)
    t.write_digital(0)
    start = utime.ticks_us()
    while e.read_digital() == 0:
        if utime.ticks_diff(utime.ticks_us(), start) > 20000:
            return -1
    t0 = utime.ticks_us()
    while e.read_digital() == 1:
        if utime.ticks_diff(utime.ticks_us(), t0) > 20000:
            return -1
    t1 = utime.ticks_us()
    duration = utime.ticks_diff(t1, t0)
    return round(duration / 58.0, 1)
`;

pythonGenerator.forBlock['sonar_distance'] = function (block) {
  const trig = pythonGenerator.valueToCode(block, 'TRIG', Order.NONE) || '2';
  const echo = pythonGenerator.valueToCode(block, 'ECHO', Order.NONE) || '3';
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['sonar_ultrasonic'] = SONAR_HELPER_MICROBIT;
  } else {
    pythonGenerator.definitions_['import_time'] = 'import time';
    pythonGenerator.definitions_['sonar_ultrasonic'] = SONAR_HELPER_MACHINE;
  }
  return [`_sonar_cm(${trig}, ${echo})`, Order.FUNCTION_CALL];
};

// ─── Time ─────────────────────────────────────────────────

pythonGenerator.forBlock['sleep_seconds'] = function (block) {
  const seconds = pythonGenerator.valueToCode(block, 'SECONDS', Order.NONE) || '1';
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_microbit_sleep'] = 'from microbit import sleep';
    return `sleep(${seconds} * 1000)\n`;
  }
  pythonGenerator.definitions_['import_time'] = 'import time';
  return `time.sleep(${seconds})\n`;
};

pythonGenerator.forBlock['sleep_ms'] = function (block) {
  const ms = pythonGenerator.valueToCode(block, 'MS', Order.NONE) || '100';
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_microbit_sleep'] = 'from microbit import sleep';
    return `sleep(${ms})\n`;
  }
  pythonGenerator.definitions_['import_time'] = 'import time';
  return `time.sleep_ms(${ms})\n`;
};

pythonGenerator.forBlock['ticks_ms'] = function () {
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_microbit_running_time'] = 'from microbit import running_time';
    return ['running_time()', Order.FUNCTION_CALL];
  }
  pythonGenerator.definitions_['import_time'] = 'import time';
  return ['time.ticks_ms()', Order.FUNCTION_CALL];
};

// ─── Random ───────────────────────────────────────────────

pythonGenerator.forBlock['random_int'] = function (block) {
  const from_ = pythonGenerator.valueToCode(block, 'FROM', Order.NONE) || '0';
  const to = pythonGenerator.valueToCode(block, 'TO', Order.NONE) || '10';
  pythonGenerator.definitions_['import_random'] = 'import random';
  return [`random.randint(${from_}, ${to})`, Order.FUNCTION_CALL];
};

pythonGenerator.forBlock['random_float'] = function () {
  pythonGenerator.definitions_['import_random'] = 'import random';
  return ['random.random()', Order.FUNCTION_CALL];
};

pythonGenerator.forBlock['random_uniform'] = function (block) {
  const low = pythonGenerator.valueToCode(block, 'LOW', Order.NONE) || '0';
  const high = pythonGenerator.valueToCode(block, 'HIGH', Order.NONE) || '1';
  pythonGenerator.definitions_['import_random'] = 'import random';
  return [`random.uniform(${low}, ${high})`, Order.FUNCTION_CALL];
};

pythonGenerator.forBlock['random_seed'] = function (block) {
  const seed = pythonGenerator.valueToCode(block, 'SEED', Order.NONE) || '0';
  pythonGenerator.definitions_['import_random'] = 'import random';
  return `random.seed(${seed})\n`;
};

// ─── WiFi ─────────────────────────────────────────────────

pythonGenerator.forBlock['wifi_connect'] = function (block) {
  if (DEVICE() === 'microbit' || DEVICE() === 'rp2040') {
    return '# WiFi not available on this device\n';
  }
  const ssid = block.getFieldValue('SSID');
  const password = block.getFieldValue('PASSWORD');
  pythonGenerator.definitions_['import_network'] = 'import network';
  return [
    `wlan = network.WLAN(network.STA_IF)\n`,
    `wlan.active(True)\n`,
    `wlan.connect('${ssid}', '${password}')\n`,
    `while not wlan.isconnected():\n`,
    `    time.sleep(0.1)\n`,
    `print('Connected:', wlan.ifconfig()[0])\n`,
  ].join('');
};

pythonGenerator.forBlock['wifi_get_ip'] = function (block) {
  if (DEVICE() === 'microbit' || DEVICE() === 'rp2040') {
    return ["''", Order.ATOMIC];
  }
  pythonGenerator.definitions_['import_network'] = 'import network';
  return ['network.WLAN(network.STA_IF).ifconfig()[0]', Order.FUNCTION_CALL];
};

// ─── NeoPixel ─────────────────────────────────────────────

pythonGenerator.forBlock['neopixel_init'] = function (block) {
  const pin = block.getFieldValue('PIN');
  const num = block.getFieldValue('NUM');
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_microbit_neopixel'] = 'from microbit import *\nfrom neopixel import NeoPixel';
    return `np = NeoPixel(pin${pin}, ${num})\n`;
  }
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  pythonGenerator.definitions_['import_neopixel'] = 'import neopixel';
  return `np = neopixel.NeoPixel(Pin(${pin}), ${num})\n`;
};

pythonGenerator.forBlock['colour_rgb'] = function (block) {
  const r = pythonGenerator.valueToCode(block, 'R', Order.NONE) || '0';
  const g = pythonGenerator.valueToCode(block, 'G', Order.NONE) || '0';
  const b = pythonGenerator.valueToCode(block, 'B', Order.NONE) || '0';
  return [`(${r}, ${g}, ${b})`, Order.ATOMIC];
};

pythonGenerator.forBlock['tuple_create_3'] = function (block) {
  const a = pythonGenerator.valueToCode(block, 'A', Order.NONE) || '0';
  const b = pythonGenerator.valueToCode(block, 'B', Order.NONE) || '0';
  const c = pythonGenerator.valueToCode(block, 'C', Order.NONE) || '0';
  return [`(${a}, ${b}, ${c})`, Order.ATOMIC];
};

pythonGenerator.forBlock['tuple_index'] = function (block) {
  const tupleCode =
    pythonGenerator.valueToCode(block, 'TUPLE', Order.MEMBER) || '(0, 0, 0)';
  const i = block.getFieldValue('I') ?? '0';
  return [`${tupleCode}[${i}]`, Order.MEMBER];
};

pythonGenerator.forBlock['neopixel_set_color'] = function (block) {
  const index = pythonGenerator.valueToCode(block, 'INDEX', Order.NONE) || '0';
  const color = pythonGenerator.valueToCode(block, 'COLOR', Order.NONE) || '(0, 0, 0)';
  return `np[${index}] = ${color}\n`;
};

pythonGenerator.forBlock['neopixel_show'] = function () {
  return DEVICE() === 'microbit' ? 'np.show()\n' : 'np.write()\n';
};

// ─── I2C ──────────────────────────────────────────────────

function i2cChannel(block) {
  return block.getFieldValue('CH') === '1' ? '1' : '0';
}

function i2cBusName(ch) {
  return ch === '1' ? 'i2c1' : 'i2c0';
}

function i2cHardwareId(ch) {
  return ch === '1' ? 1 : 0;
}

/** True if an enabled "init I2C" block exists for this hardware channel (0 or 1). */
function workspaceHasI2cInitForChannel(block, ch) {
  const ws = block.workspace;
  if (!ws) return false;
  const target = String(ch);
  for (const b of ws.getAllBlocks(false)) {
    if (!b.isEnabled()) continue;
    if (b.type === 'i2c_init' && String(b.getFieldValue('CH') ?? '0') === target) {
      return true;
    }
  }
  return false;
}

/** Emit I2C bus init into definitions (runs before user code) when no init block is present. */
function ensureAutoI2cBusDefinition(block, ch, sda, scl, freq) {
  if (DEVICE() === 'microbit') return;
  if (workspaceHasI2cInitForChannel(block, ch)) return;
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const name = i2cBusName(ch);
  const hw = i2cHardwareId(ch);
  pythonGenerator.definitions_[`i2c_init_bus_${ch}`] =
    `${name} = I2C(${hw}, sda=Pin(${sda}), scl=Pin(${scl}), freq=${freq})`;
}

const TCS34725_RGB_HELPER = `_tcs34725_inited = [False, False]

def _tcs34725_rgb(bus, ch_idx, addr):
    import time
    global _tcs34725_inited
    ch_idx = int(ch_idx) & 1
    addr = int(addr)
    if not _tcs34725_inited[ch_idx]:
        time.sleep_ms(10)
        bus.writeto(addr, bytes([0x80, 0x01]))
        time.sleep_ms(10)
        bus.writeto(addr, bytes([0x80, 0x03]))
        bus.writeto(addr, bytes([0x81, 0xFF]))
        bus.writeto(addr, bytes([0x8F, 0x03]))
        _tcs34725_inited[ch_idx] = True
    time.sleep_ms(5)
    bus.writeto(addr, bytes([0x94]))
    d = bus.readfrom(addr, 8)
    r = d[3] << 8 | d[2]
    g = d[5] << 8 | d[4]
    b = d[7] << 8 | d[6]
    return (r, g, b)
`;

const TCS34725_RGB_HELPER_MB = `_tcs34725_inited_mb = False

def _tcs34725_rgb_mb(addr):
    import time
    from microbit import i2c
    global _tcs34725_inited_mb
    addr = int(addr)
    if not _tcs34725_inited_mb:
        time.sleep_ms(10)
        i2c.write(addr, bytes([0x80, 0x01]))
        time.sleep_ms(10)
        i2c.write(addr, bytes([0x80, 0x03]))
        i2c.write(addr, bytes([0x81, 0xFF]))
        i2c.write(addr, bytes([0x8F, 0x03]))
        _tcs34725_inited_mb = True
    time.sleep_ms(5)
    i2c.write(addr, bytes([0x94]))
    d = i2c.read(addr, 8)
    r = d[3] << 8 | d[2]
    g = d[5] << 8 | d[4]
    b = d[7] << 8 | d[6]
    return (r, g, b)
`;

pythonGenerator.forBlock['i2c_init'] = function (block) {
  if (DEVICE() === 'microbit') return ''; // micro:bit has built-in i2c
  const ch = i2cChannel(block);
  const sda = block.getFieldValue('SDA');
  const scl = block.getFieldValue('SCL');
  const freq = block.getFieldValue('FREQ');
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const name = i2cBusName(ch);
  const hw = i2cHardwareId(ch);
  return `${name} = I2C(${hw}, sda=Pin(${sda}), scl=Pin(${scl}), freq=${freq})\n`;
};

pythonGenerator.forBlock['i2c_scan'] = function (block) {
  if (DEVICE() === 'microbit') {
    return ['[]', Order.ATOMIC]; // stub
  }
  const ch = i2cChannel(block);
  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const bus = i2cBusName(ch);
  return [`${bus}.scan()`, Order.FUNCTION_CALL];
};

pythonGenerator.forBlock['i2c_writeto'] = function (block) {
  if (DEVICE() === 'microbit') return '';
  const ch = i2cChannel(block);
  const addr = block.getFieldValue('ADDR');
  const data = pythonGenerator.valueToCode(block, 'DATA', Order.NONE) || 'b""';
  const bus = i2cBusName(ch);
  return `${bus}.writeto(${addr}, ${data})\n`;
};

pythonGenerator.forBlock['i2c_readfrom'] = function (block) {
  if (DEVICE() === 'microbit') return ['b""', Order.ATOMIC];
  const ch = i2cChannel(block);
  const addr = block.getFieldValue('ADDR');
  const nbytes = block.getFieldValue('NBYTES');
  const bus = i2cBusName(ch);
  return [`${bus}.readfrom(${addr}, ${nbytes})`, Order.FUNCTION_CALL];
};

pythonGenerator.forBlock['tcs34725_read_colours'] = function (block) {
  const addr = Number(block.getFieldValue('ADDR')) || 41;
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_time'] = 'import time';
    pythonGenerator.definitions_['tcs34725_rgb_mb'] = TCS34725_RGB_HELPER_MB;
    return [`_tcs34725_rgb_mb(${addr})`, Order.FUNCTION_CALL];
  }
  const ch = i2cChannel(block);
  ensureAutoI2cBusDefinition(block, ch, 21, 22, 400000);
  pythonGenerator.definitions_['import_time'] = 'import time';
  pythonGenerator.definitions_['tcs34725_rgb'] = TCS34725_RGB_HELPER;
  const bus = i2cBusName(ch);
  const chIdx = ch === '1' ? 1 : 0;
  return [`_tcs34725_rgb(${bus}, ${chIdx}, ${addr})`, Order.FUNCTION_CALL];
};

// ─── Print ────────────────────────────────────────────────

pythonGenerator.forBlock['print_text'] = function (block) {
  const text = pythonGenerator.valueToCode(block, 'TEXT', Order.NONE) || "''";
  return `print(${text})\n`;
};

// ─── H-Bridge Motor ─────────────────────────────────────

function hbSetMotorDef() {
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_microbit'] = 'from microbit import *';
    pythonGenerator.definitions_['hb_set_motor'] = [
      'def _hb_set_motor(p1, p2, speed):',
      '    speed = max(-255, min(255, int(speed)))',
      '    duty = abs(speed) * 4',
      '    if speed > 0:',
      '        p1.write_analog(duty)',
      '        p2.write_analog(0)',
      '    elif speed < 0:',
      '        p1.write_analog(0)',
      '        p2.write_analog(duty)',
      '    else:',
      '        p1.write_analog(0)',
      '        p2.write_analog(0)',
    ].join('\n');
  } else {
    pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
    pythonGenerator.definitions_['hb_set_motor'] = [
      'def _hb_set_motor(p1, p2, speed):',
      '    speed = max(-255, min(255, int(speed)))',
      '    duty = abs(speed) * 4',
      '    if speed > 0:',
      '        try: PWM(Pin(p2)).deinit()',
      '        except: pass',
      '        Pin(p2, Pin.OUT).value(0)',
      '        PWM(Pin(p1), freq=1000, duty=duty)',
      '    elif speed < 0:',
      '        try: PWM(Pin(p1)).deinit()',
      '        except: pass',
      '        Pin(p1, Pin.OUT).value(0)',
      '        PWM(Pin(p2), freq=1000, duty=duty)',
      '    else:',
      '        try: PWM(Pin(p1)).deinit()',
      '        except: pass',
      '        try: PWM(Pin(p2)).deinit()',
      '        except: pass',
      '        Pin(p1, Pin.OUT).value(0)',
      '        Pin(p2, Pin.OUT).value(0)',
    ].join('\n');
  }
}

function hbInitPin(label, pin) {
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['hb_' + label] = label + ' = ' + mbPin(pin);
  } else {
    pythonGenerator.definitions_['hb_' + label] = label + ' = ' + pin;
  }
}

pythonGenerator.forBlock['hbridge_motor_init'] = function (block) {
  var m = block.getFieldValue('MOTOR').toLowerCase();
  hbSetMotorDef();
  hbInitPin('motor_' + m + '_in1', block.getFieldValue('IN1'));
  hbInitPin('motor_' + m + '_in2', block.getFieldValue('IN2'));
  return '';
};

pythonGenerator.forBlock['hbridge_motor_speed'] = function (block) {
  var m = block.getFieldValue('MOTOR').toLowerCase();
  var speed = pythonGenerator.valueToCode(block, 'SPEED', Order.NONE) || '0';
  return '_hb_set_motor(motor_' + m + '_in1, motor_' + m + '_in2, ' + speed + ')\n';
};

pythonGenerator.forBlock['hbridge_motor_stop'] = function (block) {
  var m = block.getFieldValue('MOTOR').toLowerCase();
  return '_hb_set_motor(motor_' + m + '_in1, motor_' + m + '_in2, 0)\n';
};

pythonGenerator.forBlock['hbridge_dual_init'] = function (block) {
  hbSetMotorDef();
  hbInitPin('motor_l_in1', block.getFieldValue('L_IN1'));
  hbInitPin('motor_l_in2', block.getFieldValue('L_IN2'));
  hbInitPin('motor_r_in1', block.getFieldValue('R_IN1'));
  hbInitPin('motor_r_in2', block.getFieldValue('R_IN2'));
  return '';
};

pythonGenerator.forBlock['hbridge_dual_speed'] = function (block) {
  var left = pythonGenerator.valueToCode(block, 'LEFT', Order.NONE) || '0';
  var right = pythonGenerator.valueToCode(block, 'RIGHT', Order.NONE) || '0';
  return '_hb_set_motor(motor_l_in1, motor_l_in2, ' + left + ')\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, ' + right + ')\n';
};

pythonGenerator.forBlock['hbridge_dual_forward'] = function () {
  return '_hb_set_motor(motor_l_in1, motor_l_in2, 128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 128)\n';
};

pythonGenerator.forBlock['hbridge_dual_backward'] = function () {
  return '_hb_set_motor(motor_l_in1, motor_l_in2, -128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, -128)\n';
};

pythonGenerator.forBlock['hbridge_dual_left'] = function () {
  return '_hb_set_motor(motor_l_in1, motor_l_in2, -128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 128)\n';
};

pythonGenerator.forBlock['hbridge_dual_right'] = function () {
  return '_hb_set_motor(motor_l_in1, motor_l_in2, 128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, -128)\n';
};

pythonGenerator.forBlock['hbridge_dual_forward_x_seconds'] = function (block) {
  var s = pythonGenerator.valueToCode(block, 'SECS', Order.NONE) || '1';
  pythonGenerator.definitions_['import_time'] = 'import time';
  return '_hb_set_motor(motor_l_in1, motor_l_in2, 128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 128)\n' +
         'time.sleep(' + s + ')\n' +
         '_hb_set_motor(motor_l_in1, motor_l_in2, 0)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 0)\n';
};

pythonGenerator.forBlock['hbridge_dual_backward_x_seconds'] = function (block) {
  var s = pythonGenerator.valueToCode(block, 'SECS', Order.NONE) || '1';
  pythonGenerator.definitions_['import_time'] = 'import time';
  return '_hb_set_motor(motor_l_in1, motor_l_in2, -128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, -128)\n' +
         'time.sleep(' + s + ')\n' +
         '_hb_set_motor(motor_l_in1, motor_l_in2, 0)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 0)\n';
};

pythonGenerator.forBlock['hbridge_dual_left_x_seconds'] = function (block) {
  var s = pythonGenerator.valueToCode(block, 'SECS', Order.NONE) || '1';
  pythonGenerator.definitions_['import_time'] = 'import time';
  return '_hb_set_motor(motor_l_in1, motor_l_in2, -128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 128)\n' +
         'time.sleep(' + s + ')\n' +
         '_hb_set_motor(motor_l_in1, motor_l_in2, 0)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 0)\n';
};

pythonGenerator.forBlock['hbridge_dual_right_x_seconds'] = function (block) {
  var s = pythonGenerator.valueToCode(block, 'SECS', Order.NONE) || '1';
  pythonGenerator.definitions_['import_time'] = 'import time';
  return '_hb_set_motor(motor_l_in1, motor_l_in2, 128)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, -128)\n' +
         'time.sleep(' + s + ')\n' +
         '_hb_set_motor(motor_l_in1, motor_l_in2, 0)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 0)\n';
};

pythonGenerator.forBlock['hbridge_dual_stop'] = function () {
  return '_hb_set_motor(motor_l_in1, motor_l_in2, 0)\n' +
         '_hb_set_motor(motor_r_in1, motor_r_in2, 0)\n';
};

// ─── Sound ───────────────────────────────────────────────

pythonGenerator.forBlock['sound_play_tone_speaker'] = function (block) {
  const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
  const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
  const wait = block.getFieldValue('WAIT') === 'TRUE';

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return `music.pitch(${freq}, ${duration}, wait=${wait ? 'True' : 'False'})\n`;
  }
  return '# Built-in speaker only available on micro:bit\n';
};

pythonGenerator.forBlock['sound_stop_speaker'] = function () {
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return 'music.stop()\n';
  }
  return '# Built-in speaker only available on micro:bit\n';
};

pythonGenerator.forBlock['sound_play_tone'] = function (block) {
  const pin = block.getFieldValue('PIN');
  const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
  const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
  const wait = block.getFieldValue('WAIT') === 'TRUE';

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    pythonGenerator.definitions_['import_microbit_pins'] = 'from microbit import *';
    return `music.pitch(${freq}, ${duration}, pin=${mbPin(pin)}, wait=${wait ? 'True' : 'False'})\n`;
  }

  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const varName = `buzzer_${pin}`;
  pythonGenerator.definitions_[`buzzer_init_${pin}`] = `${varName} = PWM(Pin(${pin}))`;

  if (wait) {
    pythonGenerator.definitions_['import_time'] = 'import time';
    return `${varName}.freq(${freq})\n${varName}.duty(512)\ntime.sleep_ms(${duration})\n${varName}.duty(0)\n`;
  }

  pythonGenerator.definitions_['import_timer'] = 'from machine import Timer';
  return `${varName}.freq(${freq})\n${varName}.duty(512)\nTimer(0).init(period=${duration}, mode=Timer.ONE_SHOT, callback=lambda t: ${varName}.duty(0))\n`;
};

pythonGenerator.forBlock['sound_stop'] = function (block) {
  const pin = block.getFieldValue('PIN');

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return 'music.stop()\n';
  }

  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const varName = `buzzer_${pin}`;
  pythonGenerator.definitions_[`buzzer_init_${pin}`] = `${varName} = PWM(Pin(${pin}))`;
  return `${varName}.duty(0)\n`;
};

pythonGenerator.forBlock['sound_play_note_speaker'] = function (block) {
  const freq = block.getFieldValue('NOTE');
  const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
  const wait = block.getFieldValue('WAIT') === 'TRUE';

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return `music.pitch(${freq}, ${duration}, wait=${wait ? 'True' : 'False'})\n`;
  }
  return '# Built-in speaker only available on micro:bit\n';
};

pythonGenerator.forBlock['sound_play_note'] = function (block) {
  const pin = block.getFieldValue('PIN');
  const freq = block.getFieldValue('NOTE');
  const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
  const wait = block.getFieldValue('WAIT') === 'TRUE';

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    pythonGenerator.definitions_['import_microbit_pins'] = 'from microbit import *';
    return `music.pitch(${freq}, ${duration}, pin=${mbPin(pin)}, wait=${wait ? 'True' : 'False'})\n`;
  }

  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  const varName = `buzzer_${pin}`;
  pythonGenerator.definitions_[`buzzer_init_${pin}`] = `${varName} = PWM(Pin(${pin}))`;

  if (wait) {
    pythonGenerator.definitions_['import_time'] = 'import time';
    return `${varName}.freq(${freq})\n${varName}.duty(512)\ntime.sleep_ms(${duration})\n${varName}.duty(0)\n`;
  }

  pythonGenerator.definitions_['import_timer'] = 'from machine import Timer';
  return `${varName}.freq(${freq})\n${varName}.duty(512)\nTimer(0).init(period=${duration}, mode=Timer.ONE_SHOT, callback=lambda t: ${varName}.duty(0))\n`;
};

pythonGenerator.forBlock['sound_play_staff_note_speaker'] =
  pythonGenerator.forBlock['sound_play_note_speaker'];
pythonGenerator.forBlock['sound_play_staff_note'] =
  pythonGenerator.forBlock['sound_play_note'];

// ─── Sound – buzzer (init + pin-free) ────────────────────

pythonGenerator.forBlock['sound_buzzer_init'] = function (block) {
  const pin = block.getFieldValue('PIN');

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    pythonGenerator.definitions_['import_microbit_pins'] = 'from microbit import *';
    pythonGenerator.definitions_['buzzer_pin'] = `_buzzer_pin = ${mbPin(pin)}`;
    return '';
  }

  pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
  pythonGenerator.definitions_['buzzer_init'] = `_buzzer = PWM(Pin(${pin}))\n_buzzer.duty(0)`;
  return '';
};

pythonGenerator.forBlock['sound_buzzer_tone'] = function (block) {
  const freq = pythonGenerator.valueToCode(block, 'FREQ', Order.NONE) || '440';
  const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
  const wait = block.getFieldValue('WAIT') === 'TRUE';

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return `music.pitch(${freq}, ${duration}, pin=_buzzer_pin, wait=${wait ? 'True' : 'False'})\n`;
  }

  if (wait) {
    pythonGenerator.definitions_['import_time'] = 'import time';
    return `_buzzer.freq(${freq})\n_buzzer.duty(512)\ntime.sleep_ms(${duration})\n_buzzer.duty(0)\n`;
  }
  pythonGenerator.definitions_['import_timer'] = 'from machine import Timer';
  return `_buzzer.freq(${freq})\n_buzzer.duty(512)\nTimer(0).init(period=${duration}, mode=Timer.ONE_SHOT, callback=lambda t: _buzzer.duty(0))\n`;
};

pythonGenerator.forBlock['sound_buzzer_note'] = function (block) {
  const freq = block.getFieldValue('NOTE');
  const duration = pythonGenerator.valueToCode(block, 'DURATION', Order.NONE) || '500';
  const wait = block.getFieldValue('WAIT') === 'TRUE';

  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return `music.pitch(${freq}, ${duration}, pin=_buzzer_pin, wait=${wait ? 'True' : 'False'})\n`;
  }

  if (wait) {
    pythonGenerator.definitions_['import_time'] = 'import time';
    return `_buzzer.freq(${freq})\n_buzzer.duty(512)\ntime.sleep_ms(${duration})\n_buzzer.duty(0)\n`;
  }
  pythonGenerator.definitions_['import_timer'] = 'from machine import Timer';
  return `_buzzer.freq(${freq})\n_buzzer.duty(512)\nTimer(0).init(period=${duration}, mode=Timer.ONE_SHOT, callback=lambda t: _buzzer.duty(0))\n`;
};

pythonGenerator.forBlock['sound_buzzer_staff_note'] =
  pythonGenerator.forBlock['sound_buzzer_note'];

pythonGenerator.forBlock['sound_buzzer_stop'] = function () {
  if (DEVICE() === 'microbit') {
    pythonGenerator.definitions_['import_music'] = 'import music';
    return 'music.stop()\n';
  }
  return '_buzzer.duty(0)\n';
};

// ─── Sound – melodies ────────────────────────────────────

const MELODIES = {
  mario: [
    [660,100],[660,100],[0,100],[660,100],[0,100],[520,100],[660,100],[0,100],
    [784,100],[0,300],[392,100],[0,300],
    [520,100],[0,200],[392,100],[0,200],[330,100],[0,200],
    [440,100],[0,100],[494,100],[0,100],[466,100],[440,100],[0,100],
    [392,100],[660,100],[784,100],[880,100],[0,100],[698,100],[784,100],
    [0,100],[660,100],[0,100],[520,100],[587,100],[494,100],[0,100],
    [520,100],[0,200],[392,100],[0,200],[330,100],[0,200],
    [440,100],[0,100],[494,100],[0,100],[466,100],[440,100],[0,100],
    [392,100],[660,100],[784,100],[880,100],[0,100],[698,100],[784,100],
    [0,100],[660,100],[0,100],[520,100],[587,100],[494,100],[0,200],
  ],
  happy: [[523,150],[0,30],[659,150],[0,30],[784,200]],
  sad:   [[392,250],[0,30],[330,250],[0,30],[262,300]],
};

function melodyPython(name, buzzerExpr) {
  const notes = MELODIES[name];
  let code = `_${name}_melody = [`;
  code += notes.map(([f, d]) => `(${f},${d})`).join(',');
  code += ']\n';
  code += `for _freq, _dur in _${name}_melody:\n`;
  code += `    if _freq == 0:\n`;
  code += `        ${buzzerExpr}.duty(0)\n`;
  code += `    else:\n`;
  code += `        ${buzzerExpr}.freq(_freq)\n`;
  code += `        ${buzzerExpr}.duty(512)\n`;
  code += `    time.sleep_ms(_dur)\n`;
  code += `${buzzerExpr}.duty(0)\n`;
  return code;
}

function melodyMicrobit(name, pinExpr) {
  const notes = MELODIES[name];
  let code = `_${name}_melody = [`;
  code += notes.map(([f, d]) => `(${f},${d})`).join(',');
  code += ']\n';
  code += `for _freq, _dur in _${name}_melody:\n`;
  code += `    if _freq == 0:\n`;
  code += `        sleep(_dur)\n`;
  code += `    else:\n`;
  code += `        music.pitch(_freq, _dur, pin=${pinExpr}, wait=True)\n`;
  return code;
}

function melodyPinGenerator(name) {
  return function (block) {
    const pin = block.getFieldValue('PIN');
    pythonGenerator.definitions_['import_time'] = 'import time';

    if (DEVICE() === 'microbit') {
      pythonGenerator.definitions_['import_music'] = 'import music';
      pythonGenerator.definitions_['import_microbit_pins'] = 'from microbit import *';
      return melodyMicrobit(name, mbPin(pin));
    }

    pythonGenerator.definitions_['import_machine'] = 'from machine import Pin, PWM, ADC, I2C';
    const varName = `buzzer_${pin}`;
    pythonGenerator.definitions_[`buzzer_init_${pin}`] = `${varName} = PWM(Pin(${pin}))`;
    return melodyPython(name, varName);
  };
}

function melodyBuzzerGenerator(name) {
  return function () {
    pythonGenerator.definitions_['import_time'] = 'import time';

    if (DEVICE() === 'microbit') {
      pythonGenerator.definitions_['import_music'] = 'import music';
      pythonGenerator.definitions_['import_microbit_pins'] = 'from microbit import *';
      return melodyMicrobit(name, '_buzzer_pin');
    }

    return melodyPython(name, '_buzzer');
  };
}

pythonGenerator.forBlock['sound_melody_mario'] = melodyPinGenerator('mario');
pythonGenerator.forBlock['sound_melody_mario_buzzer'] = melodyBuzzerGenerator('mario');
pythonGenerator.forBlock['sound_melody_happy'] = melodyPinGenerator('happy');
pythonGenerator.forBlock['sound_melody_happy_buzzer'] = melodyBuzzerGenerator('happy');
pythonGenerator.forBlock['sound_melody_sad'] = melodyPinGenerator('sad');
pythonGenerator.forBlock['sound_melody_sad_buzzer'] = melodyBuzzerGenerator('sad');

// ─── micro:bit Display ────────────────────────────────────

pythonGenerator.forBlock['microbit_display_all'] = function (block) {
  if (DEVICE() !== 'microbit') {
    return '# micro:bit display block (select micro:bit device)\n';
  }
  // Build 5x5 string from checkboxes P00..P44 (each row = 5 chars, rows joined by :)
  const rows = [];
  for (let r = 0; r < 5; r++) {
    let row = '';
    for (let c = 0; c < 5; c++) {
      const val = block.getFieldValue(`P${r}${c}`);
      row += val === 'TRUE' ? '9' : '0';
    }
    rows.push(row);
  }
  const imageStr = rows.join(':');
  pythonGenerator.definitions_['import_microbit_display'] = 'from microbit import display, Image';
  return `display.show(Image("${imageStr}"))\n`;
};

// ─── SuperBit (Yahboom SuperBit V2, micro:bit only) ───────

const SUPERBIT_DEF = `from microbit import i2c, sleep
_superbit_initialized = False
def _superbit_init():
    global _superbit_initialized
    if _superbit_initialized:
        return
    PCA9685 = 0x40
    i2c.write(PCA9685, bytearray([0x00, 0x00]))
    prescale = 25000000 // 4096 // 50 - 1
    i2c.write(PCA9685, bytearray([0x00, 0x10]))
    i2c.write(PCA9685, bytearray([0xFE, prescale]))
    i2c.write(PCA9685, bytearray([0x00, 0x00]))
    sleep(5)
    i2c.write(PCA9685, bytearray([0x00, 0xa1]))
    _superbit_initialized = True
def _superbit_set_pwm(ch, on, off):
    reg = 0x06 + 4 * ch
    i2c.write(0x40, bytearray([reg, on & 0xff, on >> 8, off & 0xff, off >> 8]))
def _superbit_motor_run(index, speed):
    _superbit_init()
    speed = max(-4095, min(4095, int(speed) * 16))
    a, b = index, index + 1
    if index > 10:
        if speed >= 0:
            _superbit_set_pwm(a, 0, speed)
            _superbit_set_pwm(b, 0, 0)
        else:
            _superbit_set_pwm(a, 0, 0)
            _superbit_set_pwm(b, 0, -speed)
    else:
        if speed >= 0:
            _superbit_set_pwm(b, 0, speed)
            _superbit_set_pwm(a, 0, 0)
        else:
            _superbit_set_pwm(b, 0, 0)
            _superbit_set_pwm(a, 0, -speed)
def _superbit_motor_stop_all():
    _superbit_init()
    for ch in range(8, 16):
        _superbit_set_pwm(ch, 0, 0)
`;

const SUPERBIT_MOTOR_INDEX = { M1: 8, M2: 10, M3: 12, M4: 14 };

pythonGenerator.forBlock['superbit_motor_run'] = function (block) {
  if (DEVICE() !== 'microbit') {
    return '# SuperBit blocks are for micro:bit with Yahboom SuperBit V2 hat\n';
  }
  pythonGenerator.definitions_['superbit_lib'] = SUPERBIT_DEF;
  const motor = block.getFieldValue('MOTOR');
  const speed = pythonGenerator.valueToCode(block, 'SPEED', Order.NONE) || '0';
  const index = SUPERBIT_MOTOR_INDEX[motor] ?? 8;
  return `_superbit_motor_run(${index}, ${speed})\n`;
};

pythonGenerator.forBlock['superbit_motor_stop_all'] = function (block) {
  if (DEVICE() !== 'microbit') {
    return '# SuperBit blocks are for micro:bit with Yahboom SuperBit V2 hat\n';
  }
  pythonGenerator.definitions_['superbit_lib'] = SUPERBIT_DEF;
  return '_superbit_motor_stop_all()\n';
};

// ─── HID (Keyboard / Mouse / Gamepad) ──────────────────────
// ESP32-S3 only; requires MicroPython firmware with USB HID support (e.g. TinyUSB).

const HID_HELPER = `# HID helper (keyboard, mouse, gamepad) - ESP32-S3 USB
try:
    import usb_hid
except ImportError:
    usb_hid = None
_hid_kb = None
_hid_mouse = None
_hid_gamepad = None
def _hid_devices():
    global _hid_kb, _hid_mouse, _hid_gamepad
    if usb_hid is None or _hid_kb is not None:
        return
    for d in usb_hid.devices:
        if getattr(d, 'usage_page', None) == 0x01 and getattr(d, 'usage', None) == 0x06:
            _hid_kb = d
        elif getattr(d, 'usage_page', None) == 0x01 and getattr(d, 'usage', None) == 0x02:
            _hid_mouse = d
        elif getattr(d, 'usage_page', None) == 0x01 and getattr(d, 'usage', None) == 0x05:
            _hid_gamepad = d
def _hid_keyboard_report(modifier, key):
    return bytes([int(modifier) & 0xff, 0, int(key) & 0xff, 0, 0, 0, 0, 0])
def _hid_key_press(modifier, key):
    _hid_devices()
    if _hid_kb:
        _hid_kb.send(_hid_keyboard_report(modifier, key))
        _hid_kb.send(_hid_keyboard_report(0, 0))
def _hid_key_down(modifier, key):
    _hid_devices()
    if _hid_kb:
        _hid_kb.send(_hid_keyboard_report(modifier, key))
def _hid_key_up():
    _hid_devices()
    if _hid_kb:
        _hid_kb.send(_hid_keyboard_report(0, 0))
def _hid_type_string(s):
    _hid_devices()
    if not _hid_kb:
        return
    for c in s:
        mod = 0x02 if c.isupper() and c.isalpha() else 0
        low = c.lower() if c.isalpha() else c
        key = 0
        if len(low) == 1 and 'a' <= low <= 'z':
            key = ord(low) - ord('a') + 4
        elif '0' <= c <= '9':
            key = ord(c) - ord('0') + 30
        elif c == ' ':
            key = 44
        elif c == chr(10) or c == chr(13):
            key = 40
        if key:
            _hid_kb.send(_hid_keyboard_report(mod, key))
            _hid_kb.send(_hid_keyboard_report(0, 0))
def _s8(v):
    return (max(-127, min(127, int(v))) + 128) & 0xff
def _hid_mouse_move(dx, dy):
    _hid_devices()
    if _hid_mouse:
        _hid_mouse.send(bytes([0, _s8(dx), _s8(dy), 0]))
def _hid_mouse_click(btn):
    _hid_devices()
    if _hid_mouse:
        _hid_mouse.send(bytes([int(btn) & 7, 0, 0, 0]))
        _hid_mouse.send(bytes([0, 0, 0, 0]))
def _hid_mouse_scroll(delta):
    _hid_devices()
    if _hid_mouse:
        _hid_mouse.send(bytes([0, 0, 0, _s8(delta)]))
def _hid_gamepad_report(buttons, axes):
    out = [buttons & 0xff, buttons >> 8]
    for v in axes:
        out.append((max(-127, min(127, int(v))) + 128) & 0xff)
    return bytes(out)
_hid_gp_buttons = 0
_hid_gp_axes = [0, 0, 0, 0, 0, 0]
def _hid_gamepad_button(btn_index, state):
    global _hid_gp_buttons
    _hid_devices()
    if _hid_gamepad:
        if state:
            _hid_gp_buttons |= 1 << btn_index
        else:
            _hid_gp_buttons &= ~(1 << btn_index)
        _hid_gamepad.send(_hid_gamepad_report(_hid_gp_buttons, _hid_gp_axes))
def _hid_gamepad_axis(axis_index, value):
    _hid_devices()
    if _hid_gamepad:
        _hid_gp_axes[axis_index] = max(-127, min(127, int(value)))
        _hid_gamepad.send(_hid_gamepad_report(_hid_gp_buttons, _hid_gp_axes))
`;

function hidNotSupported() {
  return DEVICE() !== 'esp32s3';
}

pythonGenerator.forBlock['hid_key_press'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const key = block.getFieldValue('KEY');
  const mod = block.getFieldValue('MOD');
  return `_hid_key_press(${mod}, ${key})\n`;
};

pythonGenerator.forBlock['hid_key_down'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const key = block.getFieldValue('KEY');
  const mod = block.getFieldValue('MOD');
  return `_hid_key_down(${mod}, ${key})\n`;
};

pythonGenerator.forBlock['hid_key_up'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  return '_hid_key_up()\n';
};

pythonGenerator.forBlock['hid_keyboard_type'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const text = pythonGenerator.valueToCode(block, 'TEXT', Order.NONE) || "''";
  return `_hid_type_string(${text})\n`;
};

pythonGenerator.forBlock['hid_mouse_move'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const x = pythonGenerator.valueToCode(block, 'X', Order.NONE) || '0';
  const y = pythonGenerator.valueToCode(block, 'Y', Order.NONE) || '0';
  return `_hid_mouse_move(${x}, ${y})\n`;
};

pythonGenerator.forBlock['hid_mouse_click'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const btn = block.getFieldValue('BUTTON');
  return `_hid_mouse_click(${btn})\n`;
};

pythonGenerator.forBlock['hid_mouse_scroll'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const delta = pythonGenerator.valueToCode(block, 'DELTA', Order.NONE) || '1';
  return `_hid_mouse_scroll(${delta})\n`;
};

pythonGenerator.forBlock['hid_gamepad_button'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const btn = block.getFieldValue('BTN');
  const state = block.getFieldValue('STATE') === '1';
  return `_hid_gamepad_button(${btn}, ${state})\n`;
};

pythonGenerator.forBlock['hid_gamepad_axis'] = function (block) {
  if (hidNotSupported()) return '# HID blocks require ESP32-S3 with USB HID firmware\n';
  pythonGenerator.definitions_['hid_lib'] = HID_HELPER;
  const axis = block.getFieldValue('AXIS');
  const value = pythonGenerator.valueToCode(block, 'VALUE', Order.NONE) || '0';
  return `_hid_gamepad_axis(${axis}, ${value})\n`;
};

// ─── Loops ─────────────────────────────────────────────

pythonGenerator.forBlock['controls_repeat_forever'] = function (block) {
  let branch = pythonGenerator.statementToCode(block, 'DO') || '  pass\n';
  return `while True:\n${branch}`;
};
