﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Discord.Commands;
using Microsoft.Extensions.DependencyInjection;
using PeekBot.Modules.StreamScanner;
using PeekBot.Modules.StreamScanner.Logging;

namespace PeekBot
{
    /// <summary>
    ///     Class representing Program of the PeekBot application.
    /// </summary>
    internal class Program
    {
        #region Private member variables
        /// <summary>
        ///     The configuration.
        /// </summary>
        private Configuration _config;

        /// <summary>
        ///     The Discord client.
        /// </summary>
        private readonly DiscordSocketClient _client = new DiscordSocketClient();

        /// <summary>
        ///     The command service.
        /// </summary>
        private readonly CommandService _commands = new CommandService();

        /// <summary>
        ///     The service provider.
        /// </summary>
        private  IServiceProvider _services;

        /// <summary>
        ///     The stream scanner.
        /// </summary>
        private StreamScanner _scanner;

        /// <summary>
        ///     The console lock.
        /// </summary>
        private readonly object _lock = new object();

        /// <summary>
        ///     The log severities mapping.
        /// </summary>
        private readonly ReadOnlyDictionary<Severity, LogSeverity> _severities = new ReadOnlyDictionary<Severity, LogSeverity>(
            new Dictionary<Severity, LogSeverity>
            {
                { Severity.Fatal, LogSeverity.Critical },
                { Severity.Critical, LogSeverity.Critical },
                { Severity.Error, LogSeverity.Error },
                { Severity.Warning, LogSeverity.Warning },
                { Severity.Info, LogSeverity.Info },
                { Severity.Debug, LogSeverity.Debug }
            });

        /// <summary>
        ///     The console colors mapping.
        /// </summary>
        private readonly ReadOnlyDictionary<LogSeverity, Tuple<ConsoleColor, ConsoleColor>> _consoleColors = new ReadOnlyDictionary<LogSeverity, Tuple<ConsoleColor, ConsoleColor>>(
            new Dictionary<LogSeverity, Tuple<ConsoleColor, ConsoleColor>>
            {
                { LogSeverity.Critical, new Tuple<ConsoleColor, ConsoleColor>(ConsoleColor.Black, ConsoleColor.Red) },
                { LogSeverity.Error, new Tuple<ConsoleColor, ConsoleColor>(ConsoleColor.Red, ConsoleColor.Black) },
                { LogSeverity.Warning, new Tuple<ConsoleColor, ConsoleColor>(ConsoleColor.Yellow, ConsoleColor.Black) },
                { LogSeverity.Info, new Tuple<ConsoleColor, ConsoleColor>(ConsoleColor.White, ConsoleColor.Black) },
                { LogSeverity.Debug, new Tuple<ConsoleColor, ConsoleColor>(ConsoleColor.Magenta, ConsoleColor.Black) },
                { LogSeverity.Verbose, new Tuple<ConsoleColor, ConsoleColor>(ConsoleColor.Gray, ConsoleColor.Black) }
            });
        #endregion

        #region Private methods
        /// <summary>
        ///     Main function of the PeekBot application.
        /// </summary>
        /// <param name="args">The arguments.</param>
        public static void Main(string[] args) => new Program().MainAsync(args).GetAwaiter().GetResult();

        /// <summary>
        ///     Asynchronous Main function of the PeekBot application.
        /// </summary>
        /// <param name="args">The arguments.</param>
        /// <returns>
        ///     Returns instance of <see cref="Task" /> representing asynchronous Main function.
        /// </returns>
        private async Task MainAsync(IReadOnlyList<string> args)
        {
            // Check, if configuration file path was provided.
            if (args.Count < 1)
            {
                await Client_Log(new LogMessage(LogSeverity.Critical, "PeekBot",
                    "Configuration file not specified -> terminating!"));

                return;
            }

            // Load the configuration and check if it was loaded properly.
            _config = Configuration.Load(args[0]);
            if (_config == null)
            {
                await Client_Log(new LogMessage(LogSeverity.Critical, "PeekBot",
                    "Invalid configuration file -> terminating!"));

                return;
            }

            // Configure the stream scanner.
            _scanner = new StreamScanner(TimeSpan.FromSeconds(_config.ScanPeriod), _config.WatchListFileName,
                _config.SuperUsers.Select(u => u.ToString()).ToArray());
            _scanner.StreamStatusChanged += Scanner_StreamStatusChanged;
            _scanner.WriteLogRequest += Scanner_Log;            
            
            // Configure the Discord client.
            _client.Log += Client_Log;
            _commands.Log += Client_Log;
            _client.Ready += () =>
            {
                // Start the scanner when client is ready.
                _scanner.Start();

                return Task.CompletedTask;
            };

            // Install the bot commands.
            await InstallCommandsAsync();

            // Login and start the Discord client.
            await _client.LoginAsync(TokenType.Bot, _config.Token);
            await _client.StartAsync();

            // Block this task until the program is closed.
            await Task.Delay(-1);
        }

        /// <summary>
        ///     Installs all commands supported by the bot.
        /// </summary>
        /// <returns>
        ///     Returns instance of <see cref="Task" /> representing asynchronous command installation operation.
        /// </returns>
        private async Task InstallCommandsAsync()
        {
            // Set-up the message handler.
            _client.MessageReceived += MessageReceived;

            // Build dependency map.
            var sc = new ServiceCollection();
            sc.AddSingleton(_scanner);
            _services = sc.BuildServiceProvider();
                    
            // Discover all of the commands in this assembly and load them.
            await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
        }


        /// <summary>
        ///     Handles the stream status changed event of the stream scanner.
        /// </summary>
        /// <param name="channel">The stream channel.</param>
        /// <param name="e">The stream status changed event arguments.</param>
        private async void Scanner_StreamStatusChanged(Channel channel, StreamStatusChangedEventArgs e)
        {
            // If stream is private, do nothing.
            if (e.IsPrivate)
            {
                await Client_Log(new LogMessage(LogSeverity.Debug, "PeekBot",
                    $"Stream on channel '{channel.Name}' from service '{channel.ServiceType}' is private -> notification is supressed."));

                return;
            }

            // Build the message elements.
            var name = channel?.Name ?? string.Empty;
            var thumb = e.ThumbnailURL ?? string.Empty;
            var icon = e.IconURL ?? string.Empty;
            var lewd = e.IsAgeRestricted ? "Yes" : "No";
            var gaming = e.IsGaming ? "Yes" : "No";
            var service = channel?.ServiceType.ToString() ?? string.Empty;
            var link = e.IsOnline
                ? Environment.NewLine + $"https://{service.ToLower()}.tv/{name.ToLower()}"
                : string.Empty;
            var color = channel?.ServiceType == ServiceType.Picarto
                ? new Color(255, 255, 255)
                : new Color(158, 19, 204);

            // Build and send the message.
            var chan = _client.GetChannel(_config.TargetChannelID) as SocketTextChannel;
            var send = e.IsOnline
                ? chan?.SendMessageAsync($"Hey all, {name} is now online! Check them out!{link}", false,
                    new EmbedBuilder().WithColor(color)
                        .WithAuthor(new EmbedAuthorBuilder().WithName(name).WithIconUrl(icon))
                        .WithThumbnailUrl(icon).WithTitle(e.Title).WithDescription(@" ").WithUrl(link)
                        .AddInlineField("NSFW", lewd)
                        .WithFooter(new EmbedFooterBuilder().WithText(service))
                        .AddInlineField("Gaming", gaming).WithImageUrl(thumb).Build())
                : chan?.SendMessageAsync($"{name} is now offline! Check them out next time!");

            // Await the sending operation.
            if (send != null)
                await send;
        }

        /// <summary>
        ///     Handles the message received event of the Discord client.
        /// </summary>
        /// <param name="message">The received socket message.</param>
        /// <returns>
        ///     Returns instance of <see cref="Task" /> representing asynchronous event handling operation.
        /// </returns>
        private async Task MessageReceived(SocketMessage message)
        {
            // Don't process the command if it was a system message.
            if (!(message is SocketUserMessage msg))
                return;

            // Create a number to track where the prefix ends and the command begins
            var argPos = 0;

            // Determine if the message is a command, based on if it starts with '!' or a mention prefix
            if (!(msg.HasCharPrefix('!', ref argPos) || msg.HasMentionPrefix(_client.CurrentUser, ref argPos)))
                return;

            // Create a command context.
            var context = new CommandContext(_client, msg);

            // Execute the command. 
            // Result does not indicate a return value, rather an object stating if the command executed successfully.
            var result = await _commands.ExecuteAsync(context, argPos, _services);
            if (!result.IsSuccess && result.Error != CommandError.UnknownCommand)
                await context.Channel.SendMessageAsync("```" + Environment.NewLine + $"ERROR: {result.ErrorReason}" +
                                                       Environment.NewLine + "```");
        }

        /// <summary>
        ///     Writes the message to the console.
        /// </summary>
        /// <param name="msg">The message.</param>
        /// <param name="colors">The tuple with foreground and background colors.</param>
        private void WriteOutput(string msg, Tuple<ConsoleColor, ConsoleColor> colors)
        {
            // Lock the console.
            lock (_lock)
            {
                // Set the colors.
                Console.ForegroundColor = colors.Item1;
                Console.BackgroundColor = colors.Item2;

                // Write the output.
                Console.WriteLine(msg);
                Console.ResetColor();
            }
        }

        /// <summary>
        ///     Handles the logging event of the stream scanner.
        /// </summary>
        /// <param name="scanner">The scanner.</param>
        /// <param name="e">The logging event arguments.</param>
        private void Scanner_Log(StreamScanner scanner, WriteLogRequestEventArgs e) => WriteOutput(
            e.TimeStamp.ToString("HH:mm:ss") + " " + scanner + @"     " + e.Message,
            _consoleColors[_severities[e.Severity]]);

        /// <summary>
        ///     Handles the logging event of the Discord client.
        /// </summary>
        /// <param name="msg">The log message.</param>
        /// <returns>
        ///     Returns instance of <see cref="Task" /> representing asynchronous event handling operation.
        /// </returns>
        private Task Client_Log(LogMessage msg)
        {
            WriteOutput(msg.ToString(), _consoleColors[msg.Severity]);

            return Task.CompletedTask;
        } 
        #endregion
    }
}