using Newtonsoft.Json;
using RecrownedAthenaeum.Data;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;

namespace RecrownedAthenaeum.Tools.TextureAtlas
{

    public class TexturePacker
    {
        private enum SupportedExtensions
        {
            jpeg, jpg, png
        }

        int powLimit;
        Node masterNode;
        List<ImageHandler> imageHandlers;
        Dictionary<string, NinePatchData> ninePatchDictionary;
        int tpl;
        int TexturePowerLength { get { return tpl; } set { TextureLength = (int)Math.Pow(2, value); tpl = value; } }
        public int TextureLength { get; private set; }
        public int TexturesFound { get { return imageHandlers.Count; } }
        /// <summary>
        /// Machine to pack multiple textures into one large texture.
        /// </summary>
        /// <param name="rootDirectoryPath">Path to textures.</param>
        /// <param name="powLimit">Power of two limit for auto expanding texture. Default is 12 which is a 4096x4096 texture.</param>
        /// <param name="startingPower">What power to start at and build up from. Default is 8 which is a 256x256 texture.</param>
        internal TexturePacker(string rootDirectoryPath, int powLimit = 12, int startingPower = 8)
        {
            this.powLimit = powLimit;
            string[] paths;
            try
            {
                paths = Directory.GetFiles(rootDirectoryPath);
            }
            catch (IOException)
            {
                throw new ArgumentException("Path " + rootDirectoryPath + " couldn't be resolved.");
            }
            TexturePowerLength = startingPower;
            List<ImageHandler> imageHandlers = new List<ImageHandler>();
            int minAreaRequired = 0;
            for (int pathID = 0; pathID < paths.Length; pathID++)
            {
                SupportedExtensions extension;
                if (Enum.TryParse(Path.GetExtension(paths[pathID]).ToLower().Substring(1), out extension))
                {
                    ConsoleUtilities.WriteWrappedLine("Reading texture data for: " + paths[pathID]);
                    ImageHandler image = new ImageHandler(paths[pathID]);
                    imageHandlers.Add(image);
                    minAreaRequired += image.Area;
                    while (minAreaRequired > TextureLength * TextureLength)
                    {
                        TexturePowerLength++;
                    }
                }
                else if (Path.GetExtension(paths[pathID]).ToLower() == ".9p")
                {
                    if (ninePatchDictionary == null) ninePatchDictionary = new Dictionary<string, NinePatchData>();
                    ConsoleUtilities.WriteWrappedLine("Reading ninepatch data for: " + paths[pathID]);
                    string serialized = File.ReadAllText(paths[pathID]);
                    NinePatchData npData = JsonConvert.DeserializeObject<NinePatchData>(serialized);
                    ninePatchDictionary.Add(npData.textureName, npData);
                }
            }
            imageHandlers.Sort();
            this.imageHandlers = imageHandlers;
        }

        /// <summary>
        /// Builds a texture atlas.
        /// </summary>
        /// <param name="AutoCorrectAtlasSize">Whether or not to automatically upscale atlas' texture in the case it is too small. Goes up to 4096 by default.</param>
        public void Build(bool AutoCorrectAtlasSize = true)
        {
            masterNode = new Node();
            masterNode.region.Width = TextureLength;
            masterNode.region.Height = TextureLength;
            Queue<ImageHandler> imageHandlerQueue = new Queue<ImageHandler>(imageHandlers);
            ImageHandler imageHandler;
            while (imageHandlerQueue.TryDequeue(out imageHandler))
            {
                Node activeNode = null;
                activeNode = masterNode.InsertImageHandler(imageHandler);
                if (activeNode == null)
                {
                    if (!AutoCorrectAtlasSize || TexturePowerLength + 1 > powLimit)
                    {
                        throw new InvalidOperationException("Texture not large enough. Current size: " + TextureLength + "x" + TextureLength + ".");
                    }
                    TexturePowerLength += 1;
                    imageHandlerQueue.Clear();
                    Build(AutoCorrectAtlasSize);
                }
                if (ninePatchDictionary != null && ninePatchDictionary.ContainsKey(imageHandler.name))
                {
                    NinePatchData npd = ninePatchDictionary[imageHandler.name];
                    imageHandler.ninePatchData = npd;
                    if (npd.textureName.Contains("-texture"))
                    {
                        imageHandler.name = imageHandler.name.Remove(imageHandler.name.IndexOf("-texture"), 8);
                    }
                }
            }

        }

        /// <summary>
        /// Renders the build into a PNG file and generates the respective <see cref="TextureAtlasData"/> meant for serialization and later to be loaded.
        /// </summary>
        /// <param name="output">directory to output to.</param>
        /// <param name="atlasName">name of atlas.</param>
        public void Save(string output, string atlasName)
        {
            GraphicsOptions gOptions = new GraphicsOptions();

            TextureAtlasData.AtlasRegionData[] regions = new TextureAtlasData.AtlasRegionData[TexturesFound];

            using (Image<Rgba32> atlasTexture = new Image<Rgba32>(TextureLength, TextureLength))
            {
                ImageHandler[] imageHandlers = this.imageHandlers.ToArray();

                for (int i = 0; i < imageHandlers.Length; i++)
                {
                    regions[i] = new TextureAtlasData.AtlasRegionData();
                    ImageHandler ih = imageHandlers[i];
                    regions[i].SetBounds(ih.x, ih.y, ih.Width, ih.Height);
                    regions[i].ninePatchData = ih.ninePatchData;
                    if (regions[i].ninePatchData != null) regions[i].ninePatchData.textureName = null;
                    regions[i].name = Path.GetFileNameWithoutExtension(ih.name);
                    using (Image<Rgba32> image = Image.Load<Rgba32>(ih.path))
                    {
                        atlasTexture.Mutate(img => img.DrawImage(gOptions, image, new Point(ih.x, ih.y)));
                    }
                }
                Directory.CreateDirectory(output);
                using (FileStream stream = new FileStream(output + "/" + atlasName + "-texture" + ".png", FileMode.Create))
                {
                    atlasTexture.SaveAsPng(stream);
                }
            }
            string serialized = JsonConvert.SerializeObject(new TextureAtlasData(atlasName + "-texture" + ".png", regions), Formatting.Indented);
            File.WriteAllText(output + "/" + atlasName + ".tatlas", serialized);
        }

        public void SetNinePatch(string fileName, int a, int b, int c, int d)
        {
            NinePatchData ninePatchData = new NinePatchData(fileName, a, b, c, d);
            RetrieveImageHandler(fileName).ninePatchData = ninePatchData;
        }

        public void RemoveNinePatch(string name)
        {

            RetrieveImageHandler(name).ninePatchData = null;
        }

        private ImageHandler RetrieveImageHandler(string name)
        {
            for (int i = 0; i < TexturesFound; i++)
            {
                if (imageHandlers[i].name == name)
                {
                    return imageHandlers[i];
                }
            }
            throw new ArgumentException("Couldn't find texture with name: " + name);
        }

        private class Node
        {
            public Node parent;
            private Node a, b;
            public Node childA { get { if (a == null) a = new Node(this); return a; } set { value.parent = this; a = value; } }
            public Node childB { get { if (b == null) { b = new Node(this); } return b; } set { value.parent = this; b = value; } }
            public Rectangle region;
            public bool ContainsImage = false;
            public bool CanPlaceImage { get { return (a == null && b == null && !ContainsImage); } }
            public Node(Node parent = null)
            {
                this.parent = parent;
                if (parent != null) region = parent.region;
            }

            /// <summary>
            /// Attempts to insert image within the node. This builds the node to have children if needed.
            /// </summary>
            /// <param name="imageHandler">the image to insert.</param>
            /// <returns>The node the image is placed in.</returns>
            public Node InsertImageHandler(ImageHandler imageHandler)
            {
                if (imageHandler.Width != region.Width)
                {
                    if (imageHandler.Width < region.Width)
                    {
                        if (a == null)
                        {
                            childA.region.Width = imageHandler.Width;
                        }
                        Node attemptedNode = null;
                        if (!childA.ContainsImage && imageHandler.Width <= childA.region.Width)
                        {
                            attemptedNode = childA.InsertImageHandler(imageHandler);
                        }

                        if (attemptedNode == null && !childB.ContainsImage)
                        {
                            childB.region.Width = region.Width - childA.region.Width;
                            childB.region.X = childA.region.X + childA.region.Width;
                            attemptedNode = childB.InsertImageHandler(imageHandler);
                        }

                        return attemptedNode;
                    }
                }
                else if (imageHandler.Height != region.Height)
                {
                    if (imageHandler.Height < region.Height)
                    {
                        if (a == null)
                        {
                            childA.region.Height = imageHandler.Height;
                        }
                        Node attemptedNode = null;
                        if (!childA.ContainsImage && imageHandler.Height <= childA.region.Height)
                        {
                            attemptedNode = childA.InsertImageHandler(imageHandler);
                        }

                        if (attemptedNode == null && !childB.ContainsImage)
                        {
                            childB.region.Height = region.Height - childA.region.Height;
                            childB.region.Y = childA.region.Y + childA.region.Height;
                            attemptedNode = childB.InsertImageHandler(imageHandler);
                        }
                        return attemptedNode;
                    }
                }
                else if (CanPlaceImage)
                {
                    imageHandler.x = region.X;
                    imageHandler.y = region.Y;
                    ContainsImage = true;
                    return this;
                }
                return null;
            }
        }

        private class ImageHandler : IComparable<ImageHandler>
        {
            public readonly string path;
            public readonly IImageInfo image;
            public int Area { get { return image.Width * image.Height; } }
            public string name;
            public int Width { get { return image.Width; } }
            public int Height { get { return image.Height; } }
            public int x, y;
            public NinePatchData ninePatchData;

            internal ImageHandler(string path)
            {
                this.path = path;
                name = Path.GetFileName(path);
                try
                {
                    using (FileStream stream = new FileStream(path, FileMode.Open))
                    {
                        image = Image.Identify(stream);
                    }
                } catch (SecurityException)
                {
                    throw new ArgumentException("Security exception occurred for image: " + path);
                }
            }

            public int CompareTo(ImageHandler tImage)
            {
                return Area - tImage.Area;
            }
        }
    }
}
