/*
Serialize and deserialize data with custom
Pagerino message templates.

Author: Olek
*/
package serder

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

const (
	NFC_LENGTH = 7
)

var MAX_BUFFER_LENGTH = 221

var (
	ErrTooShort    error = errors.New("insufficient bytes in message")
	ErrNoType      error = errors.New("encountered unknown data type")
	ErrNoLength    error = errors.New("data type has no defined length")
	ErrUndefined   error = errors.New("encountered unexpected symbol, behaviour undefined")
	ErrInvalidData error = errors.New("invalid underlying data provided")
	ErrTooLong     error = errors.New("data provided is too long")
)

type Reaction uint8

const (
	React_OK Reaction = iota
	React_IN
	React_OUT
)

const (
	DT_NULL          byte = iota
	DT_Char               // byte
	DT_Array              // array
	DT_Location           // Location
	DT_Temperature        // float
	DT_Charge             // float
	DT_Authorization      // UID
	DT_Reaction           // byte
	DT_NFCPair            // UID
	DT_NFCUnpair          // None
)

var DataSizeMap = map[byte]int{
	DT_NULL:          0,
	DT_Array:         2, // type + length
	DT_Char:          1,
	DT_Location:      20,
	DT_Temperature:   4,
	DT_Charge:        4,
	DT_Authorization: 7,
	DT_Reaction:      1,
	DT_NFCPair:       7,
	DT_NFCUnpair:     0,
}

type Location struct {
	Latitude  float64
	Longitude float64
	Altitude  float32
}

type Field struct {
	DType byte
	Value any
}

type PagerMessage struct {
	AppID  uint16
	Fields []Field
}

// tryAppend is append function with custom contraints (like length)
func tryAppend[T byte](slice []T, elems ...T) (bool, []T) {
	if len(slice)+len(elems) > MAX_BUFFER_LENGTH {
		return false, slice
	}

	return true, append(slice, elems...)
}

// readArray reads a subset of bytes marked with DT_Array.
// This data is copied, it is not parsed.
func readArray(data []byte, offset int, dest *[]byte) (int, error) {
	if offset+2 >= len(data) {
		return offset, ErrTooShort
	}

	dtype := data[offset]
	offset++

	length := int(data[offset])
	offset++

	size, found := DataSizeMap[dtype]
	if !found {
		return offset, ErrNoLength
	}

	if size*length+offset >= len(data) {
		return len(data), ErrTooShort
	}

	// Untyped!
	*dest = append(*dest, data[offset:offset+length*size]...)
	return offset + length*size, nil
}

// readField returns single Field struct reflecting the next data segment
func readField(data []byte, offset int) (Field, int, error) {
	pair := Field{}
	if offset >= len(data) {
		return pair, offset, ErrTooShort
	}

	// Get type
	pair.DType = data[offset]
	offset++

	size, found := DataSizeMap[pair.DType]
	if !found {
		return pair, offset, ErrNoLength
	}

	if size != 0 {
		if offset+size >= len(data) {
			return pair, offset, ErrTooShort
		}

		switch pair.DType {
		case DT_Char, DT_Reaction:
			pair.Value = data[offset]
			offset++

		case DT_Array:
			sub_pair := Field{DType: data[offset]}
			val, ok := sub_pair.Value.([]byte)
			if !ok {
				return pair, offset, ErrInvalidData
			}

			offset, err := readArray(data, offset, &val)
			if err != nil {
				return pair, offset, err
			}

			pair.Value = sub_pair

		case DT_Charge, DT_Temperature:
			pair.Value = math.Float32frombits(binary.LittleEndian.Uint32(data[offset:]))
			offset += 4

		case DT_Authorization, DT_NFCPair:
			pair.Value = data[offset : offset+NFC_LENGTH]
			offset += NFC_LENGTH

		case DT_Location:
			val := Location{Latitude: math.Float64frombits(binary.LittleEndian.Uint64(data[offset:]))}
			offset += 8
			val.Longitude = math.Float64frombits(binary.LittleEndian.Uint64(data[offset:]))
			offset += 8
			val.Altitude = math.Float32frombits(binary.LittleEndian.Uint32(data[offset:]))
			offset += 4

			pair.Value = val

		default:
			return pair, offset, ErrNoType
		}
	}

	return pair, offset, nil
}

// writeField encodes a field into buffer
func writeField(field *Field, buf []byte) ([]byte, error) {
	ok, buf := tryAppend(buf, field.DType)
	if !ok {
		return buf, ErrTooLong
	}

	switch field.DType {
	case DT_Array:
		sub_pair, ok := field.Value.(Field)
		if !ok {
			return buf, ErrInvalidData
		}

		ok, buf = tryAppend(buf, sub_pair.DType)
		if !ok {
			return buf, ErrTooLong
		}

		size, ok := DataSizeMap[sub_pair.DType]
		if !ok {
			return buf, ErrNoLength
		}

		val, ok := sub_pair.Value.([]byte)
		if !ok {
			return buf, ErrInvalidData
		}

		length := len(val) / size
		if length+len(buf)+1 > MAX_BUFFER_LENGTH {
			return buf, ErrTooLong
		}

		buf = append(buf, byte(length))
		buf = append(buf, val...)
	case DT_Char, DT_Reaction:
		val, ok := field.Value.(byte)
		if !ok {
			return buf, ErrInvalidData
		}

		ok, buf = tryAppend(buf, val)
		if !ok {
			return buf, ErrTooLong
		}

	case DT_Authorization, DT_NFCPair:
		val, ok := field.Value.([7]byte)
		if !ok {
			return buf, ErrInvalidData
		}

		ok, buf = tryAppend(buf, val[:]...)
		if !ok {
			return buf, ErrTooLong
		}

	case DT_Charge, DT_Temperature:
		val, ok := field.Value.(float32)
		if !ok {
			return buf, ErrInvalidData
		}

		if len(buf)+4 > MAX_BUFFER_LENGTH {
			return buf, ErrTooLong
		}
		buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val))

	case DT_Location:
		val, ok := field.Value.(Location)
		if !ok {
			return buf, ErrInvalidData
		}

		if len(buf)+20 > MAX_BUFFER_LENGTH {
			return buf, ErrTooLong
		}

		buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Latitude))
		buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Longitude))
		buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val.Altitude))

	case DT_NULL, DT_NFCUnpair:

	default:
		return buf, ErrNoType
	}

	return buf, nil
}

func GetLengthFromDataRate(data_rate int) int {
	if data_rate >= 0 && data_rate <= 3 {
		return 51
	} else if data_rate == 4 {
		return 115
	} else if data_rate >= 5 && data_rate <= 7 {
		return 222
	} else {
		return 0
	}
}

// SplitByLimit splits encoded message into parts
// with maximum length derived from data rate.
//
// These messages are not decodable on the server, as
// data is fractured. (TODO)
func SplitByLimit(buf []byte, data_rate int) ([][]byte, error) {
	limit := GetLengthFromDataRate(data_rate)

	if limit == 0 {
		return nil, ErrUndefined
	}

	length := len(buf)
	count := length / limit
	if length%limit != 0 {
		count++
	}

	output := make([][]byte, 0, count)
	offset := 0
	for ; offset+limit < length; offset += limit {
		output = append(output, buf[offset:offset+limit])
	}
	output = append(output, buf[offset:length])

	return output, nil
}

// Deserialize deserializes bytes into a structured message
func Deserialize(input []byte) (*PagerMessage, error) {
	output := PagerMessage{}

	length := len(input)
	if length < 2 {
		return &output, fmt.Errorf("%w while reading AppID", ErrTooShort)
	}
	offset := 0
	output.AppID = binary.LittleEndian.Uint16(input[offset:])
	offset += 2

	var err error
	var field Field
	for ; err == nil && offset < length; field, offset, err = readField(input, offset) {
		output.Fields = append(output.Fields, field)
	}

	return &output, err
}

// Serialize serializes a message into bytes.
//
// Resulting array length is not limited, to get
// split messages for downlink use "SplitByLimit()"
func Serialize(msg *PagerMessage) ([]byte, error) {
	buf := make([]byte, 0, 2+len(msg.Fields)*2) // May overalloc!
	buf = binary.LittleEndian.AppendUint16(buf, msg.AppID)

	var err error
	for idx := 0; idx < len(msg.Fields) && err == nil; idx++ {
		buf, err = writeField(&msg.Fields[idx], buf)
	}

	return buf, err
}
