//go:build windows

package cat

// The TCI control panel: what the radio already tells us, gathered up.
//
// This is the cheapest panel in OpsLog, and the reason is worth saying. A K3 is
// asked — every value on its console costs a command and a reply on a serial
// line, which is why that panel reads its settings in a rotation and its meters
// only while it is on screen. TCI PUSHES: the radio announces its drive, its
// volume, its filters, its noise blanker and everything else when a client
// connects, and again whenever any of them changes, whoever changed it. There
// is nothing to poll.
//
// So this file is mostly a place to PUT what was already arriving and being
// logged as "(unhandled once)". The setters are the same names sent back the
// other way, which is how TCI works throughout: one vocabulary, both directions.

import (
	"fmt"
	"strconv"
	"strings"
)

// TCIPanelState is the whole console in one snapshot, polled by the frontend.
//
// Values the radio has not mentioned keep their zero, which is why the
// "Known" flags exist for the ones where zero is a real setting: a squelch at 0
// and a squelch never reported are different, and a panel that cannot tell them
// apart draws a control that lies until the operator touches it.
type TCIPanelState struct {
	Connected bool   `json:"connected"`
	Device    string `json:"device,omitempty"`   // what the radio calls itself
	Protocol  string `json:"protocol,omitempty"` // "ExpertSDR3,1.5"

	// Transmit.
	Drive     int  `json:"drive"`      // 0-100
	TuneDrive int  `json:"tune_drive"` // 0-100, used by TUNE
	MicLevel  int  `json:"mic_level"`  // 0-100
	TXEnabled bool `json:"tx_enabled"` // the radio's own permission (tx_enable)
	TX        bool `json:"tx"`
	Tuning    bool `json:"tuning"`

	// Receive.
	Volume    int    `json:"volume"` // dB, negative — TCI's own scale
	Mute      bool   `json:"mute"`
	AGC       string `json:"agc,omitempty"` // off/long/slow/med/fast
	SquelchOn bool   `json:"squelch_on"`
	Squelch   int    `json:"squelch"` // dBm threshold
	NB        bool   `json:"nb"`
	NR        bool   `json:"nr"`
	ANF       bool   `json:"anf"`
	APF       bool   `json:"apf"`

	// Filter edges in Hz, relative to the carrier (TCI's own convention).
	FilterLo int `json:"filter_lo"`
	FilterHi int `json:"filter_hi"`

	// Tuning aids.
	RIT       bool `json:"rit"`
	RITOffset int  `json:"rit_offset"`
	XIT       bool `json:"xit"`
	XITOffset int  `json:"xit_offset"`
	Lock      bool `json:"lock"`
	Split     bool `json:"split"`

	// SMeter is the last reported signal level in dBm — the radio pushes it
	// several times a second while receiving.
	SMeter int `json:"smeter"`

	// TXPowerW and TXSWR are the transmit meters. READ-ONLY in TCI, and only
	// answered while transmitting — asked for on every poll of a keyed radio,
	// see ReadState.
	//
	// There is no temperature in this protocol. The command list has TX_POWER
	// and TX_SWR and nothing thermal at all, so a temperature reading here would
	// have to be invented, and an invented temperature on a transmitter is the
	// kind of number somebody trusts.
	TXPowerW float64 `json:"tx_power_w"`
	TXSWR    float64 `json:"tx_swr"`

	// Modulations is what this radio will accept, straight from its own
	// announcement, so the mode buttons are the radio's and not a guess.
	Modulations []string `json:"modulations,omitempty"`
}

// tciPanel is the backing state. Guarded by TCI.mu with everything else it
// arrives alongside.
type tciPanel struct {
	st TCIPanelState
	// logged counts what has been written per message type — see handlePanel.
	logged map[string]int
}

// handlePanel takes the messages the console cares about.
//
// Returns false when the message is none of its business, so the caller can go
// on to its own cases and to the unknown-message log. Called with t.mu held.
func (t *TCI) handlePanel(name string, get func(int) string, args string) bool {
	// Most of these are per-receiver ("sql_level:0,20"), and OpsLog follows
	// receiver 0 throughout. A message for another receiver is accepted as
	// handled and dropped: it is understood, it is simply not ours.
	forRX0 := func() bool { return get(0) == "0" || get(0) == "" }
	num := func(s string) (int, bool) {
		n, err := strconv.Atoi(strings.TrimSpace(s))
		return n, err == nil
	}
	yes := func(s string) bool { return strings.EqualFold(strings.TrimSpace(s), "true") }

	p := &t.panel.st
	// Mute and squelch are LOGGED as they change, because a report from a real
	// radio says pressing MUTE lights the squelch and nothing here can explain
	// it. What the radio actually announces after the command settles whether
	// this is our reading or its doing, and no amount of reasoning will.
	switch name {
	case "mute", "sql_enable", "sql_level", "tx_power", "tx_swr", "tune",
		"tx_sensors", "rx_sensors", "rx_channel_sensors":
		// Logged on arrival so an ANSWER can be told from a SILENCE: the log
		// showed the transmit meters being asked for and nothing coming back,
		// which on its own proves nothing — a reply that arrived and failed to
		// parse leaves exactly the same trace as one that never came.
		//
		// Capped per message type. The meters are asked for four times a second
		// while transmitting, and a diagnostic that fills an evening's log is
		// one that gets switched off instead of read.
		if t.panel.logged == nil {
			t.panel.logged = map[string]int{}
		}
		if n := t.panel.logged[name]; n < 20 {
			t.panel.logged[name] = n + 1
			debugLog.Printf("TCI: %s:%s", name, args)
		}
	}
	switch name {
	case "protocol":
		p.Protocol = strings.TrimSpace(args)
	case "drive":
		if n, ok := num(get(1)); ok && forRX0() {
			p.Drive = n
		} else if n, ok := num(get(0)); ok && get(1) == "" {
			// Some firmware sends "drive:85" with no receiver index.
			p.Drive = n
		}
	case "tune_drive":
		if n, ok := num(get(1)); ok && forRX0() {
			p.TuneDrive = n
		} else if n, ok := num(get(0)); ok && get(1) == "" {
			p.TuneDrive = n
		}
	case "mic_level":
		if n, ok := num(get(0)); ok {
			p.MicLevel = n
		}
	case "volume":
		if n, ok := num(get(0)); ok {
			p.Volume = n
		}
	case "mute":
		// Both shapes. This radio reports "mute:0,false" and the reference shows
		// "mute:true" elsewhere — reading only one of them left the button
		// showing the opposite of the truth, which is worse than showing
		// nothing.
		if get(1) != "" {
			p.Mute = yes(get(1))
		} else {
			p.Mute = yes(get(0))
		}
	case "agc_mode":
		if forRX0() {
			p.AGC = strings.ToLower(strings.TrimSpace(get(1)))
		}
	case "sql_enable":
		if forRX0() {
			p.SquelchOn = yes(get(1))
		}
	case "sql_level":
		if n, ok := num(get(1)); ok && forRX0() {
			p.Squelch = n
		}
	case "rx_nb_enable":
		if forRX0() {
			p.NB = yes(get(1))
		}
	case "rx_nr_enable":
		if forRX0() {
			p.NR = yes(get(1))
		}
	case "rx_anf_enable":
		if forRX0() {
			p.ANF = yes(get(1))
		}
	case "rx_apf_enable":
		if forRX0() {
			p.APF = yes(get(1))
		}
	case "rx_filter_band":
		if forRX0() {
			if lo, ok := num(get(1)); ok {
				p.FilterLo = lo
			}
			if hi, ok := num(get(2)); ok {
				p.FilterHi = hi
			}
		}
	case "rit_enable":
		if forRX0() {
			p.RIT = yes(get(1))
		}
	case "xit_enable":
		if forRX0() {
			p.XIT = yes(get(1))
		}
	case "rit_offset":
		if n, ok := num(get(1)); ok && forRX0() {
			p.RITOffset = n
		}
	case "xit_offset":
		if n, ok := num(get(1)); ok && forRX0() {
			p.XITOffset = n
		}
	case "lock":
		if forRX0() {
			p.Lock = yes(get(1))
		}
	case "tx_power":
		if v, err := strconv.ParseFloat(strings.TrimSpace(get(0)), 64); err == nil {
			p.TXPowerW = v
		}
	case "tx_swr":
		if v, err := strconv.ParseFloat(strings.TrimSpace(get(0)), 64); err == nil {
			p.TXSWR = v
		}
	case "rx_smeter":
		if n, ok := num(get(1)); ok && forRX0() {
			p.SMeter = n
		}
	// The meters of ExpertSDR3. The S-meter used to be read only from RX_SMETER
	// and the transmit ones from TX_POWER / TX_SWR — commands this radio simply
	// never sends, which is why the console's meters sat empty on a SunSDR in
	// both RX and TX while everything else worked.
	//
	// The protocol's own answer (TCI Protocol.pdf, §4.4) is a SUBSCRIPTION:
	//
	//	RX_SENSORS:<rx>,<dBm>;                       (deprecated in 2.0)
	//	RX_CHANNEL_SENSORS:<rx>,<channel>,<dBm>;     (its replacement)
	//	TX_SENSORS:<trx>,<mic dBm>,<power W>,<peak W>,<SWR>;
	//
	// none of which arrives until the client asks with RX_SENSORS_ENABLE and
	// TX_SENSORS_ENABLE — see Connect.
	case "rx_sensors":
		if v, err := strconv.ParseFloat(strings.TrimSpace(get(1)), 64); err == nil && forRX0() {
			p.SMeter = int(v)
		}
	case "rx_channel_sensors":
		// Main channel (A) of receiver 0: the one the console is showing.
		if v, err := strconv.ParseFloat(strings.TrimSpace(get(2)), 64); err == nil &&
			get(0) == "0" && get(1) == "0" {
			p.SMeter = int(v)
		}
	case "tx_sensors":
		if get(0) != "0" {
			break
		}
		// arg3 is RMS power, arg4 the peak. The peak is what a power meter's
		// needle does on speech; the RMS is what the operator is asked to keep
		// under the amplifier's limit — so RMS is the number, as elsewhere.
		if v, err := strconv.ParseFloat(strings.TrimSpace(get(2)), 64); err == nil {
			p.TXPowerW = v
		}
		if v, err := strconv.ParseFloat(strings.TrimSpace(get(4)), 64); err == nil {
			p.TXSWR = v
		}
	case "tune":
		if forRX0() {
			p.Tuning = yes(get(1))
		}
	case "modulations_list":
		p.Modulations = splitAndTrim(args)
	default:
		return false
	}
	return true
}

// splitAndTrim turns "usb,lsb,cw" into a slice, upper-cased for display.
func splitAndTrim(s string) []string {
	parts := strings.Split(s, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if v := strings.ToUpper(strings.TrimSpace(p)); v != "" {
			out = append(out, v)
		}
	}
	return out
}

// TCIPanel returns the console snapshot.
func (t *TCI) TCIPanel() TCIPanelState {
	t.mu.Lock()
	defer t.mu.Unlock()
	st := t.panel.st
	st.Connected = t.conn != nil
	st.Device = t.device
	st.TX = t.tx
	st.Split = t.split
	st.TXEnabled = t.txAllowed || !t.txAllowedKnown
	return st
}

// ── Setters ───────────────────────────────────────────────────────────────
//
// Every one of them is a SET in the same vocabulary the radio reports in, and
// none of them updates the cached state: the radio answers with the new value,
// and taking its word rather than our own is what keeps the panel honest when a
// setting is refused, clamped, or changed from the radio's own window a second
// later.

// SetDrive sets the transmit drive, 0-100.
//
// THE TRX INDEX IS PART OF THE COMMAND — "drive:0,15;", not "drive:15;". Sent
// without it the radio simply ignores it: no error, no answer, the power
// unchanged. The rule is the one the radio's own reports follow, and it was
// there to read all along: this radio announces "drive:0,85" at connect.
func (t *TCI) SetDrive(v int) error { return t.send(fmt.Sprintf("drive:0,%d;", clampTCIPct(v))) }

// SetTuneDrive sets the drive used by TUNE, 0-100. Indexed, like drive.
func (t *TCI) SetTuneDrive(v int) error {
	return t.send(fmt.Sprintf("tune_drive:0,%d;", clampTCIPct(v)))
}

// SetMicLevel sets the microphone gain, 0-100.
// Mic gain and volume are the two that are NOT indexed — the radio reports
// them as "mic_level:100" and "volume:-12", with no receiver in front. Sending
// the shape the radio speaks in is the whole rule here.
func (t *TCI) SetMicLevel(v int) error { return t.send(fmt.Sprintf("mic_level:%d;", clampTCIPct(v))) }

// SetVolume sets the receive volume in dB. TCI's scale is negative — 0 is full
// and -60 is inaudible — so this is NOT clamped to a percentage.
func (t *TCI) SetVolume(db int) error {
	if db > 0 {
		db = 0
	}
	if db < -60 {
		db = -60
	}
	return t.send(fmt.Sprintf("volume:%d;", db))
}

// SetMute mutes or unmutes the receiver. Indexed — the radio reports
// "mute:0,false", and a mute sent without the index goes nowhere.
func (t *TCI) SetMute(on bool) error { return t.send(fmt.Sprintf("mute:0,%t;", on)) }

// SetAGC picks the AGC speed: off, long, slow, med, fast.
func (t *TCI) SetAGC(mode string) error {
	m := strings.ToLower(strings.TrimSpace(mode))
	switch m {
	case "off", "long", "slow", "med", "fast":
	default:
		return fmt.Errorf("unknown AGC mode %q", mode)
	}
	return t.send(fmt.Sprintf("agc_mode:0,%s;", m))
}

// SetSquelch turns the squelch on or off.
func (t *TCI) SetSquelch(on bool) error { return t.send(fmt.Sprintf("sql_enable:0,%t;", on)) }

// SetSquelchLevel sets the threshold in dBm.
func (t *TCI) SetSquelchLevel(v int) error { return t.send(fmt.Sprintf("sql_level:0,%d;", v)) }

// SetNB, SetNR, SetANF, SetAPF switch the receive processing.
func (t *TCI) SetNB(on bool) error  { return t.send(fmt.Sprintf("rx_nb_enable:0,%t;", on)) }
func (t *TCI) SetNR(on bool) error  { return t.send(fmt.Sprintf("rx_nr_enable:0,%t;", on)) }
func (t *TCI) SetANF(on bool) error { return t.send(fmt.Sprintf("rx_anf_enable:0,%t;", on)) }
func (t *TCI) SetAPF(on bool) error { return t.send(fmt.Sprintf("rx_apf_enable:0,%t;", on)) }

// SetFilter sets the passband edges in Hz.
func (t *TCI) SetFilter(lo, hi int) error {
	if lo > hi {
		lo, hi = hi, lo
	}
	return t.send(fmt.Sprintf("rx_filter_band:0,%d,%d;", lo, hi))
}

// SetRIT / SetXIT switch the offsets on, SetRITOffset / SetXITOffset move them.
func (t *TCI) SetRIT(on bool) error      { return t.send(fmt.Sprintf("rit_enable:0,%t;", on)) }
func (t *TCI) SetXIT(on bool) error      { return t.send(fmt.Sprintf("xit_enable:0,%t;", on)) }
func (t *TCI) SetRITOffset(hz int) error { return t.send(fmt.Sprintf("rit_offset:0,%d;", hz)) }
func (t *TCI) SetXITOffset(hz int) error { return t.send(fmt.Sprintf("xit_offset:0,%d;", hz)) }

// SetLock locks the VFO knob on the radio.
func (t *TCI) SetLock(on bool) error { return t.send(fmt.Sprintf("lock:0,%t;", on)) }

// SetTune starts or stops the tune carrier.
//
// It TRANSMITS, at tune_drive rather than at drive — which is the setting to
// check before pressing it, and why the panel shows the two side by side.
//
// The state is recorded HERE rather than waited for. This radio does not echo
// "tune:0,true", so the panel had no way of knowing a tune was running: the
// button stayed on TUNE and every further press sent another START, which is
// why it could not be switched off again. Whatever the radio says afterwards
// still wins — it simply never says anything.
func (t *TCI) SetTune(on bool) error {
	t.mu.Lock()
	t.panel.st.Tuning = on
	t.mu.Unlock()
	return t.send(fmt.Sprintf("tune:0,%t;", on))
}

func clampTCIPct(v int) int {
	if v < 0 {
		return 0
	}
	if v > 100 {
		return 100
	}
	return v
}
