const mqtt = require('mqtt');
const knx = require('knx');
const dptlib = require('./node_modules/knx/src/dptlib/index')

const instance_id = 'default:knx_inst';
const broker_url = 'mqtt://147.229.159.13/'
// const instance_id = 'knx:knx_inst';
// const broker_url = 'mqtt://127.0.0.1/'

// FlowBox MQTT API docs: https://docs.flowbox.cz/pages/viewpage.action?pageId=14712938

const SETUP_TOPIC = `flowbox/instance/${instance_id}/in/setup`;
const JOIN_TOPIC = `flowbox/instance/${instance_id}/out/join`;
const KNX_DEBUG = true;

console.log('Starting');

let connection = null;
let options = null;
let rt_mod = null;
let mqClient = mqtt.connect(broker_url);

mqClient.on('connect', function () {
  console.log('MQTT connected');
  mqClient.subscribe(SETUP_TOPIC, function (err) {
    if (err) {
      console.error("Error subscribe to SETUP!");
      process.exit();
    } else {
      console.log('Subscribed to SETUP, sending JOIN');
      mqClient.publish(JOIN_TOPIC, '');
    }
  });
});

mqClient.on('message', function (topic, message) {
  let payload = JSON.parse(message.toString());

  console.log('MQTT recv:', topic, ', data: ', JSON.stringify(payload, null, '  '));

  if (topic === SETUP_TOPIC) {
    console.log("SETUP received!");
    /*
     This is either:
      - a response to our JOIN, or
      - update from FlowBox (e.g. the component changed, was deleted etc.)
    */

    // Disconnect if we were connected
    closeKnxConnection();
    // Set-up KNX
    setupInstance(payload);
  }
  else if (topic.endsWith('out/modified')) {
    console.log("Component modified, reconnect!");
    closeKnxConnection();
    rt_mod.component = payload;
    console.log("Reconnect");
    connectToKNX();
  }
  else if (topic.endsWith('cmd/send')) {
    /*
      FlowBox wants to send something to KNX

      {dest: GroupAddress, value: ValueToSend, dt: DataType}

      Data types:
        https://www.promotic.eu/en/pmdoc/Subsystems/Comm/PmDrivers/KNXDTypes.htm
        https://www.knx.org/wAssets/docs/downloads/Certification/Interworking-Datapoint-types/03_07_02-Datapoint-Types-v02.01.02-AS.pdf
    */

    if (connection) {
      /*
      The payload looks like this:
        {"reply_to":null,"token":0,"data":{"dest":"1/1/1","value":1,"dpt":null},"timeout":0}
      value can be string, integer, object etc., depending on what the KNX DPT library wants
      */
      let data = payload.data;
      console.log("Sending", JSON.stringify(data, null, '  '));
      let dpt = fixDpt(data.dpt);
      console.log("DPT = ", dpt);
      connection.write(data.dest, data.value, dpt);
    }
  }
  else if (topic.endsWith('out/deleted')) {
    console.warn("Component deleted, close connection");
    // Our component was deleted?
    closeKnxConnection();
  }
});


function closeKnxConnection() {
  if (connection !== null) {
    console.log("Closing old connection");
    connection.Disconnect();
    connection = null;
  }
}


/** Convert DPT from the DPST-X-Y format used in XML */
function fixDpt(dpt) {
  if (dpt === null) return null;
  let matches;
  if (null !== (matches = dpt.match(/^DPST-(\d+)-(\d+)$/))) {
    return 'DPT' + matches[1]+'.'+matches[2].padStart(3, '0');
  }
  return dpt;
}


function setupInstance (options) {
  console.log('Setup instance');

  /* Example instance config:
      {
        "modules": [
          {
            "type": "knx_gateway",
            "node_id": "608bc98cca6f390ed402db22",
            "component": {
              "name": "knx",
              "alias": "knx",
              "comp_type": "knx_gateway",
              "realm_id": "608bc958ca6f390ed238df22",
              "rt_instance": "knx_inst",
              "rt_instance_id": "608bc977ca6f39001c609a22",
              "config_change_time": 1619773836,
              "_id": "608bc98cca6f390ed402db22"
            }
          }
        ]
      }
   */

  // FlowBox RT instances normally contain many modules running on the same host.
  // Only one (KNX) module is expected in this implementation.

  if (options.modules.length !== 1) {
    console.error('This instance requires exactly one module');
    return;
  }

  rt_mod = options.modules[0];

  // Check this component is something we support
  if (rt_mod.type !== 'knx_gateway') {
    console.error('Bad module type');
    return;
  }

  let comp = rt_mod.component;

  // Subscribe to topics relating to our node and its component
  mqClient.subscribe(`flowbox/node/${rt_mod.node_id}/in/cmd/send`);
  mqClient.subscribe(`flowbox/comp/${comp._id}/out/deleted`);
  mqClient.subscribe(`flowbox/comp/${comp._id}/out/modified`);

  connectToKNX();
}

function connectToKNX() {
  let comp = rt_mod.component;
  let knownAddresses = comp.groupAddresses || {};

  options = {
    // Use values from the component, or defaults
    ipAddr: comp.ipAddr || '224.0.23.12',
    ipPort: comp.ipPort || 3671,
    interface: comp.interface || null,
    physAddr: comp.physAddr || '15.15.15',
    minimumDelay: comp.minimumDelay || 0,
    debug: KNX_DEBUG,
  };

  console.log("Options: ", JSON.stringify(options, null, '  '));

  try {
    connection = knx.Connection(Object.assign({}, options, {
      handlers: {
        connected: function () {
          console.log('KNX connected!');
        },

        // get notified for all KNX events:
        event: function (event, src, dest, value) {
          console.log('KNX event: %s, src: %j, dest: %j, value:', event, src, dest, value);

          if (src === options.physAddr) {
            console.log("Discard, this was sent by our node.");
            return;
          }

          let dpt = null;
          let parsed = null;
          if (dest in knownAddresses) {
            try {
              dpt = fixDpt(knownAddresses[dest].dpt);
              let dptResolved = dptlib.resolve(dpt);
              parsed = dptlib.fromBuffer(value, dptResolved);
            } catch (e) {
              console.error("Failed parsing value using DPTlib:", e.message);
            }
          }

          let payload = {
            event,
            src,
            dest,
            dpt, // for debugging
            hex: value.toString('hex'), // for debugging
            value: parsed
          };

          console.log('Publish event:', payload);
          mqClient.publish(`flowbox/node/${rt_mod.node_id}/out/event/receive`,
            JSON.stringify(payload));
        },

        error: function (connstatus) {
          console.error("KNX error", connstatus);
        }
      }
    }));
  } catch (e) {
    console.error("Connect error:", e);
  }
}

// Keep the node event loop running
process.stdin.resume();
