// Package buffer provides bounded buffer implementations for display output.
//
// RingBuffer is designed for display-only buffers where data is persisted elsewhere
// (e.g., database) and the consumer may be slower than the producer. It uses
// drop-oldest semantics to maintain bounded memory usage at scale.
package buffer

import (
	"sync"
	"sync/atomic"
)

// RingBuffer is a generic, thread-safe circular buffer with drop-oldest semantics.
// It maintains a fixed capacity and tracks how many items have been dropped.
//
// The buffer uses a mutex for Push/Snapshot/Len/Clear operations and atomic
// operations for the dropped counter (allowing DroppedCount to be called without
// acquiring the lock).
type RingBuffer[T any] struct {
	mu       sync.Mutex
	items    []T   // Fixed-size slice (length = capacity)
	head     int   // Next write position (wraps around)
	tail     int   // Next read position (wraps around)
	count    int   // Current number of items (0 to capacity)
	capacity int   // Maximum items buffer can hold
	dropped  int64 // Atomic counter for dropped items
}

// NewRingBuffer creates a new ring buffer with the specified capacity.
// Panics if capacity is less than 1.
func NewRingBuffer[T any](capacity int) *RingBuffer[T] {
	if capacity < 1 {
		panic("buffer: capacity must be at least 1")
	}
	return &RingBuffer[T]{
		items:    make([]T, capacity),
		capacity: capacity,
	}
}

// Push adds an item to the buffer. If the buffer is full, the oldest item
// is dropped and the dropped counter is incremented.
//
// Thread-safe: can be called concurrently from multiple goroutines.
func (r *RingBuffer[T]) Push(item T) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if r.count == r.capacity {
		// Buffer full: drop oldest (advance tail) and increment dropped counter
		r.tail = (r.tail + 1) % r.capacity
		atomic.AddInt64(&r.dropped, 1)
	} else {
		r.count++
	}

	// Write item at head position and advance head
	r.items[r.head] = item
	r.head = (r.head + 1) % r.capacity
}

// Snapshot returns a copy of all items in the buffer in FIFO order.
// The returned slice is a copy - modifications to it do not affect the buffer.
//
// Thread-safe: can be called concurrently with Push.
func (r *RingBuffer[T]) Snapshot() []T {
	r.mu.Lock()
	defer r.mu.Unlock()

	result := make([]T, r.count)
	for i := 0; i < r.count; i++ {
		// Read from tail position, wrapping around if needed
		idx := (r.tail + i) % r.capacity
		result[i] = r.items[idx]
	}
	return result
}

// Len returns the current number of items in the buffer.
//
// Thread-safe: can be called concurrently with Push.
func (r *RingBuffer[T]) Len() int {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.count
}

// DroppedCount returns the total number of items that have been dropped
// due to overflow since buffer creation or last Reset (if implemented).
//
// Thread-safe: uses atomic read, no lock required.
func (r *RingBuffer[T]) DroppedCount() int64 {
	return atomic.LoadInt64(&r.dropped)
}

// Clear removes all items from the buffer but preserves the dropped count.
// Use this to reset the display buffer without losing the overflow statistics.
//
// Thread-safe: can be called concurrently with Push.
func (r *RingBuffer[T]) Clear() {
	r.mu.Lock()
	defer r.mu.Unlock()

	// Reset head, tail, and count
	r.head = 0
	r.tail = 0
	r.count = 0

	// Zero out items to allow GC of any referenced objects
	var zero T
	for i := range r.items {
		r.items[i] = zero
	}
}
