package cwdecode

import (
	"math"
	"strings"
	"testing"
)

// keyLoose keys a message with SLIGHTLY WIDE letter spacing — the way a great
// many operators actually send. letterGapDits replaces the standard 3.
//
// Same envelope shaping as keyMessage: hard edges would make an easier signal
// than anything on the air, and would prove nothing.
func keyLoose(msg string, fs, wpm int, pitch, amp float64, letterGapDits float64) []int16 {
	dot := fs * 1200 / (wpm * 1000)
	edge := fs * 5 / 1000
	c2m := charToMorse()
	var out []float64
	phase := 0.0
	dphi := 2 * math.Pi * pitch / float64(fs)

	tone := func(n int) {
		for i := 0; i < n; i++ {
			g := 1.0
			if i < edge {
				g = 0.5 - 0.5*math.Cos(math.Pi*float64(i)/float64(edge))
			} else if n-1-i < edge {
				g = 0.5 - 0.5*math.Cos(math.Pi*float64(n-1-i)/float64(edge))
			}
			out = append(out, amp*g*math.Sin(phase))
			phase += dphi
		}
	}
	silence := func(n int) {
		for i := 0; i < n; i++ {
			out = append(out, 0)
			phase += dphi
		}
	}

	silence(fs / 4)
	for i := 0; i < len(msg); i++ {
		ch := msg[i]
		if ch == ' ' {
			silence(4 * dot)
			continue
		}
		code := c2m[ch]
		for j := 0; j < len(code); j++ {
			if code[j] == '.' {
				tone(dot)
			} else {
				tone(3 * dot)
			}
			silence(dot)
		}
		// The element gap above already contributes one dit.
		silence(int((letterGapDits - 1) * float64(dot)))
	}
	silence(fs / 2)
	return toInt16(out)
}

// The first character of an over must decode, not arrive as "?".
//
// Reported on the air: "the first letter or digit often turns into ?". Each
// element is classified against the running dit estimate, and at the start of
// an over that estimate belongs to whatever was decoded last — a different
// operator, at a different speed. The first character pays for it.
func TestFirstCharacterOfAnOver(t *testing.T) {
	const fs = 16000
	for _, wpm := range []int{15, 22, 30} {
		got := decode(t, keyMessage("F4BPO DE OY1CT K", fs, wpm, 700, 9000), 0)
		if strings.HasPrefix(got, "?") {
			t.Errorf("@%d wpm: decoded %q — the first character was lost", wpm, got)
		}
	}
}

// A callsign must not be broken up when the sender's letter spacing is a little
// wide.
//
// Reported on the air: "sometimes there are spaces inside the call". A word
// boundary is declared above 4.6 dits of silence, so an operator whose letter
// gaps run to 4.5 dits has every letter turned into a word of its own.
func TestWideLetterSpacingDoesNotSplitTheCall(t *testing.T) {
	const fs = 16000
	for _, gap := range []float64{3.5, 4.0, 4.5} {
		got := decode(t, keyLoose("DE OY1CT K", fs, 20, 700, 9000, gap), 0)
		for _, split := range []string{"O Y", "Y 1", "1 C", "C T"} {
			if strings.Contains(got, split) {
				t.Errorf("letter gap %.1f dits: decoded %q — the callsign was broken at %q", gap, got, split)
			}
		}
	}
}

// decodeSeq runs several transmissions through the SAME decoder, as happens on
// the air: the estimate carried into an over is whatever the previous station
// left behind.
func decodeSeq(t *testing.T, parts ...[]int16) []string {
	t.Helper()
	var cur strings.Builder
	d := New(16000, func(s string) { cur.WriteString(s) }, nil)
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		cur.Reset()
		for i := 0; i < len(p); i += 256 {
			end := i + 256
			if end > len(p) {
				end = len(p)
			}
			d.Process(p[i:end])
		}
		out = append(out, strings.ToUpper(cur.String()))
	}
	return out
}

// A fast station, then a slow one — the real reason the first character is
// lost. A fresh decoder starts from a neutral estimate and adapts within a
// character or two; a decoder that has just followed someone at 30 wpm judges
// the newcomer's first dits against 30 wpm.
func TestFirstCharacterAfterASpeedChange(t *testing.T) {
	const fs = 16000
	got := decodeSeq(t,
		keyMessage("CQ CQ DE DL1ABC K", fs, 32, 700, 9000),
		keyMessage("DL1ABC DE OY1CT K", fs, 14, 700, 9000),
	)
	if strings.HasPrefix(got[1], "?") {
		t.Errorf("after 32→14 wpm: %q — the first character of the reply was lost", got[1])
	}
	if !strings.Contains(got[1], "OY1CT") {
		t.Errorf("after 32→14 wpm: %q — want the callsign intact", got[1])
	}
}

// Hand-sent letter gaps run wide. Above 4.6 dits every letter becomes a word,
// so a callsign arrives in pieces.
func TestVeryWideLetterSpacing(t *testing.T) {
	const fs = 16000
	for _, gap := range []float64{5.0, 5.5, 6.0} {
		got := decode(t, keyLoose("DE OY1CT K", fs, 18, 700, 9000, gap), 0)
		if strings.Contains(got, "O Y") || strings.Contains(got, "Y 1") || strings.Contains(got, "1 C") {
			t.Errorf("letter gap %.1f dits: decoded %q — the callsign was broken up", gap, got)
		}
	}
}
