#if ENABLE_ANIMATION_COLLECTION && ENABLE_ANIMATION_BURST
#define ENABLE_ANIMATION_PERFORMANCE
#endif

using UnityEngine;
using UnityEditorInternal;
using UnityEngine.U2D.Animation;
using UnityEditor.IMGUI.Controls;
using UnityEngine.U2D;
using UnityEngine.U2D.Common;

namespace UnityEditor.U2D.Animation
{
    [CustomEditor(typeof(SpriteSkin))]
    [CanEditMultipleObjects]
    class SpriteSkinEditor : Editor
    {
        private static class Contents
        {
            public static readonly GUIContent listHeaderLabel = new GUIContent("Bones", "GameObject Transform to represent the Bones defined by the Sprite that is currently used for deformation.");
            public static readonly GUIContent rootBoneLabel = new GUIContent("Root Bone", "GameObject Transform to represent the Root Bone.");
            public static readonly string spriteNotFound = L10n.Tr("Sprite not found in SpriteRenderer");
            public static readonly string spriteHasNoSkinningInformation = L10n.Tr("Sprite has no Bind Poses");
            public static readonly string spriteHasNoWeights = L10n.Tr("Sprite has no weights");
            public static readonly string rootTransformNotFound = L10n.Tr("Root Bone not set");
            public static readonly string rootTransformNotFoundInArray = L10n.Tr("Bone list doesn't contain a reference to the Root Bone");
            public static readonly string invalidTransformArray = L10n.Tr("Bone list is invalid");
            public static readonly string transformArrayContainsNull = L10n.Tr("Bone list contains unassigned references");
            public static readonly string invalidTransformArrayLength = L10n.Tr("The number of Sprite's Bind Poses and the number of Transforms should match");
            public static readonly GUIContent useManager = new GUIContent("Enable batching", "When enabled, SpriteSkin deformation will be done in batch to improve performance.");
            public static readonly GUIContent alwaysUpdate = new GUIContent("Always Update", "Executes deformation of SpriteSkin even when the associated SpriteRenderer has been culled and is not visible.");
            public static readonly string experimental = L10n.Tr("Experimental");
        }

        private static Color s_BoundingBoxHandleColor = new Color(255, 255, 255, 150) / 255;

        private SerializedProperty m_RootBoneProperty;
        private SerializedProperty m_BoneTransformsProperty;
        private SerializedProperty m_AlwaysUpdateProperty;
        private SpriteSkin m_SpriteSkin;
        private ReorderableList m_ReorderableList;
        private Sprite m_CurrentSprite;
        private BoxBoundsHandle m_BoundsHandle = new BoxBoundsHandle();
        private bool m_NeedsRebind = false;
#if ENABLE_ANIMATION_PERFORMANCE
        private SerializedProperty m_UseBatching;
        private bool m_ExperimentalFold;
#endif
        private bool m_BoneFold = true;

        private void OnEnable()
        {
            m_SpriteSkin = (SpriteSkin)target;
            m_SpriteSkin.OnEditorEnable();
            m_RootBoneProperty = serializedObject.FindProperty("m_RootBone");
#if ENABLE_ANIMATION_PERFORMANCE
            m_UseBatching = serializedObject.FindProperty("m_UseBatching");
#endif
            m_BoneTransformsProperty = serializedObject.FindProperty("m_BoneTransforms");
            m_AlwaysUpdateProperty = serializedObject.FindProperty("m_AlwaysUpdate");
            m_CurrentSprite = m_SpriteSkin.spriteRenderer.sprite;
            m_BoundsHandle.axes = BoxBoundsHandle.Axes.X | BoxBoundsHandle.Axes.Y;
            m_BoundsHandle.SetColor(s_BoundingBoxHandleColor);

            SetupReorderableList();

            Undo.undoRedoPerformed += UndoRedoPerformed;
        }

        private void OnDestroy()
        {
            Undo.undoRedoPerformed -= UndoRedoPerformed;
        }

        private void UndoRedoPerformed()
        {
            m_CurrentSprite = m_SpriteSkin.spriteRenderer.sprite;
        }

        private void SetupReorderableList()
        {
            m_ReorderableList = new ReorderableList(serializedObject, m_BoneTransformsProperty, false, true, false, false);
            m_ReorderableList.headerHeight = 1.0f;
            m_ReorderableList.elementHeightCallback = (int index) =>
                {
                    return EditorGUIUtility.singleLineHeight + 6;
                };
            m_ReorderableList.drawElementCallback = (Rect rect, int index, bool isactive, bool isfocused) =>
                {
                    var content = GUIContent.none;

                    if (m_CurrentSprite != null)
                    {
                        var bones = m_CurrentSprite.GetBones();
                        if (index < bones.Length)
                            content = new GUIContent(bones[index].name);
                    }

                    rect.y += 2f;
                    rect.height = EditorGUIUtility.singleLineHeight;
                    SerializedProperty element = m_BoneTransformsProperty.GetArrayElementAtIndex(index);
                    EditorGUI.PropertyField(rect, element, content);
                };
        }

        private void InitializeBoneTransformArray()
        {
            if (m_CurrentSprite)
            {
                var elementCount = m_BoneTransformsProperty.arraySize;
                var bindPoses = m_CurrentSprite.GetBindPoses();

                if (elementCount != bindPoses.Length)
                {
                    m_BoneTransformsProperty.arraySize = bindPoses.Length;

                    for (int i = elementCount; i < m_BoneTransformsProperty.arraySize; ++i)
                        m_BoneTransformsProperty.GetArrayElementAtIndex(i).objectReferenceValue = null;

                    m_NeedsRebind = true;
                }
            }
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            EditorGUILayout.PropertyField(m_AlwaysUpdateProperty, Contents.alwaysUpdate);

            var sprite = m_SpriteSkin.spriteRenderer.sprite;
            var spriteChanged = m_CurrentSprite != sprite;

            if (m_ReorderableList == null || spriteChanged)
            {
                m_CurrentSprite = sprite;
                InitializeBoneTransformArray();
                SetupReorderableList();
            }

            EditorGUI.BeginChangeCheck();
            EditorGUILayout.PropertyField(m_RootBoneProperty, Contents.rootBoneLabel);
            if (EditorGUI.EndChangeCheck())
            {
                m_NeedsRebind = true;
            }

            m_BoneFold = EditorGUILayout.Foldout(m_BoneFold, Contents.listHeaderLabel, true);
            if (m_BoneFold)
            {
                EditorGUILayout.Space();
                if (!serializedObject.isEditingMultipleObjects)
                {
                    EditorGUI.BeginDisabledGroup(m_SpriteSkin.rootBone == null);
                    m_ReorderableList.DoLayoutList();
                    EditorGUI.EndDisabledGroup();
                }
            }

            EditorGUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();

            EditorGUI.BeginDisabledGroup(!EnableCreateBones());
            DoGenerateBonesButton();
            EditorGUI.EndDisabledGroup();

            EditorGUI.BeginDisabledGroup(!EnableSetBindPose());
            DoResetBindPoseButton();
            EditorGUI.EndDisabledGroup();

            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

#if ENABLE_ANIMATION_PERFORMANCE
            m_ExperimentalFold = EditorGUILayout.Foldout(m_ExperimentalFold, Contents.experimental, true);
            if (m_ExperimentalFold)
            {
                EditorGUI.indentLevel++;
                EditorGUI.BeginChangeCheck();
                EditorGUILayout.PropertyField(m_UseBatching, Contents.useManager);
                if (EditorGUI.EndChangeCheck())
                {
                    foreach (var obj in targets)
                    {
                        ((SpriteSkin)obj).UseBatching(m_UseBatching.boolValue);
                    }
                }
                EditorGUI.indentLevel--;
            }
#endif

            serializedObject.ApplyModifiedProperties();

            if (m_NeedsRebind)
                Rebind();

            if (spriteChanged && !m_SpriteSkin.ignoreNextSpriteChange)
            {
                ResetBounds(Undo.GetCurrentGroupName());
                m_SpriteSkin.ignoreNextSpriteChange = false;
            }

            DoValidationWarnings();
        }

        private void Rebind()
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                if(spriteSkin.spriteRenderer.sprite == null || spriteSkin.rootBone == null)
                    continue;

                spriteSkin.Rebind();
                ResetBoundsIfNeeded(spriteSkin);
            }

            m_NeedsRebind = false;
        }

        private void ResetBounds(string undoName = "Reset Bounds")
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                if (!spriteSkin.isValid)
                    continue;

                Undo.RegisterCompleteObjectUndo(spriteSkin, undoName);
                spriteSkin.CalculateBounds();

                EditorUtility.SetDirty(spriteSkin);
            }
        }

        private void ResetBoundsIfNeeded(SpriteSkin spriteSkin)
        {
            if (spriteSkin.isValid && spriteSkin.bounds == new Bounds())
                spriteSkin.CalculateBounds();
        }

        private bool EnableCreateBones()
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;
                var sprite = spriteSkin.spriteRenderer.sprite;

                if (sprite != null && spriteSkin.rootBone == null)
                    return true;
            }
            return false;
        }

        private bool EnableSetBindPose()
        {
            return IsAnyTargetValid();
        }

        private bool IsAnyTargetValid()
        {
            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                if (spriteSkin.isValid)
                    return true;
            }
            return false;
        }

        private void DoGenerateBonesButton()
        {
            if (GUILayout.Button("Create Bones", GUILayout.MaxWidth(125f)))
            {
                foreach (var t in targets)
                {
                    var spriteSkin = t as SpriteSkin;
                    var sprite = spriteSkin.spriteRenderer.sprite;

                    if (sprite == null || spriteSkin.rootBone != null)
                        continue;

                    Undo.RegisterCompleteObjectUndo(spriteSkin, "Create Bones");

                    spriteSkin.CreateBoneHierarchy();

                    foreach (var transform in spriteSkin.boneTransforms)
                        Undo.RegisterCreatedObjectUndo(transform.gameObject, "Create Bones");

                    ResetBoundsIfNeeded(spriteSkin);

                    EditorUtility.SetDirty(spriteSkin);
                }
                BoneGizmo.instance.boneGizmoController.OnSelectionChanged();
            }
        }

        private void DoResetBindPoseButton()
        {
            if (GUILayout.Button("Reset Bind Pose", GUILayout.MaxWidth(125f)))
            {
                foreach (var t in targets)
                {
                    var spriteSkin = t as SpriteSkin;

                    if (!spriteSkin.isValid)
                        continue;

                    Undo.RecordObjects(spriteSkin.boneTransforms, "Reset Bind Pose");
                    spriteSkin.ResetBindPose();
                }
            }
        }

        private void DoValidationWarnings()
        {
            EditorGUILayout.Space();

            bool preAppendObjectName = targets.Length > 1;

            foreach (var t in targets)
            {
                var spriteSkin = t as SpriteSkin;

                var validationResult = spriteSkin.Validate();

                if (validationResult == SpriteSkinValidationResult.Ready)
                    continue;

                var text = "";

                switch (validationResult)
                {
                    case SpriteSkinValidationResult.SpriteNotFound:
                        text = Contents.spriteNotFound;
                        break;
                    case SpriteSkinValidationResult.SpriteHasNoSkinningInformation:
                        text = Contents.spriteHasNoSkinningInformation;
                        break;
                    case SpriteSkinValidationResult.SpriteHasNoWeights:
                        text = Contents.spriteHasNoWeights;
                        break;
                    case SpriteSkinValidationResult.RootTransformNotFound:
                        text = Contents.rootTransformNotFound;
                        break;
                    case SpriteSkinValidationResult.RootNotFoundInTransformArray:
                        text = Contents.rootTransformNotFoundInArray;
                        break;
                    case SpriteSkinValidationResult.InvalidTransformArray:
                        text = Contents.invalidTransformArray;
                        break;
                    case SpriteSkinValidationResult.InvalidTransformArrayLength:
                        text = Contents.invalidTransformArrayLength;
                        break;
                    case SpriteSkinValidationResult.TransformArrayContainsNull:
                        text = Contents.transformArrayContainsNull;
                        break;
                }

                if (preAppendObjectName)
                    text = spriteSkin.name + ": " + text;

                EditorGUILayout.HelpBox(text, MessageType.Warning);
            }
        }
    }
}
