using System;
using System.IO;
using UnityEngine;
using UnityEditorInternal;
using System.Collections.Generic;
using System.Text;
using UnityTexture2D = UnityEngine.Texture2D;
using UnityEditor.ShortcutManagement;
using UnityEditor.U2D.Sprites.Overlay;
using UnityEditor.U2D.Sprites.SpriteFrameEditor;
using Object = UnityEngine.Object;

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;
        SpriteFrameModeOverlay m_ModeOverlay;
        public event Action onModuleDeactivated = () => { };
        ISpriteEditorModuleMode m_CurrentModuleMode = null;
        SpriteFrameModeUndoObject m_ModeChangeUndoObject;

        private StringBuilder m_SpriteNameStringBuilder;
        bool m_SpriteRectValidated = false;

        private List<Rect> m_PotentialRects;

        Texture2D m_TextureToSlice;
        public List<Rect> potentialRects
        {
            set => m_PotentialRects = value;
        }
        internal static Func<string, string, string, string, string, int> onShowComplexDialog = EditorUtility.DisplayDialogComplex;
        internal static Func<string, string, string, string, bool> onShowDialog = EditorUtility.DisplayDialog;
        public SpriteFrameModule(ISpriteEditor sw, IEventSystem es, IUndoSystem us, IAssetDatabase ad) :
            base("Sprite Editor", sw, es, us, ad)
        {}

        public override void SetModuleModes(IEnumerable<Type> modes)
        {
            base.SetModuleModes(modes);
            foreach (var mode in this.modes)
            {
                mode.onModeRequestActivate += OnModuleModeRequestActivate;
            }
        }

        public void OnModuleModeRequestActivate(ISpriteEditorModuleMode activatingModuleMode)
        {
            if (activatingModuleMode == null)
                activatingModuleMode = GetDefaultMode();
            if (activatingModuleMode == m_CurrentModuleMode)
                return;
            var oldMode = m_CurrentModuleMode;
            m_CurrentModuleMode?.DeactivateMode();
            m_CurrentModuleMode = activatingModuleMode;
            var activated = m_CurrentModuleMode?.ActivateMode();
            // Mode did not activate
            if(activated.HasValue && !activated.Value)
            {
                m_CurrentModuleMode?.DeactivateMode();
                m_CurrentModuleMode = oldMode;
                //Reactivate old mode
                m_CurrentModuleMode?.ActivateMode();
            }
        }

        public override bool ApplyRevert(bool apply)
        {
            var returnValue = base.ApplyRevert(apply);
            if (!apply)
                ClearTextureOverride();
            var dataProviderApplied = new HashSet<Type>();
            dataProviderApplied.Add(typeof(ISpriteEditorDataProvider));
            foreach(var mode in modes)
            {
                returnValue |= mode.ApplyModeData(apply, dataProviderApplied);
            }
            if(apply)
                m_SourceOverrideCallback?.Invoke(spriteAssetPath);
            return returnValue;
        }

        public void OnModeActivate(SpriteFrameModeToolStripBase arg2)
        {
            if (!UndoObject.undoing)
            {
                if (m_ModeChangeUndoObject == null)
                {
                    m_ModeChangeUndoObject = UndoObject.Create<SpriteFrameModeUndoObject, EquatableType>(arg2.GetType(), undoSystem);
                }
                else
                {
                    m_ModeChangeUndoObject.SetData(arg2.GetType(), "Change Mode");
                }
            }
        }

        protected override void UndoCallback()
        {
            base.UndoCallback();
            UndoObject.BeginUndo();
            if (m_ModeChangeUndoObject != null && m_ModeChangeUndoObject.VersionChanged(true))
            {
                var modeToolStrip = m_ModeOverlay.GetSpriteFrameModeToolStrip(m_ModeChangeUndoObject.data);
                m_ModeOverlay.OnOverlayToggleCallback(modeToolStrip);
                var moduleMode = GetModeByType(modeToolStrip.GetSpriteFrameModeType());
                moduleMode?.RequestModeToActivate();
            }
            UndoObject.EndUndo();
        }

        class SpriteFrameModuleContext : IShortcutContext
        {
            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();
            m_ModeOverlay = spriteEditor.GetOverlay<SpriteFrameModeOverlay>(SpriteFrameModeOverlay.k_OverlayId);
            if (m_ModeOverlay != null)
            {
                m_ModeOverlay.Activate(this);
                var defaultMode = GetDefaultMode();
                OnModuleModeRequestActivate(defaultMode);
                m_ModeOverlay.OnOverlayToggleCallback(GetDefaultToolStrip());
            }
            m_SpriteRectValidated = false;
            spriteEditor.enableMouseMoveEvent = true;
            m_SpriteFrameModuleContext = new SpriteFrameModuleContext(this);
            ShortcutIntegration.instance.contextManager.RegisterToolContext(m_SpriteFrameModuleContext);
            m_SpriteNameStringBuilder = new StringBuilder(GetSpriteNamePrefix() + "_");
            m_PotentialRects = null;
            RegisterDataChangeCallback(OnTextureDataProviderDataChanged);
            SignalModuleActivate();
        }

        SpriteFrameModeToolStripBase GetDefaultToolStrip()
        {
            return m_ModeOverlay.GetSpriteFrameModeToolStrip(typeof(SpriteFrameEditorModeToggle));
        }

        ISpriteEditorModuleMode GetDefaultMode()
        {
            var defaultModeToggleStrip = GetDefaultToolStrip();
            var moduleModeType = defaultModeToggleStrip.GetSpriteFrameModeType();
            return GetModeByType(moduleModeType);
        }

        ISpriteEditorModuleMode GetModeByType(Type t)
        {
            foreach (var mode in modes)
            {
                if (mode.GetType() == t)
                {
                    return mode;
                }
            }
            return null;
        }

        void ValidateSpriteRects()
        {
            if (m_TextureDataProvider != null && !m_SpriteRectValidated)
            {
                m_SpriteRectValidated = true;
                int width, height;
                m_TextureDataProvider.GetTextureActualWidthAndHeight(out width, out height);
                HashSet<GUID> spriteIDs = new HashSet<GUID>();
                var emptyGuid = new GUID();
                List<SpriteRect> updatedSpriteRectID = new List<SpriteRect>();
                for (int i = 0; i < m_RectsCache.spriteRects.Count; ++i)
                {
                    // Validate rect is still within the bounds of the texture
                    var s = m_RectsCache.spriteRects[i];
                    if(s.rect.x < 0 || s.rect.y < 0 || s.rect.xMax > width || s.rect.yMax > height)
                    {
                        var response = onShowComplexDialog("Invalid Sprite Rect", $"Sprite Rect {s.name} is outside the bounds of the texture.", "Remove", "Keep", "Resize");
                        switch (response)
                        {
                            case 0:
                                m_RectsCache.Remove(s);
                                i--;
                                SetDataModified();
                                break;
                            case 2:
                                s.rect = ClampSpriteRect(s.rect, width, height);
                                SetDataModified();
                                break;
                        }
                    }

                    // Validate sprite id uniqueness
                    var spriteId = s.spriteID;
                    if (spriteId == emptyGuid || spriteIDs.Contains(spriteId))
                    {
                        if (onShowDialog("Invalid Sprite ID", $"Sprite Rect {s.name} has an invalid ID.\nSprite with invalid ID can result in Sprite reference breakage.", "Reassign ID", "Keep"))
                        {
                            updatedSpriteRectID.Add(s);
                        }
                    }
                    spriteIDs.Add(spriteId);
                }
                for(int i = 0; i < updatedSpriteRectID.Count; ++i)
                {
                    m_RectsCache.Remove(updatedSpriteRectID[i]);
                    updatedSpriteRectID[i].spriteID = GUID.Generate();
                    m_RectsCache.Add(updatedSpriteRectID[i], true);
                    SetDataModified();
                }
            }
        }

        public override void OnModuleDeactivate()
        {
            base.OnModuleDeactivate();
            m_CurrentModuleMode?.DeactivateMode();
            m_CurrentModuleMode = null;
            m_ModeOverlay?.Deactivate();
            m_ModeOverlay = null;
            EditorApplication.delayCall -= ValidateSpriteRects;
            m_SpriteRectValidated = true;
            ShortcutIntegration.instance.contextManager.DeregisterToolContext(m_SpriteFrameModuleContext);
            m_PotentialRects = null;
            m_AlphaPixelCache = null;
            UnregisterDataChangeCallback(OnTextureDataProviderDataChanged);
            CleanUpDataDataProviderOverride();
            foreach (var mode in modes)
            {
                mode.onModeRequestActivate -= OnModuleModeRequestActivate;
            }
            if(m_TextureToSlice != null)
                Object.DestroyImmediate(m_TextureToSlice);
            modes.Clear();
            UndoObject.Dispose(m_ModeChangeUndoObject);
            m_ModeChangeUndoObject = null;
            onModuleDeactivated();
        }

        void OnTextureDataProviderDataChanged(ISpriteEditorDataProvider obj)
        {
            if(m_TextureToSlice != null)
                Object.DestroyImmediate(m_TextureToSlice);
            m_TextureToSlice = null;
        }

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

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

        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;
        }

        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 string GetSpriteNamePrefix()
        {
            return Path.GetFileNameWithoutExtension(spriteAssetPath);
        }

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

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

            var textureToUse = GetTextureToSlice();
            List<Rect> frames = new List<Rect>(InternalSpriteUtility.GenerateAutomaticSpriteRectangles(textureToUse, minimumSpriteSize, 0));
            if (frames.Count == 0)
                frames.Add(new Rect(0, 0, textureToUse.width, textureToUse.height));

            int index = 0;
            int originalCount = m_RectsCache.spriteRects.Count;

            foreach (Rect frame in frames)
            {
                var framePivot = pivot;
                if (pivotUnitMode == PivotUnitMode.Pixels)
                    framePivot = pivotPixels / frame.size;
                m_RectsCache.AddSprite(frame, alignment, framePivot, slicingMethod, originalCount, ref index, GenerateSpriteNameWithIndex);
            }

            if (slicingMethod == AutoSlicingMethod.DeleteAll)
                m_RectsCache.ClearUnusedFileID();
            selected = null;
            NotifyOnSpriteRectChanged();
            spriteEditor.SetDataModified();
            Repaint();
        }

        UnityTexture2D GetTextureToSlice()
        {
            m_TextureDataProvider.GetTextureActualWidthAndHeight(out var width, out var 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
            if (m_TextureToSlice == null)
            {
                // we want to slice based on the original texture slice. Upscale the imported texture
                m_TextureToSlice = UnityEditor.SpriteUtility.CreateTemporaryDuplicate(readableTexture, width, height);
                m_TextureToSlice.hideFlags = HideFlags.HideAndDontSave;
            }

            return m_TextureToSlice;
        }

        public IEnumerable<Rect> GetGridRects(Vector2 size, Vector2 offset, Vector2 padding, bool keepEmptyRects)
        {
            var textureToUse = GetTextureToSlice();
            return InternalSpriteUtility.GenerateGridSpriteRectangles(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);

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

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

            if (slicingMethod == AutoSlicingMethod.DeleteAll)
                m_RectsCache.ClearUnusedFileID();
            selected = null;
            NotifyOnSpriteRectChanged();
            spriteEditor.SetDataModified();
            Repaint();
        }

        public void DoIsometricGridSlicing(Vector2 size, Vector2 offset, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, bool keepEmptyRects = false, bool isAlternate = false)
        {
            var frames = IsometricSlicingUtility.GetIsometricRects(GetTextureToSlice(), 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)});

            m_RectsCache.RegisterUndo(undoSystem, "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 = m_RectsCache.AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index, GenerateSpriteNameWithIndex);
                var outlineRect = new OutlineSpriteRect(spriteRects[spriteIndex]);
                outlineRect.outlines = outlines;
                spriteRects[spriteIndex] = outlineRect;
            }
            if (slicingMethod == AutoSlicingMethod.DeleteAll)
                m_RectsCache.ClearUnusedFileID();
            selected = null;
            NotifyOnSpriteRectChanged();
            spriteEditor.SetDataModified();
            Repaint();
        }

        public void ScaleSpriteRect(Rect r)
        {
            if (selected != null)
            {
                m_RectsCache.RegisterUndo(undoSystem, "Scale sprite");
                selected.rect = ClampSpriteRect(r, textureActualWidth, textureActualHeight);
                selected.border = ClampSpriteBorderToRect(selected.border, selected.rect);
                NotifyOnSpriteRectChanged();
                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)
            {
                m_RectsCache.RegisterUndo(undoSystem, "Duplicate sprite");
                var index = 0;
                var createdIndex = -1;
                while (createdIndex == -1)
                {
                    createdIndex = m_RectsCache.AddSprite(selected.rect, (int)selected.alignment, selected.pivot, GenerateSpriteNameWithIndex(index++), selected.border);
                }
                spriteEditor.SetDataModified();
                selected = m_RectsCache.spriteRects[createdIndex];
                NotifyOnSpriteRectChanged();
            }
        }

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

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

        public bool IsOnlyUsingDefaultNamedSpriteRects()
        {
            var onlyDefaultNames = true;
            var names = m_RectsCache.spriteNames;
            var defaultName = m_SpriteNameStringBuilder.ToString();

            foreach (var name in names)
            {
                if (!name.Contains(defaultName))
                {
                    onlyDefaultNames = false;
                    break;
                }
            }

            return onlyDefaultNames;
        }

        public List<SpriteRect> GetSpriteRectWorkingData()
        {
            return m_RectsCache.GetSpriteRects();
        }
    }
}
