'use strict';
// Roco/Fleischmann Z21 (en z21) adapter — UDP LAN-protocol (poort 21105).
// Ook bruikbaar voor Digikeijs DR5000, die het Z21-protocol emuleert.
//
// Protocol: Roco "Z21 LAN protocol specification" (openbaar). Pakket =
//   [DataLen(2, little-endian)] [Header(2, LE)] [Data...]
// X-Bus-commando's zitten in Header 0x40 met een XOR-checksum over de X-bytes.
//
// Geïmplementeerd: power on/off, rijden (128 stappen), functies, wissel, noodstop.
// Loco-adres + functies volgen de Z21-spec (DB0..). Op hardware verifiëren aanbevolen.
const dgram = require('dgram');
const EventEmitter = require('events');

class Z21 extends EventEmitter {
  constructor(opts = {}) {
    super();
    this.mode = opts.mode || 'z21';        // 'z21' of 'dr5000'
    this.host = opts.host || '192.168.0.111';
    this.port = opts.port || 21105;
    this.connected = false;
    this.sock = null;
    this.state = { power: false, locos: {}, turnouts: {}, sensors: {}, railcom: {} };
  }

  connect() {
    this.sock = dgram.createSocket('udp4');
    this.sock.on('message', (msg) => this._onData(msg));
    this.sock.on('error', (e) => this.emit('log', '[z21] fout: ' + e.message));
    this.sock.bind(() => {
      this.connected = true;
      this.emit('open');
      this.emit('log', '[z21] UDP naar ' + this.host + ':' + this.port + ' (' + this.mode + ')');
      this._send(this._pkt(0x10, []));            // LAN_GET_SERIAL_NUMBER (handshake/keepalive)
      this._send(this._xbus([0x21, 0x81]));        // LAN_X_GET_STATUS
      this._send(this._pkt(0x50, [0x03, 0x00, 0x00, 0x00]));  // LAN_SET_BROADCASTFLAGS: rijden/schakelen + R-Bus
      this._send(this._pkt(0x81, [0]));            // LAN_RMBUS_GETDATA groep 0
      this._send(this._pkt(0x81, [1]));            // LAN_RMBUS_GETDATA groep 1
      // keepalive
      clearInterval(this._ka);
      this._ka = setInterval(() => this._send(this._pkt(0x10, [])), 30000);
      this._ka.unref?.();
    });
  }

  _pkt(header, data) {
    const len = 4 + data.length;
    const b = Buffer.alloc(len);
    b.writeUInt16LE(len, 0);
    b.writeUInt16LE(header, 2);
    Buffer.from(data).copy(b, 4);
    return b;
  }
  // X-Bus: header 0x40, data = X-bytes + XOR-checksum
  _xbus(xbytes) {
    let xor = 0; for (const x of xbytes) xor ^= x;
    return this._pkt(0x40, [...xbytes, xor]);
  }
  _send(buf) {
    this.emit('log', '→ ' + buf.toString('hex').replace(/(..)/g, '$1 ').trim());
    if (this.sock && this.connected) { try { this.sock.send(buf, this.port, this.host); } catch (e) { this.emit('log', '[tx-fout] ' + e.message); } }
  }
  _onData(msg) {
    this.emit('log', '↩ ' + msg.toString('hex').replace(/(..)/g, '$1 ').trim());
    let off = 0;
    while (off + 4 <= msg.length) {                 // Z21 kan meerdere datagrammen bundelen
      const len = msg.readUInt16LE(off);
      if (len < 4 || off + len > msg.length) break;
      const header = msg.readUInt16LE(off + 2);
      const data = msg.slice(off + 4, off + len);
      if (header === 0x80) this._rbus(data);         // LAN_RMBUS_DATA → bezetmelding
      else if (header === 0x88) this._railcom(data);  // LAN_RAILCOM_DATA → gedetecteerde loc-adressen
      else if (header === 0x40 && data[0] === 0x61) { // LAN_X power-status
        if (data[1] === 0x00) { this.state.power = false; this.emit('state'); }
        else if (data[1] === 0x01) { this.state.power = true; this.emit('state'); }
      }
      off += len;
    }
  }
  // RailCom (experimenteel, read-only): record gedetecteerde loc-adressen. Op hardware te bevestigen.
  _railcom(data) {
    for (let off = 0; off + 2 <= data.length; off += 13) {   // entries ~13 bytes; adres = eerste 2 (LE)
      const addr = data.readUInt16LE(off);
      if (addr > 0 && addr < 10240) this.state.railcom[addr] = Date.now();
    }
    this.emit('state');
  }
  _rbus(data) {                                      // R-Bus: groep + 10 bytes, elke bit = een melder
    if (data.length < 11) return;
    const group = data[0];
    let changed = false;
    for (let bi = 0; bi < 10; bi++) {
      for (let bit = 0; bit < 8; bit++) {
        const contact = group * 80 + bi * 8 + bit + 1;
        const occ = !!(data[1 + bi] & (1 << bit));
        if (this.state.sensors[contact] !== occ) { this.state.sensors[contact] = occ; changed = true; }
      }
    }
    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]; }
  _adr(addr) { // Z21: 14-bit adres; >=128 vereist de twee hoogste bits gezet in MSB
    const msb = (addr >> 8) & 0x3F; const lsb = addr & 0xFF;
    return [addr >= 128 ? (0xC0 | msb) : msb, lsb];
  }

  power(on) {
    this.state.power = !!on;
    this._send(this._xbus(on ? [0x21, 0x81] : [0x21, 0x80])); // X_SET_TRACK_POWER_ON/OFF
    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;
    // 128-stappen: map 0..126 → 0..127 (1 = noodstop in DCC128, dus +1 vanaf stap 1)
    let dcc;
    if (ui < 0) dcc = 1;                       // noodstop
    else if (ui === 0) dcc = 0;
    else dcc = Math.min(127, ui + 1);
    const speedByte = (dir ? 0x80 : 0x00) | (dcc & 0x7F);
    const [aH, aL] = this._adr(addr);
    // LAN_X_SET_LOCO_DRIVE: 0xE4 0x13 AdrH AdrL Speed [xor]
    this._send(this._xbus([0xE4, 0x13, aH, aL, speedByte]));
    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 [aH, aL] = this._adr(addr);
    // LAN_X_SET_LOCO_FUNCTION: 0xE4 0xF8 AdrH AdrL (TT FFFFFF) ; TT: 00=uit 01=aan 10=wissel
    const db = ((on ? 0x40 : 0x00) | (fn & 0x3F));
    this._send(this._xbus([0xE4, 0xF8, aH, aL, db]));
    this.emit('state');
  }

  estop() {
    for (const a of Object.keys(this.state.locos)) this.state.locos[a].speed = 0;
    this._send(this._xbus([0x80]));            // LAN_X_SET_STOP (noodstop)
    this.emit('log', '⛔ NOODSTOP'); this.emit('state');
  }

  // CV-programmeren op het hoofdspoor (POM): LAN_X_CV_POM_WRITE_BYTE
  writeCV(addr, cv, value) {
    addr = parseInt(addr, 10); cv = parseInt(cv, 10); value = parseInt(value, 10);
    if (!addr || !cv || isNaN(value)) return;
    const [aH, aL] = this._adr(addr);
    const cva = cv - 1;                          // CV-adres is 0-gebaseerd
    this._send(this._xbus([0xE6, 0x30, aH, aL, 0xEC | ((cva >> 8) & 0x03), cva & 0xFF, value & 0xFF]));
    this.emit('log', '✎ CV' + cv + ' = ' + value + ' (loc ' + addr + ', POM)');
  }

  turnout(id, thrown) {
    id = parseInt(id, 10); this.state.turnouts[id] = !!thrown;
    const a = id - 1;                          // Z21 wisseladres 0-based
    const aH = (a >> 8) & 0xFF, aL = a & 0xFF;
    // LAN_X_SET_TURNOUT: 0x53 AdrH AdrL (0x80 | active<<3 | output)
    const db = 0x80 | 0x08 | (thrown ? 0x01 : 0x00);
    this._send(this._xbus([0x53, aH, aL, db]));
    this.emit('state');
  }
}

module.exports = { Z21 };
