package d2audio

import (
	"fmt"
	"math/rand"
	"strconv"

	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"

	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)

type envState int

const (
	logPrefix = "Sound Engine"
)

const (
	envAttack  = 0
	envSustain = 1
	envRelease = 2
	envStopped = 3
)

const volMax float64 = 255
const originalFPS float64 = 25

// A Sound that can be started and stopped
type Sound struct {
	effect  d2interface.SoundEffect
	entry   *d2records.SoundDetailRecord
	volume  float64
	vTarget float64
	vRate   float64
	state   envState
	// panning float64 // lets forget about this for now

	*d2util.Logger
}

func (s *Sound) update(elapsed float64) {
	// attack
	if s.state == envAttack {
		s.volume += s.vRate * elapsed
		if s.volume > s.vTarget {
			s.volume = s.vTarget
			s.state = envSustain
		}

		s.effect.SetVolume(s.volume)
	}

	// release
	if s.state == envRelease {
		s.volume -= s.vRate * elapsed
		if s.volume < 0 {
			s.effect.Stop()
			s.volume = 0
			s.state = envStopped
		}

		s.effect.SetVolume(s.volume)
	}
}

// SetPan sets the stereo pan, range -1 to 1
func (s *Sound) SetPan(pan float64) {
	s.effect.SetPan(pan)
}

// Play the sound
func (s *Sound) Play() {
	s.Info("starting sound " + s.entry.Handle)
	s.effect.Play()

	if s.entry.FadeIn != 0 {
		s.effect.SetVolume(0)
		s.volume = 0
		s.state = envAttack
		s.vTarget = float64(s.entry.Volume) / volMax
		s.vRate = s.vTarget / (float64(s.entry.FadeIn) / originalFPS)
	} else {
		s.volume = float64(s.entry.Volume) / volMax
		s.effect.SetVolume(s.volume)
		s.state = envSustain
	}
}

// Stop the sound, only required for looping sounds
func (s *Sound) Stop() {
	if s.entry.FadeOut != 0 {
		s.state = envRelease
		s.vTarget = 0
		s.vRate = s.volume / (float64(s.entry.FadeOut) / originalFPS)
	} else {
		s.state = envStopped
		s.volume = 0
		s.effect.SetVolume(s.volume)
		s.effect.Stop()
	}
}

// String returns the sound filename
func (s *Sound) String() string {
	return s.entry.Handle
}

// SoundEngine provides functions for playing sounds
type SoundEngine struct {
	asset    *d2asset.AssetManager
	provider d2interface.AudioProvider
	timer    float64
	accTime  float64
	sounds   map[*Sound]struct{}

	*d2util.Logger
}

// NewSoundEngine creates a new sound engine
func NewSoundEngine(provider d2interface.AudioProvider,
	asset *d2asset.AssetManager, l d2util.LogLevel, term d2interface.Terminal) *SoundEngine {
	r := SoundEngine{
		asset:    asset,
		provider: provider,
		sounds:   map[*Sound]struct{}{},
		timer:    1,
	}

	r.Logger = d2util.NewLogger()
	r.Logger.SetPrefix(logPrefix)
	r.Logger.SetLevel(l)

	if err := term.Bind("playsoundid", "plays the sound for a given id", []string{"id"}, r.commandPlaySoundID); err != nil {
		r.Error(err.Error())
		return nil
	}

	if err := term.Bind("playsound", "plays the sound for a given handle string", []string{"name"}, r.commandPlaySound); err != nil {
		r.Error(err.Error())
		return nil
	}

	if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil {
		r.Error(err.Error())
		return nil
	}

	if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
		r.Error(err.Error())
		return nil
	}

	return &r
}

// Advance updates sound engine state, triggering events and envelopes
func (s *SoundEngine) Advance(elapsed float64) {
	s.timer -= elapsed
	s.accTime += elapsed

	if s.timer < 0 {
		for sound := range s.sounds {
			sound.update(s.accTime)

			// Clean up finished non-looping effects
			if !sound.effect.IsPlaying() {
				delete(s.sounds, sound)
			}

			// Clean up stopped looping effects
			if sound.state == envStopped {
				delete(s.sounds, sound)
			}
		}

		s.timer = 0.2
		s.accTime = 0
	}
}

// UnbindTerminalCommands unbinds commands from the terminal
func (s *SoundEngine) UnbindTerminalCommands(term d2interface.Terminal) error {
	return term.Unbind("playsoundid", "playsound", "activesounds", "killsounds")
}

// Reset stop all sounds and reset state
func (s *SoundEngine) Reset() {
	for snd := range s.sounds {
		snd.effect.Stop()
		delete(s.sounds, snd)
	}
}

// PlaySoundID plays a sound by sounds.txt index, returning the sound here is kinda ugly
// now we could have a situation where someone holds onto the sound after the sound engine is done with it
// someone needs to be in charge of deciding when to stopping looping sounds though...
func (s *SoundEngine) PlaySoundID(id int) *Sound {
	if id == 0 {
		return nil
	}

	entry := s.asset.Records.SelectSoundByIndex(id)

	if entry.GroupSize > 0 {
		// nolint:gosec // this is client-only, no big deal if rand index isn't securely generated
		indexOffset := rand.Intn(entry.GroupSize)
		entry = s.asset.Records.SelectSoundByIndex(entry.Index + indexOffset)
	}

	effect, err := s.provider.LoadSound(entry.FileName, entry.Loop, entry.MusicVol)
	if err != nil {
		s.Error(err.Error())
		return nil
	}

	snd := Sound{
		entry:  entry,
		effect: effect,
		Logger: s.Logger,
	}

	s.sounds[&snd] = struct{}{}

	snd.Play()

	return &snd
}

// PlaySoundHandle plays a sound by sounds.txt handle
func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
	sound := s.asset.Records.Sound.Details[handle].Index
	return s.PlaySoundID(sound)
}

func (s *SoundEngine) commandPlaySoundID(args []string) error {
	id, err := strconv.Atoi(args[0])
	if err != nil {
		return fmt.Errorf("invalid argument")
	}

	s.PlaySoundID(id)

	return nil
}

func (s *SoundEngine) commandPlaySound(args []string) error {
	s.PlaySoundHandle(args[0])

	return nil
}

func (s *SoundEngine) commandActiveSounds([]string) error {
	for sound := range s.sounds {
		s.Info(sound.String())
	}

	return nil
}
func (s *SoundEngine) commandKillSounds([]string) error {
	for sound := range s.sounds {
		sound.Stop()
	}

	return nil
}
