package mediadevices

import (
	"errors"
	"math/rand"
	"sync"

	"github.com/pion/mediadevices/pkg/codec"
	"github.com/pion/mediadevices/pkg/driver"
	mio "github.com/pion/mediadevices/pkg/io"
	"github.com/pion/webrtc/v2"
	"github.com/pion/webrtc/v2/pkg/media"
)

// Tracker is an interface that represent MediaStreamTrack
// Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack
type Tracker interface {
	Track() *webrtc.Track
	LocalTrack() LocalTrack
	Stop()
	// OnEnded registers a handler to receive an error from the media stream track.
	// If the error is already occured before registering, the handler will be
	// immediately called.
	OnEnded(func(error))
}

type LocalTrack interface {
	WriteSample(s media.Sample) error
	Codec() *webrtc.RTPCodec
	ID() string
	Kind() webrtc.RTPCodecType
}

type track struct {
	localTrack LocalTrack
	d          driver.Driver
	sample     samplerFunc
	encoder    codec.ReadCloser

	onErrorHandler func(error)
	err            error
	mu             sync.Mutex
	endOnce        sync.Once
}

func newTrack(opts *MediaDevicesOptions, d driver.Driver, constraints MediaTrackConstraints) (*track, error) {
	var encoderBuilders []encoderBuilder
	var rtpCodecs []*webrtc.RTPCodec
	var buildSampler func(t LocalTrack) samplerFunc
	var err error

	err = d.Open()
	if err != nil {
		return nil, err
	}

	switch r := d.(type) {
	case driver.VideoRecorder:
		rtpCodecs = opts.codecs[webrtc.RTPCodecTypeVideo]
		buildSampler = newVideoSampler
		encoderBuilders, err = newVideoEncoderBuilders(r, constraints)
	case driver.AudioRecorder:
		rtpCodecs = opts.codecs[webrtc.RTPCodecTypeAudio]
		buildSampler = func(t LocalTrack) samplerFunc {
			return newAudioSampler(t, constraints.selectedMedia.Latency)
		}
		encoderBuilders, err = newAudioEncoderBuilders(r, constraints)
	default:
		err = errors.New("newTrack: invalid driver type")
	}

	if err != nil {
		d.Close()
		return nil, err
	}

	for _, builder := range encoderBuilders {
		var matchedRTPCodec *webrtc.RTPCodec
		for _, rtpCodec := range rtpCodecs {
			if rtpCodec.Name == builder.name {
				matchedRTPCodec = rtpCodec
				break
			}
		}

		if matchedRTPCodec == nil {
			continue
		}

		localTrack, err := opts.trackGenerator(
			matchedRTPCodec.PayloadType,
			rand.Uint32(),
			d.ID(),
			matchedRTPCodec.Type.String(),
			matchedRTPCodec,
		)
		if err != nil {
			continue
		}

		encoder, err := builder.build()
		if err != nil {
			continue
		}

		t := track{
			localTrack: localTrack,
			sample:     buildSampler(localTrack),
			d:          d,
			encoder:    encoder,
		}
		go t.start()
		return &t, nil
	}

	d.Close()
	return nil, errors.New("newTrack: failed to find a matching codec")
}

// OnEnded sets an error handler. When a track has been created and started, if an
// error occurs, handler will get called with the error given to the parameter.
func (t *track) OnEnded(handler func(error)) {
	t.mu.Lock()
	t.onErrorHandler = handler
	err := t.err
	t.mu.Unlock()

	if err != nil && handler != nil {
		// Already errored.
		t.endOnce.Do(func() {
			handler(err)
		})
	}
}

// onError is a callback when an error occurs
func (t *track) onError(err error) {
	t.mu.Lock()
	t.err = err
	handler := t.onErrorHandler
	t.mu.Unlock()

	if handler != nil {
		t.endOnce.Do(func() {
			handler(err)
		})
	}
}

// start starts the data flow from the driver all the way to the localTrack
func (t *track) start() {
	var n int
	var err error
	buff := make([]byte, 1024)
	for {
		n, err = t.encoder.Read(buff)
		if err != nil {
			if e, ok := err.(*mio.InsufficientBufferError); ok {
				buff = make([]byte, 2*e.RequiredSize)
				continue
			}

			t.onError(err)
			return
		}

		if err := t.sample(buff[:n]); err != nil {
			t.onError(err)
			return
		}
	}
}

// Stop stops the underlying driver and encoder
func (t *track) Stop() {
	t.d.Close()
	t.encoder.Close()
}

func (t *track) Track() *webrtc.Track {
	return t.localTrack.(*webrtc.Track)
}

func (t *track) LocalTrack() LocalTrack {
	return t.localTrack
}

// encoderBuilder is a generic encoder builder that acts as a delegator for codec.VideoEncoderBuilder and
// codec.AudioEncoderBuilder. The idea of having a delegator is to reduce redundant codes that are being
// duplicated for managing video and audio.
type encoderBuilder struct {
	name  string
	build func() (codec.ReadCloser, error)
}

// newVideoEncoderBuilders transforms video given by VideoRecorder with the video transformer that is passed through
// constraints and create a list of generic encoder builders
func newVideoEncoderBuilders(vr driver.VideoRecorder, constraints MediaTrackConstraints) ([]encoderBuilder, error) {
	r, err := vr.VideoRecord(constraints.selectedMedia)
	if err != nil {
		return nil, err
	}

	if constraints.VideoTransform != nil {
		r = constraints.VideoTransform(r)
	}

	encoderBuilders := make([]encoderBuilder, len(constraints.VideoEncoderBuilders))
	for i, b := range constraints.VideoEncoderBuilders {
		encoderBuilders[i].name = b.Name()
		encoderBuilders[i].build = func() (codec.ReadCloser, error) {
			return b.BuildVideoEncoder(r, constraints.selectedMedia)
		}
	}
	return encoderBuilders, nil
}

// newAudioEncoderBuilders transforms audio given by AudioRecorder with the audio transformer that is passed through
// constraints and create a list of generic encoder builders
func newAudioEncoderBuilders(ar driver.AudioRecorder, constraints MediaTrackConstraints) ([]encoderBuilder, error) {
	r, err := ar.AudioRecord(constraints.selectedMedia)
	if err != nil {
		return nil, err
	}

	if constraints.AudioTransform != nil {
		r = constraints.AudioTransform(r)
	}

	encoderBuilders := make([]encoderBuilder, len(constraints.AudioEncoderBuilders))
	for i, b := range constraints.AudioEncoderBuilders {
		encoderBuilders[i].name = b.Name()
		encoderBuilders[i].build = func() (codec.ReadCloser, error) {
			return b.BuildAudioEncoder(r, constraints.selectedMedia)
		}
	}
	return encoderBuilders, nil
}
