package main

import (
	"context"
	"time"

	"github.com/simonvetter/modbus"
	"k8s.io/klog"
)

func modbusValuesToFloat(v uint16) float32 {
	return float32(v) / 10.0
}

func (d *daemon) modbusRestart() error {
	d.modbusClient.Close()
	return d.modbusClient.Open()
}

// There are currently two devices connected to the modbus.
// The first one (slave 1) is a temperature/humidity sensor.
// The second one (slave 2) is a PTA8D08 transmitter
//
// Returns whether modbus should restart (only in case of an underlying network error)
func (d *daemon) modbusUpdate() bool {
	var err error
	var numDevicesNotResponding int

	// Switch to slave 1 (BTA1)
	d.modbusClient.SetUnitId(1)

	// Read temperature and humidity
	var registersBTA1 []uint16 // temperature, humidity
	registersBTA1, err = d.modbusClient.ReadRegisters(1, 2, modbus.INPUT_REGISTER)
	if err != nil {
		numDevicesNotResponding += 1
		klog.Warningf("error while reading registers from BTA1 %v", err)
	} else if len(registersBTA1) != 2 {
		klog.Warningf("expected two registers from modbus slave 1, but got %d", len(registersBTA1))
	} else {
		d.mu.Lock()
		d.daemonState.tempSEM = modbusValuesToFloat(registersBTA1[0])
		d.daemonState.humiditySEM = modbusValuesToFloat(registersBTA1[1])
		d.mu.Unlock()
	}

	// Switch to slave 2 (KEC2)
	d.modbusClient.SetUnitId(2)

	// PT100 mapping
	// Channel 0: Cable -WGA6, Sensor "dp bottom"
	// Channel 1: Cable -WGA8, Sensor "dp inlet"
	// Channel 2: Cable WGA7, Sensor "dp top"
	var registersKEC2 []uint16 // temperatures dp
	registersKEC2, err = d.modbusClient.ReadRegisters(0, 3, modbus.HOLDING_REGISTER)
	if err != nil {
		numDevicesNotResponding += 1
		klog.Warningf("error while reading registers from KEC2 %v", err)
	} else if len(registersKEC2) != 3 {
		klog.Warningf("expected three registers from modbus slave 2, but got %d", len(registersKEC2))
	} else {
		d.mu.Lock()
		d.daemonState.tempDPBottom = modbusValuesToFloat(registersKEC2[0])
		d.daemonState.tempDPInlet = modbusValuesToFloat(registersKEC2[1])
		d.daemonState.tempDPTop = modbusValuesToFloat(registersKEC2[2])
		d.mu.Unlock()
	}

	// Switch to slave 3 (KEC1)
	d.modbusClient.SetUnitId(3)

	// Do a read first to avoid side-effects from the subsequent write to the relay states
	var digitalInputs [8]bool
	var digitalInputRegisters []uint16

	digitalInputRegisters, err = d.modbusClient.ReadRegisters(0x81, 8, modbus.HOLDING_REGISTER)
	if err != nil {
		numDevicesNotResponding += 1
		klog.Warningf("error while reading digital inputs from KEC1 %v", err)
	} else {
		// Convert MODBUS words into bools
		for idx, value := range digitalInputRegisters {
			if value != 0 {
				digitalInputs[idx] = true
			} else {
				digitalInputs[idx] = false
			}
		}
		// TODO: Input mapping goes here
	}

	// We must wait between reading and writing to the -KEC1 relay board
	// because otherwise it chokes and times out the write registers
	// command.
	time.Sleep(time.Millisecond * 10)

	// KFA1-KFA8
	var relayState [8]bool
	d.mu.Lock()
	// -KFA1 Roughing Pump (normally closed contact)
	relayState[0] = !d.daemonState.rpOn
	// -KFA2 Diffusion Pump
	relayState[1] = d.daemonState.dpOn
	// -KFA4 Button Vent
	relayState[3] = d.daemonState.vent.output
	// -KFA5 Button Pump-Down
	relayState[4] = d.daemonState.pumpdown.output
	// -KFA6 Fake-Pirani Rough (normally closed contact)
	relayState[5] = !d.aboveRough.output
	// -KFA7 Fake-Pirani High (normally closed contact)
	relayState[6] = !d.aboveHigh.output
	d.mu.Unlock()

	// The KEC1 module uses a non-standard MODBUS interface
	// instead of coils
	// 0x0100 is the open command
	// 0x0200 is the close command
	// We write 8 words (16-bit) to address 0x01 to update the relays
	var registerValuesKEC1 [8]uint16
	// Convert the boolean values to the commands
	for idx, state := range relayState {
		if state {
			registerValuesKEC1[idx] = 0x0100
		} else {
			registerValuesKEC1[idx] = 0x0200
		}
	}

	err = d.modbusClient.WriteRegisters(0x01, registerValuesKEC1[:])
	if err != nil {
		numDevicesNotResponding += 1
		klog.Warningf("error while updating registers %v", err)
	}

	if numDevicesNotResponding >= 4 {
		klog.Warningf("no device did respond to our request. Probably a network timeout.")
		return true
	}

	return false
}

// Call modbusUpdate every 100 milliseconds
func (d *daemon) modbusProcess(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		default:
			shouldRestart := d.modbusUpdate()
			// the modbus library does not reopen the tcp socket in case of
			// a connection loss.
			if shouldRestart {
				klog.Infof("restarting modbus connection...")
				err := d.modbusRestart()
				if err != nil {
					klog.Warningf("failed to restart modbus %v", err)
				}
			}
			time.Sleep(time.Millisecond * 100)
		}
	}
}
