// Package civ implements the Icom CI-V protocol independently of the transport
// carrying it. The exact same frames travel over a USB/serial port (local
// control) and, wrapped in Icom's UDP "serial" stream, over the network
// (remote control). Keeping the wire format in one place means the USB backend
// (icomserial) and a future network backend (icomnet) share all of it — only
// the transport differs.
//
// Frame layout: FE FE <to> <from> <cmd> [sub] [data…] FD
package civ

import (
	"bytes"
	"fmt"
)

// Protocol bytes.
const (
	Pre = 0xFE // preamble (sent twice at the start of every frame)
	End = 0xFD // end-of-message
	OK  = 0xFB // rig acknowledged a set command
	NG  = 0xFA // rig rejected a set command

	// AddrController is the conventional address software uses for itself.
	AddrController = 0xE0
)

// Commands (the few Phase-1 control needs; more get added with the panel).
const (
	CmdTransceiveFreq = 0x00 // unsolicited freq update (dial turned)
	CmdTransceiveMode = 0x01 // unsolicited mode update
	CmdReadFreq       = 0x03
	CmdReadMode       = 0x04
	CmdSetFreq        = 0x05
	CmdSetMode        = 0x06
	CmdSplit          = 0x0F // read/set split & duplex (0x00 off, 0x01 split on)
	CmdVfoFreq        = 0x25 // read a specific VFO's freq (sub 0x00 selected, 0x01 unselected)
	CmdPTT            = 0x1C // sub 0x00 = PTT
	CmdExtra          = 0x1A // sub 0x06 = data mode on modern Icoms
	CmdModeDataFil    = 0x26 // sub 0x00 = selected VFO: mode + data flag + filter in one frame
	CmdReadID         = 0x19 // sub 0x00 = rig's own CI-V address (identifies model)
	CmdPower          = 0x18 // power on/off (sub 0x01 = on, 0x00 = off; on needs an FE wake preamble)

	CmdAnt    = 0x12 // antenna selector (sub 0x00 = ANT1, 0x01 = ANT2; read = no sub)
	CmdAtt    = 0x11 // attenuator (1 BCD byte of dB; 0x00 = off)
	CmdLevel  = 0x14 // analogue levels (sub + 2 BCD bytes, 0000-0255)
	CmdMeter  = 0x15 // meters (sub + 2 BCD bytes, 0000-0255): S-meter/Po/SWR
	CmdSwitch = 0x16 // on/off + multi-state DSP settings (sub + 1 byte)
	CmdATU    = 0x1C // sub 0x01 = antenna tuner (0x00 off, 0x01 through, 0x02 tune)
	CmdScope  = 0x27 // spectrum-scope waveform stream (sub 0x00 = data, 0x11 = on/off)
	CmdRIT    = 0x21 // RIT/ΔTX: sub 0x00 offset freq, 0x01 RIT on/off, 0x02 ΔTX(XIT) on/off
	CmdSendCW = 0x17 // send a CW message (ASCII, ≤30 chars) via the rig's keyer; data 0xFF = stop
	// CmdVFO selects which receiver subsequent commands address. On the two-band
	// satellite rigs (IC-9700, IC-9100) the MAIN band is the downlink and the SUB
	// band the uplink, so every satellite frequency set is "point at a band, then
	// tune it".
	CmdVFO = 0x07

	SubLevelKeySpeed = 0x0C // CmdLevel: CW keying speed (0-255 → KeyMinWPM..KeyMaxWPM)

	// CW keyer speed range for the KEY SPEED level (IC-7610: 6-48 WPM).
	KeyMinWPM = 6
	KeyMaxWPM = 48

	StopCWByte = 0xFF // 0x17 data byte that stops an in-progress CW message

	SubRITFreq = 0x00 // RIT/ΔTX offset: 2 BCD bytes (LE, 0-9999) + sign byte (00 +, 01 -)
	SubRITOn   = 0x01 // RIT on/off (00/01)
	SubXITOn   = 0x02 // ΔTX (XIT) on/off (00/01)

	SubDataMode      = 0x06
	SubPTT           = 0x00
	SubVfoSelected   = 0x00 // CmdVfoFreq: the active/RX VFO
	SubVfoUnselected = 0x01 // CmdVfoFreq: the other VFO (TX in split)

	// CmdLevel sub-commands.
	SubLevelAF      = 0x01 // AF (volume)
	SubLevelRF      = 0x02 // RF gain
	SubLevelSQL     = 0x03 // squelch level
	SubLevelPBTIn   = 0x07 // Twin PBT (inside)  — 0-255, 128 = centre
	SubLevelPBTOut  = 0x08 // Twin PBT (outside) — 0-255, 128 = centre
	SubLevelNR      = 0x06 // noise-reduction depth
	SubLevelNotch   = 0x0D // manual-notch position — 0-255, 128 = centre
	SubLevelComp    = 0x0E // speech-compressor level
	SubLevelNB      = 0x12 // noise-blanker depth
	SubLevelMon     = 0x15 // monitor gain
	SubLevelVOXGain = 0x16 // VOX gain
	SubLevelAntiVOX = 0x17 // anti-VOX level
	SubLevelRFPower = 0x0A // TX RF output power
	SubLevelMic     = 0x0B // mic gain

	// CmdMeter sub-commands.
	SubMeterS   = 0x02 // S-meter (RX)
	SubMeterPo  = 0x11 // power output (TX)
	SubMeterSWR = 0x12 // SWR (TX)

	// CmdATU / CmdPTT sub-commands.
	SubATU = 0x01 // antenna tuner (data 0x02 = start tune)

	// CmdScope sub-commands.
	SubScopeData    = 0x00 // waveform data frame (divided across several frames)
	SubScopeOnOff   = 0x10 // turn the scope display itself on/off (00/01)
	SubScopeOn      = 0x11 // enable/disable waveform data output over CI-V (00/01)
	SubScopeMode    = 0x14 // center/fixed mode (0=center, 1=fixed)
	SubScopeSpan    = 0x15 // span in center mode (±span/2 as 5 LE-BCD)
	SubScopeEdge    = 0x16 // fixed-mode ACTIVE edge set 1-4 (vfo + set#)
	SubScopeFixEdge = 0x1e // fixed-mode edge FREQUENCIES: [range][set#][low 5-BCD][high 5-BCD]

	// CmdSwitch sub-commands.
	SubSwPreamp  = 0x02 // 0=off, 1=P.AMP1, 2=P.AMP2
	SubSwAGC     = 0x12 // 1=FAST, 2=MID, 3=SLOW
	SubSwNB      = 0x22 // noise blanker on/off
	SubSwNR      = 0x40 // noise reduction on/off
	SubSwANF     = 0x41 // auto-notch on/off
	SubSwComp    = 0x44 // speech compressor on/off
	SubSwMon     = 0x45 // monitor on/off
	SubSwVOX     = 0x46 // VOX on/off
	SubSwBreakIn = 0x47 // CW break-in: 0=OFF, 1=SEMI, 2=FULL (needed so 0x17 CW keys TX)
	SubSwMN      = 0x48 // manual notch on/off
	SubSwAPF     = 0x32 // audio peak filter on/off (CW only)
	// Satellite mode (IC-9700 / IC-9100). The rig's OWN satellite mode, not an
	// imitation of one: it pairs main and sub, gives full duplex, and keeps the
	// two dials linked the way the radio's designers meant. Asking it to do that
	// is always better than building the same thing out of split.
	SubSwSatellite = 0x5A

	// CmdVFO sub-commands: which of a two-receiver rig's bands the next command
	// addresses.
	SubVFOMain     = 0xD0 // MAIN band — the downlink in satellite mode
	SubVFOSub      = 0xD1 // SUB band — the uplink
	SubVFOExchange = 0xB0 // swap main and sub
)

// CW break-in modes (CmdSwitch 0x47).
const (
	BreakInOff  = 0
	BreakInSemi = 1
	BreakInFull = 2
)

// Icom mode codes (used by CmdReadMode / CmdSetMode).
const (
	ModeLSB   = 0x00
	ModeUSB   = 0x01
	ModeAM    = 0x02
	ModeCW    = 0x03
	ModeRTTY  = 0x04
	ModeFM    = 0x05
	ModeCWR   = 0x07
	ModeRTTYR = 0x08
	ModePSK   = 0x12 // native PSK (IC-7610/7760/7851 class; older rigs NAK it)
	ModePSKR  = 0x13
)

// Frame builds a complete CI-V frame (preamble … end) for payload, which is the
// command byte followed by any sub-command/data bytes.
func Frame(to, from byte, payload ...byte) []byte {
	f := make([]byte, 0, len(payload)+5)
	f = append(f, Pre, Pre, to, from)
	f = append(f, payload...)
	f = append(f, End)
	return f
}

// FreqToBCD encodes a frequency in Hz as the 5 little-endian BCD bytes Icom
// expects (10 digits, 2 per byte, least-significant byte first).
func FreqToBCD(hz int64) []byte {
	if hz < 0 {
		hz = 0
	}
	b := make([]byte, 5)
	for i := 0; i < 5; i++ {
		lo := hz % 10
		hz /= 10
		hi := hz % 10
		hz /= 10
		b[i] = byte(lo) | byte(hi)<<4
	}
	return b
}

// BCDToFreq decodes Icom little-endian BCD frequency bytes back to Hz.
//
// It returns ok=false when a nibble is not a decimal digit. That check is the
// point of the function: a CI-V byte stream that has lost frame sync — a reply
// read at the wrong offset, a collision with the scope stream — still decodes
// into a number if 0x0A..0x0F are quietly treated as 10..15. An operator on a
// 2 m FM channel then saw entries like 16445550000 Hz appear in the status bar
// between good readings (F4JND, 2026-07-30). Garbage that LOOKS like a
// frequency is worse than a refused frame: it reaches the display, the band
// slots, and eventually the log.
func BCDToFreq(b []byte) (int64, bool) {
	var hz int64
	mult := int64(1)
	for i := 0; i < len(b) && i < 5; i++ {
		lo, hi := b[i]&0x0F, b[i]>>4
		if lo > 9 || hi > 9 {
			return 0, false
		}
		hz += int64(lo) * mult
		mult *= 10
		hz += int64(hi) * mult
		mult *= 10
	}
	return hz, true
}

// LevelToBCD encodes a 0-255 level as the 2 big-endian BCD bytes Icom's
// CmdLevel commands use (e.g. 128 → 0x01 0x28, 255 → 0x02 0x55).
func LevelToBCD(v int) []byte {
	if v < 0 {
		v = 0
	}
	if v > 255 {
		v = 255
	}
	return []byte{byte(v / 100), byte(((v/10)%10)<<4 | v%10)}
}

// BCDToLevel decodes the 2 BCD bytes of a CmdLevel response back to 0-255.
func BCDToLevel(b []byte) int {
	if len(b) < 2 {
		return 0
	}
	return int(b[0])*100 + int(b[1]>>4)*10 + int(b[1]&0x0F)
}

// RITToBCD encodes a RIT/ΔTX offset (Hz, −9999..9999) as the 3 bytes CI-V
// command 0x21 0x00 uses: 2 little-endian BCD bytes of the magnitude followed by
// a sign byte (0x00 positive, 0x01 negative).
func RITToBCD(hz int) []byte {
	neg := hz < 0
	if neg {
		hz = -hz
	}
	if hz > 9999 {
		hz = 9999
	}
	lo := byte(hz%10 | (hz/10%10)<<4)
	hi := byte(hz/100%10 | (hz/1000%10)<<4)
	sign := byte(0)
	if neg {
		sign = 1
	}
	return []byte{lo, hi, sign}
}

// BCDToRIT decodes the 3 offset bytes of a 0x21 0x00 response back to signed Hz.
func BCDToRIT(b []byte) int {
	if len(b) < 3 {
		return 0
	}
	v := int(b[0]&0x0F) + int(b[0]>>4)*10 + int(b[1]&0x0F)*100 + int(b[1]>>4)*1000
	if b[2] != 0 {
		return -v
	}
	return v
}

// WPMToKeyLevel maps a CW speed in words-per-minute to the 0-255 value the KEY
// SPEED level (CmdLevel 0x0C) expects, linear across KeyMinWPM..KeyMaxWPM.
func WPMToKeyLevel(wpm int) int {
	if wpm < KeyMinWPM {
		wpm = KeyMinWPM
	}
	if wpm > KeyMaxWPM {
		wpm = KeyMaxWPM
	}
	return (wpm - KeyMinWPM) * 255 / (KeyMaxWPM - KeyMinWPM)
}

// KeyLevelToWPM is the inverse of WPMToKeyLevel (0-255 → WPM).
func KeyLevelToWPM(v int) int {
	if v < 0 {
		v = 0
	}
	if v > 255 {
		v = 255
	}
	return KeyMinWPM + (v*(KeyMaxWPM-KeyMinWPM)+127)/255
}

// CWText is the set of characters the rig's keyer accepts (command 0x17).
// Everything else is dropped. Space keys a word gap.
const CWText = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /?.,-=+@:"

// FilterCW upper-cases text and keeps only keyer-legal characters.
func FilterCW(text string) string {
	out := make([]byte, 0, len(text))
	for i := 0; i < len(text); i++ {
		c := text[i]
		if c >= 'a' && c <= 'z' {
			c -= 32
		}
		for j := 0; j < len(CWText); j++ {
			if CWText[j] == c {
				out = append(out, c)
				break
			}
		}
	}
	return string(out)
}

// ByteToBCD / BCDToByte handle a single packed-BCD byte (used by the
// attenuator, where the value is dB: 0x00, 0x06, 0x12, 0x18…).
func ByteToBCD(v int) byte {
	if v < 0 {
		v = 0
	}
	if v > 99 {
		v = 99
	}
	return byte((v/10)<<4 | v%10)
}

func BCDToByte(b byte) int { return int(b>>4)*10 + int(b&0x0F) }

// ModeToADIF maps an Icom mode byte (plus the data-mode flag) to an ADIF mode
// string. Data mode on USB/LSB is surfaced as "DATA" so the app can substitute
// the user's preferred digital mode (FT8/RTTY/…), matching the OmniRig backend.
func ModeToADIF(m byte, data bool) string {
	switch m {
	case ModeCW, ModeCWR:
		return "CW"
	case ModeRTTY, ModeRTTYR:
		return "RTTY"
	case ModePSK, ModePSKR:
		return "PSK31"
	case ModeAM:
		return "AM"
	case ModeFM:
		return "FM"
	case ModeLSB, ModeUSB:
		if data {
			return "DATA"
		}
		return "SSB"
	}
	return ""
}

// ModelName maps a rig's default CI-V address (from CmdReadID) to a readable
// model. Unknown addresses fall back to a hex label.
//
// The name is not cosmetic: the UI derives model-dependent behaviour from it —
// notably the attenuator steps, which are 6/12/18 dB on the big rigs and a single
// 20 dB on the small ones. An address missing here therefore shows the WRONG
// attenuator buttons, and the rig NAKs them.
//
// Two entries here used to be wrong in a way that pointed at each other: 0x80 was
// labelled IC-7800 (it is the IC-7410) and 0x88 IC-7700 (it is the IC-7100), while
// the real IC-7800 (0x6A) and IC-7700 (0x74) were absent — so a 7800 came up as
// "Icom (0x6A)" with a 20 dB attenuator it does not have.
//
// Addresses cross-checked against the TR4W CI-V table and an independent
// published list; both agree on every value below.
func ModelName(addr byte) string {
	switch addr {
	case 0x6A:
		return "IC-7800"
	case 0x74:
		return "IC-7700"
	case 0x7A:
		return "IC-7600"
	case 0x7C:
		return "IC-9100"
	case 0x80:
		return "IC-7410"
	case 0x88:
		return "IC-7100"
	case 0x8E:
		return "IC-7851" // shared with the IC-7850
	case 0x94:
		return "IC-7300"
	case 0x98:
		return "IC-7610"
	case 0xA2:
		return "IC-9700"
	case 0xA4:
		return "IC-705"
	case 0xB2:
		return "IC-7760"
	case 0xB6:
		return "IC-7300MKII"
	}
	return fmt.Sprintf("Icom (0x%02X)", addr)
}

// Decoded is one parsed CI-V frame. Data is everything after the command byte
// (so it still carries the sub-command for multi-byte commands like 1A 06).
type Decoded struct {
	To   byte
	From byte
	Cmd  byte
	Data []byte
}

// Scan extracts every complete frame from buf and reports how many leading
// bytes the caller may now discard. A trailing partial frame (or a lone
// preamble byte) is left unconsumed so it can be completed by the next read.
func Scan(buf []byte) (frames []Decoded, consumed int) {
	pos := 0
	for {
		p := indexPreamble(buf, pos)
		if p < 0 {
			// No further preamble. Keep a trailing FE (possible start of the
			// next preamble); otherwise everything seen is consumable.
			if len(buf) > 0 && buf[len(buf)-1] == Pre {
				return frames, len(buf) - 1
			}
			return frames, len(buf)
		}
		start := p + 2
		for start < len(buf) && buf[start] == Pre { // tolerate padding FEs
			start++
		}
		end := bytes.IndexByte(buf[start:], End)
		if end < 0 {
			return frames, p // incomplete frame — keep from its preamble
		}
		end += start
		if body := buf[start:end]; len(body) >= 3 {
			frames = append(frames, Decoded{
				To:   body[0],
				From: body[1],
				Cmd:  body[2],
				Data: append([]byte(nil), body[3:]...),
			})
		}
		pos = end + 1
		consumed = pos
	}
}

// indexPreamble returns the index of the next FE FE pair at or after from.
func indexPreamble(buf []byte, from int) int {
	for i := from; i+1 < len(buf); i++ {
		if buf[i] == Pre && buf[i+1] == Pre {
			return i
		}
	}
	return -1
}

// ── Band stacking registers (CI-V 0x1A sub 0x01) ──────────────────────────
//
// Every modern Icom remembers the last few frequency/mode pairs used on each
// band, and its front-panel band key walks through them. That is why pressing
// [14] on the radio lands on the FT8 watering hole rather than on a number
// somebody chose in software: the register is the operator's OWN last visit.
//
// The frame is a READ — it asks the rig what a register holds and changes
// nothing — so a rig that does not know the command answers NG and the caller
// is exactly where it started.
//
//	→ 1A 01 <band> <reg>
//	← 1A 01 <band> <reg> <freq 5 BCD, LE> <mode> <filter> <data mode> …
//
// Anything past the data-mode byte (duplex, tone, DV squelch on the VHF rigs)
// is not read here: the question is where the operator last was, and the answer
// to that is the frequency and the mode.
const SubBandStack = 0x01

// BandStack is one register's contents.
type BandStack struct {
	FreqHz int64
	Mode   byte
	Data   bool // the data-mode flag that goes with Mode
}

// DecodeBandStack reads the payload of a 0x1A 0x01 reply, i.e. everything after
// the command byte. ok is false for a frame that is not the register asked for,
// which is what a desynchronised read looks like.
func DecodeBandStack(data []byte, band, reg byte) (BandStack, bool) {
	if len(data) < 9 || data[0] != SubBandStack || data[1] != band || data[2] != reg {
		return BandStack{}, false
	}
	hz, ok := BCDToFreq(data[3:8])
	if !ok || hz <= 0 {
		return BandStack{}, false
	}
	bs := BandStack{FreqHz: hz, Mode: data[8]}
	// Filter then data mode. A rig that stops at the filter byte is not an
	// error — it is a register without a data flag, so the flag stays false.
	if len(data) >= 11 {
		bs.Data = data[10] != 0
	}
	return bs, true
}
