using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;
namespace Unity.Cinemachine.Editor
{
    /// 
    /// Static class that manages Cinemachine Tools. It knows which tool is active,
    /// and ensures that exclusive tools are not active at the same time.
    /// The tools and editors requiring tools register/unregister themselves here.
    /// 
    static class CinemachineSceneToolUtility
    {
        static Type s_ActiveExclusiveTool;
        static Dictionary s_RequiredTools;
        /// 
        /// Checks whether tool is the currently active exclusive tool.
        /// 
        /// Tool to check.
        /// True, when the tool is the active exclusive tool. False, otherwise.
        public static bool IsToolActive(Type tool) => s_ActiveExclusiveTool == tool;
        /// 
        /// Register your Type from the editor script's OnEnable function.
        /// This way CinemachineTools will know which tools to display.
        /// 
        /// Tool to register
        public static void RegisterTool(Type tool)
        {
            if (s_RequiredTools.ContainsKey(tool))
                s_RequiredTools[tool]++;
            else
                s_RequiredTools.Add(tool, 1);
            s_TriggerToolBarRefresh = true;
        }
        
        /// 
        /// Unregister your Type from the editor script's OnDisable function.
        /// This way CinemachineTools will know which tools to display.
        /// 
        /// Tool to register
        public static void UnregisterTool(Type tool)
        {
            if (s_RequiredTools.ContainsKey(tool))
            {
                --s_RequiredTools[tool];
                if (s_RequiredTools[tool] <= 0)
                    s_RequiredTools.Remove(tool);
            }
            s_TriggerToolBarRefresh = true;
        }
        internal static bool IsToolRequired(Type tool) => s_RequiredTools.ContainsKey(tool);
        internal static void SetTool(bool active, Type tool)
        {
            if (active)
                s_ActiveExclusiveTool = tool;
            else
                s_ActiveExclusiveTool = s_ActiveExclusiveTool == tool ? null : s_ActiveExclusiveTool;
            
            s_TriggerToolBarRefresh = true;
        }
        static CinemachineSceneToolUtility()
        {
            s_RequiredTools = new Dictionary();
            EditorApplication.update += RefreshToolbar;
        }
        static bool s_TriggerToolBarRefresh;
        static void RefreshToolbar()
        {
            if (s_TriggerToolBarRefresh)
            {
                ToolManager.RefreshAvailableTools();
                s_TriggerToolBarRefresh = false;
            }
        }
    }
    
    static class CinemachineSceneToolHelpers
    {
        public const float LineThickness = 2f;
        public static readonly Color HelperLineDefaultColor = new Color(255, 255, 255, 25);
        const float k_DottedLineSpacing = 4f;
        static GUIStyle s_LabelStyle = new GUIStyle 
        { 
            normal =
            {
                background = AssetDatabase.LoadAssetAtPath(
                    $"{CinemachineCore.kPackageRoot}/Editor/EditorResources/SceneToolsLabelBackground.png"),
                textColor = Handles.selectedColor,
            },
            fontStyle = FontStyle.Bold,
            padding = new RectOffset(5, 0, 5, 0)
        };
        
        public static float SliderHandleDelta(Vector3 newPos, Vector3 oldPos, Vector3 forward)
        {
            var delta = newPos - oldPos;
            return Mathf.Sign(Vector3.Dot(delta, forward)) * delta.magnitude;
        }
        /// 
        /// Calculate delta and discard imprecision.
        /// 
        public static Vector3 PositionHandleDelta(Quaternion rot, Vector3 newPos, Vector3 oldPos)
        {
            var delta =
                Quaternion.Inverse(rot) * (newPos - oldPos);
            delta = new Vector3(
                Mathf.Abs(delta.x) < UnityVectorExtensions.Epsilon ? 0 : delta.x,
                Mathf.Abs(delta.y) < UnityVectorExtensions.Epsilon ? 0 : delta.y,
                Mathf.Abs(delta.z) < UnityVectorExtensions.Epsilon ? 0 : delta.z);
            return delta;
        }
        
        public static void DrawLabel(Vector3 position, string text)
        {
            var labelOffset = HandleUtility.GetHandleSize(position) / 5f;
            Handles.Label(position + new Vector3(0, -labelOffset, 0), text, s_LabelStyle);
        }
        public static float CubeHandleCapSize(Vector3 position) => HandleUtility.GetHandleSize(position) / 10f;
        static int s_ScaleSliderHash = "ScaleSliderHash".GetHashCode();
        static float s_FOVAfterLastToolModification;
        public static void FovToolHandle(
            CinemachineVirtualCameraBase vcam, SerializedProperty lensProperty,
            in LensSettings lens, bool isLensHorizontal)
        {
            var orthographic = lens.Orthographic;
            if (GUIUtility.hotControl == 0)
                s_FOVAfterLastToolModification = orthographic ? lens.OrthographicSize : lens.FieldOfView;
            var originalColor = Handles.color;
            Handles.color = Handles.preselectionColor;
            
            var camPos = vcam.State.GetFinalPosition();
            var camRot = vcam.State.GetFinalOrientation();
            var camForward = camRot * Vector3.forward;
                
            EditorGUI.BeginChangeCheck();
            var fovHandleId = GUIUtility.GetControlID(s_ScaleSliderHash, FocusType.Passive);
            var newFov = Handles.ScaleSlider(fovHandleId, s_FOVAfterLastToolModification, 
                camPos, camForward, camRot, HandleUtility.GetHandleSize(camPos), 0.1f);
            if (EditorGUI.EndChangeCheck())
            {
                if (orthographic)
                {
                    lensProperty.FindPropertyRelative("OrthographicSize").floatValue += 
                        (s_FOVAfterLastToolModification - newFov);
                }
                else
                {
                    lensProperty.FindPropertyRelative("FieldOfView").floatValue += 
                        (s_FOVAfterLastToolModification - newFov);
                    lensProperty.FindPropertyRelative("FieldOfView").floatValue = 
                        Mathf.Clamp(lensProperty.FindPropertyRelative("FieldOfView").floatValue, 1f, 179f);
                }
                lensProperty.serializedObject.ApplyModifiedProperties();
            }
            s_FOVAfterLastToolModification = newFov;
            var fovHandleDraggedOrHovered = 
                GUIUtility.hotControl == fovHandleId || HandleUtility.nearestControl == fovHandleId;
            if (fovHandleDraggedOrHovered)
            {
                var labelPos = camPos + camForward * HandleUtility.GetHandleSize(camPos);
                if (lens.IsPhysicalCamera)
                {
                    DrawLabel(labelPos, "Focal Length (" + 
                        Camera.FieldOfViewToFocalLength(lens.FieldOfView, lens.PhysicalProperties.SensorSize.y).ToString("F1") + ")");
                }
                else if (orthographic)
                {
                    DrawLabel(labelPos, "Orthographic Size (" + 
                        lens.OrthographicSize.ToString("F1") + ")");
                }
                else if (isLensHorizontal)
                {
                    DrawLabel(labelPos, "Horizontal FOV (" +
                        Camera.VerticalToHorizontalFieldOfView(lens.FieldOfView, lens.Aspect).ToString("F1") + ")");
                }
                else
                {
                    DrawLabel(labelPos, "Vertical FOV (" + 
                        lens.FieldOfView.ToString("F1") + ")");
                }
            }
            
            Handles.color = fovHandleDraggedOrHovered ? Handles.selectedColor : HelperLineDefaultColor;
            var vcamLocalToWorld = Matrix4x4.TRS(camPos, camRot, Vector3.one);
            DrawFrustum(vcamLocalToWorld, lens);
                
            SoloOnDrag(GUIUtility.hotControl == fovHandleId, vcam, fovHandleId);
            Handles.color = originalColor;
        }
        public static void NearFarClipHandle(CinemachineVirtualCameraBase vcam, SerializedProperty lens)
        {
            var originalColor = Handles.color;
            Handles.color = Handles.preselectionColor;
            
            var vcamState = vcam.State;
            var camPos = vcamState.GetFinalPosition();
            var camRot = vcamState.GetFinalOrientation();
            var camForward = camRot * Vector3.forward;
            var nearClipPlane = lens.FindPropertyRelative("NearClipPlane");
            var farClipPlane = lens.FindPropertyRelative("FarClipPlane");
            var nearClipPos = camPos + camForward * nearClipPlane.floatValue;
            var farClipPos = camPos + camForward * farClipPlane.floatValue;
            var vcamLens = vcamState.Lens;
            
            EditorGUI.BeginChangeCheck();
            var ncHandleId = GUIUtility.GetControlID(FocusType.Passive);
            var newNearClipPos = Handles.Slider(ncHandleId, nearClipPos, camForward, 
                CubeHandleCapSize(nearClipPos), Handles.CubeHandleCap, 0.5f);
            var fcHandleId = GUIUtility.GetControlID(FocusType.Passive);
            var newFarClipPos = Handles.Slider(fcHandleId, farClipPos, camForward, 
                CubeHandleCapSize(farClipPos), Handles.CubeHandleCap, 0.5f);
            if (EditorGUI.EndChangeCheck())
            {
                nearClipPlane.floatValue += 
                    SliderHandleDelta(newNearClipPos, nearClipPos, camForward);
                if (!vcamLens.Orthographic)
                {
                    nearClipPlane.floatValue = Mathf.Max(0.01f, nearClipPlane.floatValue);
                }
                farClipPlane.floatValue += 
                    SliderHandleDelta(newFarClipPos, farClipPos, camForward);
                lens.serializedObject.ApplyModifiedProperties();
            }
            
            var vcamLocalToWorld = Matrix4x4.TRS(camPos, camRot, Vector3.one);
            Handles.color = HelperLineDefaultColor;
            DrawFrustum(vcamLocalToWorld, vcamLens);
            if (GUIUtility.hotControl == ncHandleId || HandleUtility.nearestControl == ncHandleId)
            {
                DrawPreFrustum(vcamLocalToWorld, vcamLens);
                DrawLabel(nearClipPos, nearClipPlane.displayName + " (" + nearClipPlane.floatValue.ToString("F1") + ")");
            }
            if (GUIUtility.hotControl == fcHandleId || HandleUtility.nearestControl == fcHandleId)
            {
                DrawPreFrustum(vcamLocalToWorld, vcamLens);
                DrawLabel(farClipPos, farClipPlane.displayName + " (" + farClipPlane.floatValue.ToString("F1") + ")");
            }
            
            SoloOnDrag(GUIUtility.hotControl == ncHandleId || GUIUtility.hotControl == fcHandleId, 
                vcam, Mathf.Min(ncHandleId, fcHandleId));
            Handles.color = originalColor;
        }
        static void DrawPreFrustum(Matrix4x4 transform, LensSettings lens)
        {
            if (!lens.Orthographic && lens.NearClipPlane >= 0)
            {
                DrawPerspectiveFrustum(transform, lens.FieldOfView, 
                    lens.NearClipPlane, 0, lens.Aspect, true);
            }
        }
        static void DrawFrustum(Matrix4x4 transform, LensSettings lens)
        {
            if (lens.Orthographic)
            {
                DrawOrthographicFrustum(transform, lens.OrthographicSize,
                    lens.FarClipPlane, lens.NearClipPlane, lens.Aspect);
            }
            else
            {
                DrawPerspectiveFrustum(transform, lens.FieldOfView, 
                    lens.FarClipPlane, lens.NearClipPlane, lens.Aspect, false);
            }
        }
        static void DrawOrthographicFrustum(Matrix4x4 transform, 
            float orthographicSize, float farClipPlane, float nearClipRange, float aspect)
        {
            var originalMatrix = Handles.matrix;
            Handles.matrix = transform;
            
            var size = new Vector3(aspect * orthographicSize * 2, 
                orthographicSize * 2, farClipPlane - nearClipRange);
            Handles.DrawWireCube(new Vector3(0, 0, (size.z / 2) + nearClipRange), size);
            
            Handles.matrix = originalMatrix;
        }
        
        static void DrawPerspectiveFrustum(Matrix4x4 transform, 
            float fov, float farClipPlane, float nearClipRange, float aspect, bool dottedLine)
        {
            var originalMatrix = Handles.matrix;
            Handles.matrix = transform;
            
            fov = fov * 0.5f * Mathf.Deg2Rad;
            var tanfov = Mathf.Tan(fov);
            var farEnd = new Vector3(0, 0, farClipPlane);
            var endSizeX = new Vector3(farClipPlane * tanfov * aspect, 0, 0);
            var endSizeY = new Vector3(0, farClipPlane * tanfov, 0);
            Vector3 s1, s2, s3, s4;
            var e1 = farEnd + endSizeX + endSizeY;
            var e2 = farEnd - endSizeX + endSizeY;
            var e3 = farEnd - endSizeX - endSizeY;
            var e4 = farEnd + endSizeX - endSizeY;
            if (nearClipRange <= 0.0f)
            {
                s1 = s2 = s3 = s4 = Vector3.zero;
            }
            else
            {
                var startSizeX = new Vector3(nearClipRange * tanfov * aspect, 0, 0);
                var startSizeY = new Vector3(0, nearClipRange * tanfov, 0);
                var startPoint = new Vector3(0, 0, nearClipRange);
                s1 = startPoint + startSizeX + startSizeY;
                s2 = startPoint - startSizeX + startSizeY;
                s3 = startPoint - startSizeX - startSizeY;
                s4 = startPoint + startSizeX - startSizeY;
                if (dottedLine)
                {
                    Handles.DrawDottedLine(s1, s2, k_DottedLineSpacing);
                    Handles.DrawDottedLine(s2, s3, k_DottedLineSpacing);
                    Handles.DrawDottedLine(s3, s4, k_DottedLineSpacing);
                    Handles.DrawDottedLine(s4, s1, k_DottedLineSpacing);
                }
                else
                {
                    Handles.DrawLine(s1, s2);
                    Handles.DrawLine(s2, s3);
                    Handles.DrawLine(s3, s4);
                    Handles.DrawLine(s4, s1);
                }
            }
            if (dottedLine)
            {
                Handles.DrawDottedLine(e1, e2, k_DottedLineSpacing);
                Handles.DrawDottedLine(e2, e3, k_DottedLineSpacing);
                Handles.DrawDottedLine(e3, e4, k_DottedLineSpacing);
                Handles.DrawDottedLine(e4, e1, k_DottedLineSpacing);
                Handles.DrawDottedLine(s1, e1, k_DottedLineSpacing);
                Handles.DrawDottedLine(s2, e2, k_DottedLineSpacing);
                Handles.DrawDottedLine(s3, e3, k_DottedLineSpacing);
                Handles.DrawDottedLine(s4, e4, k_DottedLineSpacing);
            }
            else
            {
                Handles.DrawLine(e1, e2);
                Handles.DrawLine(e2, e3);
                Handles.DrawLine(e3, e4);
                Handles.DrawLine(e4, e1);
                Handles.DrawLine(s1, e1);
                Handles.DrawLine(s2, e2);
                Handles.DrawLine(s3, e3);
                Handles.DrawLine(s4, e4);
            }
            Handles.matrix = originalMatrix;
        }
        public static void TrackedObjectOffsetTool(
            CinemachineVirtualCameraBase vcam, SerializedProperty trackedObjectOffset, CinemachineCore.Stage stage)
        {
            var target = vcam.LookAt;
            if (target == null)
                return;
            var originalColor = Handles.color;
            
            var lookAtPos = target.position;
            var lookAtRot = target.rotation;
            var trackedObjectPos = lookAtPos + lookAtRot * trackedObjectOffset.vector3Value;
            EditorGUI.BeginChangeCheck();
            var tooHandleIds = Handles.PositionHandleIds.@default;
            var newTrackedObjectPos = Handles.PositionHandle(tooHandleIds, trackedObjectPos, lookAtRot);
            var tooHandleMinId = tooHandleIds.x - 1;
            var tooHandleMaxId = tooHandleIds.xyz + 1;
            if (EditorGUI.EndChangeCheck())
            {
                trackedObjectOffset.vector3Value += 
                    PositionHandleDelta(lookAtRot, newTrackedObjectPos, trackedObjectPos);
                trackedObjectOffset.serializedObject.ApplyModifiedProperties();
            }
            var isDragged = 
                tooHandleMinId < GUIUtility.hotControl && GUIUtility.hotControl < tooHandleMaxId;
            var isDraggedOrHovered = isDragged || 
                tooHandleMinId < HandleUtility.nearestControl && HandleUtility.nearestControl < tooHandleMaxId;
            if (isDraggedOrHovered)
            {
                DrawLabel(trackedObjectPos, "(" + stage + ") " + trackedObjectOffset.displayName + " "
                    + trackedObjectOffset.vector3Value.ToString("F1"));
            }
            
            Handles.color = isDraggedOrHovered ? Handles.selectedColor : HelperLineDefaultColor;
            Handles.DrawDottedLine(lookAtPos, trackedObjectPos, k_DottedLineSpacing);
            Handles.DrawLine(trackedObjectPos, vcam.State.GetFinalPosition());
            SoloOnDrag(isDragged, vcam, tooHandleMaxId);
            
            Handles.color = originalColor;
        }
        public static void FollowOffsetTool(
            CinemachineVirtualCameraBase vcam, SerializedProperty offsetProperty, 
            Vector3 camPos, Vector3 targetPosition, Quaternion targetRotation, 
            Action OnChanged = null)
        {
            var originalColor = Handles.color;
            
            EditorGUI.BeginChangeCheck();
            var foHandleIds = Handles.PositionHandleIds.@default;
            var newPos = Handles.PositionHandle(foHandleIds, camPos, targetRotation);
            var foHandleMinId = foHandleIds.x - 1;
            var foHandleMaxId = foHandleIds.xyz + 1;
            if (EditorGUI.EndChangeCheck())
            {
                offsetProperty.vector3Value += PositionHandleDelta(targetRotation, newPos, camPos);
                offsetProperty.serializedObject.ApplyModifiedProperties();
                OnChanged?.Invoke();
            }
        
            var offset = offsetProperty.vector3Value;
            var isDragged = foHandleMinId < GUIUtility.hotControl && GUIUtility.hotControl < foHandleMaxId;
            var isDraggedOrHovered = isDragged || 
                foHandleMinId < HandleUtility.nearestControl && HandleUtility.nearestControl < foHandleMaxId;
            if (isDraggedOrHovered)
                DrawLabel(camPos, offsetProperty.displayName + " " + offset.ToString("F1"));
        
            Handles.color = isDraggedOrHovered ? Handles.selectedColor : HelperLineDefaultColor;
            Handles.DrawDottedLine(targetPosition, camPos, k_DottedLineSpacing);
            
            SoloOnDrag(isDragged, vcam, foHandleMaxId);
            
            Handles.color = originalColor;
        }
#if !CINEMACHINE_NO_CM2_SUPPORT
        /// 
        /// Draws Orbit handles (e.g. for freelook)
        /// 
        /// Index of the rig being edited, or -1 if none
        [Obsolete]
        public static int OrbitControlHandleFreelook(
            CinemachineFreeLook vcam, Quaternion rotationFrame, SerializedProperty orbits)
        {
            var originalColor = Handles.color;
            var followPos = vcam.Follow.position;
            var draggedRig = -1;
            var minIndex = 1;
            for (var rigIndex = 0; rigIndex < orbits.arraySize; ++rigIndex)
            {
                var orbit = orbits.GetArrayElementAtIndex(rigIndex);
                var orbitHeight = orbit.FindPropertyRelative("m_Height");
                var orbitRadius = orbit.FindPropertyRelative("m_Radius");
                
                if (OrbitHandles(
                    orbits.serializedObject, orbitHeight, orbitRadius, 
                    followPos, rotationFrame,
                    out var heightHandleId, out var radiusHandleId))
                {
                    draggedRig = rigIndex;
                    minIndex = Mathf.Min(heightHandleId, radiusHandleId);
                }
            }
            SoloOnDrag(draggedRig != -1, vcam, minIndex);
            Handles.color = originalColor;
            return draggedRig;
        }
#endif
        /// 
        /// Draws Orbit handles for OrbitalFollow
        /// 
        /// Index of the rig being edited, or -1 if none
        public static int ThreeOrbitRigHandle(
            CinemachineVirtualCameraBase vcam, Quaternion rotationFrame, 
            SerializedProperty orbitSetting, Vector3 orbitCenter)
        {
            Cinemachine3OrbitRig.Settings def = new();
            var originalColor = Handles.color;
            var draggedRig = -1;
            var minIndex = 1;
            SerializedProperty[] orbits =
            {
                orbitSetting.FindPropertyRelative(() => def.Top),
                orbitSetting.FindPropertyRelative(() => def.Center),
                orbitSetting.FindPropertyRelative(() => def.Bottom),
            };
            for (var rigIndex = 0; rigIndex < orbits.Length; ++rigIndex)
            {
                var orbit = orbits[rigIndex];
                var orbitHeight = orbit.FindPropertyRelative(() => def.Top.Height);
                var orbitRadius = orbit.FindPropertyRelative(() => def.Top.Radius);
                
                if (OrbitHandles(
                    orbitSetting.serializedObject, orbitHeight, orbitRadius, 
                    orbitCenter, rotationFrame,
                    out var heightHandleId, out var radiusHandleId))
                {
                    draggedRig = rigIndex;
                    minIndex = Mathf.Min(heightHandleId, radiusHandleId);
                }
            }
            SoloOnDrag(draggedRig != -1, vcam, minIndex);
            Handles.color = originalColor;
            return draggedRig;
        }
        static bool OrbitHandles(
            SerializedObject orbit, 
            SerializedProperty orbitHeight, SerializedProperty orbitRadius, 
            Vector3 followPos, Quaternion rotationFrame,
            out int heightHandleId, out int radiusHandleId)
        {
            var oldMatrix = Handles.matrix;
            Handles.matrix = Matrix4x4.TRS(followPos, rotationFrame, Vector3.one);
            Handles.color = Handles.preselectionColor;
            EditorGUI.BeginChangeCheck();
            
            heightHandleId = GUIUtility.GetControlID(FocusType.Passive);
            var height = Vector3.up * orbitHeight.floatValue;
            var newHeight = Handles.Slider(
                heightHandleId, height, Vector3.up, CubeHandleCapSize(height), Handles.CubeHandleCap, 0.5f);
                
            radiusHandleId = GUIUtility.GetControlID(FocusType.Passive);
            var radius = Vector3.up * orbitHeight.floatValue + Vector3.right * orbitRadius.floatValue;
            var newRadius = Handles.Slider(
                radiusHandleId, radius, Vector3.right, CubeHandleCapSize(radius), Handles.CubeHandleCap, 0.5f);
            if (EditorGUI.EndChangeCheck())
            {
                orbitHeight.floatValue += SliderHandleDelta(newHeight, height, Vector3.up);
                orbitRadius.floatValue += SliderHandleDelta(newRadius, radius, Vector3.right);
                orbit.ApplyModifiedProperties();
            }
            var isDragged = GUIUtility.hotControl == heightHandleId || GUIUtility.hotControl == radiusHandleId;
            Handles.color = isDragged || HandleUtility.nearestControl == heightHandleId 
                || HandleUtility.nearestControl == radiusHandleId ? Handles.selectedColor : HelperLineDefaultColor;
            if (GUIUtility.hotControl == heightHandleId || HandleUtility.nearestControl == heightHandleId)
                DrawLabel(height, orbitHeight.displayName + ": " + orbitHeight.floatValue);
            if (GUIUtility.hotControl == radiusHandleId || HandleUtility.nearestControl == radiusHandleId)
                DrawLabel(radius, orbitRadius.displayName + ": " + orbitRadius.floatValue);
            Handles.DrawWireDisc(newHeight, Vector3.up, orbitRadius.floatValue);
            Handles.matrix = oldMatrix;
            return isDragged;
        }
        
        static bool s_IsDragging;
        static ICinemachineCamera s_UserSolo;
        public static void SoloOnDrag(bool isDragged, CinemachineVirtualCameraBase vcam, int handleMaxId)
        {
            if (isDragged)
            {
                if (!s_IsDragging)
                {
                    s_UserSolo = CinemachineCore.SoloCamera;
                    s_IsDragging = true;
                }
                CinemachineCore.SoloCamera = vcam;
            }
            else if (s_IsDragging && handleMaxId != -1) // Handles sometimes return -1 as id, ignore those frames
            {
                CinemachineCore.SoloCamera = s_UserSolo;
                InspectorUtility.RepaintGameView();
                s_IsDragging = false;
                s_UserSolo = null;
            }
        }
    } 
}