package mk2driver

import (
	"errors"
	"fmt"
	"io"
	"sync"
	"time"

	"github.com/sirupsen/logrus"
)

type scaling struct {
	scale     float64
	offset    float64
	signed    bool
	supported bool
}

//nolint:deadcode,varcheck
const (
	ramVarVMains = iota
	ramVarIMains
	ramVarVInverter
	ramVarIInverter
	ramVarVBat
	ramVarIBat
	ramVarVBatRipple
	ramVarInverterPeriod
	ramVarMainPeriod
	ramVarIACLoad
	ramVarVirSwitchPos
	ramVarIgnACInState
	ramVarMultiFuncRelay
	ramVarChargeState
	ramVarInverterPower1
	ramVarInverterPower2
	ramVarOutPower

	ramVarMaxOffset = 14
)

const (
	infoFrameHeader = 0x20
	frameHeader     = 0xff
)

const (
	acL1InfoFrame  = 0x08
	dcInfoFrame    = 0x0C
	setTargetFrame = 0x41
	infoReqFrame   = 0x46 //F
	ledFrame       = 0x4C
	vFrame         = 0x56
	winmonFrame    = 0x57
)

// info frame types
const (
	infoReqAddrDC   = 0x00
	infoReqAddrACL1 = 0x01
)

// winmon frame commands
const (
	commandReadRAMVar    = 0x30
	commandGetRAMVarInfo = 0x36

	commandReadRAMResponse       = 0x85
	commandGetRAMVarInfoResponse = 0x8E
)

type mk2Ser struct {
	info       *Mk2Info
	p          io.ReadWriter
	scales     []scaling
	scaleCount int
	run        chan struct{}
	frameLock  bool
	infochan   chan *Mk2Info
	wg         sync.WaitGroup
}

func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
	mk2 := &mk2Ser{}
	mk2.p = dev
	mk2.info = &Mk2Info{}
	mk2.scaleCount = 0
	mk2.frameLock = false
	mk2.scales = make([]scaling, 0, ramVarMaxOffset)
	mk2.setTarget()
	mk2.run = make(chan struct{})
	mk2.infochan = make(chan *Mk2Info)
	mk2.wg.Add(1)
	go mk2.frameLocker()
	return mk2, nil
}

// Locks to incoming frame.
func (m *mk2Ser) frameLocker() {
	frame := make([]byte, 256)
	var frameLength byte
	for {
		select {
		case <-m.run:
			m.wg.Done()
			return
		default:
		}
		if m.frameLock {
			frameLength = m.readByte()
			frameLengthOffset := int(frameLength) + 1
			l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
			if err != nil {
				m.addError(fmt.Errorf("Read Error: %v", err))
				m.frameLock = false
			} else if l != frameLengthOffset {
				m.addError(errors.New("Read Length Error"))
				m.frameLock = false
			} else {
				m.handleFrame(frameLength, frame[:frameLengthOffset])
			}
		} else {
			tmp := m.readByte()
			frameLengthOffset := int(frameLength)
			if tmp == frameHeader || tmp == infoFrameHeader {
				l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
				if err != nil {
					m.addError(fmt.Errorf("Read Error: %v", err))
					time.Sleep(1 * time.Second)
				} else if l != frameLengthOffset {
					m.addError(errors.New("Read Length Error"))
				} else {
					if checkChecksum(frameLength, tmp, frame[:frameLengthOffset]) {
						m.frameLock = true
						logrus.Info("Locked")
					}
				}
			}
			frameLength = tmp
		}
	}
}

// Close Mk2
func (m *mk2Ser) Close() {
	close(m.run)
	m.wg.Wait()
}

func (m *mk2Ser) C() chan *Mk2Info {
	return m.infochan
}

func (m *mk2Ser) readByte() byte {
	buffer := make([]byte, 1)
	_, err := io.ReadFull(m.p, buffer)
	if err != nil {
		m.addError(fmt.Errorf("Read error: %v", err))
		return 0
	}
	return buffer[0]
}

// Adds error to error slice.
func (m *mk2Ser) addError(err error) {
	if m.info.Errors == nil {
		m.info.Errors = make([]error, 0)
	}
	m.info.Errors = append(m.info.Errors, err)
	m.info.Valid = false
}

// Updates report.
func (m *mk2Ser) updateReport() {
	m.info.Timestamp = time.Now()
	select {
	case m.infochan <- m.info:
	default:
	}
	m.info = &Mk2Info{}
}

// Checks for valid frame and chooses decoding.
func (m *mk2Ser) handleFrame(l byte, frame []byte) {
	logrus.Debugf("frame %#v", frame)
	if checkChecksum(l, frame[0], frame[1:]) {
		switch frame[0] {
		case frameHeader:
			switch frame[1] {
			case vFrame:
				m.versionDecode(frame[2:])
			case winmonFrame:
				switch frame[2] {
				case commandGetRAMVarInfoResponse:
					m.scaleDecode(frame[2:])
				case commandReadRAMResponse:
					m.stateDecode(frame[2:])
				}

			case ledFrame:
				m.ledDecode(frame[2:])
			}

		case infoFrameHeader:
			switch frame[5] {
			case dcInfoFrame:
				m.dcDecode(frame[1:])
			case acL1InfoFrame:
				m.acDecode(frame[1:])
			}
		}
	} else {
		logrus.Errorf("Invalid incoming frame checksum: %x", frame)
		m.frameLock = false
	}
}

// Set the target VBus device.
func (m *mk2Ser) setTarget() {
	cmd := make([]byte, 3)
	cmd[0] = setTargetFrame
	cmd[1] = 0x01
	cmd[2] = 0x00
	m.sendCommand(cmd)
}

// Request the scaling factor for entry 'in'.
func (m *mk2Ser) reqScaleFactor(in byte) {
	cmd := make([]byte, 4)
	cmd[0] = winmonFrame
	cmd[1] = commandGetRAMVarInfo
	cmd[2] = in
	m.sendCommand(cmd)
}

func int16Abs(in int16) uint16 {
	if in < 0 {
		return uint16(-in)
	}
	return uint16(in)
}

// Decode the scale factor frame.
func (m *mk2Ser) scaleDecode(frame []byte) {
	tmp := scaling{}
	logrus.Debugf("Scale frame(%d): 0x%x", len(frame), frame)
	if len(frame) < 6 {
		tmp.supported = false
		logrus.Warnf("Skiping scaling factors for: %d", m.scaleCount)
	} else {
		tmp.supported = true
		var scl int16
		var ofs int16
		if len(frame) == 6 {
			scl = int16(frame[2])<<8 + int16(frame[1])
			ofs = int16(uint16(frame[4])<<8 + uint16(frame[3]))
		} else {
			scl = int16(frame[2])<<8 + int16(frame[1])
			ofs = int16(uint16(frame[5])<<8 + uint16(frame[4]))
		}
		if scl < 0 {
			tmp.signed = true
		}
		tmp.offset = float64(ofs)
		scale := int16Abs(scl)
		if scale >= 0x4000 {
			tmp.scale = 1 / (0x8000 - float64(scale))
		} else {
			tmp.scale = float64(scale)
		}
	}
	logrus.Debugf("scalecount %v: %#v \n", m.scaleCount, tmp)
	m.scales = append(m.scales, tmp)
	m.scaleCount++
	if m.scaleCount < ramVarMaxOffset {
		m.reqScaleFactor(byte(m.scaleCount))
	} else {
		logrus.Info("Monitoring starting.")
	}
}

// Decode the version number
func (m *mk2Ser) versionDecode(frame []byte) {
	logrus.Debugf("versiondecode %v", frame)
	m.info.Version = 0
	m.info.Valid = true
	for i := 0; i < 4; i++ {
		m.info.Version += uint32(frame[i]) << uint(i) * 8
	}

	if m.scaleCount < ramVarMaxOffset {
		logrus.Info("Get scaling factors.")
		m.reqScaleFactor(byte(m.scaleCount))
	} else {
		// Send DC status request
		cmd := make([]byte, 2)
		cmd[0] = infoReqFrame
		cmd[1] = infoReqAddrDC
		m.sendCommand(cmd)
	}
}

// Decode with correct signedness and apply scale
func (m *mk2Ser) applyScaleAndSign(data []byte, scale int) float64 {
	var value float64
	if !m.scales[scale].supported {
		return 0
	}
	if m.scales[scale].signed {
		value = getSigned(data)
	} else {
		value = getUnsigned16(data)
	}
	return m.applyScale(value, scale)
}

// Apply scaling to float
func (m *mk2Ser) applyScale(value float64, scale int) float64 {
	if !m.scales[scale].supported {
		return value
	}
	return m.scales[scale].scale * (value + m.scales[scale].offset)
}

// Convert bytes->int16->float
func getSigned(data []byte) float64 {
	return float64(int16(data[0]) + int16(data[1])<<8)
}

// Convert bytes->int16->float
func getUnsigned16(data []byte) float64 {
	return float64(uint16(data[0]) + uint16(data[1])<<8)
}

// Convert bytes->uint32->float
func getUnsigned(data []byte) float64 {
	return float64(uint32(data[0]) + uint32(data[1])<<8 + uint32(data[2])<<16)
}

// Decodes DC frame.
func (m *mk2Ser) dcDecode(frame []byte) {
	m.info.BatVoltage = m.applyScaleAndSign(frame[5:7], ramVarVBat)

	usedC := m.applyScale(getUnsigned(frame[7:10]), ramVarIBat)
	chargeC := m.applyScale(getUnsigned(frame[10:13]), ramVarIBat)
	m.info.BatCurrent = usedC - chargeC

	m.info.OutFrequency = m.calcFreq(frame[13], ramVarInverterPeriod)
	logrus.Debugf("dcDecode %#v", m.info)

	// Send L1 status request
	cmd := make([]byte, 2)
	cmd[0] = infoReqFrame
	cmd[1] = infoReqAddrACL1
	m.sendCommand(cmd)
}

// Decodes AC frame.
func (m *mk2Ser) acDecode(frame []byte) {
	m.info.InVoltage = m.applyScale(getSigned(frame[5:7]), ramVarVMains)
	m.info.InCurrent = m.applyScale(getSigned(frame[7:9]), ramVarIMains)
	m.info.OutVoltage = m.applyScale(getSigned(frame[9:11]), ramVarVInverter)
	m.info.OutCurrent = m.applyScale(getSigned(frame[11:13]), ramVarIInverter)
	m.info.InFrequency = m.calcFreq(frame[13], ramVarMainPeriod)

	logrus.Debugf("acDecode %#v", m.info)

	// Send status request
	cmd := make([]byte, 1)
	cmd[0] = ledFrame
	m.sendCommand(cmd)
}

func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 {
	if data == 0xff || data == 0x00 {
		return 0
	}
	return 10 / (m.applyScale(float64(data), scaleIndex))
}

// Decode charge state of battery.
func (m *mk2Ser) stateDecode(frame []byte) {
	m.info.ChargeState = m.applyScaleAndSign(frame[1:3], ramVarChargeState)
	logrus.Debugf("battery state decode %#v", m.info)
	m.updateReport()
}

// Decode the LED state frame.
func (m *mk2Ser) ledDecode(frame []byte) {

	m.info.LEDs = getLEDs(frame[0], frame[1])
	// Send charge state request
	cmd := make([]byte, 4)
	cmd[0] = winmonFrame
	cmd[1] = commandReadRAMVar
	cmd[2] = ramVarChargeState
	m.sendCommand(cmd)
}

// Adds active LEDs to list.
func getLEDs(ledsOn, ledsBlink byte) map[Led]LEDstate {

	leds := map[Led]LEDstate{}
	for i := 0; i < 8; i++ {
		on := (ledsOn >> uint(i)) & 1
		blink := (ledsBlink >> uint(i)) & 1
		if on == 1 {
			leds[Led(i)] = LedOn
		} else if blink == 1 {
			leds[Led(i)] = LedBlink
		} else {
			leds[Led(i)] = LedOff
		}
	}
	return leds
}

// Adds header and trailing crc for frame to send.
func (m *mk2Ser) sendCommand(data []byte) {
	l := len(data)
	dataOut := make([]byte, l+3)
	dataOut[0] = byte(l + 1)
	dataOut[1] = frameHeader
	cr := -dataOut[0] - dataOut[1]
	for i := 0; i < len(data); i++ {
		cr = cr - data[i]
		dataOut[i+2] = data[i]
	}
	dataOut[l+2] = cr

	logrus.Debugf("sendCommand %#v", dataOut)
	_, err := m.p.Write(dataOut)
	if err != nil {
		m.addError(fmt.Errorf("Write error: %v", err))
	}
}

// Checks the frame crc.
func checkChecksum(l, t byte, d []byte) bool {
	cr := (uint16(l) + uint16(t)) % 256
	for i := 0; i < len(d); i++ {
		cr = (cr + uint16(d[i])) % 256
	}
	return cr == 0
}
