package cat

// CW keying through the Kenwood/Elecraft keyer — the KY command.
//
// Same idea as the Yaesu KY engine and the Icom / Flex keyers: the radio holds
// the text and keys it with its own timing, so an Elecraft K3 (or any rig that
// speaks this dialect) needs NO WinKeyer and NO second COM port — the single CAT
// link does frequency, mode AND CW. That matters on a K3, whose one USB port is
// the CAT port; a separate serial keyer would need a second cable OpsLog can't
// give it.
//
//	KY;            → KYn;  n=0 buffer has room, n=1 buffer full
//	KY <text>;            queue up to 24 characters (the space after KY is part
//	                      of the command, not padding)
//	KS nnn;               keyer speed in WPM (three digits)
//	RX;                   drop to receive — used to abort a send
//
// KY is the Elecraft-documented CW-over-CAT path on the K3/K4. A rig that refuses
// it answers "?;", which SendCW turns into a stated reason rather than silence.

import (
	"fmt"
	"strings"
	"time"
)

// kenwoodCWChunk is the most characters one KY command accepts.
const kenwoodCWChunk = 24

// kenwoodCWAllowed is what the keyer can send; anything else is dropped, since an
// unsupported byte can abort the buffer and lose the rest of the message.
// The semicolon is deliberately absent: it TERMINATES a CAT frame. The TS-590
// manual says so outright for P2, and one in a macro would close the command
// early and leave the rest of the text to be read as commands of its own.
const kenwoodCWAllowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /?.,-=+:()"

// padCW pads a piece to the fixed 24-character P2 a Kenwood demands.
//
// This is where CW over CAT failed on a TS-590SG. The KY command was written
// against Elecraft's, which takes a variable-length string, so "KY OH5CX;" went
// out and the radio answered "?;" — read as "this rig refuses CW over CAT", and
// the operator was told to go and buy a serial keyer.
//
// The TS-590 manual is explicit: P2 has a FIXED length of 24, and characters
// left blank are filled with spaces which are NOT keyed. So the padding costs
// nothing on air — it is simply the shape the command has to have.
//
// Elecraft is left variable-length: it accepts short strings, and padding there
// would key the trailing spaces as word gaps.
func (k *Kenwood) padCW(chunk string) string {
	if k.elecraft || len(chunk) >= kenwoodCWChunk {
		return chunk
	}
	return chunk + strings.Repeat(" ", kenwoodCWChunk-len(chunk))
}

// SendCW queues a message on the rig's keyer, fed in 24-character pieces, waiting
// for buffer room between pieces so a long macro doesn't lose its tail.
func (k *Kenwood) SendCW(text string) error {
	msg := filterKenwoodCW(text)
	if msg == "" {
		return nil
	}
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	for len(msg) > 0 {
		n := kenwoodCWChunk
		if len(msg) < n {
			n = len(msg)
		}
		chunk := msg[:n]
		msg = msg[n:]
		k.waitCWBuffer(3 * time.Second)
		if err := k.write("KY " + k.padCW(chunk) + ";"); err != nil {
			return err
		}
		if err := k.afterKY(); err != nil {
			return err
		}
		// Pace the next piece by how long this one takes to key, so we never overrun
		// the 24-character buffer (the rig silently drops what doesn't fit).
		if len(msg) > 0 {
			time.Sleep(kenwoodCWDuration(chunk, k.keyerWPM()))
		}
	}
	return nil
}

// afterKY reads briefly after a KY write. An accepted KY says nothing; a REJECT
// answers "?;". Reading it straight off the port (not through the shared rx
// buffer) keeps that stray frame from being picked up by the next poll's ask —
// which would mis-mark an unrelated command unsupported and desync the link — and
// turns a silent non-transmission into a stated reason. The caller holds k.mu.
func (k *Kenwood) afterKY() error {
	deadline := time.Now().Add(150 * time.Millisecond)
	tmp := make([]byte, 64)
	var buf []byte
	for time.Now().Before(deadline) {
		n, err := k.port.Read(tmp)
		if err != nil {
			break
		}
		if n > 0 {
			buf = append(buf, tmp[:n]...)
		}
	}
	if strings.Contains(string(buf), "?;") {
		return fmt.Errorf("this radio rejected CW over CAT (it answered \"?;\" to KY). " +
			"Switch the keyer engine to the serial-port keyer (DTR=CW) on a COM port instead")
	}
	return nil
}

// waitCWBuffer blocks until the keyer reports room, or the deadline passes. A rig
// that never answers the KY; status query is not a reason to refuse to send — we
// go ahead, and the per-chunk pacing covers the worst case. The caller holds k.mu.
func (k *Kenwood) waitCWBuffer(within time.Duration) {
	deadline := time.Now().Add(within)
	for time.Now().Before(deadline) {
		if k.unsupported["KY"] {
			return // this rig doesn't report buffer state — pacing covers it
		}
		r, err := k.ask("KY;")
		if err != nil {
			return // unsupported / timeout — send anyway, pacing covers it
		}
		if !kenwoodCWBufferFull(r) {
			return
		}
		time.Sleep(50 * time.Millisecond)
	}
}

// kenwoodCWBufferFull reads the KY; status reply. Deliberately asymmetric: only a
// clear "1" after KY means full. Anything else reads as "go ahead" — refusing to
// send because a status line was phrased unexpectedly is the worse failure.
func kenwoodCWBufferFull(reply string) bool {
	r := strings.TrimSpace(reply)
	if !strings.HasPrefix(strings.ToUpper(r), "KY") {
		return false
	}
	for _, c := range r[2:] {
		switch c {
		case '0':
			return false
		case '1':
			return true
		case ' ', ';':
			continue
		default:
			return false
		}
	}
	return false
}

// StopCW aborts the message being sent. Kenwood documents no KY buffer-clear, so
// this drops the transmitter — RX; forces receive, which is what an operator
// pressing Escape wants; anything still queued is not keyed on the air.
func (k *Kenwood) StopCW() error {
	k.mu.Lock()
	defer k.mu.Unlock()
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	return k.write("RX;")
}

// SetKeySpeed sets the keyer speed (WPM) via KS and remembers it for pacing.
func (k *Kenwood) SetKeySpeed(wpm int) error {
	if wpm < 4 {
		wpm = 4
	}
	if wpm > 99 {
		wpm = 99 // KS is three digits but the K3 keyer tops out well below 100
	}
	k.mu.Lock()
	defer k.mu.Unlock()
	k.keyWPM = wpm
	if k.port == nil {
		return fmt.Errorf("kenwood: not connected")
	}
	return k.write(fmt.Sprintf("KS%03d;", wpm))
}

// keyerWPM is the speed to pace the buffer by. The caller holds k.mu.
func (k *Kenwood) keyerWPM() int {
	if k.keyWPM >= 4 {
		return k.keyWPM
	}
	return 20
}

// kenwoodCWDuration estimates how long a piece of text takes to key (PARIS
// timing: a character averages 10 dits, a dit is 1.2/wpm seconds).
func kenwoodCWDuration(text string, wpm int) time.Duration {
	if wpm < 4 {
		wpm = 20
	}
	ditMs := 1200.0 / float64(wpm)
	return time.Duration(float64(len(text))*10*ditMs) * time.Millisecond
}

// filterKenwoodCW upper-cases and strips what the keyer cannot send; whitespace
// becomes a single word gap.
func filterKenwoodCW(text string) string {
	var b strings.Builder
	for _, r := range strings.ToUpper(text) {
		if r == '\t' || r == '\n' || r == '\r' {
			b.WriteByte(' ')
			continue
		}
		if strings.ContainsRune(kenwoodCWAllowed, r) {
			b.WriteRune(r)
		}
	}
	return strings.Join(strings.Fields(b.String()), " ")
}
