#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Ayu.Discord.Voice;
using NadekoBot.Common;
using NadekoBot.Modules.Music.Common;
using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions;
using NadekoBot.Modules.Music;
using Serilog;

namespace NadekoBot.Modules.Music
{
    public sealed class MusicPlayer : IMusicPlayer
    {
        private delegate void AdjustVolumeDelegate(Span<byte> data, float volume);

        private AdjustVolumeDelegate AdjustVolume;
        private readonly VoiceClient _vc;

        public bool IsKilled { get; private set; }
        public bool IsStopped { get; private set; }
        public bool IsPaused { get; private set; }
        public PlayerRepeatType Repeat { get; private set; }
        
        public int CurrentIndex => _queue.Index;
        
        public float Volume => _volume;
        private float _volume = 1.0f;

        private readonly IMusicQueue _queue;
        private readonly ITrackResolveProvider _trackResolveProvider;
        private readonly IVoiceProxy _proxy;
        private readonly ISongBuffer _songBuffer;

        private bool _skipped;
        private int? _forceIndex;
        private readonly Thread _thread;
        private readonly Random _rng;

        public MusicPlayer(
            IMusicQueue queue,
            ITrackResolveProvider trackResolveProvider,
            IVoiceProxy proxy,
            QualityPreset qualityPreset)
        {
            _queue = queue;
            _trackResolveProvider = trackResolveProvider;
            _proxy = proxy;
            _rng = new NadekoRandom();

            _vc = GetVoiceClient(qualityPreset);
            if (_vc.BitDepth == 16)
                AdjustVolume = AdjustVolumeInt16;
            else
                AdjustVolume = AdjustVolumeFloat32;
            
            _songBuffer = new PoopyBufferImmortalized(_vc.InputLength);

            _thread = new Thread(async () =>
            {
                await PlayLoop();
            });
            _thread.Start();
        }

        private static VoiceClient GetVoiceClient(QualityPreset qualityPreset)
            => qualityPreset switch
            {
                QualityPreset.Highest => new VoiceClient(
                    SampleRate._48k,
                    Bitrate._192k,
                    Channels.Two,
                    FrameDelay.Delay20,
                    BitDepthEnum.Float32
                ),
                QualityPreset.High => new VoiceClient(
                    SampleRate._48k,
                    Bitrate._128k,
                    Channels.Two,
                    FrameDelay.Delay40,
                    BitDepthEnum.Float32
                ),
                QualityPreset.Medium => new VoiceClient(
                    SampleRate._48k,
                    Bitrate._96k,
                    Channels.Two,
                    FrameDelay.Delay40,
                    BitDepthEnum.UInt16
                ),
                QualityPreset.Low => new VoiceClient(
                    SampleRate._48k,
                    Bitrate._64k,
                    Channels.Two,
                    FrameDelay.Delay40,
                    BitDepthEnum.UInt16
                ),
                _ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null)
            };

        private async Task PlayLoop()
        {
            var sw = new Stopwatch();

            while (!IsKilled)
            {
                // wait until a song is available in the queue
                // or until the queue is resumed
                var track = _queue.GetCurrent(out int index);
                
                if (track is null || IsStopped)
                {
                    await Task.Delay(500);
                    continue;
                }

                if (_skipped)
                {
                    _skipped = false;
                    _queue.Advance();
                    continue;
                }

                using var cancellationTokenSource = new CancellationTokenSource();
                var token = cancellationTokenSource.Token;
                try
                {
                    // light up green in vc
                    _ = _proxy.StartSpeakingAsync();

                    _ = OnStarted?.Invoke(this, track, index);

                    // make sure song buffer is ready to be (re)used
                    _songBuffer.Reset();

                    var streamUrl = await track.GetStreamUrl();
                    // start up the data source
                    using var source = FfmpegTrackDataSource.CreateAsync(
                        _vc.BitDepth,
                        streamUrl,
                        track.Platform == MusicPlatform.Local
                    );
                    
                    // start moving data from the source into the buffer
                    // this method will return once the sufficient prebuffering is done
                    await _songBuffer.BufferAsync(source, token);

                    // // Implemenation with multimedia timer. Works but a hassle because no support for switching
                    // // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option
                    // // for selfhosters.
                    // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                    // {
                    //     var cancelSource = new CancellationTokenSource();
                    //     var cancelToken = cancelSource.Token;
                    //     using var timer = new MultimediaTimer(_ =>
                    //     {
                    //         if (IsStopped || IsKilled)
                    //         {
                    //             cancelSource.Cancel();
                    //             return;
                    //         }
                    //         
                    //         if (_skipped)
                    //         {
                    //             _skipped = false;
                    //             cancelSource.Cancel();
                    //             return;
                    //         }
                    //
                    //         if (IsPaused)
                    //             return;
                    //
                    //         try
                    //         {
                    //             // this should tolerate certain number of errors
                    //             var result = CopyChunkToOutput(_songBuffer, _vc);
                    //             if (!result)
                    //                 cancelSource.Cancel();
                    //               
                    //         }
                    //         catch (Exception ex)
                    //         {
                    //             Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
                    //             cancelSource.Cancel();
                    //         }
                    //
                    //     }, null, 20);
                    //     
                    //     while(true)
                    //         await Task.Delay(1000, cancelToken);
                    // }

                    // start sending data
                    var ticksPerMs = 1000f / Stopwatch.Frequency;
                    sw.Start();
                    Thread.Sleep(2);

                    var delay = sw.ElapsedTicks * ticksPerMs > 3f
                        ? _vc.Delay - 16
                        : _vc.Delay - 3;
                    
                    var errorCount = 0;
                    while (!IsStopped && !IsKilled)
                    {
                        // doing the skip this way instead of in the condition
                        // ensures that a song will for sure be skipped
                        if (_skipped)
                        {
                            _skipped = false;
                            break;
                        }
                        
                        if (IsPaused)
                        {
                            await Task.Delay(200);
                            continue;
                        }
                        
                        sw.Restart();
                        var ticks = sw.ElapsedTicks;
                        try
                        {
                            var result = CopyChunkToOutput(_songBuffer, _vc);
                        
                            // if song is finished
                            if (result is null)
                                break;
                        
                            if (result is true)
                            {
                                if (errorCount > 0)
                                {
                                    _ = _proxy.StartSpeakingAsync();
                                    errorCount = 0;
                                }
                                    
                                // todo future windows multimedia api
                                    
                                // wait for slightly less than the latency
                                Thread.Sleep(delay);
                                    
                                // and then spin out the rest
                                while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f)
                                    Thread.SpinWait(100);
                            }
                            else
                            {
                                // result is false is either when the gateway is being swapped 
                                // or if the bot is reconnecting, or just disconnected for whatever reason
                        
                                // tolerate up to 15x200ms of failures (3 seconds)
                                if (++errorCount <= 15)
                                {
                                    await Task.Delay(200);
                                    continue;
                                }
                        
                                Log.Warning("Can't send data to voice channel");
                        
                                IsStopped = true;
                                // if errors are happening for more than 3 seconds
                                // Stop the player
                                break;
                            }
                        }
                        catch (Exception ex)
                        {
                            Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message);
                        }
                    }
                }
                catch (Win32Exception)
                {
                    IsStopped = true;
                    Log.Error("Please install ffmpeg and make sure it's added to your " +
                              "PATH environment variable before trying again");
                }
                catch (OperationCanceledException)
                {
                    Log.Information("Song skipped");
                }
                catch (Exception ex)
                {
                    Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message);
                }
                finally
                {
                    cancellationTokenSource.Cancel();
                    // turn off green in vc
                    _ = OnCompleted?.Invoke(this, track);
                    
                    HandleQueuePostTrack();
                    _skipped = false;
                    
                    _ = _proxy.StopSpeakingAsync();;
                    
                    await Task.Delay(100);
                }
            }
        }

        private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc)
        {
            var data = sb.Read(vc.InputLength, out var length);

            // if nothing is read from the buffer, song is finished
            if (data.Length == 0)
            {
                return null;
            }

            AdjustVolume(data, _volume);
            return _proxy.SendPcmFrame(vc, data, length);
        }

        private void HandleQueuePostTrack()
        {
            if (_forceIndex is int forceIndex)
            {
                _queue.SetIndex(forceIndex);
                _forceIndex = null;
                return;
            }

            var (repeat, isStopped) = (Repeat, IsStopped);

            if (repeat == PlayerRepeatType.Track || isStopped)
                return;
            
            // if queue is being repeated, advance no matter what
            if (repeat == PlayerRepeatType.None)
            {
                // if this is the last song,
                // stop the queue
                if (_queue.IsLast())
                {
                    IsStopped = true;
                    OnQueueStopped?.Invoke(this);
                    return;
                }
                
                _queue.Advance();
                return;
            }
            
            _queue.Advance();
        }

        

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void AdjustVolumeInt16(Span<byte> audioSamples, float volume)
        {
            if (Math.Abs(volume - 1f) < 0.0001f) return;
        
            var samples = MemoryMarshal.Cast<byte, short>(audioSamples);
        
            for (var i = 0; i < samples.Length; i++)
            {
                ref var sample = ref samples[i];
                sample = (short) (sample * volume);
            }
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void AdjustVolumeFloat32(Span<byte> audioSamples, float volume)
        {
            if (Math.Abs(volume - 1f) < 0.0001f) return;
        
            var samples = MemoryMarshal.Cast<byte, float>(audioSamples);
        
            for (var i = 0; i < samples.Length; i++)
            {
                ref var sample = ref samples[i];
                sample = (float) (sample * volume);
            }
        }

        public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(
            string query, 
            string queuer,
            bool asNext,
            MusicPlatform? forcePlatform = null)
        {
            var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform);
            if (song is null)
                return default;

            int index;

            if (asNext)
                return (_queue.EnqueueNext(song, queuer, out index), index);

            return (_queue.Enqueue(song, queuer, out index), index);
        }
        
        public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)
        {
            var errorCount = 0;
            foreach (var chunk in queries.Chunk(5))
            {
                if (IsKilled)
                    break;
                
                var queueTasks = chunk.Select(async data =>
                {
                    var (query, platform) = data;
                    try
                    {
                        await TryEnqueueTrackAsync(query, queuer, false, forcePlatform: platform).ConfigureAwait(false);
                        errorCount = 0;
                    }
                    catch (Exception ex)
                    {
                        Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
                        ++errorCount;
                    }
                });

                await Task.WhenAll(queueTasks);
                await Task.Delay(1000);
                
                // > 10 errors in a row = kill
                if (errorCount > 10)
                    break;
            }
        }
        
        public void EnqueueTrack(ITrackInfo track, string queuer)
        {
            _queue.Enqueue(track, queuer, out _);
        }

        public void EnqueueTracks(IEnumerable<ITrackInfo> tracks, string queuer)
        {
            _queue.EnqueueMany(tracks, queuer);
        }

        public void SetRepeat(PlayerRepeatType type)
        {
            Repeat = type;
        }

        public void ShuffleQueue()
        {
            _queue.Shuffle(_rng);
        }

        public void Stop()
        {
            IsStopped = true;
        }

        public void Clear()
        {
            _queue.Clear();
            _skipped = true;
        }

        public IReadOnlyCollection<IQueuedTrackInfo> GetQueuedTracks()
            => _queue.List();

        public IQueuedTrackInfo? GetCurrentTrack(out int index)
            => _queue.GetCurrent(out index);

        public void Next()
        {
            _skipped = true;
            IsStopped = false;
            IsPaused = false;
        }

        public bool MoveTo(int index)
        {
            if (_queue.SetIndex(index))
            {
                _forceIndex = index;
                _skipped = true;
                IsStopped = false;
                IsPaused = false;
                return true;
            }

            return false;
        }

        public void SetVolume(int newVolume)
        {
            var normalizedVolume = newVolume / 100f;
            if (normalizedVolume < 0f || normalizedVolume > 1f)
                throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100");

            _volume = normalizedVolume;
        }

        public void Kill()
        {
            IsKilled = true;
            IsStopped = true;
            IsPaused = false;
            _skipped = true;
        }

        public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo)
        {
            if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent))
                return false;

            if (isCurrent)
                _skipped = true;

            return true;
        }
        
        public bool TogglePause() => IsPaused = !IsPaused;
        public IQueuedTrackInfo? MoveTrack(int from, int to) => _queue.MoveTrack(from, to);

        public void Dispose()
        {
            IsKilled = true;
            OnCompleted = null;
            OnStarted = null;
            OnQueueStopped = null;
            _queue.Clear();
            _songBuffer.Dispose();
            _vc.Dispose();
        }

        public event Func<IMusicPlayer, IQueuedTrackInfo, Task>? OnCompleted;
        public event Func<IMusicPlayer, IQueuedTrackInfo, int, Task>? OnStarted;
        public event Func<IMusicPlayer, Task>? OnQueueStopped;
    }
}