package player

import (
	"math"
	"sync/atomic"

	"github.com/gopxl/beep/v2"
)

// Time-stretching constants tuned for natural speech and music, adapted from
// SoundTouch's proven defaults. The long sequence length means crossfade
// events are infrequent (~12/sec), and most output is a direct source copy.
const (
	tsSeq    = 3584           // sequence: ~81ms @44.1kHz — time between crossfades
	tsOvlp   = 512            // overlap: ~12ms — crossfade region
	tsWin    = tsSeq + tsOvlp // source window per frame (4096)
	tsSearch = 1024           // search: ±~23ms — covers multiple pitch periods
)

// Pre-computed linear crossfade table: alpha[i] = i / tsOvlp.
// Avoids per-sample division in the hot crossfade loop.
var tsAlpha [tsOvlp]float64

func init() {
	for i := range tsAlpha {
		tsAlpha[i] = float64(i) / float64(tsOvlp)
	}
}

// speedStreamer wraps a beep.Streamer and adjusts playback speed without
// changing pitch, using WSOLA (Waveform Similarity Overlap-Add) time-stretching.
// The speed ratio is stored atomically so the UI thread can change it
// while the audio thread reads it.
type speedStreamer struct {
	s     beep.Streamer
	speed *atomic.Uint64 // ratio as Float64bits; 1.0 = normal

	in    [][2]float64 // source buffer
	inN   int          // valid sample count
	inPos float64      // fractional analysis cursor

	out   [][2]float64 // output ring buffer
	outRd int
	outWr int

	tail [tsOvlp][2]float64 // previous frame's trailing samples for crossfade
	// No tail data exists until the first frame writes to it;
	// outWr == 0 && outRd == 0 signals this initial state.
}

func newSpeedStreamer(s beep.Streamer, speed *atomic.Uint64) *speedStreamer {
	return &speedStreamer{
		s:     s,
		speed: speed,
		in:    make([][2]float64, 16384),
		out:   make([][2]float64, 8192),
	}
}

// Stream produces output samples. At speed 1.0x it passes through directly.
// At other speeds it applies WSOLA time-stretching to preserve pitch.
func (ss *speedStreamer) Stream(samples [][2]float64) (int, bool) {
	speed := math.Float64frombits(ss.speed.Load())
	if speed <= 0 || speed == 1.0 {
		return ss.passthrough(samples)
	}

	for ss.outWr-ss.outRd < len(samples) {
		if !ss.wsolaFrame(speed) {
			break
		}
	}
	n := ss.drainOut(samples)
	return n, n > 0
}

func (ss *speedStreamer) passthrough(samples [][2]float64) (int, bool) {
	d := ss.drainOut(samples)
	if d == len(samples) {
		return d, true
	}
	// Drain unconsumed source samples before switching to direct reads.
	srcStart := int(math.Round(ss.inPos))
	if srcAvail := ss.inN - srcStart; srcAvail > 0 {
		n := min(len(samples)-d, srcAvail)
		copy(samples[d:d+n], ss.in[srcStart:srcStart+n])
		d += n
		ss.inPos += float64(n)
		if d == len(samples) {
			return d, true
		}
	}
	// Reset WSOLA state for clean re-entry.
	ss.outRd = 0
	ss.outWr = 0
	ss.inN = 0
	ss.inPos = 0
	n, ok := ss.s.Stream(samples[d:])
	total := d + n
	return total, ok || total > 0
}

func (ss *speedStreamer) drainOut(dst [][2]float64) int {
	avail := ss.outWr - ss.outRd
	n := min(len(dst), avail)
	if n <= 0 {
		return 0
	}
	copy(dst[:n], ss.out[ss.outRd:ss.outRd+n])
	ss.outRd += n
	if ss.outRd > 8192 {
		rem := ss.outWr - ss.outRd
		if rem > 0 {
			copy(ss.out, ss.out[ss.outRd:ss.outWr])
		}
		ss.outRd = 0
		ss.outWr = rem
	}
	return n
}

func (ss *speedStreamer) fillSource(need int) bool {
	if drop := int(ss.inPos) - tsSearch; drop > 0 {
		keep := ss.inN - drop
		if keep > 0 {
			copy(ss.in[:keep], ss.in[drop:ss.inN])
		} else {
			keep = 0
		}
		ss.inN = keep
		ss.inPos -= float64(drop)
	}
	for ss.inN < need {
		toRead := max(need-ss.inN, 4096)
		if ss.inN+toRead > cap(ss.in) {
			newIn := make([][2]float64, ss.inN+toRead)
			copy(newIn[:ss.inN], ss.in[:ss.inN])
			ss.in = newIn
		}
		n, _ := ss.s.Stream(ss.in[ss.inN : ss.inN+toRead])
		ss.inN += n
		if n == 0 {
			return ss.inN >= need
		}
	}
	return true
}

// wsolaFrame produces one synthesis frame of tsSeq output samples.
//
// Frame layout in source:
//
//	[crossfade tsOvlp][direct copy tsSeq-tsOvlp][tail tsOvlp]
//	|<----------- tsSeq (output) ------------>||<-- saved -->|
//	|<------------------ tsWin (source read) ---------------->|
func (ss *speedStreamer) wsolaFrame(speed float64) bool {
	expected := int(math.Round(ss.inPos))
	needed := expected + tsWin + tsSearch + 1
	if !ss.fillSource(needed) && expected+tsSeq > ss.inN {
		return false
	}

	first := ss.outWr == 0 && ss.outRd == 0

	srcOff := expected
	if !first {
		srcOff = ss.searchBestOffset(expected)
	}

	if srcOff+tsWin > ss.inN {
		srcOff = max(0, ss.inN-tsWin)
	}
	if srcOff+tsSeq > ss.inN {
		return false
	}

	// Grow output buffer if needed.
	if ss.outWr+tsSeq > cap(ss.out) {
		newOut := make([][2]float64, ss.outWr+tsSeq+4096)
		copy(newOut[:ss.outWr], ss.out[:ss.outWr])
		ss.out = newOut
	}

	if first {
		copy(ss.out[ss.outWr:ss.outWr+tsSeq], ss.in[srcOff:srcOff+tsSeq])
	} else {
		// Crossfade the overlap region using pre-computed alpha table.
		for i := range tsOvlp {
			a := tsAlpha[i]
			b := 1 - a
			ss.out[ss.outWr+i] = [2]float64{
				b*ss.tail[i][0] + a*ss.in[srcOff+i][0],
				b*ss.tail[i][1] + a*ss.in[srcOff+i][1],
			}
		}
		// Direct copy the rest — unmodified source samples.
		copy(ss.out[ss.outWr+tsOvlp:ss.outWr+tsSeq],
			ss.in[srcOff+tsOvlp:srcOff+tsSeq])
	}
	ss.outWr += tsSeq

	// Save tail for next frame's crossfade.
	copy(ss.tail[:], ss.in[srcOff+tsSeq:srcOff+tsWin])

	ss.inPos += float64(tsSeq) * speed
	return true
}

// searchBestOffset finds the source position near expected whose start best
// matches the previous tail, using normalized cross-correlation.
// Normalizing prevents bias toward loud sections. If no good match is found
// (all correlations negative or silent), falls back to the expected offset.
func (ss *speedStreamer) searchBestOffset(expected int) int {
	lo := max(0, expected-tsSearch)
	hi := max(min(ss.inN-tsWin, expected+tsSearch), lo)

	bestOff := min(max(expected, lo), hi)
	var bestScore float64

	for off := lo; off <= hi; off++ {
		var corr, norm float64
		for i := range tsOvlp {
			corr += ss.tail[i][0]*ss.in[off+i][0] + ss.tail[i][1]*ss.in[off+i][1]
			norm += ss.in[off+i][0]*ss.in[off+i][0] + ss.in[off+i][1]*ss.in[off+i][1]
		}
		if norm < 1e-9 || corr <= 0 {
			continue
		}
		// corr^2/norm avoids sqrt; equivalent ranking to corr/sqrt(norm).
		score := corr * corr / norm
		if score > bestScore {
			bestScore = score
			bestOff = off
		}
	}
	return bestOff
}

// Err forwards to the wrapped streamer's error method.
func (ss *speedStreamer) Err() error {
	return ss.s.Err()
}
