package modbus

import (
	"encoding/binary"
	"fmt"
	"math"
)

type convert32 func([]byte) uint32

func binaryMSWLEU32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(binary.LittleEndian.Uint16(b[0:]))<<16 | uint32(binary.LittleEndian.Uint16(b[2:]))
}

func binaryLSWBEU32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(binary.BigEndian.Uint16(b[2:]))<<16 | uint32(binary.BigEndian.Uint16(b[0:]))
}

func endiannessConverter32(byteOrder string) (convert32, error) {
	switch byteOrder {
	case "ABCD": // Big endian (Motorola)
		return binary.BigEndian.Uint32, nil
	case "BADC": // Big endian with bytes swapped
		return binaryMSWLEU32, nil
	case "CDAB": // Little endian with bytes swapped
		return binaryLSWBEU32, nil
	case "DCBA": // Little endian (Intel)
		return binary.LittleEndian.Uint32, nil
	}
	return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
}

// I32 - no scale
func determineConverterI32(outType, byteOrder string) (fieldConverterFunc, error) {
	tohost, err := endiannessConverter32(byteOrder)
	if err != nil {
		return nil, err
	}

	switch outType {
	case "native":
		return func(b []byte) interface{} {
			return int32(tohost(b))
		}, nil
	case "INT64":
		return func(b []byte) interface{} {
			return int64(int32(tohost(b)))
		}, nil
	case "UINT64":
		return func(b []byte) interface{} {
			return uint64(int32(tohost(b)))
		}, nil
	case "FLOAT64":
		return func(b []byte) interface{} {
			return float64(int32(tohost(b)))
		}, nil
	}
	return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

// U32 - no scale
func determineConverterU32(outType, byteOrder string) (fieldConverterFunc, error) {
	tohost, err := endiannessConverter32(byteOrder)
	if err != nil {
		return nil, err
	}

	switch outType {
	case "native":
		return func(b []byte) interface{} {
			return tohost(b)
		}, nil
	case "INT64":
		return func(b []byte) interface{} {
			return int64(tohost(b))
		}, nil
	case "UINT64":
		return func(b []byte) interface{} {
			return uint64(tohost(b))
		}, nil
	case "FLOAT64":
		return func(b []byte) interface{} {
			return float64(tohost(b))
		}, nil
	}
	return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

// F32 - no scale
func determineConverterF32(outType, byteOrder string) (fieldConverterFunc, error) {
	tohost, err := endiannessConverter32(byteOrder)
	if err != nil {
		return nil, err
	}

	switch outType {
	case "native":
		return func(b []byte) interface{} {
			raw := tohost(b)
			return math.Float32frombits(raw)
		}, nil
	case "FLOAT64":
		return func(b []byte) interface{} {
			raw := tohost(b)
			in := math.Float32frombits(raw)
			return float64(in)
		}, nil
	}
	return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

// I32 - scale
func determineConverterI32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
	tohost, err := endiannessConverter32(byteOrder)
	if err != nil {
		return nil, err
	}

	switch outType {
	case "native":
		return func(b []byte) interface{} {
			in := int32(tohost(b))
			return int32(float64(in) * scale)
		}, nil
	case "INT64":
		return func(b []byte) interface{} {
			in := int32(tohost(b))
			return int64(float64(in) * scale)
		}, nil
	case "UINT64":
		return func(b []byte) interface{} {
			in := int32(tohost(b))
			return uint64(float64(in) * scale)
		}, nil
	case "FLOAT64":
		return func(b []byte) interface{} {
			in := int32(tohost(b))
			return float64(in) * scale
		}, nil
	}
	return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

// U32 - scale
func determineConverterU32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
	tohost, err := endiannessConverter32(byteOrder)
	if err != nil {
		return nil, err
	}

	switch outType {
	case "native":
		return func(b []byte) interface{} {
			in := tohost(b)
			return uint32(float64(in) * scale)
		}, nil
	case "INT64":
		return func(b []byte) interface{} {
			in := tohost(b)
			return int64(float64(in) * scale)
		}, nil
	case "UINT64":
		return func(b []byte) interface{} {
			in := tohost(b)
			return uint64(float64(in) * scale)
		}, nil
	case "FLOAT64":
		return func(b []byte) interface{} {
			in := tohost(b)
			return float64(in) * scale
		}, nil
	}
	return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

// F32 - scale
func determineConverterF32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
	tohost, err := endiannessConverter32(byteOrder)
	if err != nil {
		return nil, err
	}

	switch outType {
	case "native":
		return func(b []byte) interface{} {
			raw := tohost(b)
			in := math.Float32frombits(raw)
			return float32(float64(in) * scale)
		}, nil
	case "FLOAT64":
		return func(b []byte) interface{} {
			raw := tohost(b)
			in := math.Float32frombits(raw)
			return float64(in) * scale
		}, nil
	}
	return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
