package main

import (
	"fmt"
	"math"
	"math/rand"
	"sort"
	"testing"

	"github.com/pion/rtp"
	"gopkg.in/hraban/opus.v2"
)

// Simulate: sender emits a packet every 20ms*(1+ppm) of its clock; network adds jitter;
// playout pulls one frame every 20ms of MOTU clock. Count audible damage over 3 hours.
func sim(t *testing.T, name string, noiseFloor float64, ppm float64, jitterMs func(r *rand.Rand) float64) {
	enc, _ := opus.NewEncoder(rate, channels, opus.AppAudio)
	nr := rand.New(rand.NewSource(7))
	mk := func(i int, talk bool) []byte {
		pcm := make([]int16, frameSamps*channels)
		for k := range pcm {
			n := nr.NormFloat64() * noiseFloor * 32768
			v := n
			if talk { v += 8000 * math.Sin(float64(i*frameSamps+k/2)*2*math.Pi*220/rate) * (0.5 + 0.5*math.Sin(float64(i)*0.9)) }
			pcm[k] = int16(v)
		}
		buf := make([]byte, 4000); n, _ := enc.Encode(pcm, buf); return append([]byte(nil), buf[:n]...)
	}
	var talkP, quietP [][]byte
	for i := 0; i < 50; i++ { talkP = append(talkP, mk(i, true)); quietP = append(quietP, mk(i, false)) }
	payloadFor := func(seq int) []byte { if seq%175 < 125 { return talkP[seq%50] }; return quietP[seq%50] }
	dec, _ := opus.NewDecoder(rate, channels)
	jb := newJitterBuffer(dec, 111)
	r := rand.New(rand.NewSource(1))
	const dur = 3 * 3600 * 1000.0
	type ev struct{ at float64; seq uint16 }
	var arr []ev
	period := 20.0 * (1 + ppm*1e-6)
	for i := 0; float64(i)*period < dur; i++ {
		arr = append(arr, ev{float64(i)*period + 50 + jitterMs(r), uint16(i)})
	}
	sort.Slice(arr, func(a, b int) bool { return arr[a].at < arr[b].at })
	k := 0
	depthSum, frames, nearEmpty := 0, 0, 0
	for tms := 0.0; tms < dur; tms += 20 {
		for k < len(arr) && arr[k].at <= tms { jb.push(&rtp.Packet{Header: rtp.Header{SequenceNumber: arr[k].seq}, Payload: payloadFor(int(arr[k].seq) + 65536*0)}); k++ }
		jb.next()
		d := len(jb.pkts); depthSum += d; frames++
		if d == 0 { nearEmpty++ }
	}
	s := jb.stats()
	bad := s["plc"].(int) + s["fec"].(int) + s["dropped"].(int)
	fmt.Printf("%-34s glitches(PLC+FEC)=%5d (~%.1f/min)  underruns=%4d late-discarded=%5d drops=%4d inserted=%4d  avg depth=%.0fms  empty %.0f%% of time  final target=%v\n",
		name, bad, float64(bad)/180, s["underruns"], s["late"], s["dropped"], s["inserted"], float64(depthSum)/float64(frames)*20, 100*float64(nearEmpty)/float64(frames), s["targetMs"])
}

func TestSim(t *testing.T) {
	for _, nf := range []float64{0.0005, 0.006} {
	fmt.Printf("--- pauses at %.0f dBFS noise floor\n", 20*math.Log10(nf))
	gauss := func(sd float64) func(*rand.Rand) float64 { return func(r *rand.Rand) float64 { return math.Abs(r.NormFloat64() * sd) } }
	spiky := func(r *rand.Rand) float64 { // typical home Wi-Fi: small jitter + occasional 80-200ms stalls
		j := math.Abs(r.NormFloat64() * 4)
		if r.Float64() < 0.002 { j += 80 + r.Float64()*120 }
		return j
	}
	sim(t, "clean net, no drift", nf, 0, gauss(0.5))
	sim(t, "5ms jitter, no drift", nf, 0, gauss(5))
	sim(t, "5ms jitter, guest clock 30ppm FAST", nf, -30, gauss(5))
	sim(t, "5ms jitter, guest clock 30ppm SLOW", nf, 30, gauss(5))
	sim(t, "Wi-Fi-ish spikes, no drift", nf, 0, spiky)
	sim(t, "Wi-Fi-ish spikes, guest 30ppm SLOW", nf, 30, spiky)
	// correlated stall: every packet in a 150 ms window is held until the window ends
	burst := func() func(*rand.Rand) float64 {
		i, until := 0, -1.0
		return func(r *rand.Rand) float64 {
			now := float64(i) * 20; i++
			if now > until && r.Float64() < 0.0005 { until = now + 150 }
			j := math.Abs(r.NormFloat64() * 3)
			if now <= until { j += until - now }
			return j
		}
	}
	sim(t, "burst stalls 150ms, no drift", nf, 0, burst())
	sim(t, "burst stalls 150ms, guest SLOW", nf, 30, burst())
}
}
