package binary

import (
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"
)

type converterFunc func(value interface{}, order binary.ByteOrder) ([]byte, error)

// Entry defines a single entry in the binary serializer configuration.
type Entry struct {
	ReadFrom         string `toml:"read_from"`         // field, tag, time, name
	Name             string `toml:"name"`              // name of entry
	DataFormat       string `toml:"data_format"`       // int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, string
	StringTerminator string `toml:"string_terminator"` // for string metrics: null, 0x00, 00, ....
	StringLength     uint64 `toml:"string_length"`     // for string only, target size
	TimeFormat       string `toml:"time_format"`       // for time metrics: unix, unix_ms, unix_us, unix_ns

	converter   converterFunc
	termination byte
}

func (e *Entry) fillDefaults() error {
	// Normalize
	e.ReadFrom = strings.ToLower(e.ReadFrom)

	// Check input constraints
	switch e.ReadFrom {
	case "":
		e.ReadFrom = "field"
		fallthrough
	case "field", "tag":
		if e.Name == "" {
			return errors.New("missing name")
		}
	case "time":
		switch e.TimeFormat {
		case "":
			e.TimeFormat = "unix"
		case "unix", "unix_ms", "unix_us", "unix_ns":
		default:
			return errors.New("invalid time format")
		}
	case "name":
		if e.DataFormat == "" {
			e.DataFormat = "string"
		} else if e.DataFormat != "string" {
			return errors.New("name data format has to be string")
		}
	default:
		return fmt.Errorf("unknown assignment %q", e.ReadFrom)
	}

	// Check data format
	switch e.DataFormat {
	case "":
		return errors.New("missing data format")
	case "float64":
		e.converter = convertToFloat64
	case "float32":
		e.converter = convertToFloat32
	case "uint64":
		e.converter = convertToUint64
	case "uint32":
		e.converter = convertToUint32
	case "uint16":
		e.converter = convertToUint16
	case "uint8":
		e.converter = convertToUint8
	case "int64":
		e.converter = convertToInt64
	case "int32":
		e.converter = convertToInt32
	case "int16":
		e.converter = convertToInt16
	case "int8":
		e.converter = convertToInt8
	case "string":
		switch e.StringTerminator {
		case "", "null":
			e.termination = 0x00
		default:
			e.StringTerminator = strings.TrimPrefix(e.StringTerminator, "0x")
			termination, err := hex.DecodeString(e.StringTerminator)
			if err != nil {
				return fmt.Errorf("decoding terminator failed for %q: %w", e.Name, err)
			}
			if len(termination) != 1 {
				return fmt.Errorf("terminator must be a single byte, got %q", e.StringTerminator)
			}
			e.termination = termination[0]
		}

		if e.StringLength < 1 {
			return errors.New("string length must be at least 1")
		}
		e.converter = e.convertToString
	default:
		return fmt.Errorf("invalid data format %q for field %q", e.ReadFrom, e.DataFormat)
	}

	return nil
}

func (e *Entry) serializeValue(value interface{}, order binary.ByteOrder) ([]byte, error) {
	// Handle normal fields, tags, etc
	if e.ReadFrom != "time" {
		return e.converter(value, order)
	}

	// We need to serialize the time, make sure we actually do get a time and
	// convert it to the correct timestamp (with scale) first.
	t, ok := value.(time.Time)
	if !ok {
		return nil, fmt.Errorf("time expected but got %T", value)
	}

	var timestamp int64
	switch e.TimeFormat {
	case "unix":
		timestamp = t.Unix()
	case "unix_ms":
		timestamp = t.UnixMilli()
	case "unix_us":
		timestamp = t.UnixMicro()
	case "unix_ns":
		timestamp = t.UnixNano()
	}
	return e.converter(timestamp, order)
}
