using System.Collections.Generic;
using System.Linq;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.U2D.Common;
using UnityEngine.U2D.IK;

namespace UnityEditor.U2D.IK
{
    [DefaultExecutionOrder(-3)]
    internal class IKEditorManager : ScriptableObject
    {
        static IKEditorManager s_Instance;
        
        readonly HashSet<IKManager2D> m_DirtyManagers = new HashSet<IKManager2D>();
        readonly HashSet<IKManager2D> m_IKManagers = new HashSet<IKManager2D>();
        readonly Dictionary<IKChain2D, Vector3> m_ChainPositionOverrides = new Dictionary<IKChain2D, Vector3>();
        readonly List<Vector3> m_TargetPositions = new List<Vector3>();

        GameObject m_Helper;
        GameObject[] m_SelectedGameobjects;
        
        internal bool isDraggingATool { get; private set; }

        bool m_Initialized;

        [InitializeOnLoadMethod]
        static void CreateInstance()
        {
            if (s_Instance != null)
                return;
            
            var ikManagers = Resources.FindObjectsOfTypeAll<IKEditorManager>();
            if (ikManagers.Length > 0)
                s_Instance = ikManagers[0];
            else
                s_Instance = ScriptableObject.CreateInstance<IKEditorManager>();
            s_Instance.hideFlags = HideFlags.HideAndDontSave;            
        }
        
        public static IKEditorManager instance
        {
            get
            {
                if (s_Instance == null)
                    CreateInstance();
                return s_Instance;
            }
        }

        void OnEnable()
        {
            if (s_Instance == null)
                s_Instance = this;

            IKManager2D.onEnabledEditor += AddIKManager2D;
            IKManager2D.onDisabledEditor += RemoveIKManager2D;
        }

        void OnDisable()
        {
            IKManager2D.onEnabledEditor -= AddIKManager2D;
            IKManager2D.onDisabledEditor -= RemoveIKManager2D;

            Dispose();
        }

        public void Initialize()
        {
            var currentStage = StageUtility.GetCurrentStageHandle();
            var managers = currentStage.FindComponentsOfType<IKManager2D>().Where(x => x.gameObject.scene.isLoaded).ToArray();
            foreach (var ikManager2D in managers)
                m_IKManagers.Add(ikManager2D);

            RegisterCallbacks();

            m_Initialized = true;
        }

        void Dispose()
        {
            UnregisterCallbacks();
            Clear();

            m_Initialized = false;
        }

        void AddIKManager2D(IKManager2D manager)
        {
            if (manager == null)
                return;

            if (!m_Initialized)
                Initialize();

            m_IKManagers.Add(manager);
        }

        void RemoveIKManager2D(IKManager2D manager)
        {
            m_IKManagers.Remove(manager);

            if (m_IKManagers.Count == 0)
                Dispose();
        }

        private void RegisterCallbacks()
        {
#if UNITY_2019_1_OR_NEWER
            SceneView.duringSceneGui += OnSceneGUI;
#else
            SceneView.onSceneGUIDelegate += OnSceneGUI;
#endif
            Selection.selectionChanged += OnSelectionChanged;
        }

        private void UnregisterCallbacks()
        {
#if UNITY_2019_1_OR_NEWER
            SceneView.duringSceneGui -= OnSceneGUI;
#else
            SceneView.onSceneGUIDelegate -= OnSceneGUI;
#endif
            Selection.selectionChanged -= OnSelectionChanged;
        }

        private bool m_EnableGizmos;
        private bool m_CurrentEnableGizmoState;
        
        void OnDrawGizmos()
        {
            m_EnableGizmos = true;
            IKManager2D.onDrawGizmos.RemoveListener(OnDrawGizmos);
        }
        
        public void CheckGizmoToggle()
        {
            //Ignore events other than Repaint
            if (Event.current.type != EventType.Repaint)
                return;

            if (m_CurrentEnableGizmoState != m_EnableGizmos)
                SceneView.RepaintAll();

            m_CurrentEnableGizmoState = m_EnableGizmos;

            //Assume the Gizmo toggle is disabled and listen to the event again
            m_EnableGizmos = false;
            IKManager2D.onDrawGizmos.RemoveListener(OnDrawGizmos);
            IKManager2D.onDrawGizmos.AddListener(OnDrawGizmos);
        }
        
        private void OnSelectionChanged()
        {
            m_SelectedGameobjects = null;
        }

        void Clear()
        {
            m_IKManagers.Clear();
            m_DirtyManagers.Clear();
            m_ChainPositionOverrides.Clear();
        }

        public IKManager2D FindManager(Solver2D solver)
        {
            foreach (IKManager2D manager in m_IKManagers)
            {
                if (manager == null)
                    continue;

                foreach (Solver2D s in manager.solvers)
                {
                    if (s == null)
                        continue;

                    if (s == solver)
                        return manager;
                }
            }

            return null;
        }

        public void Record(Solver2D solver, string undoName)
        {
            var manager = FindManager(solver);

            DoUndo(manager, undoName, true);
        }

        public void RegisterUndo(Solver2D solver, string undoName)
        {
            var manager = FindManager(solver);

            DoUndo(manager, undoName, false);
        }

        public void Record(IKManager2D manager, string undoName)
        {
            DoUndo(manager, undoName, true);
        }

        public void RegisterUndo(IKManager2D manager, string undoName)
        {
            DoUndo(manager, undoName, false);
        }

        private void DoUndo(IKManager2D manager, string undoName, bool record)
        {
            if (manager == null)
                return;
            foreach (var solver in manager.solvers)
            {
                if (solver == null || !solver.isActiveAndEnabled)
                    continue;

                if (!solver.isValid)
                    solver.Initialize();

                if (!solver.isValid)
                    continue;

                for (int i = 0; i < solver.chainCount; ++i)
                {
                    var chain = solver.GetChain(i);

                    if (record)
                    {
                        foreach(var t in chain.transforms)
                            Undo.RecordObject(t, undoName);

                        if(chain.target)
                            Undo.RecordObject(chain.target, undoName);
                    }
                    else
                    {
                        foreach(var t in chain.transforms)
                            Undo.RegisterCompleteObjectUndo(t, undoName);

                        if(chain.target)
                            Undo.RegisterCompleteObjectUndo(chain.target, undoName);
                    }
                }
            }
        }

        public void UpdateManagerImmediate(IKManager2D manager, bool recordRootLoops)
        {
            SetManagerDirty(manager);
            UpdateDirtyManagers(recordRootLoops);
        }

        public void UpdateSolverImmediate(Solver2D solver, bool recordRootLoops)
        {
            SetSolverDirty(solver);
            UpdateDirtyManagers(recordRootLoops);
        }

        public void SetChainPositionOverride(IKChain2D chain, Vector3 position)
        {
            m_ChainPositionOverrides[chain] = position;
        }

        private bool IsViewToolActive()
        {
            int button = Event.current.button;
            return Tools.current == Tool.View || Event.current.alt || (button == 1) || (button == 2);
        }

        private bool IsDraggingATool()
        {
            //If a tool has used EventType.MouseDrag, we won't be able to detect it. Instead we check for delta magnitude
            return GUIUtility.hotControl != 0 && Event.current.button == 0 && Event.current.delta.sqrMagnitude > 0f && !IsViewToolActive();
        }

        private void OnSceneGUI(SceneView sceneView)
        {
            CheckGizmoToggle();
            if (!m_CurrentEnableGizmoState)
                return;
            
            if (m_SelectedGameobjects == null)
                m_SelectedGameobjects = Selection.gameObjects;

            foreach (var ikManager2D in m_IKManagers)
            {
                if (ikManager2D != null && ikManager2D.isActiveAndEnabled)
                    IKGizmos.instance.DoSolversGUI(ikManager2D);
            }

            if (!IKGizmos.instance.isDragging && IsDraggingATool())
            {
                //We expect the object to be selected while dragged
                foreach (var gameObject in m_SelectedGameobjects)
                {
                    if (gameObject != null && gameObject.transform != null)
                        SetDirtySolversAffectedByTransform(gameObject.transform);
                }

                if(m_DirtyManagers.Count > 0 && !isDraggingATool)
                {
                    isDraggingATool = true;
                    Undo.SetCurrentGroupName("IK Update");

                    RegisterUndoForDirtyManagers();
                }
            }

            if(GUIUtility.hotControl == 0)
                isDraggingATool = false;
        }

        private void SetSolverDirty(Solver2D solver)
        {
            if (solver && solver.isValid && solver.isActiveAndEnabled)
                SetManagerDirty(FindManager(solver));
        }

        private void SetManagerDirty(IKManager2D manager)
        {
            if (manager && manager.isActiveAndEnabled)
                m_DirtyManagers.Add(manager);
        }

        private void SetDirtySolversAffectedByTransform(Transform transform)
        {
            foreach (var manager in m_IKManagers)
            {
                if (manager != null && manager.isActiveAndEnabled)
                {
                    var dirty = false;
                    var solvers = manager.solvers;
                    for (var s = 0; s < solvers.Count; s++)
                    {
                        if (dirty)
                            break;

                        var solver = solvers[s];
                        if (solver != null && solver.isValid)
                        {
                            for (var c = 0; c < solver.chainCount; ++c)
                            {
                                var chain = solver.GetChain(c);

                                if (chain.target == null)
                                    continue;

                                if (!(IKUtility.IsDescendentOf(chain.target, transform) && IKUtility.IsDescendentOf(chain.rootTransform, transform)) &&
                                    (chain.target == transform || IKUtility.IsDescendentOf(chain.target, transform) || IKUtility.IsDescendentOf(chain.effector, transform)))
                                {
                                    SetManagerDirty(manager);
                                    dirty = true;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }

        private void RegisterUndoForDirtyManagers()
        {
            foreach (var manager in m_DirtyManagers)
                RegisterUndo(manager, Undo.GetCurrentGroupName());
        }

        private void UpdateDirtyManagers(bool recordRootLoops)
        {
            foreach (var manager in m_DirtyManagers)
            {
                if (manager == null || !manager.isActiveAndEnabled)
                    continue;

                foreach (var solver in manager.solvers)
                {
                    if (solver == null || !solver.isActiveAndEnabled)
                        continue;

                    if (!solver.isValid)
                        solver.Initialize();

                    if (!solver.isValid)
                        continue;

                    if(solver.allChainsHaveTargets)
                        solver.UpdateIK(manager.weight);
                    else if(PrepareTargetOverrides(solver))
                        solver.UpdateIK(m_TargetPositions, manager.weight);

                    for (int i = 0; i < solver.chainCount; ++i)
                    {
                        var chain = solver.GetChain(i);

                        if (recordRootLoops)
                            InternalEngineBridge.SetLocalEulerHint(chain.rootTransform);

                        if(solver.constrainRotation && chain.target != null)
                            InternalEngineBridge.SetLocalEulerHint(chain.effector);
                    }
                }
            }

            m_DirtyManagers.Clear();
            m_ChainPositionOverrides.Clear();
        }

        private bool PrepareTargetOverrides(Solver2D solver)
        {
            m_TargetPositions.Clear();

            for (int i = 0; i < solver.chainCount; ++i)
            {
                var chain = solver.GetChain(i);

                Vector3 positionOverride;
                if (!m_ChainPositionOverrides.TryGetValue(chain, out positionOverride))
                {
                    m_TargetPositions.Clear();
                    return false;
                }

                m_TargetPositions.Add(positionOverride);
            }

            return true;
        }
    }
}
