package proto

import (
	"encoding/binary"
	"math"
	"strings"
	"time"
)

// epicsEpochOffset is the number of Unix seconds between the Unix epoch
// (1970-01-01 00:00:00 UTC) and the EPICS epoch (1990-01-01 00:00:00 UTC).
const epicsEpochOffset = 631152000

// maxStringSize is the fixed size of a CA string value (MAX_STRING_SIZE).
const maxStringSize = 40

// maxEnumStates is the maximum number of enum strings (MAX_ENUM_STATES).
const maxEnumStates = 16

// maxEnumStringSize is the fixed size of each enum string (MAX_ENUM_STRING_SIZE).
const maxEnumStringSize = 26

// -------------------------------------------------------------------------- //
// Alarm types                                                                 //
// -------------------------------------------------------------------------- //

// AlarmSeverity mirrors epicsAlarmSeverity.
type AlarmSeverity int

const (
	SeverityNone    AlarmSeverity = 0
	SeverityMinor   AlarmSeverity = 1
	SeverityMajor   AlarmSeverity = 2
	SeverityInvalid AlarmSeverity = 3
)

func (s AlarmSeverity) String() string {
	switch s {
	case SeverityNone:
		return "NO_ALARM"
	case SeverityMinor:
		return "MINOR"
	case SeverityMajor:
		return "MAJOR"
	case SeverityInvalid:
		return "INVALID"
	default:
		return "UNKNOWN"
	}
}

// -------------------------------------------------------------------------- //
// TimeValue — decoded DBR_TIME_* payload                                     //
// -------------------------------------------------------------------------- //

// TimeValue is the decoded form of any DBR_TIME_* payload.
// Exactly one of the value fields is populated based on the DBR type.
type TimeValue struct {
	Timestamp time.Time
	Status    int16
	Severity  AlarmSeverity
	// Value fields — only one is set.
	Double float64
	Float  float32
	Long   int32
	Short  int16
	Enum   uint16
	Char   uint8
	Str    string
	// Waveform values (when element count > 1).
	Doubles []float64
}

// decodeTimestamp converts EPICS secPastEpoch+nsec to a Go time.Time.
func decodeTimestamp(secPastEpoch, nsec uint32) time.Time {
	return time.Unix(int64(secPastEpoch)+epicsEpochOffset, int64(nsec)).UTC()
}

// caString extracts a null-terminated string from a fixed-size byte slice.
func caString(b []byte) string {
	idx := strings.IndexByte(string(b), 0)
	if idx < 0 {
		return string(b)
	}
	return string(b[:idx])
}

// DBR_TIME_* wire layouts (all big-endian, matching EPICS db_access.h structs):
//
//	Common header (12 bytes):
//	  [0:2]  int16  status
//	  [2:4]  int16  severity
//	  [4:8]  uint32 secPastEpoch
//	  [8:12] uint32 nsec
//
//	DBR_TIME_STRING (type 14): [40]byte at [12:52].                             Total=52.
//	DBR_TIME_SHORT  (type 15): 2-byte RISC pad at [12:14], int16   at [14:16].  Total=16.
//	DBR_TIME_FLOAT  (type 16): float32 at [12:16].                              Total=16.
//	DBR_TIME_ENUM   (type 17): 2-byte RISC pad at [12:14], uint16  at [14:16].  Total=16.
//	DBR_TIME_CHAR   (type 18): RISC_pad0[12:14], RISC_pad1[14], uint8 at [15]. Total=16.
//	DBR_TIME_LONG   (type 19): int32   at [12:16].                              Total=16.
//	DBR_TIME_DOUBLE (type 20): 4-byte RISC pad at [12:16], float64 at [16:24]. Total=24.

// DecodeTimeValue decodes a DBR_TIME_* payload.
// dbrType is one of the DBRTime* constants; payload is the full message payload
// (may include multiple elements for waveforms; count is the element count).
func DecodeTimeValue(dbrType uint16, count uint32, payload []byte) (TimeValue, bool) {
	if len(payload) < 12 {
		return TimeValue{}, false
	}

	status := int16(binary.BigEndian.Uint16(payload[0:]))
	severity := AlarmSeverity(binary.BigEndian.Uint16(payload[2:]))
	sec := binary.BigEndian.Uint32(payload[4:])
	nsec := binary.BigEndian.Uint32(payload[8:])

	tv := TimeValue{
		Timestamp: decodeTimestamp(sec, nsec),
		Status:    status,
		Severity:  severity,
	}

	switch dbrType {
	case DBRTimeDouble:
		// 4-byte RISC pad at [12:16], value at [16:24]
		if count > 1 {
			if len(payload) < 16+int(count)*8 {
				return TimeValue{}, false
			}
			vals := make([]float64, count)
			for i := range vals {
				bits := binary.BigEndian.Uint64(payload[16+i*8:])
				vals[i] = math.Float64frombits(bits)
			}
			tv.Doubles = vals
			tv.Double = vals[0]
		} else {
			if len(payload) < 24 {
				return TimeValue{}, false
			}
			bits := binary.BigEndian.Uint64(payload[16:])
			tv.Double = math.Float64frombits(bits)
		}

	case DBRTimeFloat:
		if len(payload) < 16 {
			return TimeValue{}, false
		}
		bits := binary.BigEndian.Uint32(payload[12:])
		tv.Float = math.Float32frombits(bits)
		tv.Double = float64(tv.Float)

	case DBRTimeLong:
		if len(payload) < 16 {
			return TimeValue{}, false
		}
		tv.Long = int32(binary.BigEndian.Uint32(payload[12:]))
		tv.Double = float64(tv.Long)

	case DBRTimeShort:
		// 2-byte RISC pad at [12:14], value at [14:16]
		if len(payload) < 16 {
			return TimeValue{}, false
		}
		tv.Short = int16(binary.BigEndian.Uint16(payload[14:]))
		tv.Double = float64(tv.Short)

	case DBRTimeEnum:
		// 2-byte RISC pad at [12:14], value at [14:16]
		if len(payload) < 16 {
			return TimeValue{}, false
		}
		tv.Enum = binary.BigEndian.Uint16(payload[14:])
		tv.Double = float64(tv.Enum)

	case DBRTimeChar:
		// RISC_pad0[12:14], RISC_pad1[14], value[15] per db_access.h dbr_time_char
		if len(payload) < 16 {
			return TimeValue{}, false
		}
		tv.Char = payload[15]
		tv.Double = float64(tv.Char)

	case DBRTimeString:
		if len(payload) < 12+maxStringSize {
			return TimeValue{}, false
		}
		tv.Str = caString(payload[12 : 12+maxStringSize])
		tv.Double = 0

	default:
		return TimeValue{}, false
	}

	return tv, true
}

// -------------------------------------------------------------------------- //
// CtrlDouble — decoded DBR_CTRL_DOUBLE payload (type 34)                     //
// -------------------------------------------------------------------------- //
//
// Wire layout (88 bytes total, all big-endian):
//
//	[0:2]   int16   status
//	[2:4]   int16   severity
//	[4:6]   int16   precision
//	[6:8]   uint16  RISC_pad0
//	[8:16]  [8]byte units
//	[16:24] float64 upper_disp_limit
//	[24:32] float64 lower_disp_limit
//	[32:40] float64 upper_alarm_limit
//	[40:48] float64 upper_warning_limit
//	[48:56] float64 lower_warning_limit
//	[56:64] float64 lower_alarm_limit
//	[64:72] float64 upper_ctrl_limit
//	[72:80] float64 lower_ctrl_limit
//	[80:88] float64 value

// CtrlDouble is the decoded form of a DBR_CTRL_DOUBLE payload.
type CtrlDouble struct {
	Status          int16
	Severity        AlarmSeverity
	Precision       int16
	Units           string
	UpperDispLimit  float64
	LowerDispLimit  float64
	UpperAlarmLimit float64
	UpperWarnLimit  float64
	LowerWarnLimit  float64
	LowerAlarmLimit float64
	UpperCtrlLimit  float64
	LowerCtrlLimit  float64
	Value           float64
}

// DecodeCtrlDouble decodes a DBR_CTRL_DOUBLE payload (minimum 88 bytes).
func DecodeCtrlDouble(p []byte) (CtrlDouble, bool) {
	if len(p) < 88 {
		return CtrlDouble{}, false
	}
	f64 := func(off int) float64 {
		return math.Float64frombits(binary.BigEndian.Uint64(p[off:]))
	}
	return CtrlDouble{
		Status:          int16(binary.BigEndian.Uint16(p[0:])),
		Severity:        AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
		Precision:       int16(binary.BigEndian.Uint16(p[4:])),
		Units:           caString(p[8:16]),
		UpperDispLimit:  f64(16),
		LowerDispLimit:  f64(24),
		UpperAlarmLimit: f64(32),
		UpperWarnLimit:  f64(40),
		LowerWarnLimit:  f64(48),
		LowerAlarmLimit: f64(56),
		UpperCtrlLimit:  f64(64),
		LowerCtrlLimit:  f64(72),
		Value:           f64(80),
	}, true
}

// -------------------------------------------------------------------------- //
// CtrlLong — decoded DBR_CTRL_LONG payload (type 33)                        //
// -------------------------------------------------------------------------- //
//
// Wire layout (48 bytes total):
//
//	[0:2]   int16   status
//	[2:4]   int16   severity
//	[4:12]  [8]byte units
//	[12:16] int32   upper_disp_limit
//	[16:20] int32   lower_disp_limit
//	[20:24] int32   upper_alarm_limit
//	[24:28] int32   upper_warning_limit
//	[28:32] int32   lower_warning_limit
//	[32:36] int32   lower_alarm_limit
//	[36:40] int32   upper_ctrl_limit
//	[40:44] int32   lower_ctrl_limit
//	[44:48] int32   value

// CtrlLong is the decoded form of a DBR_CTRL_LONG payload.
type CtrlLong struct {
	Status          int16
	Severity        AlarmSeverity
	Units           string
	UpperDispLimit  int32
	LowerDispLimit  int32
	UpperAlarmLimit int32
	UpperWarnLimit  int32
	LowerWarnLimit  int32
	LowerAlarmLimit int32
	UpperCtrlLimit  int32
	LowerCtrlLimit  int32
	Value           int32
}

// DecodeCtrlLong decodes a DBR_CTRL_LONG payload (minimum 48 bytes).
func DecodeCtrlLong(p []byte) (CtrlLong, bool) {
	if len(p) < 48 {
		return CtrlLong{}, false
	}
	i32 := func(off int) int32 {
		return int32(binary.BigEndian.Uint32(p[off:]))
	}
	return CtrlLong{
		Status:          int16(binary.BigEndian.Uint16(p[0:])),
		Severity:        AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
		Units:           caString(p[4:12]),
		UpperDispLimit:  i32(12),
		LowerDispLimit:  i32(16),
		UpperAlarmLimit: i32(20),
		UpperWarnLimit:  i32(24),
		LowerWarnLimit:  i32(28),
		LowerAlarmLimit: i32(32),
		UpperCtrlLimit:  i32(36),
		LowerCtrlLimit:  i32(40),
		Value:           i32(44),
	}, true
}

// -------------------------------------------------------------------------- //
// CtrlEnum — decoded DBR_CTRL_ENUM payload (type 31)                        //
// -------------------------------------------------------------------------- //
//
// Wire layout (424 bytes total):
//
//	[0:2]   int16      status
//	[2:4]   int16      severity
//	[4:6]   int16      no_str  (number of valid enum strings, max 16)
//	[6:422] [16][26]byte strs (enum string table)
//	[422:424] uint16   value

// CtrlEnum is the decoded form of a DBR_CTRL_ENUM payload.
type CtrlEnum struct {
	Status   int16
	Severity AlarmSeverity
	Strings  []string // len == no_str
	Value    uint16
}

const ctrlEnumSize = 2 + 2 + 2 + maxEnumStates*maxEnumStringSize + 2 // 424

// DecodeCtrlEnum decodes a DBR_CTRL_ENUM payload (minimum 424 bytes).
func DecodeCtrlEnum(p []byte) (CtrlEnum, bool) {
	if len(p) < ctrlEnumSize {
		return CtrlEnum{}, false
	}
	noStr := int(binary.BigEndian.Uint16(p[4:]))
	noStr = min(noStr, maxEnumStates)
	strs := make([]string, noStr)
	for i := range strs {
		off := 6 + i*maxEnumStringSize
		strs[i] = caString(p[off : off+maxEnumStringSize])
	}
	return CtrlEnum{
		Status:   int16(binary.BigEndian.Uint16(p[0:])),
		Severity: AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
		Strings:  strs,
		Value:    binary.BigEndian.Uint16(p[422:]),
	}, true
}

// -------------------------------------------------------------------------- //
// CtrlString — decoded DBR_CTRL_STRING payload (type 28)                    //
// -------------------------------------------------------------------------- //
//
// Wire layout (44 bytes):
//
//	[0:2]   int16   status
//	[2:4]   int16   severity
//	[4:44]  [40]byte value

// CtrlString is the decoded form of a DBR_CTRL_STRING payload.
type CtrlString struct {
	Status   int16
	Severity AlarmSeverity
	Value    string
}

// DecodeCtrlString decodes a DBR_CTRL_STRING payload (minimum 44 bytes).
func DecodeCtrlString(p []byte) (CtrlString, bool) {
	if len(p) < 4+maxStringSize {
		return CtrlString{}, false
	}
	return CtrlString{
		Status:   int16(binary.BigEndian.Uint16(p[0:])),
		Severity: AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
		Value:    caString(p[4 : 4+maxStringSize]),
	}, true
}

// -------------------------------------------------------------------------- //
// Put payload encoders                                                        //
// -------------------------------------------------------------------------- //

// EncodeDouble encodes a float64 value as a big-endian DBR_DOUBLE payload
// padded to 8 bytes.
func EncodeDouble(v float64) []byte {
	b := make([]byte, 8)
	binary.BigEndian.PutUint64(b, math.Float64bits(v))
	return b
}

// EncodeLong encodes an int32 value as a big-endian DBR_LONG payload
// padded to 8 bytes.
func EncodeLong(v int32) []byte {
	b := make([]byte, 8) // 4 bytes value + 4 bytes pad
	binary.BigEndian.PutUint32(b, uint32(v))
	return b
}

// EncodeShort encodes an int16 value as a big-endian DBR_SHORT payload
// padded to 8 bytes.
func EncodeShort(v int16) []byte {
	b := make([]byte, 8)
	binary.BigEndian.PutUint16(b, uint16(v))
	return b
}

// EncodeString encodes a string as a null-terminated, 8-byte padded
// DBR_STRING payload (capped at maxStringSize characters).
func EncodeString(v string) []byte {
	if len(v) >= maxStringSize {
		v = v[:maxStringSize-1]
	}
	b := make([]byte, maxStringSize)
	copy(b, v)
	return PadBytes(b)
}

// EncodeEventMask builds the 16-byte payload for a CA_PROTO_EVENT_ADD request.
// mask is typically DBEDefault (DBEValue | DBEAlarm).
//
// Wire layout matches caProto.h struct mon_info (all big-endian):
//
//	[0:4]   float32  m_lval  (low delta, zero = disabled)
//	[4:8]   float32  m_hval  (high delta, zero = disabled)
//	[8:12]  float32  m_toval (period between samples, zero = disabled)
//	[12:14] uint16   m_mask  (DBE_VALUE | DBE_ALARM | ...)
//	[14:16] uint16   m_pad   (alignment padding)
func EncodeEventMask(mask uint16) []byte {
	b := make([]byte, 16)
	binary.BigEndian.PutUint16(b[12:], mask) // m_mask is at offset 12 per caProto.h
	return b
}
