using System.IO;
using NAudio.Wave;

namespace ytLive.Services.Audio;

/// <summary>
/// Streams background music (TASK 9 TRAX) to the DEFAULT render device at a
/// fixed quiet volume so it rides the WASAPI loopback into the mix — the same
/// physical path game audio takes, so music is ducked with the game, heard in
/// headphones, and bounced on the desktop audio meter. Loops on natural end.
/// The desktop/game bar's gain and mute scale <see cref="LocalGain"/>, so the
/// creator hears the game-audio controls work on the music too.
/// </summary>
public sealed class MusicPlayer : IDisposable
{
    /// <summary>The one and only music bed — quiet, no slider (the creator's
    /// voice must stay on top; game/music loudness is the ducker's job). The
    /// desktop audio bar scales this via <see cref="LocalGain"/>.</summary>
    public const float MusicVolume = 0.20f;

    private MediaFoundationReader? _reader;
    private VolumeWaveProvider16? _volume;
    private WaveOutEvent? _output;
    private bool _disposed;
    private bool _stopping;
    private float _localGain = 1f;

    /// <summary>Raised when playback ends or is stopped (not on natural-end loop).</summary>
    public event Action? PlaybackEnded;

    public string? TrackPath { get; private set; }

    public string? TrackName => TrackPath == null ? null : Path.GetFileNameWithoutExtension(TrackPath);

    public bool IsPlaying => _output?.PlaybackState == PlaybackState.Playing;

    /// <summary>Local gain multiplier from the desktop/game audio bar (0 = mute,
    /// 1 = full bed). Applied live on top of the fixed <see cref="MusicVolume"/>.</summary>
    public float LocalGain
    {
        get => _localGain;
        set
        {
            _localGain = Math.Clamp(value, 0f, 1f);
            if (_volume != null)
                _volume.Volume = MusicVolume * _localGain;
        }
    }

    /// <summary>Loads a track and prepares playback (does not start it).</summary>
    public void Load(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
            throw new ArgumentException("A music file path is required.", nameof(path));

        Stop();
        TrackPath = path;
        _reader = new MediaFoundationReader(path);
        _volume = new VolumeWaveProvider16(_reader) { Volume = MusicVolume * _localGain };
        _output = new WaveOutEvent();
        _output.PlaybackStopped += OnPlaybackStopped;
        _output.Init(_volume);
    }

    public void Play()
    {
        if (_output == null)
            return;
        _output.Play();
    }

    public void Pause()
    {
        if (_output == null)
            return;
        _output.Pause();
    }

    public void Stop()
    {
        if (_output == null)
            return;

        _stopping = true;
        try
        {
            _output.Stop();
        }
        catch
        {
        }
        _output.PlaybackStopped -= OnPlaybackStopped;
        _output.Dispose();
        _output = null;
        _volume = null;
        _reader?.Dispose();
        _reader = null;
        _stopping = false;
        TrackPath = null;
    }

    public void Dispose()
    {
        _disposed = true;
        Stop();
        _disposed = false;
    }

    private void OnPlaybackStopped(object? sender, StoppedEventArgs e)
    {
        if (_disposed || _stopping || _output == null || _reader == null)
            return;

        // Any clean stop is a natural end → loop the track (the Position/Length
        // comparison is unreliable for MediaFoundationReader, so a track can
        // otherwise play once and stop). Only an explicit stop or a device
        // failure surfaces via PlaybackEnded so the UI can reset the dot.
        if (e.Exception == null)
        {
            _reader.Position = 0;
            _output.Play();
            return;
        }

        PlaybackEnded?.Invoke();
    }
}
