exports.id = 'modbus_reader';
exports.title = 'Modbus reader';
exports.version = '2.0.0';
exports.group = 'Worksys';
exports.color = '#2134B0';
exports.input = 1;
exports.output = ["red", "white", "yellow"];
exports.click = false;
exports.author = 'Rastislav Kovac';
exports.icon = 'bolt';
exports.readme = `
  Modbus requests to modbus devices (electromer, twilight sensor, thermometer.
  Component keeps running arround deviceConfig array in "timeoutInterval" intervals. Array items are objects with single modbus devices.
  Everything is sent to dido_controller. If requests to device fail (all registers must fail to send NOK status) , we send "NOK-'device'" status to dido_controller. 
  This device needs to be configured in dido_controller!!! Double check if it is. In dido_controller we calculate final status and all values with status are pushed to tb.
`;

const modbus = require('jsmodbus');
const SerialPort = require('serialport');
const { timeoutInterval, deviceConfig } = require("../databases/modbus_config");
const { sendNotification } = require('./helper/notification_reporter');

const DELAY_BETWEEN_DEVICES = 10000;

const SEND_TO = {
  debug: 0,
  dido_controller: 1,
  tb: 2
};

//to handle NOK and OK sendNotifications s
const numberOfNotResponding = {};
let tbName = null;
let mainSocket;
//number of phases inRVO
let phases;
//phases where voltage is 0 (set)
let noVoltage;
let energyToSwitchLamps;

exports.install = function(instance) {

  class SocketWithClients {

    constructor() {
      this.stream = null;
      this.socket = null;
      this.clients = {};
      this.allValues = {};
      this.errors = 0;
      this.index = 0;
      this.timeoutInterval = 5000;

      // we need to go always around for all devices. So we need index value, device address, as well as number of registers for single device
      this.deviceAddress = null;   // device address (1 - EM340 and 2 for twilight_sensor)
      this.indexInDeviceConfig = 0;  // first item in deviceConfig
      this.lengthOfActualDeviceStream = null;
      this.device = null;

      // lampSwitchNotification helper variables
      this.onNotificationSent = false;
      this.offNotificationSent = false;
      this.phases = this.buildPhases();

      this.startSocket();
    }

    buildPhases = () => {
      let a = [];
      for (let i = 1; i <= phases; i++) {
        a.push(`Phase_${i}_voltage`)
      }
      return a;
    }

    startSocket = () => {

      let obj = this;

      if (this.socket) {
        this.socket.removeAllListeners();
        this.socket = null;
      }

      this.socket = new SerialPort("/dev/ttymxc0", {
        baudRate: 9600,
      })

      // we create a client for every deviceAddress ( = address) in  list and push them into dictionary 
      for (let i = 0; i < deviceConfig.length; i++) {
        this.clients[deviceConfig[i].deviceAddress] = new modbus.client.RTU(this.socket, deviceConfig[i].deviceAddress, 2000); // 2000 is timeout in register request, default is 5000, which is too long
      }

      this.socket.on('error', function(e) {
        console.log('Modbus_reader: Socket connection error', e); //'ECONNREFUSED' or 'ECONNRESET' ??
      });

      this.socket.on('close', function() {
        console.log('Modbus_reader: Socket connection closed - Waiting 10 seconds before connecting again');
        setTimeout(obj.startSocket, 10000);
      });

      this.socket.on('open', function() {
        console.log("socket connected");
        obj.getActualStreamAndDevice();
        obj.timeoutInterval = timeoutInterval - DELAY_BETWEEN_DEVICES; // to make sure readout always runs in timeoutinterval we substract DELAY_BETWEEN_DEVICES
      })

    };

    getActualStreamAndDevice = () => {
      const dev = deviceConfig[this.indexInDeviceConfig];
      this.index = 0;
      this.errors = 0;
      this.stream = dev.stream;
      this.lengthOfActualDeviceStream = dev.stream.length;
      this.deviceAddress = dev.deviceAddress; // 1 or 2 or any number
      this.device = dev.device; //em340, twilight_sensor

      //if we just start to loop devices from the beginning, or there is just 1 device in config, we wait whole timeoutInterval
      if (this.indexInDeviceConfig == 0 || deviceConfig.length === 1) setTimeout(this.readRegisters, this.timeoutInterval);
      else setTimeout(this.readRegisters, DELAY_BETWEEN_DEVICES);
    }

    readRegisters = () => {

      const str = this.stream[this.index];
      const register = str.register;
      const size = str.size;
      const tbAttribute = str.tbAttribute;

      let obj = this;

      this.clients[this.deviceAddress].readHoldingRegisters(register, size)
        .then(function(resp) {

          resp = resp.response._body.valuesAsArray; //resp is array of length 1 or 2, f.e. [2360,0]
          // console.log(deviceAddress, register, tbAttribute, resp);

          //device is responding again after NOK status
          if (numberOfNotResponding.hasOwnProperty(obj.device)) {
            let message = "";
            if (obj.device == "em340") {
              message = "electrometer_ok";
            }
            else if (obj.device == "twilight_sensor") {
              message = "twilight_sensor_ok";
            }
            message && sendNotification("modbus_reader: readRegisters", tbName, message, {}, "", SEND_TO.tb, instance);
            delete numberOfNotResponding[obj.device];
          }

          obj.transformResponse(resp, register);

          //obj.errors = 0;
          obj.index++;
          obj.readAnotherRegister();

        }).catch(function() {

          //console.log("errors pri citani modbus registra", register, obj.indexInDeviceConfig, tbName, tbAttribute);

          obj.errors++;
          if (obj.errors == obj.lengthOfActualDeviceStream) {
            instance.send(SEND_TO.dido_controller, { status: "NOK-" + obj.device }); // NOK-em340, NOK-em111, NOK-twilight_sensor, NOK-thermometer

            //todo - neposlalo notification, ked sme vypojili twilight a neposle to do tb, ale do dido ??
            if (!numberOfNotResponding.hasOwnProperty(obj.device)) {
              let message = "";
              if (obj.device == "twilight_sensor") {
                message = "twilight_sensor_nok";
              }
              else if (obj.device == "em340") {
                message = "electrometer_nok";
              }
              message && sendNotification("modbus_reader: readingTimeouted", tbName, message, {}, "", SEND_TO.tb, instance);
              numberOfNotResponding[obj.device] = 1;
            }

            obj.errors = 0;
            numberOfNotResponding[obj.device] += 1;
          }

          // console.error(require('util').inspect(arguments, {
          //   depth: null
          // }))

          // if reading out of device's last register returns error, we send accumulated allValues to dido_controller (if allValues are not an empty object)
          if (obj.index + 1 >= obj.lengthOfActualDeviceStream) {
            if (!isObjectEmpty(obj.allValues)) instance.send(SEND_TO.dido_controller, { values: obj.allValues });
            obj.allValues = {};
          }
          obj.index++;
          obj.readAnotherRegister();
        })

    };

    readAnotherRegister = () => {
      if (this.index < this.lengthOfActualDeviceStream) setTimeout(this.readRegisters, 0);
      else this.setNewStream();
    }

    transformResponse = (response, register) => {

      for (let i = 0; i < this.lengthOfActualDeviceStream; i++) {

        let a = this.stream[i];
        if (a.register === register) {
          let tbAttribute = a.tbAttribute;
          let multiplier = a.multiplier;

          let value = this.calculateValue(response, multiplier);
          // console.log(register, tbName, tbAttribute, response, a.multiplier, value);

          // if(tbName == undefined) return;

          if (this.index + 1 < this.lengthOfActualDeviceStream) {
            this.allValues[tbAttribute] = value;
            return;
          }

          const values = {
            ...this.allValues,
            [tbAttribute]: value,
          };

          this.checkNullVoltage(values);
          this.lampSwitchNotification(values);

          instance.send(SEND_TO.dido_controller, { values: values });

          this.allValues = {};
          break;
        }
      }
    }

    setNewStream = () => {
      if (this.lengthOfActualDeviceStream == this.index) {
        if (this.indexInDeviceConfig + 1 == deviceConfig.length) {
          this.indexInDeviceConfig = 0;
        }
        else {
          this.indexInDeviceConfig += 1;
        }

        this.getActualStreamAndDevice();
      }
    }

    calculateValue = (response, multiplier) => {
      let value = 0;

      let l = response.length;
      if (l === 2) {
        value = (response[1] * (2 ** 16) + response[0]);

        if (value >= (2 ** 31))                              // ak je MSB bit nastavený, eventuálne sa dá použiť aj (value & 0x80000000), ak vieš robiť logický súčin
        {
          value = value - "0xFFFFFFFF" + 1;
        }
      }
      else if (l === 1) {
        value = response[0];

        if (value >= (2 ** 15))                              // ak je MSB bit nastavený, eventuálne sa dá použiť aj (value & 0x8000), ak vieš robiť logický súčin
        {
          value = value - "0xFFFF" + 1;
        }
      }

      return Math.round(value * multiplier * 10) / 10;
    }

    checkNullVoltage = (values) => {

      if (!(values.hasOwnProperty("Phase_1_voltage") || values.hasOwnProperty("Phase_2_voltage") || values.hasOwnProperty("Phase_3_voltage"))) return;

      Object.keys(values).map(singleValue => {
        if (this.phases.includes(singleValue)) {
          let l = singleValue.split("_");
          let phase = parseInt(l[1]);

          // console.log(values[singleValue], tbName);

          if (values[singleValue] == 0) {
            noVoltage.add(phase);
            sendNotification("modbus_reader: checkNullVoltage", tbName, "no_voltage_on_phase", { phase: phase }, "", SEND_TO.tb, instance, "voltage" + phase);
            // console.log('no voltage')
          }
          else {
            noVoltage.delete(phase);
            // console.log('voltage detected')
            sendNotification("modbus_reader: checkNullVoltage", tbName, "voltage_on_phase_restored", { phase: phase }, "", SEND_TO.tb, instance, "voltage" + phase);
          }
        }
      })
    }

    /**
     * function sends notification to slack and to tb, if EM total_power value changes more than numberOfNodes*15. This should show, that RVO lamps has been switched on or off
     */
    lampSwitchNotification = (values) => {

      if (!values.hasOwnProperty("total_power")) return;

      const actualTotalPower = values.total_power;

      if (actualTotalPower > energyToSwitchLamps && this.onNotificationSent == false) {
        sendNotification("modbus_reader: lampSwitchNotification", tbName, "lamps_have_turned_on", {}, "", SEND_TO.tb, instance);
        this.onNotificationSent = true;
        this.offNotificationSent = false;
      }
      else if (actualTotalPower <= energyToSwitchLamps && this.offNotificationSent == false) {
        sendNotification("modbus_reader: lampSwitchNotification", tbName, "lamps_have_turned_off", {}, "", SEND_TO.tb, instance);
        this.onNotificationSent = false;
        this.offNotificationSent = true;
      }
    }

  }

  const isObjectEmpty = (objectName) => {
    return Object.keys(objectName).length === 0 && objectName.constructor === Object;
  }

  function main() {

    phases = FLOW.GLOBALS.settings.phases;
    tbName = FLOW.GLOBALS.settings.rvoTbName;
    noVoltage = FLOW.GLOBALS.settings.no_voltage;
    energyToSwitchLamps = FLOW.GLOBALS.settings.energy_to_switch_lamps / 2.5; //half value is enought to show if lamps are turned on or off
    if (deviceConfig.length) mainSocket = new SocketWithClients();
    else console.log("Modbus_reader: no modbus device in configuration");

    // this notification is to show, that flow (unipi) has been restarted
    sendNotification("modbus_reader", tbName, "flow_restart", {}, "", SEND_TO.slack, instance);
  }

  instance.on("0", function(_) {
    main();
  })

}

