using System;
using System.IO;
using UnityEngine;
using UnityEditorInternal;
using System.Collections.Generic;
using System.Text;
using UnityTexture2D = UnityEngine.Texture2D;
using UnityEditor.ShortcutManagement;

namespace UnityEditor.U2D.Sprites
{
    [RequireSpriteDataProvider(typeof(ITextureDataProvider))]
    internal partial class SpriteFrameModule : SpriteFrameModuleBase
    {
        public enum AutoSlicingMethod
        {
            DeleteAll = 0,
            Smart = 1,
            Safe = 2
        }

        private bool[] m_AlphaPixelCache;
        SpriteFrameModuleContext m_SpriteFrameModuleContext;

        private const float kOverlapTolerance = 0.00001f;
        private StringBuilder m_SpriteNameStringBuilder;

        private List<Rect> m_PotentialRects;
        public List<Rect> potentialRects
        {
            set => m_PotentialRects = value;
        }

        public SpriteFrameModule(ISpriteEditor sw, IEventSystem es, IUndoSystem us, IAssetDatabase ad) :
            base("Sprite Editor", sw, es, us, ad)
        {}

        class SpriteFrameModuleContext : IShortcutToolContext
        {
            SpriteFrameModule m_SpriteFrameModule;

            public SpriteFrameModuleContext(SpriteFrameModule spriteFrame)
            {
                m_SpriteFrameModule = spriteFrame;
            }

            public bool active
            {
                get { return true; }
            }
            public SpriteFrameModule spriteFrameModule
            {
                get { return m_SpriteFrameModule; }
            }
        }

        [FormerlyPrefKeyAs("Sprite Editor/Trim", "#t")]
        [Shortcut("Sprite Editor/Trim", typeof(SpriteFrameModuleContext), KeyCode.T, ShortcutModifiers.Shift)]
        static void ShortcutTrim(ShortcutArguments args)
        {
            if (!string.IsNullOrEmpty(GUI.GetNameOfFocusedControl()))
                return;
            var spriteFrameContext = (SpriteFrameModuleContext)args.context;
            spriteFrameContext.spriteFrameModule.TrimAlpha();
            spriteFrameContext.spriteFrameModule.spriteEditor.RequestRepaint();
        }

        public override void OnModuleActivate()
        {
            base.OnModuleActivate();
            spriteEditor.enableMouseMoveEvent = true;
            m_SpriteFrameModuleContext = new SpriteFrameModuleContext(this);
            ShortcutIntegration.instance.contextManager.RegisterToolContext(m_SpriteFrameModuleContext);
            m_SpriteNameStringBuilder = new StringBuilder(GetSpriteNamePrefix() + "_");
            m_PotentialRects = null;
        }

        public override void OnModuleDeactivate()
        {
            base.OnModuleDeactivate();
            ShortcutIntegration.instance.contextManager.DeregisterToolContext(m_SpriteFrameModuleContext);
            m_PotentialRects = null;
            m_AlphaPixelCache = null;
        }

        public static SpriteImportMode GetSpriteImportMode(ISpriteEditorDataProvider dataProvider)
        {
            return dataProvider == null ? SpriteImportMode.None : dataProvider.spriteImportMode;
        }

        public override bool CanBeActivated()
        {
            return GetSpriteImportMode(spriteEditor.GetDataProvider<ISpriteEditorDataProvider>()) != SpriteImportMode.Polygon;
        }

        private string GenerateSpriteNameWithIndex(int startIndex)
        {
            int originalLength = m_SpriteNameStringBuilder.Length;
            m_SpriteNameStringBuilder.Append(startIndex);
            var name = m_SpriteNameStringBuilder.ToString();
            m_SpriteNameStringBuilder.Length = originalLength;
            return name;
        }

        // 1. Find top-most rectangle
        // 2. Sweep it vertically to find out all rects from that "row"
        // 3. goto 1.
        // This will give us nicely sorted left->right top->down list of rectangles
        // Works for most sprite sheets pretty nicely
        private List<Rect> SortRects(List<Rect> rects)
        {
            List<Rect> result = new List<Rect>();

            while (rects.Count > 0)
            {
                // Because the slicing algorithm works from bottom-up, the topmost rect is the last one in the array
                Rect r = rects[rects.Count - 1];
                Rect sweepRect = new Rect(0, r.yMin, textureActualWidth, r.height);

                List<Rect> rowRects = RectSweep(rects, sweepRect);

                if (rowRects.Count > 0)
                    result.AddRange(rowRects);
                else
                {
                    // We didn't find any rects, just dump the remaining rects and continue
                    result.AddRange(rects);
                    break;
                }
            }
            return result;
        }

        private List<Rect> RectSweep(List<Rect> rects, Rect sweepRect)
        {
            if (rects == null || rects.Count == 0)
                return new List<Rect>();

            List<Rect> containedRects = new List<Rect>();

            foreach (Rect rect in rects)
            {
                if (rect.Overlaps(sweepRect))
                    containedRects.Add(rect);
            }

            // Remove found rects from original list
            foreach (Rect rect in containedRects)
                rects.Remove(rect);

            // Sort found rects by x position
            containedRects.Sort((a, b) => a.x.CompareTo(b.x));

            return containedRects;
        }

        private int AddSprite(Rect frame, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, int originalCount, ref int nameIndex)
        {
            int outSprite = -1;
            switch (slicingMethod)
            {
                case AutoSlicingMethod.DeleteAll:
                {
                    while (outSprite == -1)
                    {
                        outSprite = AddSprite(frame, alignment, pivot, GenerateSpriteNameWithIndex(nameIndex++), Vector4.zero, false);
                    }
                }
                break;
                case AutoSlicingMethod.Smart:
                {
                    outSprite = GetExistingOverlappingSprite(frame, originalCount, true);
                    if (outSprite != -1)
                    {
                        var existingRect = m_RectsCache.spriteRects[outSprite];
                        existingRect.rect = frame;
                        existingRect.alignment = (SpriteAlignment)alignment;
                        existingRect.pivot = pivot;
                    }
                    else
                    {
                        while (outSprite == -1)
                        {
                            outSprite = AddSprite(frame, alignment, pivot, GenerateSpriteNameWithIndex(nameIndex++), Vector4.zero);
                        }
                    }
                }
                break;
                case AutoSlicingMethod.Safe:
                {
                    outSprite = GetExistingOverlappingSprite(frame, originalCount);
                    while (outSprite == -1)
                    {
                        outSprite = AddSprite(frame, alignment, pivot, GenerateSpriteNameWithIndex(nameIndex++), Vector4.zero);
                    }
                }
                break;
            }
            return outSprite;
        }

        private int GetExistingOverlappingSprite(Rect rect, int originalCount, bool bestFit = false)
        {
            var count = Math.Min(originalCount, m_RectsCache.spriteRects.Count);
            int bestRect = -1;
            float rectArea = rect.width * rect.height;
            if (rectArea < kOverlapTolerance)
                return bestRect;

            float bestRatio = float.MaxValue;
            float bestArea = float.MaxValue;
            for (int i = 0; i < count; i++)
            {
                Rect existingRect = m_RectsCache.spriteRects[i].rect;
                if (existingRect.Overlaps(rect))
                {
                    if (bestFit)
                    {
                        float dx = Math.Min(rect.xMax, existingRect.xMax) - Math.Max(rect.xMin, existingRect.xMin);
                        float dy = Math.Min(rect.yMax, existingRect.yMax) - Math.Max(rect.yMin, existingRect.yMin);
                        float overlapArea = dx * dy;
                        float overlapRatio = Math.Abs((overlapArea / rectArea) - 1.0f);
                        float existingArea = existingRect.width * existingRect.height;
                        if (overlapRatio < bestRatio || (overlapRatio < kOverlapTolerance && existingArea < bestArea))
                        {
                            bestRatio = overlapRatio;
                            if (overlapRatio < kOverlapTolerance)
                                bestArea = existingArea;
                            bestRect = i;
                        }
                    }
                    else
                    {
                        bestRect = i;
                        break;
                    }
                }
            }
            return bestRect;
        }

        private bool PixelHasAlpha(int x, int y, UnityTexture2D texture)
        {
            if (m_AlphaPixelCache == null)
            {
                m_AlphaPixelCache = new bool[texture.width * texture.height];
                Color32[] pixels = texture.GetPixels32();

                for (int i = 0; i < pixels.Length; i++)
                    m_AlphaPixelCache[i] = pixels[i].a != 0;
            }
            int index = y * (int)texture.width + x;
            return m_AlphaPixelCache[index];
        }

        private int AddSprite(Rect rect, int alignment, Vector2 pivot, string name, Vector4 border, bool uniqueNameCheck = true)
        {
            var sed = spriteEditor.GetDataProvider<ISpriteEditorDataProvider>();
            long internalID = AssetImporter.MakeLocalFileIDWithHash(spriteType.persistentTypeID, name, 0);
            if (m_RectsCache.HasName(name))
                return -1;
            if (m_RectsCache.HasInternalID(internalID))
                return -1;

            SpriteRect spriteRect = new SpriteRect();
            spriteRect.rect = rect;
            spriteRect.alignment = (SpriteAlignment)alignment;
            spriteRect.pivot = pivot;
            spriteRect.name = name;
            spriteRect.originalName = spriteRect.name;
            spriteRect.border = border;

            spriteRect.internalID = internalID;
            spriteRect.spriteID = GUID.CreateGUIDFromSInt64(internalID);

            // check if someone is using the internal id, if so, we change it to us.
            // Only TextureImporter needs this now.
            var ai = sed.targetObject as TextureImporter;
            var oldName = "";
            if (ai != null && ai.GetNameFromInternalIDMap(internalID, ref oldName))
            {
                if (string.IsNullOrEmpty(oldName))
                    return -1;
                spriteRect.originalName = oldName;
            }
            else
            {
                spriteRect.m_RegisterInternalID = true;
            }

            m_RectsCache.Add(spriteRect);
            spriteEditor.SetDataModified();

            return m_RectsCache.spriteRects.Count - 1;
        }

        private string GetSpriteNamePrefix()
        {
            return Path.GetFileNameWithoutExtension(spriteAssetPath);
        }

        public void DoAutomaticSlicing(int minimumSpriteSize, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod)
        {
            undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Automatic Slicing");

            if (slicingMethod == AutoSlicingMethod.DeleteAll)
                m_RectsCache.Clear();

            var textureToUse = GetTextureToSlice();
            List<Rect> frames = new List<Rect>(InternalSpriteUtility.GenerateAutomaticSpriteRectangles((UnityTexture2D)textureToUse, minimumSpriteSize, 0));
            frames = SortRects(frames);
            int index = 0;
            int originalCount = m_RectsCache.spriteRects.Count;

            foreach (Rect frame in frames)
                AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index);

            selected = null;
            spriteEditor.SetDataModified();
            Repaint();
        }

        UnityTexture2D GetTextureToSlice()
        {
            int width, height;
            m_TextureDataProvider.GetTextureActualWidthAndHeight(out width, out height);
            var readableTexture = m_TextureDataProvider.GetReadableTexture2D();
            if (readableTexture == null || (readableTexture.width == width && readableTexture.height == height))
                return readableTexture;
            // we want to slice based on the original texture slice. Upscale the imported texture
            var texture = UnityEditor.SpriteUtility.CreateTemporaryDuplicate(readableTexture, width, height);
            return texture;
        }

        public IEnumerable<Rect> GetGridRects(Vector2 size, Vector2 offset, Vector2 padding, bool keepEmptyRects)
        {
            var textureToUse = GetTextureToSlice();
            return InternalSpriteUtility.GenerateGridSpriteRectangles((UnityTexture2D)textureToUse, offset, size, padding, keepEmptyRects);
        }

        public void DoGridSlicing(Vector2 size, Vector2 offset, Vector2 padding, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, bool keepEmptyRects = false)
        {
            var frames = GetGridRects(size, offset, padding, keepEmptyRects);

            undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Grid Slicing");
            if (slicingMethod == AutoSlicingMethod.DeleteAll)
                m_RectsCache.Clear();

            int index = 0;
            int originalCount = m_RectsCache.spriteRects.Count;
            foreach (Rect frame in frames)
                AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index);

            selected = null;
            spriteEditor.SetDataModified();
            Repaint();
        }

        public IEnumerable<Rect> GetIsometricRects(Vector2 size, Vector2 offset, bool isAlternate, bool keepEmptyRects)
        {
            var textureToUse = GetTextureToSlice();
            var gradient = (size.x / 2) / (size.y / 2);
            bool isAlt = isAlternate;
            float x = offset.x;
            if (isAlt)
                x += size.x / 2;
            float y = textureToUse.height - offset.y;
            while (y - size.y >= 0)
            {
                while (x + size.x <= textureToUse.width)
                {
                    var rect = new Rect(x, y - size.y, size.x, size.y);
                    if (!keepEmptyRects)
                    {
                        int sx = (int)rect.x;
                        int sy = (int)rect.y;
                        int width = (int)size.x;
                        int odd = ((int)size.y) % 2;
                        int topY = ((int)size.y / 2) - 1;
                        int bottomY = topY + odd;
                        int totalPixels = 0;
                        int alphaPixels = 0;
                        {
                            for (int ry = 0; ry <= topY; ry++)
                            {
                                var pixelOffset = Mathf.CeilToInt(gradient * ry);
                                for (int rx = pixelOffset; rx < width - pixelOffset; ++rx)
                                {
                                    if (PixelHasAlpha(sx + rx, sy + topY - ry, textureToUse))
                                        alphaPixels++;
                                    if (PixelHasAlpha(sx + rx, sy + bottomY + ry, textureToUse))
                                        alphaPixels++;
                                    totalPixels += 2;
                                }
                            }
                        }
                        if (odd > 0)
                        {
                            int ry = topY + 1;
                            for (int rx = 0; rx < size.x; ++rx)
                            {
                                if (PixelHasAlpha(sx + rx, sy + ry, textureToUse))
                                    alphaPixels++;
                                totalPixels++;
                            }
                        }
                        if (totalPixels > 0 && ((float)alphaPixels) / totalPixels > 0.01f)
                            yield return rect;
                    }
                    else
                        yield return rect;
                    x += size.x;
                }
                isAlt = !isAlt;
                x = offset.x;
                if (isAlt)
                    x += size.x / 2;
                y -= size.y / 2;
            }
        }

        public void DoIsometricGridSlicing(Vector2 size, Vector2 offset, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, bool keepEmptyRects = false, bool isAlternate = false)
        {
            var frames = GetIsometricRects(size, offset, isAlternate, keepEmptyRects);

            List<Vector2[]> outlines = new List<Vector2[]>(4);
            outlines.Add(new[] { new Vector2(0.0f, -size.y / 2)
                                 , new Vector2(size.x / 2, 0.0f)
                                 , new Vector2(0.0f, size.y / 2)
                                 , new Vector2(-size.x / 2, 0.0f)});

            undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Isometric Grid Slicing");
            if (slicingMethod == AutoSlicingMethod.DeleteAll)
                m_RectsCache.Clear();

            int index = 0;
            var spriteRects = m_RectsCache.GetSpriteRects();
            int originalCount = spriteRects.Count;
            foreach (var frame in frames)
            {
                var spriteIndex = AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index);
                var outlineRect = new OutlineSpriteRect(spriteRects[spriteIndex]);
                outlineRect.outlines = outlines;
                spriteRects[spriteIndex] = outlineRect;
            }

            selected = null;
            spriteEditor.SetDataModified();
            Repaint();
        }

        public void ScaleSpriteRect(Rect r)
        {
            if (selected != null)
            {
                undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Scale sprite");
                selected.rect = ClampSpriteRect(r, textureActualWidth, textureActualHeight);
                selected.border = ClampSpriteBorderToRect(selected.border, selected.rect);
                spriteEditor.SetDataModified();
            }
        }

        public void TrimAlpha()
        {
            var texture = GetTextureToSlice();
            if (texture == null)
                return;

            Rect rect = selected.rect;

            int xMin = (int)rect.xMax;
            int xMax = (int)rect.xMin;
            int yMin = (int)rect.yMax;
            int yMax = (int)rect.yMin;

            for (int y = (int)rect.yMin; y < (int)rect.yMax; y++)
            {
                for (int x = (int)rect.xMin; x < (int)rect.xMax; x++)
                {
                    if (PixelHasAlpha(x, y, texture))
                    {
                        xMin = Mathf.Min(xMin, x);
                        xMax = Mathf.Max(xMax, x);
                        yMin = Mathf.Min(yMin, y);
                        yMax = Mathf.Max(yMax, y);
                    }
                }
            }
            // Case 582309: Return an empty rectangle if no pixel has an alpha
            if (xMin > xMax || yMin > yMax)
                rect = new Rect(0, 0, 0, 0);
            else
                rect = new Rect(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);

            if (rect.width <= 0 && rect.height <= 0)
            {
                m_RectsCache.Remove(selected);
                spriteEditor.SetDataModified();
                selected = null;
            }
            else
            {
                rect = ClampSpriteRect(rect, texture.width, texture.height);
                if (selected.rect != rect)
                    spriteEditor.SetDataModified();

                selected.rect = rect;
                PopulateSpriteFrameInspectorField();
            }
        }

        public void DuplicateSprite()
        {
            if (selected != null)
            {
                undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Duplicate sprite");
                var index = 0;
                var createdIndex = -1;
                while (createdIndex == -1)
                {
                    createdIndex = AddSprite(selected.rect, (int)selected.alignment, selected.pivot, GenerateSpriteNameWithIndex(index++), selected.border);
                }
                selected = m_RectsCache.spriteRects[createdIndex];
            }
        }

        public void CreateSprite(Rect rect)
        {
            rect = ClampSpriteRect(rect, textureActualWidth, textureActualHeight);
            undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Create sprite");
            var index = 0;
            var createdIndex = -1;
            while (createdIndex == -1)
            {
                createdIndex = AddSprite(rect, 0, Vector2.zero, GenerateSpriteNameWithIndex(index++), Vector4.zero);
            }
            selected = m_RectsCache.spriteRects[createdIndex];
        }

        public void DeleteSprite()
        {
            if (selected != null)
            {
                undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Delete sprite");
                m_RectsCache.Remove(selected);
                selected = null;
                spriteEditor.SetDataModified();
            }
        }
    }
}