using Discord;
using Discord.Audio;
using Discord.Commands;
using Kasbot.Extensions;
using System.Diagnostics;

namespace Kasbot.Services
{
    public class PlayerService
    {
        public Dictionary<ulong, Connection> Clients { get; set; }
        public YoutubeService YoutubeService { get; set; }

        public PlayerService(YoutubeService youtubeService)
        {
            this.YoutubeService = youtubeService;

            Clients = new Dictionary<ulong, Connection>();
        }

        private async Task<Connection> CreateConnection(SocketCommandContext Context)
        {
            IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
            var conn = new Connection();
            IAudioClient audioClient = await channel.ConnectAsync();

            audioClient.Disconnected += (ex) => Stop(Context.Guild.Id);
            audioClient.StreamDestroyed += (ex) => Stop(Context.Guild.Id);

            conn.AudioClient = audioClient;

            if (Clients.ContainsKey(Context.Guild.Id))
                Clients.Remove(Context.Guild.Id);

            Clients.Add(Context.Guild.Id, conn);

            return conn;
        }

        public async Task Play(SocketCommandContext Context, string arguments)
        {
            var media = new Media()
            {
                Message = Context.Message,
                Search = arguments,
                Name = ""
            };

            if (Clients.TryGetValue(Context.Guild.Id, out var conn))
            {
                await Enqueue(Context.Guild.Id, conn, media);
                return;
            }

            conn = await CreateConnection(Context);
            await Enqueue(Context.Guild.Id, conn, media);
        }

        private async Task Enqueue(ulong guildId, Connection conn, Media media)
        {
            var startPlay = conn.Queue.Count == 0;

            switch (YoutubeService.GetSearchType(media.Search))
            {
                case SearchType.StringSearch:
                case SearchType.VideoURL:
                    media = await YoutubeService.DownloadMetadataFromYoutube(media);

                    if (media.VideoId == null)
                    {
                        await media.Channel.SendTemporaryMessageAsync($"No video found for \"{media.Search}\".");
                        return;
                    }

                    conn.Queue.Enqueue(media);
                    if (startPlay)
                        await PlayNext(guildId);
                    else
                    {
                        var message = $"Queued **{media.Name}** *({media.Length.TotalMinutes:00}:{media.Length.Seconds:00})*";
                        media.QueueMessage = await media.Channel.SendMessageAsync(message);
                    }

                    break;
                case SearchType.ChannelURL:
                case SearchType.PlaylistURL:
                    var collection = await YoutubeService.DownloadPlaylistMetadataFromYoutube(media.Message, media.Search);

                    collection.Medias.ForEach(m => conn.Queue.Enqueue(m));
                    
                    await media.Channel.SendMessageAsync($"Queued **{collection.Medias.Count}** items from *{collection.CollectionName}* playlist.");

                    if (startPlay)
                        await PlayNext(guildId);

                    break;
                case SearchType.None:
                default:
                    break;
            }
        }

        private async Task PlayNext(ulong guildId)
        {
            if (!Clients.ContainsKey(guildId) || Clients[guildId].Queue.Count == 0)
            {
                return;
            }

            var nextMedia = Clients[guildId].Queue.FirstOrDefault();

            if (nextMedia == null)
            {
                await Stop(guildId);
                return;
            }

            var mp3Stream = await YoutubeService.DownloadAudioFromYoutube(nextMedia);

            if (mp3Stream == null)
            {
                await Stop(guildId);
                return;
            }

            var audioClient = Clients[guildId].AudioClient;
            var ffmpeg = CreateStream();

            var message = $"⏯ Playing: **{nextMedia.Name}** *({nextMedia.Length.TotalMinutes:00}:{nextMedia.Length.Seconds:00})*";
            nextMedia.PlayMessage = await nextMedia.Channel.SendMessageAsync(message);

            if (nextMedia.QueueMessage != null)
            {
                await nextMedia.QueueMessage.TryDeleteAsync();
            }

            Task stdin = new Task(() =>
            {
                using (var input = mp3Stream)
                {
                    try
                    {
                        input.CopyTo(ffmpeg.StandardInput.BaseStream);
                        ffmpeg.StandardInput.Close();
                    }
                    catch { }
                    finally
                    {
                        input.Flush();
                        input.Dispose();
                    }
                }
            });

            Task stdout = new Task(() =>
            {
                using (var output = ffmpeg.StandardOutput.BaseStream)
                using (var discord = audioClient.CreatePCMStream(AudioApplication.Music))
                {
                    try
                    {
                        Clients[guildId].CurrentAudioStream = output;
                        output.CopyTo(discord);
                    }
                    catch { }
                    finally
                    {
                        discord.Flush();
                        output.Close();
                    }
                }
            });

            stdin.Start();
            stdout.Start();

            await stdin.ContinueWith(async ac =>
            {
                if (ac.Exception != null)
                {
                    await nextMedia.Channel.SendTemporaryMessageAsync("Error in input stream: " + ac.Exception.ToString());
                }
            });
            await stdout.ContinueWith(async ac =>
            {
                if (ac.Exception!= null)
                {
                    await nextMedia.Channel.SendTemporaryMessageAsync("Error while playing: " + ac.Exception.ToString());
                }
            });

            Task.WaitAll(stdin, stdout);

            ffmpeg.Close();

            await nextMedia.PlayMessage.TryDeleteAsync();

            if (Clients[guildId].Queue.Count > 0)
                Clients[guildId].Queue.Dequeue();

            await PlayNext(guildId);
        }

        private Process CreateStream()
        {
            var process = Process.Start(new ProcessStartInfo
            {
                FileName = "ffmpeg",
                Arguments = $"-hide_banner -loglevel panic -i pipe:0 -ac 2 -f s16le -ar 48000 pipe:1",
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true
            });

            if (process == null || process.HasExited)
            {
                throw new Exception("Sorry, ffmpeg killed itself in a tragic accident!");
            }

            return process;
        }

        public Task Skip(ulong guildId)
        {
            if (!Clients.ContainsKey(guildId))
                return Task.CompletedTask;

            var media = Clients[guildId];

            if (media.CurrentAudioStream == null)
                return Task.CompletedTask;

            media.CurrentAudioStream.Close();

            return Task.CompletedTask;
        }

        public async Task Stop(ulong guildId)
        {
            if (!Clients.ContainsKey(guildId))
                return;

            var media = Clients[guildId];

            foreach (var v in media.Queue.Skip(1))
            {
                await v.PlayMessage.TryDeleteAsync();
            }

            media.Queue.Clear();

            if (media.CurrentAudioStream != null)
                media.CurrentAudioStream.Close();
        }

        public async Task Leave(ulong guildId)
        {
            if (!Clients.ContainsKey(guildId))
                return;

            await Stop(guildId);
            var media = Clients[guildId];

            if (media.AudioClient != null)
                await media.AudioClient.StopAsync();

            Clients.Remove(guildId);
        }
    }

    public class Connection
    {
        public IAudioClient AudioClient { get; set; }
        public Stream? CurrentAudioStream { get; set; }
        public Queue<Media> Queue { get; set; } = new Queue<Media>();
    }
}
