using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RecrownedAthenaeum.Render;
using System;
using System.Collections.Generic;
using System.Linq;

namespace RecrownedAthenaeum.SpecialTypes
{
    /// <summary>
    /// Holds information about an image file that contains various textures in various regions in the file.
    /// </summary>
    public class TextureAtlas : IDisposable
    {
        private Texture2D texture;
        private bool disposed;
        private Dictionary<string, Region> dictionaryOfRegions = new Dictionary<string, Region>();

        /// <summary>
        /// Given a name, can return a <see cref="Region"/>.
        /// </summary>
        /// <param name="name">Name of <see cref="Region"/> to obtain.</param>
        /// <returns><see cref="Region"/> based off name.</returns>
        public Region this[string name] { get { if (name != null && dictionaryOfRegions.ContainsKey(name)) return dictionaryOfRegions[name]; else throw new KeyNotFoundException("Given key \"" + name + "\" does not exist."); } }

        /// <summary>
        /// Creates a texture atlas with given main texture as well as an array of <see cref="Region"/> to represent locations of which textures reside within the atlas. Region names will be used to refer to the regions within the dictionary.
        /// </summary>
        /// <param name="texture">The texture representing the overall atlas.</param>
        /// <param name="regions">The sub regions that represent the individual textures.</param>
        public TextureAtlas(Texture2D texture, Region[] regions)
        {
            this.texture = texture;
            foreach (Region region in regions)
            {
                dictionaryOfRegions.Add(region.name, region);
            }
        }

        /// <summary>
        /// Creates a texture region given a dictionary of regions keyed to strings that can be used to refer to them.
        /// </summary>
        /// <param name="texture">The texture representing the overall atlas.</param>
        /// <param name="dictionaryOfRegions"></param>
        public TextureAtlas(Texture2D texture, Dictionary<string, Region> dictionaryOfRegions)
        {
            this.texture = texture;
            this.dictionaryOfRegions = dictionaryOfRegions;
        }

        /// <summary>
        /// Draw the region given by a string in the atlas onto a destination rectangle.
        /// </summary>
        /// <param name="name">Name of region to draw.</param>
        /// <param name="batch">SpriteBatch to be used.</param>
        /// <param name="destination">The location to draw this region.</param>
        /// <param name="color">Color to use.</param>
        /// <param name="rotation">Rotation of texture drawn.</param>
        /// <param name="origin">Origin used by rotation.</param>
        public void Draw(string name, ConsistentSpriteBatch batch, Rectangle destination, Color color = default(Color), float rotation = 0, Vector2 origin = new Vector2())
        {
            dictionaryOfRegions[name].Draw(batch, destination, color, rotation, origin);
        }

        /// <summary>
        /// Creates or obtains a previously created texture of a region.
        /// </summary>
        /// <param name="name">Name of region.</param>
        /// <param name="graphicsDevice">graphics device to be used to generate the texture.</param>
        /// <returns>The texture from the region.</returns>
        public Texture2D ObtainRegionAsTexture(string name, GraphicsDevice graphicsDevice)
        {
            return dictionaryOfRegions[name].AsTexture2D(graphicsDevice);
        }

        /// <summary>
        /// Whether or not this atlas contains the given region name.
        /// </summary>
        /// <param name="regionName">The name of the region to check for.</param>
        /// <returns>True if this atlas does contain the region given by name.</returns>
        public bool ContainsRegion(string regionName)
        {
            return dictionaryOfRegions.ContainsKey(regionName);
        }

        /// <summary>
        /// Disposes unmanaged resources for the texture atlas.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>
        /// Overridable disposal method.
        /// </summary>
        /// <param name="disposing">Only true if user calls <see cref="Dispose()"/></param>
        public virtual void Dispose(bool disposing)
        {
            disposed = true;
            if (!disposed && disposing)
            {
                texture.Dispose();
                for (int i = 0; i < dictionaryOfRegions.Count; i++)
                {
                    Region region = dictionaryOfRegions.ElementAt(i).Value;

                    if (!region.Disposed)
                    {
                        dictionaryOfRegions.ElementAt(i).Value.Dispose();
                    }
                }
                dictionaryOfRegions.Clear();
            }
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~TextureAtlas()
        {
            Dispose(false);
        }

        /// <summary>
        /// A region of a <see cref="TextureAtlas"/>.
        /// </summary>
        public class Region : ISpecialDrawable, IDisposable
        {
            /// <summary>
            /// The name of the region. Mostly used to be refered to within the context of a <see cref="TextureAtlas"/>.
            /// </summary>
            public readonly string name;

            /// <summary>
            /// The location and dimensions of where the original texture resides on the texture representing the atlas.
            /// </summary>
            public readonly Rectangle sourceRectangle;
            readonly NinePatch ninepatch;
            Texture2D atlasTexture;
            Texture2D regionTexture;

            /// <summary>
            /// If region has already been disposed.
            /// </summary>
            public bool Disposed { get; private set; }

            /// <summary>
            /// A specified region in a texture atlas.
            /// </summary>
            /// <param name="name">Name of region.</param>
            /// <param name="sourceRegion">The location of the region on the atlas.</param>
            /// <param name="ninePatch">A <see cref="NinePatch"/> definition for the region.</param>
            /// <param name="atlasTexture">The texture that holds the image data for the atlas.</param>
            public Region(string name, Rectangle sourceRegion, NinePatch ninePatch, Texture2D atlasTexture)
            {
                this.atlasTexture = atlasTexture ?? throw new ArgumentNullException("Name parameters can be null.");
                this.name = name ?? throw new ArgumentNullException("Name parameters can be null.");
                sourceRectangle = sourceRegion;
                ninepatch = ninePatch;
            }

            /// <summary>
            /// Draws the region. If ninepatch, rotation and origin are ignored.
            /// </summary>
            /// <param name="batch">The batch to use. Should be began.</param>
            /// <param name="destination">The destination rectangle to draw to.</param>
            /// <param name="color">The color to use.</param>
            /// <param name="rotation">Rotation of the final drawing. Ignored if is a 9patch.</param>
            /// <param name="origin">The origin of the drawing. Ignored if is a 9patch.</param>
            public void Draw(ConsistentSpriteBatch batch, Rectangle destination, Color color, float rotation = 0, Vector2 origin = default(Vector2))
            {
                if (Disposed) throw new ObjectDisposedException(GetType().Name);

                if (ninepatch != null)
                {
                    ninepatch.Draw(batch, Color.White, destination);
                }
                else
                {
                    batch.Draw(atlasTexture, destination, sourceRectangle, color, rotation, origin, SpriteEffects.None, 0f);
                }
            }

            /// <summary>
            /// Create or obtains a previously created texture of this region.
            /// </summary>
            /// <param name="graphicsDevice">The graphics device to use to create the texture.</param>
            /// <returns>The texture of the region.</returns>
            public Texture2D AsTexture2D(GraphicsDevice graphicsDevice)
            {
                if (Disposed) throw new ObjectDisposedException(GetType().Name);

                if (regionTexture == null)
                {
                    Color[] data = new Color[sourceRectangle.Width * sourceRectangle.Height];
                    regionTexture = new Texture2D(graphicsDevice, sourceRectangle.Width, sourceRectangle.Height);
                    atlasTexture.GetData(0, sourceRectangle, data, 0, sourceRectangle.Width * sourceRectangle.Height);
                    regionTexture.SetData(data);
                }
                return regionTexture;
            }

            /// <summary>
            /// Call this to dispose.
            /// </summary>
            public void Dispose()
            {
                if (Disposed) throw new ObjectDisposedException(GetType().Name);
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            /// <summary>
            /// Overridable dispose.
            /// </summary>
            /// <param name="disposing">Whether or not this was a user made call.</param>
            public virtual void Dispose(bool disposing)
            {
                if (disposing && !Disposed)
                {
                    regionTexture?.Dispose();
                }
                Disposed = true;
            }

            /// <summary>
            /// Destructor.
            /// </summary>
            ~Region()
            {
                Dispose(false);
            }
        }
    }
}
