package stream

import (
	"fmt"

	"github.com/bluenviron/gortsplib/v5/pkg/description"
	"github.com/bluenviron/gortsplib/v5/pkg/format"
	"github.com/bluenviron/gortsplib/v5/pkg/ringbuffer"
	"github.com/bluenviron/mediamtx/internal/counterdumper"
	"github.com/bluenviron/mediamtx/internal/logger"
	"github.com/bluenviron/mediamtx/internal/unit"
)

// OnDataFunc is the callback passed to OnData().
type OnDataFunc func(unit.Unit) error

// Reader is a stream reader.
type Reader struct {
	SkipBytesSent bool
	Parent        logger.Writer

	onDatas         map[*description.Media]map[format.Format]OnDataFunc
	queueSize       int
	buffer          *ringbuffer.RingBuffer
	discardedFrames *counterdumper.CounterDumper

	// out
	err chan error
}

// OnData registers a callback that is called when data from given format is available.
func (r *Reader) OnData(medi *description.Media, forma format.Format, cb OnDataFunc) {
	if r.onDatas == nil {
		r.onDatas = make(map[*description.Media]map[format.Format]OnDataFunc)
	}
	if r.onDatas[medi] == nil {
		r.onDatas[medi] = make(map[format.Format]OnDataFunc)
	}
	r.onDatas[medi][forma] = cb
}

// Formats returns all formats for which the reader has registered a OnData callback.
func (r *Reader) Formats() []format.Format {
	var out []format.Format

	for _, formats := range r.onDatas {
		for forma := range formats {
			out = append(out, forma)
		}
	}

	return out
}

// error returns whenever there's an error.
// It can be called only after stream.AddReader().
func (r *Reader) Error() chan error {
	return r.err
}

func (r *Reader) start() {
	buffer, _ := ringbuffer.New(uint64(r.queueSize))
	r.buffer = buffer
	r.err = make(chan error)

	r.discardedFrames = &counterdumper.CounterDumper{
		OnReport: func(val uint64) {
			r.Parent.Log(logger.Warn, "reader is too slow, discarding %d %s",
				val,
				func() string {
					if val == 1 {
						return "frame"
					}
					return "frames"
				}())
		},
	}
	r.discardedFrames.Start()

	go r.run()
}

func (r *Reader) stop() {
	r.buffer.Close()
	r.discardedFrames.Stop()
	<-r.err
}

func (r *Reader) run() {
	r.err <- r.runInner()
	close(r.err)
}

func (r *Reader) runInner() error {
	for {
		cb, ok := r.buffer.Pull()
		if !ok {
			return fmt.Errorf("terminated")
		}

		err := cb.(func() error)()
		if err != nil {
			return err
		}
	}
}

func (r *Reader) push(cb func() error) {
	ok := r.buffer.Push(cb)
	if !ok {
		r.discardedFrames.Increase()
	}
}
