package oto

import (
	"encoding/binary"
	"errors"
	"fmt"
	"math"
	"sync"

	"github.com/ebitengine/oto/v3"
	"github.com/vsariola/sointu"
)

const latency = 2048 // in samples at 44100 Hz = ~46 ms

type (
	OtoContext oto.Context

	OtoPlayer struct {
		player *oto.Player
		reader *OtoReader
	}

	OtoReader struct {
		audioSource sointu.AudioSource
		tmpBuffer   sointu.AudioBuffer
		waitGroup   sync.WaitGroup
		err         error
		errMutex    sync.RWMutex
	}
)

func NewContext() (*OtoContext, error) {
	op := oto.NewContextOptions{}
	op.SampleRate = 44100
	op.ChannelCount = 2
	op.Format = oto.FormatFloat32LE
	context, readyChan, err := oto.NewContext(&op)
	if err != nil {
		return nil, fmt.Errorf("cannot create oto context: %w", err)
	}
	<-readyChan
	return (*OtoContext)(context), nil
}

func (c *OtoContext) Play(r sointu.AudioSource) sointu.CloserWaiter {
	reader := &OtoReader{audioSource: r}
	reader.waitGroup.Add(1)
	player := (*oto.Context)(c).NewPlayer(reader)
	player.SetBufferSize(latency * 8)
	player.Play()
	return OtoPlayer{player: player, reader: reader}
}

func (o OtoPlayer) Wait() {
	o.reader.waitGroup.Wait()
}

func (o OtoPlayer) Close() error {
	o.reader.closeWithError(errors.New("OtoPlayer was closed"))
	return o.player.Close()
}

func (o *OtoReader) Read(b []byte) (n int, err error) {
	o.errMutex.RLock()
	if o.err != nil {
		o.errMutex.RUnlock()
		return 0, o.err
	}
	o.errMutex.RUnlock()
	if len(b)%8 != 0 {
		return o.closeWithError(fmt.Errorf("oto: Read buffer length must be a multiple of 8"))
	}
	samples := len(b) / 8
	if samples > len(o.tmpBuffer) {
		o.tmpBuffer = append(o.tmpBuffer, make(sointu.AudioBuffer, samples-len(o.tmpBuffer))...)
	} else if samples < len(o.tmpBuffer) {
		o.tmpBuffer = o.tmpBuffer[:samples]
	}
	err = o.audioSource(o.tmpBuffer)
	if err != nil {
		return o.closeWithError(err)
	}
	for i := range o.tmpBuffer {
		binary.LittleEndian.PutUint32(b[i*8:], math.Float32bits(o.tmpBuffer[i][0]))
		binary.LittleEndian.PutUint32(b[i*8+4:], math.Float32bits(o.tmpBuffer[i][1]))
	}
	return samples * 8, nil
}

func (o *OtoReader) closeWithError(err error) (int, error) {
	o.errMutex.Lock()
	defer o.errMutex.Unlock()
	if o.err == nil {
		o.err = err
		o.waitGroup.Done()
	}
	return 0, err
}
