package player

import (
	"sync"
	"time"

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

// BufferedStreamer wraps a beep.Streamer with a goroutine-driven
// read-ahead ring buffer. It decouples the source streamer's I/O
// timing from the speaker callback's real-time deadline, preventing
// audible glitches caused by disk stalls, GC pauses, or CPU
// scheduling delays.
//
// The read-ahead goroutine continuously fills the ring buffer from
// the source. The speaker callback drains the ring buffer without
// ever touching the source directly. If the ring buffer is
// temporarily empty (read-ahead hasn't caught up), Stream returns
// silence rather than blocking or signaling end-of-stream.
type BufferedStreamer struct {
	// srcMu serializes all access to the underlying source. The
	// read-ahead goroutine holds it while calling source.Stream;
	// callers that need to Seek the source must hold it too (via
	// LockSource/UnlockSource) so the non-thread-safe decoder is
	// never read and seeked concurrently.
	srcMu   sync.Mutex
	mu      sync.Mutex
	source  beep.Streamer
	ring    [][2]float64
	readPos int
	writPos int
	count   int
	done    bool
	err     error
	closed  chan struct{}
}

// NewBufferedStreamer creates a BufferedStreamer that pre-fills
// bufferSize samples from source via a background goroutine.
// A typical bufferSize is 2× the sample rate (~2 seconds of audio).
func NewBufferedStreamer(
	source beep.Streamer,
	bufferSize int,
) *BufferedStreamer {
	bs := &BufferedStreamer{
		source: source,
		ring:   make([][2]float64, bufferSize),
		closed: make(chan struct{}),
	}

	go bs.readAhead()

	return bs
}

// readAhead continuously reads from the source into the ring buffer
// until the source is drained, an error occurs, or Close is called.
func (bs *BufferedStreamer) readAhead() {
	// Temporary buffer for reading from source outside the lock.
	// 512 samples per chunk keeps the critical section short.
	const chunkSize = 512

	tmp := make([][2]float64, chunkSize)

	for {
		// Check if closed.
		select {
		case <-bs.closed:
			return
		default:
		}

		bs.mu.Lock()
		space := len(bs.ring) - bs.count

		if space == 0 {
			// Buffer full — release lock and wait briefly.
			bs.mu.Unlock()

			select {
			case <-bs.closed:
				return
			case <-time.After(1 * time.Millisecond):
			}

			continue
		}

		// Determine how many samples to request.
		toRead := space
		if toRead > chunkSize {
			toRead = chunkSize
		}

		bs.mu.Unlock()

		// Read from source WITHOUT holding bs.mu so disk I/O does
		// not block the speaker goroutine. srcMu is held to keep
		// this read from racing a concurrent source Seek.
		bs.srcMu.Lock()
		n, ok := bs.source.Stream(tmp[:toRead])
		bs.srcMu.Unlock()

		if n > 0 {
			bs.mu.Lock()

			for i := range n {
				bs.ring[bs.writPos] = tmp[i]
				bs.writPos = (bs.writPos + 1) % len(bs.ring)
			}

			bs.count += n
			bs.mu.Unlock()
		}

		if !ok {
			bs.mu.Lock()
			bs.done = true

			if srcErr := bs.source.Err(); srcErr != nil {
				bs.err = srcErr
			}

			bs.mu.Unlock()

			return
		}

		// If source returned 0 samples but is still ok, yield
		// briefly to avoid busy-spinning.
		if n == 0 {
			select {
			case <-bs.closed:
				return
			case <-time.After(1 * time.Millisecond):
			}
		}
	}
}

// Stream copies samples from the ring buffer into the provided
// slice. If the buffer is temporarily empty but the source is not
// yet drained, it fills the output with silence and returns
// (len(samples), true) to avoid speaker underrun.
func (bs *BufferedStreamer) Stream(
	samples [][2]float64,
) (int, bool) {
	bs.mu.Lock()
	defer bs.mu.Unlock()

	if bs.count == 0 && bs.done {
		return 0, false
	}

	if bs.count == 0 {
		// Buffer temporarily empty — fill with silence.
		for i := range samples {
			samples[i] = [2]float64{}
		}

		return len(samples), true
	}

	// Copy available samples from ring buffer.
	n := len(samples)
	if n > bs.count {
		n = bs.count
	}

	for i := range n {
		samples[i] = bs.ring[bs.readPos]
		bs.readPos = (bs.readPos + 1) % len(bs.ring)
	}

	bs.count -= n

	return n, true
}

// Err returns any error encountered by the source streamer.
func (bs *BufferedStreamer) Err() error {
	bs.mu.Lock()
	defer bs.mu.Unlock()

	return bs.err
}

// Flush discards all buffered samples so the next Stream call
// returns freshly-read data from the source. This must be called
// after seeking the underlying source to prevent stale pre-seek
// audio from being played back.
func (bs *BufferedStreamer) Flush() {
	bs.mu.Lock()
	defer bs.mu.Unlock()

	bs.readPos = 0
	bs.writPos = 0
	bs.count = 0
}

// LockSource blocks the read-ahead goroutine from touching the
// underlying source, giving the caller exclusive access so it can
// safely Seek the non-thread-safe decoder. Every LockSource must
// be paired with an UnlockSource.
func (bs *BufferedStreamer) LockSource() {
	bs.srcMu.Lock()
}

// UnlockSource releases the exclusive source access acquired by
// LockSource, allowing the read-ahead goroutine to resume.
func (bs *BufferedStreamer) UnlockSource() {
	bs.srcMu.Unlock()
}

// Close signals the read-ahead goroutine to stop. It is safe to
// call multiple times.
func (bs *BufferedStreamer) Close() {
	select {
	case <-bs.closed:
		// Already closed.
	default:
		close(bs.closed)
	}
}
