package can

import (
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"
)

const (
	idBits         = 11
	extendedIDBits = 29
)

// CAN format constants.
const (
	MaxID         = 0x7ff
	MaxExtendedID = 0x1fffffff
)

// Frame represents a CAN frame.
//
// A Frame is intentionally designed to fit into 16 bytes on common architectures
// and is therefore amenable to pass-by-value and judicious copying.
type Frame struct {
	// ID is the CAN ID
	ID uint32
	// Length is the number of bytes of data in the frame.
	Length uint16
	// Data is the frame data.
	Data Data
	// IsRemote is true for remote frames.
	IsRemote bool
	// IsExtended is true for extended frames, i.e. frames with 29-bit IDs.
	IsExtended bool
}

// Validate returns an error if the Frame is not a valid CAN frame.
func (f *Frame) Validate() error {
	// Validate: ID
	if f.IsExtended && f.ID > MaxExtendedID {
		return fmt.Errorf(
			"invalid extended CAN id: %v does not fit in %v bits",
			f.ID,
			extendedIDBits,
		)
	} else if !f.IsExtended && f.ID > MaxID {
		return fmt.Errorf(
			"invalid standard CAN id: %v does not fit in %v bits",
			f.ID,
			idBits,
		)
	}
	// Validate: Data
	if f.Length > MaxDataLength {
		return fmt.Errorf("invalid data length: %v", f.Length)
	}
	return nil
}

// String returns an ASCII representation the CAN frame.
//
// Format:
//
//   ([0-9A-F]{3}|[0-9A-F]{3})#(R[0-8]?|[0-9A-F]{0,16})
//
// The format is compatible with the candump(1) log file format.
func (f Frame) String() string {
	var id string
	if f.IsExtended {
		id = fmt.Sprintf("%08X", f.ID)
	} else {
		id = fmt.Sprintf("%03X", f.ID)
	}
	if f.IsRemote && f.Length == 0 {
		return id + "#R"
	} else if f.IsRemote {
		return id + "#R" + strconv.Itoa(int(f.Length))
	}
	return id + "#" + strings.ToUpper(hex.EncodeToString(f.Data[:f.Length]))
}

// UnmarshalString sets *f using the provided ASCII representation of a Frame.
func (f *Frame) UnmarshalString(s string) error {
	// Split split into parts
	parts := strings.Split(s, "#")
	if len(parts) != 2 {
		return fmt.Errorf("invalid frame format: %v", s)
	}
	idPart, dataPart := parts[0], parts[1]
	var frame Frame
	// Parse: IsExtended
	if len(idPart) != 3 && len(idPart) != 8 {
		return fmt.Errorf("invalid ID length: %v", s)
	}
	frame.IsExtended = len(idPart) == 8
	// Parse: ID
	id, err := strconv.ParseUint(idPart, 16, 32)
	if err != nil {
		return fmt.Errorf("invalid frame ID: %v", s)
	}
	frame.ID = uint32(id)
	if len(dataPart) == 0 {
		*f = frame
		return nil
	}
	// Parse: IsRemote
	if dataPart[0] == 'R' {
		frame.IsRemote = true
		if len(dataPart) > 2 {
			return fmt.Errorf("invalid remote length: %v", s)
		} else if len(dataPart) == 2 {
			dataLength, err := strconv.Atoi(dataPart[1:2])
			if err != nil {
				return fmt.Errorf("invalid remote length: %v: %w", s, err)
			}
			frame.Length = uint16(dataLength)
		}
		*f = frame
		return nil
	}
	// Parse: Length
	if len(dataPart) > 16 || len(dataPart)%2 != 0 {
		return fmt.Errorf("invalid data length: %v", s)
	}
	frame.Length = uint16(len(dataPart) / 2)
	// Parse: Data
	decodedData, err := hex.DecodeString(dataPart)
	if err != nil {
		return fmt.Errorf("invalid data: %v: %w", s, err)
	}
	copy(frame.Data[:], decodedData)
	*f = frame
	return nil
}
