using System;

#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
using System.Collections.Generic;
using System.Linq;
#endif

namespace UnityEngine.Tilemaps
{
    /// <summary>
    /// Animated Tiles are tiles which run through and display a list of sprites in sequence.
    /// </summary>
    [Serializable]
    [HelpURL("https://docs.unity3d.com/Packages/com.unity.2d.tilemap.extras@latest/index.html?subfolder=/manual/AnimatedTile.html")]
    public class AnimatedTile : TileBase
    {
        /// <summary>
        /// The List of Sprites set for the Animated Tile.
        /// This will be played in sequence.
        /// </summary>
        public Sprite[] m_AnimatedSprites;
        /// <summary>
        /// The minimum possible speed at which the Animation of the Tile will be played.
        /// A speed value will be randomly chosen between the minimum and maximum speed.
        /// </summary>
        public float m_MinSpeed = 1f;
        /// <summary>
        /// The maximum possible speed at which the Animation of the Tile will be played.
        /// A speed value will be randomly chosen between the minimum and maximum speed.
        /// </summary>
        public float m_MaxSpeed = 1f;
        /// <summary>
        /// The starting time of this Animated Tile.
        /// This allows you to start the Animation from time in the list of Animated Sprites depending on the 
        /// Tilemap's Animation Frame Rate.
        /// </summary>
        public float m_AnimationStartTime;
        /// <summary>
        /// The starting frame of this Animated Tile.
        /// This allows you to start the Animation from a particular Sprite in the list of Animated Sprites.
        /// If this is set, this overrides m_AnimationStartTime. 
        /// </summary>
        public int m_AnimationStartFrame = 0;
        /// <summary>
        /// The Collider Shape generated by the Tile.
        /// </summary>
        public Tile.ColliderType m_TileColliderType;

        /// <summary>
        /// Retrieves any tile rendering data from the scripted tile.
        /// </summary>
        /// <param name="position">Position of the Tile on the Tilemap.</param>
        /// <param name="tilemap">The Tilemap the tile is present on.</param>
        /// <param name="tileData">Data to render the tile.</param>
        public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
        {
            tileData.transform = Matrix4x4.identity;
            tileData.color = Color.white;
            if (m_AnimatedSprites != null && m_AnimatedSprites.Length > 0)
            {
                tileData.sprite = m_AnimatedSprites[m_AnimatedSprites.Length - 1];
                tileData.colliderType = m_TileColliderType;
            }
        }

        /// <summary>
        /// Retrieves any tile animation data from the scripted tile.
        /// </summary>
        /// <param name="position">Position of the Tile on the Tilemap.</param>
        /// <param name="tilemap">The Tilemap the tile is present on.</param>
        /// <param name="tileAnimationData">Data to run an animation on the tile.</param>
        /// <returns>Whether the call was successful.</returns>
        public override bool GetTileAnimationData(Vector3Int position, ITilemap tilemap, ref TileAnimationData tileAnimationData)
        {
            if (m_AnimatedSprites.Length > 0)
            {
                tileAnimationData.animatedSprites = m_AnimatedSprites;
                tileAnimationData.animationSpeed = Random.Range(m_MinSpeed, m_MaxSpeed);
                tileAnimationData.animationStartTime = m_AnimationStartTime;
                if (0 < m_AnimationStartFrame && m_AnimationStartFrame <= m_AnimatedSprites.Length)
                {
                    var tilemapComponent = tilemap.GetComponent<Tilemap>();
                    if (tilemapComponent != null && tilemapComponent.animationFrameRate > 0)
                        tileAnimationData.animationStartTime = (m_AnimationStartFrame - 1) / tilemapComponent.animationFrameRate;
                }
                return true;
            }
            return false;
        }
    }

#if UNITY_EDITOR
    [CustomEditor(typeof(AnimatedTile))]
    public class AnimatedTileEditor : Editor
    {
        private static class Styles
        {
            public static readonly GUIContent orderAnimatedTileSpritesInfo =
                EditorGUIUtility.TrTextContent("Place sprites shown based on the order of animation.");
            public static readonly GUIContent emptyAnimatedTileInfo =
                EditorGUIUtility.TrTextContent(
                    "Drag Sprite or Sprite Texture assets \n" +
                    " to start creating an Animated Tile.");
            public static readonly GUIContent minimumSpeedLabel = EditorGUIUtility.TrTextContent("Minimum Speed",
                "The minimum possible speed at which the Animation of the Tile will be played. A speed value will be randomly chosen between the minimum and maximum speed.");
            public static readonly GUIContent maximumSpeedLabel = EditorGUIUtility.TrTextContent("Maximum Speed",
                "The maximum possible speed at which the Animation of the Tile will be played. A speed value will be randomly chosen between the minimum and maximum speed.");
            public static readonly GUIContent startTimeLabel = EditorGUIUtility.TrTextContent("Start Time", "The starting time of this Animated Tile. This allows you to start the Animation from a particular time.");
            public static readonly GUIContent startFrameLabel = EditorGUIUtility.TrTextContent("Start Frame", "The starting frame of this Animated Tile. This allows you to start the Animation from a particular Sprite in the list of Animated Sprites.");
            public static readonly GUIContent colliderTypeLabel = EditorGUIUtility.TrTextContent("Collider Type", "The Collider Shape generated by the Tile.");
        }

        private static readonly string k_UndoName = L10n.Tr("Change AnimatedTile");

        private SerializedProperty m_AnimatedSprites;
        
        private AnimatedTile tile { get { return (target as AnimatedTile); } }

        private List<Sprite> dragAndDropSprites;

        private ReorderableList reorderableList;

        private void OnEnable()
        {
            reorderableList = new ReorderableList(tile.m_AnimatedSprites, typeof(Sprite), true, true, true, true);
            reorderableList.drawHeaderCallback = OnDrawHeader;
            reorderableList.drawElementCallback = OnDrawElement;
            reorderableList.elementHeightCallback = GetElementHeight;
            reorderableList.onAddCallback = OnAddElement;
            reorderableList.onRemoveCallback = OnRemoveElement;
            reorderableList.onReorderCallback = OnReorderElement;

            m_AnimatedSprites = serializedObject.FindProperty("m_AnimatedSprites");
        }

        private void OnDrawHeader(Rect rect)
        {
            GUI.Label(rect, Styles.orderAnimatedTileSpritesInfo);
        }

        private void OnDrawElement(Rect rect, int index, bool isActive, bool isFocused)
        {
            if (tile.m_AnimatedSprites != null && index < tile.m_AnimatedSprites.Length)
            {
                var spriteName = tile.m_AnimatedSprites[index] != null ? tile.m_AnimatedSprites[index].name : "Null";
                tile.m_AnimatedSprites[index] = (Sprite) EditorGUI.ObjectField(rect
                    , $"Sprite {index + 1}: {spriteName}"
                    , tile.m_AnimatedSprites[index]
                    , typeof(Sprite)
                    , false);
            }
        }

        private float GetElementHeight(int index)
        {
            return 3 * EditorGUI.GetPropertyHeight(SerializedPropertyType.ObjectReference,
                null);
        }

        private void OnAddElement(ReorderableList list)
        {
            var count = tile.m_AnimatedSprites != null ? tile.m_AnimatedSprites.Length + 1 : 1;  
            ResizeAnimatedSpriteList(count);
            
            if (list.index == 0  || list.index < list.count)
            {
                Array.Copy(tile.m_AnimatedSprites, list.index + 1, tile.m_AnimatedSprites, list.index + 2, list.count - list.index - 1);
                tile.m_AnimatedSprites[list.index + 1] = null;
                if (list.IsSelected(list.index))
                    list.index += 1;
            }
            else
            {
                tile.m_AnimatedSprites[count - 1] = null;
            }
        }

        private void OnRemoveElement(ReorderableList list)
        {
            if (tile.m_AnimatedSprites != null && tile.m_AnimatedSprites.Length > 0 && list.index < tile.m_AnimatedSprites.Length)
            {
                var sprites = tile.m_AnimatedSprites.ToList();
                sprites.RemoveAt(list.index);
                tile.m_AnimatedSprites = sprites.ToArray();
            }
        }

        private void OnReorderElement(ReorderableList list)
        {
            // Fix for 2020.1, which does not track changes when reordering in the list
            EditorUtility.SetDirty(tile);
        }
        
        private void DisplayClipboardText(GUIContent clipboardText, Rect position)
        {
            Color old = GUI.color;
            GUI.color = Color.gray;
            var infoSize = GUI.skin.label.CalcSize(clipboardText);
            Rect rect = new Rect(position.center.x - infoSize.x * .5f
                , position.center.y - infoSize.y * .5f
                , infoSize.x
                , infoSize.y);
            GUI.Label(rect, clipboardText);
            GUI.color = old;
        }

        private bool dragAndDropActive
        {
            get
            {
                return dragAndDropSprites != null
                       && dragAndDropSprites.Count > 0;
            }
        }
        
        private void DragAndDropClear()
        {
            dragAndDropSprites = null;
            DragAndDrop.visualMode = DragAndDropVisualMode.None;
            Event.current.Use();
        }

        private static List<Sprite> GetSpritesFromTexture(Texture2D texture)
        {
            string path = AssetDatabase.GetAssetPath(texture);
            Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path);
            List<Sprite> sprites = new List<Sprite>();

            foreach (Object asset in assets)
            {
                if (asset is Sprite)
                {
                    sprites.Add(asset as Sprite);
                }
            }

            return sprites;
        }

        private static List<Sprite> GetValidSingleSprites(Object[] objects)
        {
            List<Sprite> result = new List<Sprite>();
            foreach (Object obj in objects)
            {
                if (obj is Sprite)
                {
                    result.Add(obj as Sprite);
                }
                else if (obj is Texture2D)
                {
                    Texture2D texture = obj as Texture2D;
                    List<Sprite> sprites = GetSpritesFromTexture(texture);
                    if (sprites.Count > 0)
                    {
                        result.AddRange(sprites);
                    }
                }
            }
            return result;
        }

        private void HandleDragAndDrop(Rect guiRect)
        {
            if (DragAndDrop.objectReferences.Length == 0 || !guiRect.Contains(Event.current.mousePosition))
                return;

            switch (Event.current.type)
            {
                case EventType.DragUpdated:
                {
                    dragAndDropSprites = GetValidSingleSprites(DragAndDrop.objectReferences);
                    if (dragAndDropActive)
                    {
                        DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
                        Event.current.Use();
                        GUI.changed = true;
                    }
                }
                break;
                case EventType.DragPerform:
                {
                    if (!dragAndDropActive)
                        return;

                    Undo.RegisterCompleteObjectUndo(tile, "Drag and Drop to Animated Tile");
                    ResizeAnimatedSpriteList(dragAndDropSprites.Count);
                    Array.Copy(dragAndDropSprites.ToArray(), tile.m_AnimatedSprites, dragAndDropSprites.Count);
                    DragAndDropClear();
                    GUI.changed = true;
                    EditorUtility.SetDirty(tile);
                    GUIUtility.ExitGUI();
                }
                break;
                case EventType.Repaint:
                    // Handled in Render()
                    break;
            }

            if (Event.current.type == EventType.DragExited ||
                Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
            {
                DragAndDropClear();
            }
        }

        /// <summary>
        /// Draws an Inspector for the AnimatedTile.
        /// </summary>
        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            
            Undo.RecordObject(tile, k_UndoName);
            
            EditorGUI.BeginChangeCheck();
            int count = EditorGUILayout.DelayedIntField("Number of Animated Sprites", tile.m_AnimatedSprites != null ? tile.m_AnimatedSprites.Length : 0);
            if (count < 0)
                count = 0;

            if (tile.m_AnimatedSprites == null || tile.m_AnimatedSprites.Length != count)
                ResizeAnimatedSpriteList(count);

            if (count == 0)
            {
                Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight * 5);
                HandleDragAndDrop(rect);
                EditorGUI.DrawRect(rect, dragAndDropActive && rect.Contains(Event.current.mousePosition) ? Color.white : Color.black);
                var innerRect = new Rect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
                EditorGUI.DrawRect(innerRect, EditorGUIUtility.isProSkin
                    ? (Color) new Color32 (56, 56, 56, 255)
                    : (Color) new Color32 (194, 194, 194, 255));
                DisplayClipboardText(Styles.emptyAnimatedTileInfo, rect);
                GUILayout.Space(rect.height);
                EditorGUILayout.Space();
            }

            if (reorderableList != null)
            {
                var tileCount = tile.m_AnimatedSprites != null ? tile.m_AnimatedSprites.Length : 0;
                if (reorderableList.list == null || reorderableList.count != tileCount)
                    reorderableList.list = tile.m_AnimatedSprites;
                reorderableList.DoLayoutList();
            }

            using (new EditorGUI.DisabledScope(tile.m_AnimatedSprites == null || tile.m_AnimatedSprites.Length == 0))
            {
                float minSpeed = EditorGUILayout.FloatField(Styles.minimumSpeedLabel, tile.m_MinSpeed);
                float maxSpeed = EditorGUILayout.FloatField(Styles.maximumSpeedLabel, tile.m_MaxSpeed);
                if (minSpeed < 0.0f)
                    minSpeed = 0.0f;

                if (maxSpeed < 0.0f)
                    maxSpeed = 0.0f;

                if (maxSpeed < minSpeed)
                    maxSpeed = minSpeed;

                tile.m_MinSpeed = minSpeed;
                tile.m_MaxSpeed = maxSpeed;

                using (new EditorGUI.DisabledScope(tile.m_AnimatedSprites == null 
                                                   || (0 < tile.m_AnimationStartFrame 
                                                    && tile.m_AnimationStartFrame <= tile.m_AnimatedSprites.Length)))
                {
                    tile.m_AnimationStartTime = EditorGUILayout.FloatField(Styles.startTimeLabel, tile.m_AnimationStartTime);    
                }
                tile.m_AnimationStartFrame = EditorGUILayout.IntField(Styles.startFrameLabel, tile.m_AnimationStartFrame);
                tile.m_TileColliderType = (Tile.ColliderType) EditorGUILayout.EnumPopup(Styles.colliderTypeLabel, tile.m_TileColliderType);
            }

            if (EditorGUI.EndChangeCheck())
            {
                serializedObject.ApplyModifiedProperties();
                EditorUtility.SetDirty(tile);
            }
        }
        
        private void ResizeAnimatedSpriteList(int count)
        {
            m_AnimatedSprites.arraySize = count;
            serializedObject.ApplyModifiedProperties();
        }
    }
#endif
}