package canjson

import (
	"encoding/json"
	"fmt"
	"strconv"

	can "github.com/fiskerinc/cloud-services/pkg/can-go"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
	"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
)

// preAllocatedBytesPerSignal is an estimate of how many bytes each signal needs.
const preAllocatedBytesPerSignal = 40

// Marshal a CAN message to JSON.
func Marshal(m generated.Message) ([]byte, error) {
	f := m.Frame()
	bytes := make([]byte, 0, len(m.Descriptor().Signals)*preAllocatedBytesPerSignal)
	bytes = append(bytes, '{')
	for i, s := range m.Descriptor().Signals {
		s := s
		bytes = append(bytes, '"')
		bytes = append(bytes, s.Name...)
		bytes = append(bytes, `":`...)
		sig := &signal{}
		sig.set(s, f)
		jsonSig, err := json.Marshal(sig)
		if err != nil {
			return nil, fmt.Errorf("marshal json: %w", err)
		}
		bytes = append(bytes, jsonSig...)
		if i < len(m.Descriptor().Signals)-1 {
			bytes = append(bytes, ',')
		}
	}
	bytes = append(bytes, '}')
	return bytes, nil
}

type signal struct {
	Raw         json.Number
	Physical    json.Number
	Unit        string `json:",omitempty"`
	Description string `json:",omitempty"`
}

func (s *signal) set(desc *descriptor.Signal, f can.Frame) {
	switch {
	case desc.Length == 1: // bool
		s.setBoolValue(desc.UnmarshalBool(f.Data), desc)
	case desc.IsSigned: // signed
		s.setSignedValue(desc.UnmarshalSigned(f.Data), desc)
	default: // unsigned
		s.setUnsignedValue(desc.UnmarshalUnsigned(f.Data), desc)
	}
}

func (s *signal) setUnsignedValue(value uint64, desc *descriptor.Signal) {
	s.Raw = uintToJSON(value)
	s.Physical = floatToJSON(desc.ToPhysical(float64(value)))
	s.Unit = desc.Unit
	if value, ok := desc.ValueDescription(int(value)); ok {
		s.Description = value
	}
}

func (s *signal) setSignedValue(value int64, desc *descriptor.Signal) {
	s.Raw = intToJSON(value)
	s.Physical = floatToJSON(desc.ToPhysical(float64(value)))
	s.Unit = desc.Unit
	if value, ok := desc.ValueDescription(int(value)); ok {
		s.Description = value
	}
}

func (s *signal) setBoolValue(value bool, desc *descriptor.Signal) {
	if value {
		s.Raw = "1"
		s.Physical = floatToJSON(desc.ToPhysical(1))
	} else {
		s.Raw = "0"
		s.Physical = floatToJSON(desc.ToPhysical(0))
	}
	s.Unit = desc.Unit
	intValue := 0
	if value {
		intValue = 1
	}
	if value, ok := desc.ValueDescription(intValue); ok {
		s.Description = value
	}
}

func floatToJSON(f float64) json.Number {
	return json.Number(strconv.FormatFloat(f, 'f', -1, 64))
}

func intToJSON(i int64) json.Number {
	return json.Number(strconv.Itoa(int(i)))
}

func uintToJSON(i uint64) json.Number {
	return json.Number(strconv.Itoa(int(i)))
}
