'use strict';
// Twin Center / Twin Control adapter (Fleischmann = Uhlenbrock Intellibox-familie).
// Verbinding: serieel (RS232, vaak via USB-serieel) met het P50X-protocol (binair).
// LDT-wisseldecoders worden via DCC-accessory dóór de centrale geschakeld; ESU-locs = DCC.
// s88-terugmelding (LDT) komt via P50X-sensorevents (XEvtSen).
//
// Protocol-bronnen (zie ook README):
//   - p50x.h (Digital-MoBa) — opcodes XHalt 0xA5, XPwrOff 0xA6, XPwrOn 0xA7, lead-in 0x78
//   - espacerails / Rocrail-wiki — XLok 0x80 byte-layout + status-bits
//
// P50X (binair) — elk commando vooraf met lead-in 0x78:
//   XLok   : 78 80 adrLo adrHi speed status
//            speed:  0=stop, 1=noodstop, 2..127 = rijstap
//            status: bit7 ChgF(=F1..F4 toepassen) · bit6 Force · bit5 Dir(1=vooruit)
//                    · bit4 FL(=F0/licht) · bit3..0 = F4..F1
//   XFunc  : 78 88 adrLo adrHi f   (f bit0..3 = F5..F8)
//   XFunc2 : 78 89 adrLo adrHi f   (f bit0..3 = F9..F12)
//   XTrnt  : 78 90 adrLo adrHi/status  (wissel; kleur-bit 1=rechtdoor/groen, 0=afbuigend/rood)
//   XPwrOn : 78 A7    XPwrOff: 78 A6    XHalt(noodstop): 78 A5
//   XSensOff: 78 99   XEvtSen: 78 CB  (terugmelding uitlezen)
//
// ⚠️ De rij-/functie-/power-telegrammen volgen de gedocumenteerde layout. XTrnt-statusbit en
//    de exacte XEvtSen-reply moeten nog op het apparaat geverifieerd worden (gemarkeerd TODO).
const EventEmitter = require('events');

const LEADIN = 0x78;
const OP = { XHALT: 0xA5, XPWROFF: 0xA6, XPWRON: 0xA7, XLOK: 0x80, XFUNC: 0x88, XFUNC2: 0x89, XTRNT: 0x90, XSENSOFF: 0x99, XEVTSEN: 0xCB };

class TwinCenter extends EventEmitter {
  constructor(opts = {}) {
    super();
    this.mode = 'twincenter';
    this.path = opts.path || '/dev/ttyUSB0';
    this.baud = opts.baud || 19200;        // Twin Center serieel (verifieer: 2400/19200)
    this.connected = false;
    this.port = null;
    this.state = { power: false, locos: {}, turnouts: {}, sensors: {} };
  }

  connect() {
    let SerialPort;
    try { ({ SerialPort } = require('serialport')); }
    catch (_) {
      this.emit('log', '[twincenter] serialport ontbreekt — `npm i serialport` op de Pi. 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', '[twincenter] poort open: ' + this.path + ' @' + this.baud);
        this._raw([OP.XSENSOFF]);          // meld alle niet-vrije sensoren als event
        this._startSensorPoll();
      });
      this.port.on('data', (d) => this._onData(d));
      this.port.on('error', (e) => this.emit('log', '[twincenter] fout: ' + e.message));
      this.port.on('close', () => { this.connected = false; this.emit('close'); });
    } catch (e) { this.emit('log', '[twincenter] kon poort niet openen: ' + e.message); }
  }

  // lead-in + opcode + params
  _raw(bytes) {
    const frame = Buffer.from([LEADIN, ...bytes]);
    this.emit('log', '→ ' + frame.toString('hex').replace(/(..)/g, '$1 ').trim());
    if (this.connected && this.port) { try { this.port.write(frame); } catch (e) { this.emit('log', '[tx-fout] ' + e.message); } }
  }

  // s88/LDT-terugmelding: periodiek XEvtSen pollen. De centrale antwoordt met de
  // gewijzigde modules; bij geen wijziging een korte reply.
  _startSensorPoll() {
    this._rx = Buffer.alloc(0);
    this._await = null;
    clearInterval(this._poll);
    this._poll = setInterval(() => {
      if (!this.connected) return;
      this._await = 'sensors';
      this._raw([OP.XEVTSEN]);
    }, 400);
    this._poll.unref?.();
  }

  _onData(buf) {
    this.emit('log', '↩ ' + buf.toString('hex').replace(/(..)/g, '$1 ').trim());
    this._rx = Buffer.concat([this._rx || Buffer.alloc(0), buf]);
    if (this._await === 'sensors') this._tryParseSensors();
  }

  // XEvtSen-reply: reeks module-groepen [moduleNr][contacten 1..8][contacten 9..16],
  // afgesloten met moduleNr 0x00 (of een enkele 0x00 = geen wijziging).
  // ⚠️ Framing/terminator nog te verifiëren op de Twin Center — ruwe hex blijft gelogd.
  _tryParseSensors() {
    const b = this._rx;
    if (b.length === 0) return;
    if (b[0] === 0x00) { this._rx = b.slice(1); this._await = null; return; } // geen wijziging
    let i = 0, changed = false;
    while (i + 2 < b.length) {
      const mod = b[i];
      if (mod === 0x00) { i += 1; break; }                 // einde lijst
      const lo = b[i + 1], hi = b[i + 2]; i += 3;
      for (let bit = 0; bit < 8; bit++) {
        const c1 = (mod - 1) * 16 + bit + 1;               // contacten 1..8
        const c2 = (mod - 1) * 16 + bit + 9;               // contacten 9..16
        const v1 = !!(lo & (1 << bit)), v2 = !!(hi & (1 << bit));
        if (this.state.sensors[c1] !== v1) { this.state.sensors[c1] = v1; changed = true; }
        if (this.state.sensors[c2] !== v2) { this.state.sensors[c2] = v2; changed = true; }
      }
    }
    if (i > 0) this._rx = b.slice(i);
    this._await = null;
    if (changed) this.emit('state');
  }

  _loco(addr) { if (!this.state.locos[addr]) this.state.locos[addr] = { speed: 0, dir: 1, fns: {} }; return this.state.locos[addr]; }
  _lohi(n) { return [n & 0xFF, (n >> 8) & 0xFF]; }

  // Bouw het XLok-statusbyte uit richting + F0..F4 van deze loc.
  _status(l) {
    let s = 0x80;                          // ChgF: pas F1..F4 toe
    if (l.dir) s |= 0x20;                  // bit5 richting (1=vooruit)
    if (l.fns[0]) s |= 0x10;               // bit4 FL = F0 (licht)
    if (l.fns[1]) s |= 0x01;
    if (l.fns[2]) s |= 0x02;
    if (l.fns[3]) s |= 0x04;
    if (l.fns[4]) s |= 0x08;
    return s;
  }

  power(on) { this.state.power = !!on; this._raw([on ? OP.XPWRON : OP.XPWROFF]); this.emit('state'); }

  throttle(addr, speed, dir) {
    addr = parseInt(addr, 10);
    let uiSpeed = Math.max(-1, Math.min(126, parseInt(speed, 10)));
    const l = this._loco(addr);
    l.dir = dir ? 1 : 0;
    let p50;
    if (uiSpeed < 0) { p50 = 1; l.speed = 0; }       // noodstop deze loc
    else if (uiSpeed === 0) { p50 = 0; l.speed = 0; } // stop
    else { p50 = Math.min(127, uiSpeed + 1); l.speed = uiSpeed; } // 1..126 → 2..127
    const [lo, hi] = this._lohi(addr);
    this._raw([OP.XLOK, lo, hi, p50, this._status(l)]);
    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 [lo, hi] = this._lohi(addr);
    if (fn <= 4) {
      // F0..F4 zitten in het XLok-statusbyte → stuur XLok opnieuw met huidige snelheid/richting
      let p50 = l.speed === 0 ? 0 : Math.min(127, l.speed + 1);
      this._raw([OP.XLOK, lo, hi, p50, this._status(l)]);
    } else if (fn <= 8) {
      let f = 0; for (let i = 5; i <= 8; i++) if (l.fns[i]) f |= (1 << (i - 5));
      this._raw([OP.XFUNC, lo, hi, f]);
    } else {
      let f = 0; for (let i = 9; i <= 12; i++) if (l.fns[i]) f |= (1 << (i - 9));
      this._raw([OP.XFUNC2, lo, hi, f]);
    }
    this.emit('state');
  }

  estop() {
    for (const a of Object.keys(this.state.locos)) this.state.locos[a].speed = 0;
    this._raw([OP.XHALT]);                  // XHalt: alle locs stoppen, DCC blijft actief
    this.emit('log', '⛔ NOODSTOP (XHalt)'); this.emit('state');
  }

  turnout(id, thrown) {
    id = parseInt(id, 10); this.state.turnouts[id] = !!thrown;
    const [lo, hi] = this._lohi(id);
    // TODO: exact statusbit verifiëren. p50x.h: kleur-bit 1=rechtdoor(groen), 0=afbuigend(rood).
    const status = (hi & 0x7F) | (thrown ? 0x00 : 0x80);
    this._raw([OP.XTRNT, lo, status]);
    this.emit('state');
  }
}

module.exports = { TwinCenter };
