using System;
using System.IO;
using Microsoft.Extensions.Logging;
using NAudio.Wave;

namespace HellionChat.Integrations;

// Plays the three bundled WAV notification sounds via NAudio WaveOutEvent.
// WaveOutEvent/WinMM is the correct backend for FFXIV on Wine: it works
// without Media Foundation (which Wine does not support for MP3/AAC).
//
// Playback volume comes from Configuration.CustomSoundVolume via the Play
// parameter, clamped to [0,1]. The 16 game sounds are unaffected — they go
// through UIGlobals.PlaySoundEffect, which the plugin cannot scale.
internal sealed class CustomAudioPlayer : IDisposable
{
    // Sound bytes are read once at construction so each Play() wraps a fresh
    // MemoryStream rather than re-reading the manifest stream (which becomes
    // unreadable after the first read and would require Seek support).
    private readonly byte[][] _soundData;
    private readonly ILogger<CustomAudioPlayer> _logger;

    private WaveOutEvent? _outputDevice;
    private WaveFileReader? _reader;
    private readonly object _lock = new();

    public CustomAudioPlayer(ILogger<CustomAudioPlayer> logger)
    {
        _logger = logger;
        _soundData = new byte[3][];

        for (var i = 0; i < 3; i++)
        {
            var resourceName = $"HellionChat.Sounds.notification-{i + 1}.wav";
            using var stream = typeof(CustomAudioPlayer).Assembly.GetManifestResourceStream(
                resourceName
            );
            if (stream is null)
            {
                _logger.LogWarning(
                    "Embedded sound resource not found: {Resource}. "
                        + "Custom sound {Index} will be silent.",
                    resourceName,
                    i + 1
                );
                _soundData[i] = Array.Empty<byte>();
                continue;
            }

            using var ms = new MemoryStream();
            stream.CopyTo(ms);
            _soundData[i] = ms.ToArray();
        }
    }

    // customIndex is 1, 2, or 3, matching the sound file suffix.
    // Stops any currently playing sound before starting the new one.
    // NAudio playback runs on its own thread; this method returns immediately.
    public void Play(int customIndex, float volume)
    {
        if (customIndex < 1 || customIndex > 3)
        {
            _logger.LogWarning(
                "CustomAudioPlayer.Play called with out-of-range index {Index}",
                customIndex
            );
            return;
        }

        var data = _soundData[customIndex - 1];
        if (data.Length == 0)
        {
            _logger.LogWarning(
                "Sound data for index {Index} is empty; skipping playback",
                customIndex
            );
            return;
        }

        lock (_lock)
        {
            try
            {
                StopCurrent();

                var ms = new MemoryStream(data, writable: false);
                _reader = new WaveFileReader(ms);

                _outputDevice = new WaveOutEvent();
                // Init opens the device and creates the WinMM handle. Volume
                // must be set after Init, otherwise waveOutSetVolume fails with
                // InvalidHandle.
                _outputDevice.Init(_reader);
                // AUDIO-1: volume comes from Configuration.CustomSoundVolume.
                // Clamp here too — a hand-edited config could carry an
                // out-of-range value, and WaveOutEvent.Volume rejects those.
                _outputDevice.Volume = Math.Clamp(volume, 0f, 1f);
                _outputDevice.Play();
            }
            catch (Exception ex)
            {
                _logger.LogWarning(
                    ex,
                    "Failed to play custom notification sound {Index}",
                    customIndex
                );
                StopCurrent();
            }
        }
    }

    // Stops and tears down the active WaveOutEvent + WaveFileReader without
    // throwing. Called on Play (to interrupt previous sound) and from Dispose.
    // Guards Stop() with a PlaybackState check because waveOutReset blocks even
    // when playback already finished; under Wine this can stall the WinMM
    // callback thread if many sounds arrive in quick succession.
    private void StopCurrent()
    {
        try
        {
            if (_outputDevice?.PlaybackState == PlaybackState.Playing)
                _outputDevice.Stop();
            _outputDevice?.Dispose();
            _outputDevice = null;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Exception while stopping current WaveOutEvent");
        }

        try
        {
            _reader?.Dispose();
            _reader = null;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Exception while disposing WaveFileReader");
        }
    }

    // At plugin unload the PendingMessageThread is already cancelled and the
    // draw loop is gone, so _lock is uncontended here. Calling StopCurrent
    // outside the lock avoids holding it across the blocking waveOutReset /
    // WaveOutEvent.Dispose, which can freeze on Wine during unload.
    public void Dispose()
    {
        StopCurrent();
    }
}
