// Package psu drives a bench power supply over Modbus RTU — the BSIDE / Wanptek
// family of programmable supplies that sit in a shack feeding the radios.
//
// The register map and the wire settings come from the manufacturer's own
// document ("This machine only support function code 03,06", version 20180611):
//
//	9600 baud, 8 data bits, no parity, 1 stop bit
//	function 03  read holding registers
//	function 06  write single register
//	slave address 1…15, address 0 broadcast
//
// NOTHING ELSE IS WRITTEN. The map also carries the output voltage and current
// SET points, and the over-voltage, over-current and over-power trip levels, all
// read/write. This driver reads them and writes exactly one register: 0x0001,
// the output on/off. A wrong value in any of the others is not a wrong reading —
// it is 30 V where a radio expected 13.8, or a protection trip lifted on a
// supply feeding an amplifier. There is no reason for a logbook to set them, so
// it cannot.
package psu

import (
	"encoding/binary"
	"fmt"
	"io"
	"time"
)

// Registers, from the manufacturer's table. Addresses are as printed there.
const (
	regOnOff    = 0x0001 // output on/off — 1 or 0. The ONLY register written.
	regProtect  = 0x0002 // protection status word
	regModel    = 0x0003 // specification model
	regDecimals = 0x0005 // "V_A_W number of digits" — see readDecimals
	regVolts    = 0x0010 // measured output voltage, 2 decimals
	regAmps     = 0x0011 // measured output current, 3 decimals
	regWatts    = 0x0012 // measured output power, 32-bit across 0x0012/0x0013, 3 decimals
	regSetVolts = 0x0030 // voltage set point, 2 decimals
	regSetAmps  = 0x0031 // current set point, 3 decimals
)

const (
	fnRead  = 0x03
	fnWrite = 0x06
)

// Fixed scaling from the manufacturer's "Decimal place" column. The supply also
// reports its own digit counts in 0x0005, but the document's "Note 2" that
// explains how to decode that word is not in the manual we have — so the
// documented per-register values are used, and the raw word is logged once at
// connect. If an operator ever reports readings out by a factor of ten, that
// line is what says how to decode it properly.
const (
	voltScale = 100.0  // 2 decimals
	ampScale  = 1000.0 // 3 decimals
	wattScale = 1000.0 // 3 decimals
)

// crc16 is the Modbus RTU frame check: CRC-16/MODBUS — reflected, polynomial
// 0xA001, initial value 0xFFFF, no final xor. Transmitted low byte first.
func crc16(b []byte) uint16 {
	crc := uint16(0xFFFF)
	for _, c := range b {
		crc ^= uint16(c)
		for i := 0; i < 8; i++ {
			if crc&1 != 0 {
				crc = (crc >> 1) ^ 0xA001
			} else {
				crc >>= 1
			}
		}
	}
	return crc
}

// appendCRC closes a frame: low byte first, as the manual states.
func appendCRC(f []byte) []byte {
	c := crc16(f)
	return append(f, byte(c&0xFF), byte(c>>8))
}

// buildRead frames a function 03 "read holding registers".
func buildRead(addr byte, reg uint16, count uint16) []byte {
	f := []byte{addr, fnRead, byte(reg >> 8), byte(reg), byte(count >> 8), byte(count)}
	return appendCRC(f)
}

// buildWrite frames a function 06 "write single register".
func buildWrite(addr byte, reg, val uint16) []byte {
	f := []byte{addr, fnWrite, byte(reg >> 8), byte(reg), byte(val >> 8), byte(val)}
	return appendCRC(f)
}

// modbusError is an exception response — the supply understood the frame and
// refused it. Kept distinct from a transport failure: one means "ask
// differently", the other means "the cable".
type modbusError struct {
	fn   byte
	code byte
}

func (e modbusError) Error() string {
	what := map[byte]string{
		1: "illegal function",
		2: "illegal data address",
		3: "illegal data value",
		4: "slave device failure",
		6: "device busy",
	}[e.code]
	if what == "" {
		what = fmt.Sprintf("exception %d", e.code)
	}
	return fmt.Sprintf("supply refused function 0x%02X: %s", e.fn, what)
}

// parseRead validates a function 03 reply and returns the register values.
func parseRead(addr byte, want uint16, frame []byte) ([]uint16, error) {
	if err := checkFrame(addr, fnRead, frame, 5); err != nil {
		return nil, err
	}
	n := int(frame[2])
	if n != int(want)*2 {
		return nil, fmt.Errorf("reply carries %d data byte(s), expected %d", n, want*2)
	}
	if len(frame) != 3+n+2 {
		return nil, fmt.Errorf("reply is %d bytes, expected %d", len(frame), 3+n+2)
	}
	out := make([]uint16, want)
	for i := range out {
		out[i] = binary.BigEndian.Uint16(frame[3+i*2:])
	}
	return out, nil
}

// parseWriteEcho validates a function 06 reply, which echoes the request.
func parseWriteEcho(addr byte, reg, val uint16, frame []byte) error {
	if err := checkFrame(addr, fnWrite, frame, 8); err != nil {
		return err
	}
	if len(frame) != 8 {
		return fmt.Errorf("write reply is %d bytes, expected 8", len(frame))
	}
	if got := binary.BigEndian.Uint16(frame[2:]); got != reg {
		return fmt.Errorf("write reply is for register 0x%04X, not 0x%04X", got, reg)
	}
	// The echoed VALUE is the confirmation that the output actually changed.
	// Accepting the frame without checking it would report an on/off that the
	// supply never made.
	if got := binary.BigEndian.Uint16(frame[4:]); got != val {
		return fmt.Errorf("supply echoed value %d, not the %d it was sent", got, val)
	}
	return nil
}

// checkFrame covers what every reply must satisfy: our address, our function
// (or its exception), and a good CRC.
func checkFrame(addr, fn byte, frame []byte, min int) error {
	if len(frame) < 4 {
		return fmt.Errorf("short reply (%d bytes)", len(frame))
	}
	if frame[0] != addr {
		return fmt.Errorf("reply from address %d, expected %d", frame[0], addr)
	}
	if frame[1] == fn|0x80 {
		if len(frame) < 5 {
			return fmt.Errorf("short exception reply (%d bytes)", len(frame))
		}
		if !crcOK(frame[:5]) {
			return fmt.Errorf("exception reply failed its CRC")
		}
		return modbusError{fn: fn, code: frame[2]}
	}
	if frame[1] != fn {
		return fmt.Errorf("reply to function 0x%02X, expected 0x%02X", frame[1], fn)
	}
	if len(frame) < min {
		return fmt.Errorf("short reply (%d bytes, expected at least %d)", len(frame), min)
	}
	if !crcOK(frame) {
		return fmt.Errorf("reply failed its CRC")
	}
	return nil
}

// crcOK checks a whole frame, trailing CRC included: the CRC of the entire
// frame is zero when it is intact.
func crcOK(frame []byte) bool {
	if len(frame) < 3 {
		return false
	}
	body := frame[:len(frame)-2]
	want := uint16(frame[len(frame)-2]) | uint16(frame[len(frame)-1])<<8
	return crc16(body) == want
}

// frameGap is the silence that separates two Modbus RTU frames: 3.5 character
// times, which at 9600 baud 8N1 (10 bits per character) is 3.65 ms. Rounded up,
// because the cost of waiting is nothing and the cost of being early is a
// supply that treats our request as the tail of the previous one.
const frameGap = 4 * time.Millisecond

// replyWait is how long a reply may take. The manual promises under 5 ms at
// 9600 baud or better; this is generous by two orders of magnitude so a USB
// serial bridge that buffers cannot be mistaken for a supply that is not there.
const replyWait = 500 * time.Millisecond

// readFrame collects a reply until it stops arriving.
//
// A serial read that times out returns (0, nil) on Windows — a timeout is not
// an error on this transport — so a loop that trusts an error to end it never
// ends. Modbus RTU has no terminator either: a frame is over when the line has
// been quiet for 3.5 character times. Both facts point at the same shape, a
// deadline and a quiet-time.
func readFrame(conn io.Reader, d time.Duration) ([]byte, error) {
	deadline := time.Now().Add(d)
	buf := make([]byte, 0, 64)
	tmp := make([]byte, 64)
	lastByte := time.Time{}
	for {
		n, err := conn.Read(tmp)
		if err != nil {
			return buf, err
		}
		if n > 0 {
			buf = append(buf, tmp[:n]...)
			lastByte = time.Now()
			continue
		}
		// Nothing this time: either the frame has ended, or it never started.
		if len(buf) > 0 && time.Since(lastByte) >= frameGap {
			return buf, nil
		}
		if time.Now().After(deadline) {
			if len(buf) > 0 {
				return buf, nil // partial — let the parser say what is wrong with it
			}
			return nil, fmt.Errorf("no reply after %s", d)
		}
	}
}
