'use strict';
// LocoNet (Uhlenbrock IB-II/III/Basic, Digikeijs, Tams, LoDi e.a.) — serieel via een
// LocoNet-interface (PR3/LocoBuffer/USB), of LAN bij sommige interfaces.
//
// LocoNet is slot-gebaseerd: een loc-adres krijgt eerst een "slot" toegewezen
// (OPC_LOCO_ADR 0xBF), daarna stuur je snelheid/richting/functie naar dat slotnummer.
// Berichten: opcode, [args], checksum (= XOR van alle bytes daarvoor, dan complement 0xFF^).
//   OPC_GPON  0x83   globaal power on
//   OPC_GPOFF 0x82   globaal power off
//   OPC_LOCO_SPD 0xA0 slot, spd
//   OPC_LOCO_DIRF 0xA1 slot, dirf
//   OPC_LOCO_FN ... (uitgebreide functies) / OPC_SW_REQ 0xB0 wissel
//   OPC_INPUT_REP 0xB2 (terugmelding van bezetmelders)
//
// ⚠️ SCAFFOLD: opcodes + checksum volgen de LocoNet-spec; slot-toewijzing en terugmelding op
//    een echte interface verifiëren. Vereist `npm i serialport`.
const EventEmitter = require('events');

class LocoNet extends EventEmitter {
  constructor(opts = {}) {
    super();
    this.mode = 'loconet';
    this.path = opts.path || '/dev/ttyUSB0';
    this.baud = opts.baud || 57600;              // LocoNet PR3/USB: 57600
    this.connected = false;
    this.port = null;
    this.state = { power: false, locos: {}, turnouts: {}, sensors: {} };
    this._slot = {};                             // addr -> slotnummer
    this._nextSlot = 1;
    this._rx = Buffer.alloc(0);                  // ontvangstbuffer voor LocoNet-frames
  }

  connect() {
    let SerialPort;
    try { ({ SerialPort } = require('serialport')); }
    catch (_) { this.emit('log', '[loconet] serialport ontbreekt — `npm i serialport`. Nu: log-only.'); this.connected = false; this.emit('close'); return; }
    try {
      this.port = new SerialPort({ path: this.path, baudRate: this.baud });
      this.port.on('open', () => { this.connected = true; this.emit('open'); this.emit('log', '[loconet] poort open: ' + this.path + ' @' + this.baud); });
      this.port.on('data', (d) => this._onData(d));
      this.port.on('error', (e) => this.emit('log', '[loconet] fout: ' + e.message));
      this.port.on('close', () => { this.connected = false; this.emit('close'); });
    } catch (e) { this.emit('log', '[loconet] kon poort niet openen: ' + e.message); }
  }

  _msg(bytes, label) {
    let chk = 0xFF; for (const b of bytes) chk ^= b;
    const buf = Buffer.from([...bytes, chk]);
    this.emit('log', '→ ' + (label || '') + ' [' + buf.toString('hex') + ']');
    if (this.connected && this.port) { try { this.port.write(buf); } catch (e) { this.emit('log', '[tx-fout] ' + e.message); } }
  }
  _onData(d) {
    this.emit('log', '↩ ' + d.toString('hex'));
    this._rx = Buffer.concat([this._rx, d]);
    while (this._rx.length) {
      const op = this._rx[0];
      if (!(op & 0x80)) { this._rx = this._rx.slice(1); continue; }   // sync op opcode-byte
      const hi = op & 0xE0;
      let len = hi === 0x80 ? 2 : hi === 0xA0 ? 4 : hi === 0xC0 ? 6 : 0;
      if (len === 0) { if (this._rx.length < 2) break; len = this._rx[1]; if (len < 3) { this._rx = this._rx.slice(1); continue; } }
      if (this._rx.length < len) break;
      const msg = this._rx.slice(0, len); this._rx = this._rx.slice(len);
      if (msg[0] === 0xB2 && msg.length >= 4) {        // OPC_INPUT_REP → bezetmelding
        const in1 = msg[1], in2 = msg[2];
        const a = (in1 & 0x7F) | ((in2 & 0x0F) << 7);
        const sensor = a * 2 + ((in2 >> 5) & 0x01) + 1;
        this.state.sensors[sensor] = !!(in2 & 0x10);
        this.emit('state');
      }
    }
  }

  _loco(addr) { if (!this.state.locos[addr]) this.state.locos[addr] = { speed: 0, dir: 1, fns: {} }; return this.state.locos[addr]; }
  _slotFor(addr) {
    if (!this._slot[addr]) {
      this._slot[addr] = this._nextSlot++;
      // OPC_LOCO_ADR: vraag/registreer slot voor adres (centrale antwoordt met slot-data)
      this._msg([0xBF, (addr >> 7) & 0x7F, addr & 0x7F], 'slot-adr ' + addr);
    }
    return this._slot[addr];
  }

  power(on) { this.state.power = !!on; this._msg([on ? 0x83 : 0x82], on ? 'GPON' : 'GPOFF'); this.emit('state'); }
  throttle(addr, speed, dir) {
    addr = parseInt(addr, 10); let ui = Math.max(-1, Math.min(126, parseInt(speed, 10)));
    const l = this._loco(addr); l.dir = dir ? 1 : 0; l.speed = ui < 0 ? 0 : ui;
    const slot = this._slotFor(addr);
    const spd = ui < 0 ? 1 : (ui === 0 ? 0 : Math.min(127, ui + 1));
    this._msg([0xA0, slot & 0x7F, spd & 0x7F], 'spd');
    // DIRF: dir + F0..F4
    let dirf = (dir ? 0 : 0x20); if (l.fns[0]) dirf |= 0x10; if (l.fns[1]) dirf |= 0x01; if (l.fns[2]) dirf |= 0x02; if (l.fns[3]) dirf |= 0x04; if (l.fns[4]) dirf |= 0x08;
    this._msg([0xA1, slot & 0x7F, dirf & 0x7F], 'dirf');
    this.emit('state');
  }
  func(addr, fn, on) {
    addr = parseInt(addr, 10); fn = parseInt(fn, 10); const l = this._loco(addr); l.fns[fn] = !!on;
    const slot = this._slotFor(addr);
    if (fn <= 4) {
      let dirf = (l.dir ? 0 : 0x20); if (l.fns[0]) dirf |= 0x10; if (l.fns[1]) dirf |= 0x01; if (l.fns[2]) dirf |= 0x02; if (l.fns[3]) dirf |= 0x04; if (l.fns[4]) dirf |= 0x08;
      this._msg([0xA1, slot & 0x7F, dirf & 0x7F], 'dirf');
    } else { // F5..F8 via OPC_LOCO_SND (0xA2)
      let snd = 0; for (let i = 5; i <= 8; i++) if (l.fns[i]) snd |= (1 << (i - 5));
      this._msg([0xA2, slot & 0x7F, snd & 0x7F], 'snd');
    }
    this.emit('state');
  }
  estop() { for (const a of Object.keys(this.state.locos)) { this.state.locos[a].speed = 0; const s = this._slot[a]; if (s) this._msg([0xA0, s & 0x7F, 1], 'estop'); } this.emit('log', '⛔ NOODSTOP'); this.emit('state'); }
  turnout(id, thrown) {
    id = parseInt(id, 10); this.state.turnouts[id] = !!thrown; const a = id - 1;
    const sw2 = 0x10 | (thrown ? 0x20 : 0x00) | ((a >> 7) & 0x0F); // ON + richting + hoge adresbits
    this._msg([0xB0, a & 0x7F, sw2], 'wissel ' + id); this.emit('state');
  }
}

module.exports = { LocoNet };
