using System.Drawing;
using System.Numerics;
using System.Text;
using ImageMagick;
using Kynareth.Managers;
using Kynareth.Extensions;
using CollectionExtensions = Kynareth.Extensions.CollectionExtensions;

namespace Kynareth.Helpers;

public enum AmogusRole
{
    Imposter,
    Crewmate,
    Unknown
}
	
public enum AmogusColour
{
    Red,
    Blue,
    Green,
    Pink,
    Orange,
    Yellow,
    Black,
    White,
    Purple,
    Brown,
    Cyan,
    Lime,
    Maroon,
    Rose,
    Banana,
    Gray,
    Tan,
    Coral,
}
	
public enum AmogusSkin
{
    Archaeologist,
    Astronaut,
    Captain,
    Hazmat,
    Mechanic,
    Military,
    Miner,
    Police,
    SecurityGuard,
    Scientist,
    BlackSuit,
    WhiteSuit,
    Tarmac,
    Wall,
    Winter,
    None,
}

public static class AmogusHelper
{
    #region CoreVariables
    public class ColourHelper
    {
        private static readonly Dictionary<AmogusColour, MagickColor> _RedReplaceColors = new()
        {
            { AmogusColour.Red, new(215, 30, 34, 255) },
            { AmogusColour.Blue, new(29, 60, 233, 255) },
            { AmogusColour.Green, new(27, 145, 62, 255) },
            { AmogusColour.Pink, new(255, 99, 212, 255) },
            { AmogusColour.Orange, new(255, 141, 28, 255) },
            { AmogusColour.Yellow, new(255, 255, 103, 255) },
            { AmogusColour.Black, new(74, 86, 94, 255) },
            { AmogusColour.White, new(233, 247, 255, 255) },
            { AmogusColour.Purple, new(120, 61, 210, 255) },
            { AmogusColour.Brown, new(128, 88, 45, 255) },
            { AmogusColour.Cyan, new(68, 255, 247, 255) },
            { AmogusColour.Lime, new(91, 255, 75, 255) },
            { AmogusColour.Maroon, new(108, 43, 61, 255) },
            { AmogusColour.Rose, new(255, 214, 236, 255) },
            { AmogusColour.Banana, new(255, 255, 190, 255) },
            { AmogusColour.Gray, new(131, 151, 167, 255) },
            { AmogusColour.Tan, new(159, 153, 137, 255) },
            { AmogusColour.Coral, new(236, 117, 120, 255) },
        };

        private static readonly Dictionary<AmogusColour, MagickColor> _BlueReplaceColors = new()
        {
            { AmogusColour.Red, new(122, 8, 56, 255) },
            { AmogusColour.Blue, new(9, 21, 142, 255) },
            { AmogusColour.Green, new(10, 77, 46, 255) },
            { AmogusColour.Pink, new(172, 43, 174, 255) },
            { AmogusColour.Orange, new(180, 62, 21, 255) },
            { AmogusColour.Yellow, new(195, 136, 34, 255) },
            { AmogusColour.Black, new(30, 31, 38, 255) },
            { AmogusColour.White, new(132, 149, 192, 255) },
            { AmogusColour.Purple, new(59, 23, 124, 255) },
            { AmogusColour.Brown, new(94, 38, 21, 255) },
            { AmogusColour.Cyan, new(36, 169, 191, 255) },
            { AmogusColour.Lime, new(21, 168, 66, 255) },
            { AmogusColour.Maroon, new(65, 15, 26, 255) },
            { AmogusColour.Rose, new(222, 146, 179, 255) },
            { AmogusColour.Banana, new(210, 188, 137, 255) },
            { AmogusColour.Gray, new(70, 86, 100, 255) },
            { AmogusColour.Tan, new(81, 65, 62, 255) },
            { AmogusColour.Coral, new(180, 67, 98, 255) },
        };

        private static readonly MagickColor _green = new MagickColor(149, 202, 220, 255);
        private readonly MagickColor _red;
        private readonly MagickColor _blue;

        public MagickColor Red => _red;
        public MagickColor Green => _green;
        public MagickColor Blue => _blue;

        public ColourHelper(AmogusColour colour = AmogusColour.Blue)
        {
            _red = _RedReplaceColors[colour];
            _blue = _BlueReplaceColors[colour];
        }
    }

    public static readonly Dictionary<AmogusSkin, string> IdleSkins = new()
    {
        { AmogusSkin.Archaeologist, "archae-idle.png" },
        { AmogusSkin.Astronaut, "astro-idle.png" },
        { AmogusSkin.Captain, "captain-idle.png" },
        { AmogusSkin.Hazmat, "hazmat-idle.png" },
        { AmogusSkin.Mechanic, "mech-idle.png" },
        { AmogusSkin.Military, "military-idle.png" },
        { AmogusSkin.Miner, "miner-idle.png" },
        { AmogusSkin.Police, "pol-idle.png" },
        { AmogusSkin.SecurityGuard, "secguard-idle.png" },
        { AmogusSkin.Scientist, "sci-idle.png" },
        { AmogusSkin.BlackSuit, "suitBlack-idle.png" },
        { AmogusSkin.WhiteSuit, "suitWhite-idle.png" },
        { AmogusSkin.Tarmac, "tarmac-idle.png" },
        { AmogusSkin.Wall, "wall-idle.png" },
        { AmogusSkin.Winter, "winter-idle.png" },
    };

    public static readonly Dictionary<AmogusSkin, string> EjectSkins = new()
    {
        { AmogusSkin.Archaeologist, "archae-eject.png" },
        { AmogusSkin.Astronaut, "astro-eject.png" },
        { AmogusSkin.Captain, "captain-eject.png" },
        { AmogusSkin.Hazmat, "hazmat-eject.png" },
        { AmogusSkin.Mechanic, "mech-eject.png" },
        { AmogusSkin.Military, "military-eject.png" },
        { AmogusSkin.Miner, "miner-eject.png" },
        { AmogusSkin.Police, "police-eject.png" },
        { AmogusSkin.SecurityGuard, "secguard-eject.png" },
        { AmogusSkin.Scientist, "sci-eject.png" },
        { AmogusSkin.BlackSuit, "suitBlack-eject.png" },
        { AmogusSkin.WhiteSuit, "suitWhite-eject.png" },
        { AmogusSkin.Tarmac, "tarmac-eject.png" },
        { AmogusSkin.Wall, "wall-eject.png" },
        { AmogusSkin.Winter, "winter-eject.png" },
    };

    public static readonly Dictionary<AmogusSkin, Point> IdleOffset = new()
    {
        { AmogusSkin.Archaeologist, new(13, 41) },
        { AmogusSkin.Astronaut, new(13, 46) },
        { AmogusSkin.Captain, new(14, 45) },
        { AmogusSkin.Hazmat, new(12, 34) },
        { AmogusSkin.Mechanic, new(13, 46) },
        { AmogusSkin.Military, new(11, 45) },
        { AmogusSkin.Miner, new(13, 40) },
        { AmogusSkin.Police, new(10, 45) },
        { AmogusSkin.SecurityGuard, new(13, 42) },
        { AmogusSkin.Scientist, new(14, 43) },
        { AmogusSkin.BlackSuit, new(14, 44) },
        { AmogusSkin.WhiteSuit, new(14, 44) },
        { AmogusSkin.Tarmac, new(14, 40) },
        { AmogusSkin.Wall, new(10, 44) },
        { AmogusSkin.Winter, new(9, 35) },
    };

    public static readonly Dictionary<AmogusSkin, Point> EjectOffset = new()
    {
        { AmogusSkin.Archaeologist, new(12, 35) },
        { AmogusSkin.Astronaut, new(12, 35) },
        { AmogusSkin.Captain, new(12, 35) },
        { AmogusSkin.Hazmat, new(13, 37) },
        { AmogusSkin.Mechanic, new(13, 35) },
        { AmogusSkin.Military, new(12, 35) },
        { AmogusSkin.Miner, new(12, 36) },
        { AmogusSkin.Police, new(12, 35) },
        { AmogusSkin.SecurityGuard, new(13, 35) },
        { AmogusSkin.Scientist, new(-10, 35) },
        { AmogusSkin.BlackSuit, new(12, 35) },
        { AmogusSkin.WhiteSuit, new(11, 35) },
        { AmogusSkin.Tarmac, new(14, 37) },
        { AmogusSkin.Wall, new(12, 35) },
        { AmogusSkin.Winter, new(5, 30) },
    };

    public static string IdleRoot, EjectRoot, CommonRoot;

    public const string Arial = "Arial";
    #endregion CoreVariables

    public static void Configure(IWebHostEnvironment appEnvironment)
    {
        string amogusBase = Path.Combine(appEnvironment.WebRootPath, "amogus");

        IdleRoot = Path.Combine(amogusBase, "idle");
        EjectRoot = Path.Combine(amogusBase, "eject");
        CommonRoot = Path.Combine(amogusBase, "common");
    }

    /// <summary>
    /// Generates a base image for an AmongUs Asset that needs color replacement
    /// The default image is the standing crewmate (idle) and the default colour is blue
    /// </summary>
    /// <param name="baseImageName">The image to load up</param>
    /// <param name="colour">The colour to make the base image, must be one <see cref="AmogusColour"/></param>
    /// <returns>A <see cref="MagickImage"/> generated from <paramref name="baseImageName"/></returns>
    static MagickImage GenerateBase(string baseImageName = "idle.png", AmogusColour colour = AmogusColour.Blue)
    {
        string baseIdle = Path.Combine(CommonRoot, baseImageName);
        MagickImage body = new MagickImage(baseIdle);

        body = ColourReplace(body, colour);
        body.BackgroundColor = MagickColors.Transparent;

        return body;
    }

    /// <summary>
    /// Replaces the Colour of an AmogieCrewMate with the target colour
    /// </summary>
    /// <param name="image">Image to Colour Replace</param>
    /// <param name="colour">Colour to replace with</param>
    /// <returns>Colour Replaced Amogie Crewmate</returns>
    static MagickImage ColourReplace(MagickImage image, AmogusColour colour = AmogusColour.Blue)
    {
        var colours = new ColourHelper(colour);

        var pixels = image.GetPixels();

        for (int y = 0; y < image.Height; y++)
        {
            for (int x = 0; x < image.Width; x++)
            {
                var pixel = pixels.GetPixel(x, y);
                var pixelColor = pixel.ToColor();

                if (pixelColor is null) continue;

                float pixelSum = pixelColor.R + pixelColor.G + pixelColor.B;

                var areEqual = pixelColor.R == pixelColor.G && pixelColor.G == pixelColor.B;

                if (pixelColor.A == 0 || areEqual) continue;

                var pixBr = pixelColor.ToByteArray();

                int SumWithinRange(byte lower, byte upper)
                {
                    int count = 0;

                    if (pixelColor.R >= lower && pixelColor.R <= upper) count++;
                    if (pixelColor.G >= lower && pixelColor.G <= upper) count++;
                    if (pixelColor.B >= lower && pixelColor.B <= upper) count++;

                    return count;
                }

                if (SumWithinRange(45, 65) < 3)
                {
                    byte[] 
                        newR = Enumerable.Repeat((byte)(pixelColor.R / 255), 3).ToArray(), 
                        newG = new byte[3], 
                        newB = Enumerable.Repeat((byte)(pixelColor.B / 255), 3).ToArray();

                    var rBArr = colours.Red.ToByteArray();
                    var gBArr = colours.Green.ToByteArray();
                    var bBArr = colours.Blue.ToByteArray();

                    int m = 0;
                    foreach (var mask in gBArr)
                    {
                        if (m == 3) break;
                        newG[m] = (byte)(mask * pixelColor.G / 255);
                        m++;
                    }

                    if (pixelColor.G == 0 || pixelSum is >= 508 and <= 520)
                    {
                        m = 0;
                        foreach (var mask in rBArr)
                        {
                            if (m == 3) break;
                            newR[m] = (byte)(mask * pixelColor.R / 255);
                            m++;
                        }


                        m = 0;
                        foreach (var mask in bBArr)
                        {
                            if (m == 3) break;
                            newB[m] = (byte)(mask * pixelColor.B / 255);
                            m++;
                        }
                    }
                    else if (pixelColor.R == pixelColor.B && pixelSum > 512)
                    {
                        m = 0;
                        foreach (var mask in newG)
                        {
                            if (m == 3) break;
                            byte val = (byte)(((pixelColor.R / 255) * (255 - mask)) / 2);
                            newR[m] = val;
                            newB[m] = val;
                        }
                    }

                    byte minR = (byte)Math.Min(newR[0] + newG[0] + newB[0], 255);
                    byte minG = (byte)Math.Min(newR[1] + newG[1] + newB[1], 255);
                    byte minB = (byte)Math.Min(newR[2] + newG[2] + newB[2], 255);

                    byte[] newColors = { minR, minG, minB, pixelColor.A };
                    
                    pixels.SetPixel(x, y, newColors);
                }
            }
        }

        image.ImportPixels(pixels.ToArray(), new PixelImportSettings(0, 0, image.Width, image.Height, StorageType.Quantum, PixelMapping.RGBA));

        return image;
    }

    /// <summary>
    /// Applies the Eject variant of the Skin
    /// </summary>
    /// <param name="baseImage">Base Crewmate Image</param>
    /// <param name="layerImage">Skin to Apply</param>
    /// <param name="idleOffset">Skin offset from <see cref="baseImage"/></param>
    /// <param name="baseOrigin">Origin of <see cref="baseImage"/></param>
    /// <returns>Crewmate with Skin Applied</returns>
    static MagickImage ApplyLayer(MagickImage baseImage, MagickImage layerImage, Point layerOrigin, Point baseOrigin)
    {
        int left = layerOrigin.X + baseOrigin.X;
        int top = layerOrigin.Y + baseOrigin.Y;
        int right = layerOrigin.X + layerImage.Width - baseImage.Width;
        int bottom = layerOrigin.Y + layerImage.Height - baseImage.Height;
        
        left = left < 0 ? Math.Abs(left) : 0;
        top = top < 0 ? Math.Abs(top) : 0;

        MagickImage newImage = new MagickImage(MagickColors.Transparent, left + right + baseImage.Width, top + bottom + baseImage.Height);
        newImage.BackgroundColor = MagickColors.Transparent;
        
        baseOrigin = new Point(left, top);
        Point newLayerOrigin = new Point(baseOrigin.X + layerOrigin.X, baseOrigin.Y + layerOrigin.Y);

        newImage.Composite(baseImage, baseOrigin.X, baseOrigin.Y, CompositeOperator.Over);
        newImage.Composite(layerImage, newLayerOrigin.X, newLayerOrigin.Y, CompositeOperator.Over);

        return newImage;
    }

    /// <summary>
    /// Generate a crewmate with the given settings
    /// </summary>
    /// <param name="colour">Colour of the crewmate</param>
    /// <param name="skin">Skin that the crewmate is wearing</param>
    /// <param name="ejected">If the crewmate has been ejected or not</param>
    /// <returns>Generated crewmate with the given settings</returns>
    static MagickImage GenerateCrewmate(AmogusColour colour, AmogusSkin skin, bool ejected = true)
    {
        MagickImage baseImage = GenerateBase(ejected ? "ejected.png" : "idle.png", colour);
        MagickImage skinImage = null;
            
        if (skin != AmogusSkin.None)
        {
            skinImage = new MagickImage(
                Path.Combine(
                    ejected ? EjectRoot : IdleRoot, 
                    ejected ? EjectSkins[skin] : IdleSkins[skin]
                )
            );
        }

        var outImage = skinImage != null ? 
            ApplyLayer(
                baseImage, 
                skinImage, 
                ejected ? EjectOffset[skin] : IdleOffset[skin],  new Point(0, 0)
            ) : baseImage;
        
        outImage.Trim();

        return outImage;
    }

    /// <summary>
    /// Generate Amongus Stars
    /// </summary>
    /// <param name="width">Width of the stars</param>
    /// <returns>Generated image of stars</returns>
    static MagickImage GenerateStars(int width = 2000)
    {
        MagickImage baseImage = new MagickImage("xc:transparent", width, 588);
        
        using MagickImage stars = new MagickImage(Path.Combine(CommonRoot, "Stars.png"));
        stars.Distort(DistortMethod.ScaleRotateTranslate, 90);
        using MagickImage stars2 = new MagickImage(stars);
        stars2.Distort(DistortMethod.ScaleRotateTranslate, 180);

        foreach (var x in CollectionExtensions.Range(-800, width, 320))
        {
            baseImage.Composite(stars2, Gravity.Northwest, x, (int)(x / 60f) - 200, CompositeOperator.Over);
            baseImage.Composite(stars, Gravity.Northwest, x, 100 + (int)(x / 120f), CompositeOperator.Over);
            baseImage.Composite(stars2, Gravity.Northwest, x, 300 - (int)(x / 60f), CompositeOperator.Over);
            baseImage.Composite(stars, Gravity.Northwest, x, 500 + (int)(x / 120f), CompositeOperator.Over);
            baseImage.Composite(stars, Gravity.Northwest, x + 160 + (int)(x / 80f), (int)(x / 100f) - 100, CompositeOperator.Over);
            baseImage.Composite(stars2, Gravity.Northwest, x + 160 - (int)(x / 80f), 100 + (int)(x / 120f), CompositeOperator.Over);
            baseImage.Composite(stars, Gravity.Northwest, x + 160 + (int)(x / 80f), 300 - (int)(x / 100f), CompositeOperator.Over);
            baseImage.Composite(stars2, Gravity.Northwest, x + 160 - (int)(x / 80f), 500 - (int)(x / 100f), CompositeOperator.Over);
        }

        return baseImage;
    }

    static string GenerateEjectionMessage(string name = "I", AmogusRole role = AmogusRole.Unknown)
    {
        StringBuilder message = new StringBuilder(name);
        message.Append(" was");

        if (role == AmogusRole.Unknown)
        {
            message.Append(" ejected.");
            return message.ToString();
        }

        if (role == AmogusRole.Crewmate)
        {
            message.Append(" not");
        }

        message.Append(" An Impostor.");

        return message.ToString();
    }

    /// <summary>
    /// Generates the Ejection Gif with all modifications applied
    /// </summary>
    /// <param name="name"></param>
    /// <param name="colour"></param>
    /// <param name="skin"></param>
    /// <param name="role"></param>
    /// <returns>A <see cref="MemoryStream"/> containing the Amogus Ejected Gif</returns>
    public static MagickImageCollection GenerateEjectionGif(string name, AmogusColour? colour = null, AmogusRole? role = null, AmogusSkin? skin = null, bool addStars = true)
    {
        if (role is null) role = EnumHelper.GetRandomEnumValue<AmogusRole>();

        return GenerateCustomEjectionGif(GenerateEjectionMessage(name, role.Value), colour, role, skin, addStars);
    }

    public static MagickImageCollection GenerateCustomEjectionGif(string text, AmogusColour? colour = null, AmogusRole? role = null, AmogusSkin? skin = null, bool addStars = true)
    {
        if (colour is null) colour = EnumHelper.GetRandomEnumValue<AmogusColour>();
        if (role is null) role = EnumHelper.GetRandomEnumValue<AmogusRole>();
        if (skin is null) skin = EnumHelper.GetRandomEnumValue<AmogusSkin>();
        
        var crewmate = GenerateCrewmate(colour.Value, skin.Value);
        crewmate = MakeSquare(crewmate);
        var textImg = new MagickImage(MagickColors.Transparent, 4000, 64);
        textImg.WriteText(text, 0, 0, MagickColors.White, Encoding.Default, 24, Arial, 0, MagickColors.Transparent);
        textImg.Crop(textImg.BoundingBox);
        textImg.RePage();

        (int X, int Y) center = new(crewmate.Width / 2, crewmate.Height / 2);

        using var background = new MagickImage(MagickColors.Black, Math.Max(textImg.Width + 100, 600), 300);

        MagickImage stars = null, stars1 = null, stars2 = null, stars3 = null;

        if (addStars)
        {
            stars = GenerateStars(background.Width * 3);
            
            stars1 = stars.Clone() as MagickImage;
            stars1.BackgroundColor = MagickColors.Transparent;
            stars1.Resize((int)(stars.Width * 3.5f), (int)(stars.Height * 3.5f));

            stars2 = stars1.Clone() as MagickImage;
            stars2.BackgroundColor = MagickColors.Transparent;
            
            stars3 = stars1.Clone() as MagickImage;
            stars3.BackgroundColor = MagickColors.Transparent;
        }

        var range = CollectionExtensions.Range(-120, background.Width * 3, 12);

        MagickImageCollection gifFrames = new MagickImageCollection();
        
        foreach (var bodyX in range)
        {
            var image = background.Clone() as MagickImage;

            if (addStars)
            {
                image.Composite(stars1, -background.Width + (int)(bodyX / 24f) - 100, -200, CompositeOperator.Over);
                image.Composite(stars2, -background.Width + (int)(bodyX / 12f) - 200, -800, CompositeOperator.Over);
                image.Composite(stars3, -background.Width + (int)(bodyX / 6f) - 100, -425, CompositeOperator.Over);
            }

            using var rotatedBody = crewmate.Clone() as MagickImage;
            rotatedBody.BackgroundColor = MagickColors.Transparent;
            rotatedBody.Distort(DistortMethod.ScaleRotateTranslate, -(2 * bodyX / 3));
            rotatedBody.RePage();
            
            var rotateX = bodyX - center.X;
            var rotateY = background.Height / 2 - center.Y;
            
            image.Composite(rotatedBody, Gravity.Northwest, rotateX, rotateY, CompositeOperator.Atop);
            
            using var txt = textImg.Clone() as MagickImage;
            txt.BackgroundColor = MagickColors.Transparent;
            
            if (bodyX > background.Width / 2 && bodyX <= 3 * background.Width / 2)
            {
                var midX = background.Width / 2;
                var textY = background.Height / 2 - txt.Height / 2;
                var textX = Convert.ToInt32(midX - ((float)bodyX - midX) / background.Width * txt.Width / 2f);
                var textWidth = (midX - textX) * 2; 
                
                txt.Crop(new MagickGeometry(0, 0, textWidth, txt.Height));
                
                image.Composite(txt, Gravity.Northwest, textX, textY, CompositeOperator.Over);
            }
            else if (bodyX > (int)(background.Width * 1.5f) && bodyX <= (int)(background.Width * 2.5f))
            {
                image.Composite(txt, (int)(background.Width / 2f - txt.Width / 2f), (int)(background.Height / 2f - txt.Height / 2f), CompositeOperator.Atop);
            }
            
            image.AnimationIterations = 0;
            image.AnimationDelay = 4;

            gifFrames.Add(image);
        }

        if (addStars)
        {
            stars.Dispose();
            stars1.Dispose();
            stars2.Dispose();
            stars3.Dispose();
        }

        crewmate.Dispose();
        
        return gifFrames;
    }
    
    #region Utilities
    static MagickImage MakeSquare(MagickImage image)
    {
        int size = Math.Max(image.Width, image.Height);

        MagickImage newImage = new MagickImage("xc:transparent", size, size);
        newImage.Format = MagickFormat.Png;

        int x = (size - image.Width) / 2;
        int y = (size - image.Height) / 2;
        
        newImage.Composite(image, Gravity.Center, x, y, CompositeOperator.Over);

        return newImage;
    }
    #endregion Utilities
}