using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Db;
using Image = SixLabors.ImageSharp.Image;
using Color = SixLabors.ImageSharp.Color;

namespace NadekoBot.Modules.Gambling.Services
{
    public class PlantPickService : INService
    {
        private readonly DbService _db;
        private readonly IBotStrings _strings;
        private readonly IImageCache _images;
        private readonly FontProvider _fonts;
        private readonly ICurrencyService _cs;
        private readonly CommandHandler _cmdHandler;
        private readonly NadekoRandom _rng;
        private readonly DiscordSocketClient _client;
        private readonly GamblingConfigService _gss;

        public readonly ConcurrentHashSet<ulong> _generationChannels = new ConcurrentHashSet<ulong>();
        //channelId/last generation
        public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new ConcurrentDictionary<ulong, DateTime>();
        private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1);

        public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings,
            IDataCache cache, FontProvider fonts, ICurrencyService cs,
            CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss)
        {
            _db = db;
            _strings = strings;
            _images = cache.LocalImages;
            _fonts = fonts;
            _cs = cs;
            _cmdHandler = cmdHandler;
            _rng = new NadekoRandom();
            _client = client;
            _gss = gss;

            cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
            using (var uow = db.GetDbContext())
            {
                var guildIds = client.Guilds.Select(x => x.Id).ToList();
                var configs = uow.Set<GuildConfig>()
                    .AsQueryable()
                    .Include(x => x.GenerateCurrencyChannelIds)
                    .Where(x => guildIds.Contains(x.GuildId))
                    .ToList();
                
                _generationChannels = new ConcurrentHashSet<ulong>(configs
                    .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
            }
        }

        private string GetText(ulong gid, LocStr str)
            => _strings.GetText(str, gid);

        public bool ToggleCurrencyGeneration(ulong gid, ulong cid)
        {
            bool enabled;
            using (var uow = _db.GetDbContext())
            {
                var guildConfig = uow.GuildConfigsForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds));

                var toAdd = new GCChannelId() { ChannelId = cid };
                if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd))
                {
                    guildConfig.GenerateCurrencyChannelIds.Add(toAdd);
                    _generationChannels.Add(cid);
                    enabled = true;
                }
                else
                {
                    var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
                    if (toDelete != null)
                    {
                        uow.Remove(toDelete);
                    }
                    _generationChannels.TryRemove(cid);
                    enabled = false;
                }
                uow.SaveChanges();
            }
            return enabled;
        }

        public IEnumerable<GuildConfigExtensions.GeneratingChannel> GetAllGeneratingChannels()
        {
            using (var uow = _db.GetDbContext())
            {
                var chs = uow.GuildConfigs.GetGeneratingChannels();
                return chs;
            }
        }

        /// <summary>
        /// Get a random currency image stream, with an optional password sticked onto it.
        /// </summary>
        /// <param name="pass">Optional password to add to top left corner.</param>
        /// <returns>Stream of the currency image</returns>
        public Stream GetRandomCurrencyImage(string pass, out string extension)
        {
            // get a random currency image bytes
            var rng = new NadekoRandom();
            var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)];

            if (string.IsNullOrWhiteSpace(pass))
            {
                // determine the extension
                using (var img = Image.Load(curImg, out var format))
                {
                    extension = format.FileExtensions.FirstOrDefault() ?? "png";
                }
                // return the image
                return curImg.ToStream();
            }

            // get the image stream and extension
            var (s, ext) = AddPassword(curImg, pass);
            // set the out extension parameter to the extension we've got
            extension = ext;
            // return the image
            return s;
        }

        /// <summary>
        /// Add a password to the image.
        /// </summary>
        /// <param name="curImg">Image to add password to.</param>
        /// <param name="pass">Password to add to top left corner.</param>
        /// <returns>Image with the password in the top left corner.</returns>
        private (Stream, string) AddPassword(byte[] curImg, string pass)
        {
            // draw lower, it looks better
            pass = pass.TrimTo(10, true).ToLowerInvariant();
            using (var img = Image.Load<Rgba32>(curImg, out var format))
            {
                // choose font size based on the image height, so that it's visible
                var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold);
                img.Mutate(x =>
                {
                    // measure the size of the text to be drawing
                    var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0)));

                    // fill the background with black, add 5 pixels on each side to make it look better
                    x.FillPolygon(Color.ParseHex("00000080"),
                        new PointF(0, 0),
                        new PointF(size.Width + 5, 0),
                        new PointF(size.Width + 5, size.Height + 10),
                        new PointF(0, size.Height + 10));

                    // draw the password over the background
                    x.DrawText(pass,
                        font,
                        SixLabors.ImageSharp.Color.White,
                        new PointF(0, 0));
                });
                // return image as a stream for easy sending
                return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png");
            }
        }

        private Task PotentialFlowerGeneration(IUserMessage imsg)
        {
            var msg = imsg as SocketUserMessage;
            if (msg is null || msg.Author.IsBot)
                return Task.CompletedTask;

            if (!(imsg.Channel is ITextChannel channel))
                return Task.CompletedTask;

            if (!_generationChannels.Contains(channel.Id))
                return Task.CompletedTask;

            var _ = Task.Run(async () =>
            {
                try
                {
                    var config = _gss.Data;
                    var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
                    var rng = new NadekoRandom();

                    if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again
                        return;

                    var num = rng.Next(1, 101) + config.Generation.Chance * 100;
                    if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
                    {
                        var dropAmount = config.Generation.MinAmount;
                        var dropAmountMax = config.Generation.MaxAmount;

                        if (dropAmountMax > dropAmount)
                            dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1);

                        if (dropAmount > 0)
                        {
                            var prefix = _cmdHandler.GetPrefix(channel.Guild.Id);
                            var toSend = dropAmount == 1
                                ? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign))
                                    + " " + GetText(channel.GuildId, strs.pick_sn(prefix))
                                : GetText(channel.GuildId, strs.curgen_pl(dropAmount, config.Currency.Sign))
                                    + " " + GetText(channel.GuildId, strs.pick_pl(prefix));

                            var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;

                            IUserMessage sent;
                            using (var stream = GetRandomCurrencyImage(pw, out var ext))
                            {
                                sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false);
                            }

                            await AddPlantToDatabase(channel.GuildId,
                                channel.Id,
                                _client.CurrentUser.Id,
                                sent.Id,
                                dropAmount,
                                pw).ConfigureAwait(false);
                        }
                    }
                }
                catch
                {
                }
            });
            return Task.CompletedTask;
        }

        /// <summary>
        /// Generate a hexadecimal string from 1000 to ffff.
        /// </summary>
        /// <returns>A hexadecimal string from 1000 to ffff</returns>
        private string GenerateCurrencyPassword()
        {
            // generate a number from 1000 to ffff
            var num = _rng.Next(4096, 65536);
            // convert it to hexadecimal
            return num.ToString("x4");
        }

        public async Task<long> PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass)
        {
            await pickLock.WaitAsync();
            try
            {
                long amount;
                ulong[] ids;
                using (var uow = _db.GetDbContext())
                {
                    // this method will sum all plants with that password,
                    // remove them, and get messageids of the removed plants

                    pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant();
                    // gets all plants in this channel with the same password
                    var entries = uow.PlantedCurrency
                        .AsQueryable()
                        .Where(x => x.ChannelId == ch.Id && pass == x.Password)
                        .ToList();
                    // sum how much currency that is, and get all of the message ids (so that i can delete them)
                    amount = entries.Sum(x => x.Amount);
                    ids = entries.Select(x => x.MessageId).ToArray();
                    // remove them from the database
                    uow.RemoveRange(entries);


                    if (amount > 0)
                    {
                        // give the picked currency to the user
                        await _cs.AddAsync(uid, "Picked currency", amount, gamble: false);
                    }
                    uow.SaveChanges();
                }

                try
                {
                    // delete all of the plant messages which have just been picked
                    var _ = ch.DeleteMessagesAsync(ids);
                }
                catch { }

                // return the amount of currency the user picked
                return amount;
            }
            finally
            {
                pickLock.Release();
            }
        }

        public async Task<ulong?> SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass)
        {
            try
            {
                // get the text
                var prefix = _cmdHandler.GetPrefix(gid);
                var msgToSend = GetText(gid,
                    strs.planted(
                        Format.Bold(user),
                        amount + _gss.Data.Currency.Sign));

                if (amount > 1)
                    msgToSend += " " + GetText(gid, strs.pick_pl(prefix));
                else
                    msgToSend += " " + GetText(gid, strs.pick_sn(prefix));

                //get the image
                using (var stream = GetRandomCurrencyImage(pass, out var ext))
                {
                    // send it
                    var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false);
                    // return sent message's id (in order to be able to delete it when it's picked)
                    return msg.Id;
                }
            }
            catch
            {
                // if sending fails, return null as message id
                return null;
            }
        }

        public async Task<bool> PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass)
        {
            // normalize it - no more than 10 chars, uppercase
            pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant();
            // has to be either null or alphanumeric
            if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
                return false;

            // remove currency from the user who's planting
            if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false))
            {
                // try to send the message with the currency image
                var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false);
                if (msgId is null)
                {
                    // if it fails it will return null, if it returns null, refund
                    await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false);
                    return false;
                }
                // if it doesn't fail, put the plant in the database for other people to pick
                await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false);
                return true;
            }
            // if user doesn't have enough currency, fail
            return false;
        }

        private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass)
        {
            using (var uow = _db.GetDbContext())
            {
                uow.PlantedCurrency.Add(new PlantedCurrency
                {
                    Amount = amount,
                    GuildId = gid,
                    ChannelId = cid,
                    Password = pass,
                    UserId = uid,
                    MessageId = mid,
                });
                await uow.SaveChangesAsync();
            }
        }
    }
}
