package winkeyer

// serialcw.go — CW keying by toggling a serial control line, the "hardware CW
// keying" method N1MM / WSJT / fldigi use with a Yaesu SCU-17 and similar
// interfaces (DTR = CW key, RTS = PTT). No K1EL WinKeyer chip involved: the PC
// bit-bangs the Morse itself on the DTR (or RTS) line at the configured WPM.
//
// A single worker goroutine consumes queued text and keys it element by element,
// so Send returns immediately (like the WinKeyer's buffered send) and Stop can
// abort mid-message. Speed changes apply live between characters. PTT (when
// enabled) is asserted on the OTHER line for the whole transmission plus a
// lead-in / tail, and held through a short hang so send-on-type doesn't chatter.

import (
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"hamlog/internal/applog"
)

// lineCtl drives a serial port's DTR and RTS control lines. On Windows it uses
// EscapeCommFunction (SETDTR/CLRDTR/SETRTS/CLRRTS) — the direct method N1MM/WSJT
// use, reliable on USB-serial adapters (CP210x/FTDI). go.bug.st/serial drives the
// lines via SetCommState instead, which on a CP210x drops the held RTS (PTT) as
// soon as DTR (CW) is toggled — the "rig drops to RX between words" bug. See
// linectl_windows.go / linectl_other.go.
type lineCtl interface {
	setDTR(on bool) error
	setRTS(on bool) error
	close() error
}

// morseTable maps a keyable character to its Morse element string (. = dit,
// - = dah). Mirrors winkeyer.allowedCW.
var morseTable = map[rune]string{
	'A': ".-", 'B': "-...", 'C': "-.-.", 'D': "-..", 'E': ".", 'F': "..-.",
	'G': "--.", 'H': "....", 'I': "..", 'J': ".---", 'K': "-.-", 'L': ".-..",
	'M': "--", 'N': "-.", 'O': "---", 'P': ".--.", 'Q': "--.-", 'R': ".-.",
	'S': "...", 'T': "-", 'U': "..-", 'V': "...-", 'W': ".--", 'X': "-..-",
	'Y': "-.--", 'Z': "--..",
	'0': "-----", '1': ".----", '2': "..---", '3': "...--", '4': "....-",
	'5': ".....", '6': "-....", '7': "--...", '8': "---..", '9': "----.",
	'.': ".-.-.-", ',': "--..--", '?': "..--..", '/': "-..-.", '=': "-...-",
	'+': ".-.-.", '-': "-....-", ':': "---...", '(': "-.--.", ')': "-.--.-",
	';': "-.-.-.", '"': ".-..-.", '\'': ".----.", '@': ".--.-.",
}

// serialKeyer bit-bangs CW on a serial control line. Owned by a winkeyer.Manager
// while Type == "serial"; all keying happens in run(). The line-control options
// (key line, invert, PTT, lead/tail) are atomic so ApplyConfig can update them
// LIVE — e.g. ticking "Key PTT line" takes effect without reconnecting the keyer.
type serialKeyer struct {
	line   lineCtl
	keyDTR atomic.Bool  // true: CW on DTR, PTT on RTS (N1MM default). false: swapped.
	invert atomic.Bool  // true: active-LOW — asserting a line means driving it LOW
	usePTT atomic.Bool  // hold the PTT line for the whole transmission
	leadMs atomic.Int32 // PTT lead-in
	tailMs atomic.Int32 // PTT hold (hang) after the last element
	onBusy func(bool)

	mu    sync.Mutex
	wpm   int
	farns int
	abort chan struct{} // recreated by Stop; keying aborts when it closes

	in   chan string
	stop chan struct{}
	done chan struct{}
}

func newSerialKeyer(line lineCtl, cfg Config, onBusy func(bool)) *serialKeyer {
	k := &serialKeyer{
		line:   line,
		onBusy: onBusy,
		wpm:    cfg.WPM,
		farns:  cfg.Farnsworth,
		abort:  make(chan struct{}),
		in:     make(chan string, 256),
		stop:   make(chan struct{}),
		done:   make(chan struct{}),
	}
	k.keyDTR.Store(!strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts")) // default DTR=CW
	k.invert.Store(cfg.CWInvert)
	k.usePTT.Store(cfg.UsePTT)
	k.leadMs.Store(int32(cfg.LeadInMs))
	k.tailMs.Store(int32(cfg.TailMs))
	k.idle() // start inactive: key up, PTT off
	go k.run()
	return k
}

// ApplyConfig live-updates the line-control options (no port reopen), so a
// settings change is felt from the next character without a reconnect. Speed and
// Farnsworth are updated too. keyText re-reads these per element.
func (k *serialKeyer) ApplyConfig(cfg Config) {
	k.keyDTR.Store(!strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts"))
	k.invert.Store(cfg.CWInvert)
	k.usePTT.Store(cfg.UsePTT)
	k.leadMs.Store(int32(cfg.LeadInMs))
	k.tailMs.Store(int32(cfg.TailMs))
	k.mu.Lock()
	k.wpm = cfg.WPM
	k.farns = cfg.Farnsworth
	k.mu.Unlock()
	k.idle() // re-assert idle lines with any new polarity/key-line choice
}

// level maps a logical "active" state to the physical line level, honouring the
// invert (active-LOW) option: normally active = HIGH, inverted active = LOW.
func (k *serialKeyer) level(active bool) bool { return active != k.invert.Load() }

// setKey raises/lowers the CW key line; setPTT the PTT line (only when enabled).
func (k *serialKeyer) setKey(down bool) {
	if k.keyDTR.Load() {
		_ = k.line.setDTR(k.level(down))
	} else {
		_ = k.line.setRTS(k.level(down))
	}
}
func (k *serialKeyer) setPTT(on bool) {
	if !k.usePTT.Load() {
		return
	}
	if k.keyDTR.Load() {
		_ = k.line.setRTS(k.level(on))
	} else {
		_ = k.line.setDTR(k.level(on))
	}
}

// idle drives BOTH lines to their inactive level (key up, PTT off), respecting
// the invert option — so an active-LOW interface isn't left keyed at rest.
func (k *serialKeyer) idle() {
	_ = k.line.setDTR(k.level(false))
	_ = k.line.setRTS(k.level(false))
}

// SetSpeed / Send / Stop mirror the WinKeyer manager's control surface.
func (k *serialKeyer) SetSpeed(wpm int) {
	k.mu.Lock()
	k.wpm = wpm
	k.mu.Unlock()
}

func (k *serialKeyer) Send(text string) {
	select {
	case k.in <- text:
	default: // queue full (pathological) — drop rather than block the app binding
	}
}

// Stop aborts the character being keyed and flushes anything queued.
func (k *serialKeyer) Stop() {
	k.mu.Lock()
	close(k.abort)
	k.abort = make(chan struct{})
	k.mu.Unlock()
	for drained := false; !drained; {
		select {
		case <-k.in:
		default:
			drained = true
		}
	}
	k.setKey(false)
}

func (k *serialKeyer) Close() {
	close(k.stop)
	<-k.done
	_ = k.line.close()
}

func (k *serialKeyer) run() {
	defer close(k.done)
	defer k.idle()
	for {
		select {
		case <-k.stop:
			return
		case s := <-k.in:
			k.onBusy(true)
			// Diagnostic: confirms whether PTT is actually held for the whole macro
			// (a rig dropping to RX between words = usePTT off, or the interface's
			// PTT line isn't wired / honoured in CW).
			applog.Printf("winkeyer serial: TX %.40q ptt=%v line=%s lead=%dms tail=%dms",
				s, k.usePTT.Load(), map[bool]string{true: "DTR-CW/RTS-PTT", false: "RTS-CW/DTR-PTT"}[k.keyDTR.Load()],
				k.leadMs.Load(), k.tailMs.Load())
			k.setPTT(true)
			k.sleep(time.Duration(k.leadMs.Load()) * time.Millisecond)
			k.keyText(s)
			hang := time.Duration(k.tailMs.Load()) * time.Millisecond
			if hang <= 0 {
				hang = 5 * time.Millisecond
			}
		hold: // hold PTT briefly so back-to-back sends (send-on-type) don't chatter
			for {
				select {
				case <-k.stop:
					k.idle()
					k.onBusy(false)
					return
				case s2 := <-k.in:
					k.keyText(s2)
				case <-time.After(hang):
					break hold
				}
			}
			k.setPTT(false)
			k.onBusy(false)
		}
	}
}

// keyText keys one string. Element gaps use the character speed (WPM); the
// inter-character (3-unit) and inter-word (7-unit) gaps use the Farnsworth speed
// when one is set, so slow-copy CW keeps full-speed characters with wider spacing.
func (k *serialKeyer) keyText(s string) {
	k.mu.Lock()
	abort := k.abort
	k.mu.Unlock()
	for _, r := range strings.ToUpper(s) {
		select {
		case <-abort:
			k.setKey(false)
			return
		case <-k.stop:
			k.setKey(false)
			return
		default:
		}
		ditW, ditF := k.dits()
		if r == ' ' {
			// Word gap: 7 units total. A 3-unit inter-char gap was already emitted
			// after the previous character, so add 4 more.
			if !k.gap(4*ditF, abort) {
				return
			}
			continue
		}
		code, ok := morseTable[r]
		if !ok {
			continue
		}
		for j, el := range code {
			if el == '.' {
				if !k.mark(ditW, abort) {
					return
				}
			} else {
				if !k.mark(3*ditW, abort) {
					return
				}
			}
			if j < len(code)-1 { // intra-character (element) gap: 1 unit
				if !k.gap(ditW, abort) {
					return
				}
			}
		}
		if !k.gap(3*ditF, abort) { // inter-character gap: 3 units
			return
		}
	}
}

// mark holds the key down for d; gap holds it up for d. Both abort cleanly
// (leaving the key UP) when Stop closes abort or the keyer shuts down.
func (k *serialKeyer) mark(d time.Duration, abort chan struct{}) bool {
	k.setKey(true)
	ok := k.wait(d, abort)
	k.setKey(false)
	return ok
}
func (k *serialKeyer) gap(d time.Duration, abort chan struct{}) bool { return k.wait(d, abort) }

func (k *serialKeyer) wait(d time.Duration, abort chan struct{}) bool {
	if d <= 0 {
		return true
	}
	t := time.NewTimer(d)
	defer t.Stop()
	select {
	case <-t.C:
		return true
	case <-abort:
		return false
	case <-k.stop:
		return false
	}
}

// sleep is a non-abortable pause (used for the short PTT lead-in).
func (k *serialKeyer) sleep(d time.Duration) {
	if d <= 0 {
		return
	}
	t := time.NewTimer(d)
	defer t.Stop()
	select {
	case <-t.C:
	case <-k.stop:
	}
}

// dits returns the element (character-speed) dit and the spacing (Farnsworth)
// dit as durations, read live so a speed change mid-message takes effect.
func (k *serialKeyer) dits() (elem, space time.Duration) {
	k.mu.Lock()
	w, f := k.wpm, k.farns
	k.mu.Unlock()
	if w < 5 {
		w = 5
	}
	if w > 99 {
		w = 99
	}
	elem = time.Duration(float64(time.Millisecond) * 1200.0 / float64(w))
	space = elem
	if f > 0 && f < w {
		space = time.Duration(float64(time.Millisecond) * 1200.0 / float64(f))
	}
	return elem, space
}
