package udp

import (
	"bytes"
	"encoding/binary"
	"time"
)

// Sending WSJT-X UDP messages, as opposed to parsing them (wsjt.go).
//
// This exists for Logger32, and for anything else that listens on the WSJT-X
// interface rather than for plain text. Logger32's "additional WSJT/JTDX UDP
// sockets" receivers — ports 2250, 2251, 2252 — are documented as receiving
// "UDP logging packets": they speak the WSJT-X v2 protocol, and a raw ADIF
// record posted to them is discarded without a word.
//
// That is a real distinction and not a detail: an operator can watch OpsLog
// report "sent 1153 bytes to 127.0.0.1:2250" and see nothing whatsoever appear
// in Logger32, because both statements are true.

// buildQString encodes a Qt QString/QByteArray as QDataStream writes it: a
// big-endian int32 length followed by the UTF-8 bytes. A negative length means
// null, which is not what we ever want here — an empty string is length 0.
func buildQString(s string) []byte {
	out := make([]byte, 4, 4+len(s))
	binary.BigEndian.PutUint32(out, uint32(len(s)))
	return append(out, s...)
}

// BuildWSJTLoggedADIF frames a WSJT-X "Logged ADIF" datagram (message type 12).
//
//	uint32  magic   0xadbccbda
//	uint32  schema  2
//	uint32  type    12
//	QString id      the sending program's name
//	QString adif    the ADIF record
//
// id matters more than it looks: a receiver uses it to tell instances apart, and
// some show it in their log. "OpsLog" is honest — pretending to be WSJT-X would
// make a second instance of the real thing indistinguishable from us.
func BuildWSJTLoggedADIF(id, adif string) []byte {
	var b bytes.Buffer
	var hdr [12]byte
	binary.BigEndian.PutUint32(hdr[0:4], wsjtMagic)
	binary.BigEndian.PutUint32(hdr[4:8], 2) // schema 2 — what WSJT-X 2.x speaks
	binary.BigEndian.PutUint32(hdr[8:12], wsjtMsgLoggedADIF)
	b.Write(hdr[:])
	b.Write(buildQString(id))
	b.Write(buildQString(adif))
	return b.Bytes()
}

// LoggedQSO is what a WSJT-X "QSO Logged" message carries. Declared here rather
// than taking a qso.QSO so this package stays free of the logbook — it speaks a
// wire protocol and nothing else.
type LoggedQSO struct {
	DXCall       string
	DXGrid       string
	TxFreqHz     uint64
	Mode         string
	ReportSent   string
	ReportRcvd   string
	TxPower      string
	Comments     string
	Name         string
	TimeOn       time.Time // UTC
	TimeOff      time.Time // UTC
	OperatorCall string
	MyCall       string
	MyGrid       string
	ExchangeSent string
	ExchangeRcvd string
	PropMode     string
}

// BuildWSJTQSOLogged frames a WSJT-X "QSO Logged" datagram (message type 5).
//
// This is the message most loggers listen for — MacLoggerDX takes it by default
// and offers the ADIF one as an alternative, and WSJT-X itself sends BOTH for
// every contact. Sending only the ADIF form was the reason a QSO reached
// Logger32's socket and never reached its log.
//
// Field order is fixed by WSJT-X's NetworkMessage.hpp and cannot be rearranged:
// a receiver reads them positionally, so one field out of place shifts every
// one after it.
func BuildWSJTQSOLogged(id string, q LoggedQSO) []byte {
	var b bytes.Buffer
	var hdr [12]byte
	binary.BigEndian.PutUint32(hdr[0:4], wsjtMagic)
	binary.BigEndian.PutUint32(hdr[4:8], 2)
	binary.BigEndian.PutUint32(hdr[8:12], wsjtMsgQSOLogged)
	b.Write(hdr[:])
	b.Write(buildQString(id))
	b.Write(buildQDateTimeUTC(q.TimeOff))
	b.Write(buildQString(q.DXCall))
	b.Write(buildQString(q.DXGrid))
	var f [8]byte
	binary.BigEndian.PutUint64(f[:], q.TxFreqHz)
	b.Write(f[:])
	b.Write(buildQString(q.Mode))
	b.Write(buildQString(q.ReportSent))
	b.Write(buildQString(q.ReportRcvd))
	b.Write(buildQString(q.TxPower))
	b.Write(buildQString(q.Comments))
	b.Write(buildQString(q.Name))
	b.Write(buildQDateTimeUTC(q.TimeOn))
	b.Write(buildQString(q.OperatorCall))
	b.Write(buildQString(q.MyCall))
	b.Write(buildQString(q.MyGrid))
	b.Write(buildQString(q.ExchangeSent))
	b.Write(buildQString(q.ExchangeRcvd))
	b.Write(buildQString(q.PropMode))
	return b.Bytes()
}

// buildQDateTimeUTC encodes a QDateTime the way QDataStream has since Qt 5:
//
//	qint64  Julian day number
//	quint32 milliseconds since midnight
//	quint8  time spec — 1 is UTC, which is the only one we ever send
//
// Everything in the logbook is already UTC, and a receiver that guessed local
// time from a spec of 0 would file the contact in the wrong hour.
func buildQDateTimeUTC(t time.Time) []byte {
	t = t.UTC()
	out := make([]byte, 0, 13)
	var jd [8]byte
	binary.BigEndian.PutUint64(jd[:], uint64(julianDay(t)))
	out = append(out, jd[:]...)
	ms := uint32(t.Hour()*3600000 + t.Minute()*60000 + t.Second()*1000 + t.Nanosecond()/1e6)
	var m [4]byte
	binary.BigEndian.PutUint32(m[:], ms)
	out = append(out, m[:]...)
	return append(out, 1) // Qt::UTC
}

// julianDay is the standard Gregorian-to-JDN conversion. Integer division
// throughout — this is the arithmetic Qt uses, and a floating-point version
// lands a day out at the edges.
func julianDay(t time.Time) int64 {
	y := int64(t.Year())
	m := int64(t.Month())
	d := int64(t.Day())
	a := (14 - m) / 12
	y2 := y + 4800 - a
	m2 := m + 12*a - 3
	return d + (153*m2+2)/5 + 365*y2 + y2/4 - y2/100 + y2/400 - 32045
}
