#if UNITY_2021_2_OR_NEWER
using System;
using System.Collections.Generic;
using Cinemachine.Utility;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;

namespace Cinemachine.Editor
{
    /// <summary>
    /// 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.
    /// </summary>
    static class CinemachineSceneToolUtility
    {
#if UNITY_2022_1_OR_NEWER
        /// <summary>
        /// Checks whether tool is the currently active exclusive tool.
        /// </summary>
        /// <param name="tool">Tool to check.</param>
        /// <returns>True, when the tool is the active exclusive tool. False, otherwise.</returns>
        public static bool IsToolActive(Type tool)
        {
            return s_ActiveExclusiveTool == tool;
        }
        static Type s_ActiveExclusiveTool;

        /// <summary>
        /// Register your Type from the editor script's OnEnable function.
        /// This way CinemachineTools will know which tools to display.
        /// </summary>
        /// <param name="tool">Tool to register</param>
        public static void RegisterTool(Type tool)
        {
            if (s_RequiredTools.ContainsKey(tool))
            {
                s_RequiredTools[tool]++;
            }
            else
            {
                s_RequiredTools.Add(tool, 1);
            }

            s_TriggerToolBarRefresh = true;
        }
        
        /// <summary>
        /// Unregister your Type from the editor script's OnDisable function.
        /// This way CinemachineTools will know which tools to display.
        /// </summary>
        /// <param name="tool">Tool to register</param>
        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)
        {
            return s_RequiredTools.ContainsKey(tool);
        }
        static Dictionary<Type, int> s_RequiredTools;
        
        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<Type, int>();
            EditorApplication.update += RefreshToolbar;
        }

        static bool s_TriggerToolBarRefresh;
        static void RefreshToolbar()
        {
            if (s_TriggerToolBarRefresh)
            {
#if UNITY_2022_2_OR_NEWER
                ToolManager.RefreshAvailableTools();
#else
                // The following is a hack to refresh the toolbars
                foreach (var scene in SceneView.sceneViews)
                {
                    if (((SceneView)scene).TryGetOverlay("unity-transform-toolbar", out var tools))
                    {
                        if (tools.displayed)
                        {
                            tools.displayed = false;
                            tools.displayed = true;
                            break;
                        }
                    }
                }
#endif
                s_TriggerToolBarRefresh = false;
            }
        }
#else
        /// <summary>
        /// Checks whether tool is the currently active exclusive tool.
        /// </summary>
        /// <param name="tool">Tool to check.</param>
        /// <returns>True, when the tool is the active exclusive tool. False, otherwise.</returns>
        public static bool IsToolActive(Type tool)
        {
            return s_ActiveExclusiveTool == tool;
        }
        static Type s_ActiveExclusiveTool;

        /// <summary>
        /// Register your Type from the editor script's OnEnable function.
        /// This way CinemachineTools will know which tools to display.
        /// </summary>
        /// <param name="tool">Tool to register</param>
        public static void RegisterTool(Type tool)
        {
            if (s_RequiredTools.ContainsKey(tool))
            {
                s_RequiredTools[tool]++;
            }
            else
            {
                s_RequiredTools.Add(tool, 1);
            }
        }
        
        /// <summary>
        /// Unregister your Type from the editor script's OnDisable function.
        /// This way CinemachineTools will know which tools to display.
        /// </summary>
        /// <param name="tool">Tool to register</param>
        public static void UnregisterTool(Type tool)
        {
            if (s_RequiredTools.ContainsKey(tool))
            {
                s_RequiredTools[tool]--;
                if (s_RequiredTools[tool] <= 0)
                {
                    s_RequiredTools.Remove(tool);
                }
            }
        }
        static Dictionary<Type, int> s_RequiredTools;

        public delegate void ToolHandler(bool v);
        struct CinemachineSceneToolDelegates
        {
            public ToolHandler ToggleSetter;
            public ToolHandler IsDisplayedSetter;
        }
        static Dictionary<Type, CinemachineSceneToolDelegates> s_ExclusiveTools; // tools that can't be active at the same time
        static Dictionary<Type, CinemachineSceneToolDelegates> s_Tools; // tools without restrictions
        
        /// <summary>
        /// Use for registering tool handlers for tools that are exclusive with each other,
        /// meaning they cannot be active at the same time.
        /// </summary>
        /// <param name="tool">The tool to register.</param>
        /// <param name="toggleSetter">The tool's toggle value setter.</param>
        /// <param name="isDisplayedSetter">The tool's isDisplayed setter.</param>
        internal static void RegisterExclusiveToolHandlers(Type tool, ToolHandler toggleSetter, ToolHandler isDisplayedSetter)
        {
            RegisterToolHandlers(ref s_ExclusiveTools, tool, toggleSetter, isDisplayedSetter);
        }
        
        /// <summary>
        /// Use for registering tool handlers for tools that can be active anytime
        /// without taking other tools into consideration.
        /// </summary>
        /// <param name="tool">The tool to register.</param>
        /// <param name="toggleSetter">The tool's toggle value setter.</param>
        /// <param name="isDisplayedSetter">The tool's isDisplayed setter.</param>
        internal static void RegisterToolHandlers(Type tool, ToolHandler toggleSetter, ToolHandler isDisplayedSetter)
        {
            RegisterToolHandlers(ref s_Tools, tool, toggleSetter, isDisplayedSetter);
        }

        static void RegisterToolHandlers(ref Dictionary<Type, CinemachineSceneToolDelegates> tools,
            Type tool, ToolHandler toggleSetter, ToolHandler isDisplayedSetter)
        {
            if (tools.ContainsKey(tool))
            {
                tools.Remove(tool);
            }
            
            tools.Add(tool, new CinemachineSceneToolDelegates
            {
                ToggleSetter = toggleSetter,
                IsDisplayedSetter = isDisplayedSetter,
            });
        }
        
        internal delegate bool ToolbarHandler();
        static ToolbarHandler s_ToolBarIsDisplayed;
        internal static void RegisterToolbarIsDisplayedHandler(ToolbarHandler handler)
        {
            s_ToolBarIsDisplayed = handler;
        }

        static bool s_ToolbarTurnOffByMe;
        internal delegate bool ToolbarTurnOnOffHandler(bool on);
        static ToolbarTurnOnOffHandler s_ToolBarDisplay;
        internal static void RegisterToolbarDisplayHandler(ToolbarTurnOnOffHandler handler)
        {
            s_ToolBarDisplay = handler;
        }

        internal static void SetTool(bool active, Type tool)
        {
            if (active)
            {
                s_ActiveExclusiveTool = tool;
                EnsureCinemachineToolsAreExclusiveWithUnityTools();
            }
            else
            {
                s_ActiveExclusiveTool = s_ActiveExclusiveTool == tool ? null : s_ActiveExclusiveTool;
            }
        }

        static void EnsureCinemachineToolsAreExclusiveWithUnityTools()
        {
            foreach (var toolHandle in s_ExclusiveTools)
            {
                toolHandle.Value.ToggleSetter(toolHandle.Key == s_ActiveExclusiveTool);
            }
            if (s_ActiveExclusiveTool != null)
            {
                Tools.current = Tool.None; // Cinemachine tools are exclusive with unity tools
            }
        }
        
        static CinemachineSceneToolUtility()
        {
            s_ExclusiveTools = new Dictionary<Type, CinemachineSceneToolDelegates>();
            s_Tools = new Dictionary<Type, CinemachineSceneToolDelegates>();
            s_RequiredTools = new Dictionary<Type, int>();

            EditorApplication.update += () =>
            {
                if (s_RequiredTools.Count <= 0)
                {
                    s_ToolbarTurnOffByMe |= s_ToolBarDisplay(false);
                }
                else if (s_ToolbarTurnOffByMe)
                {
                    s_ToolBarDisplay(true);
                    s_ToolbarTurnOffByMe = false;
                }
                
                var cmToolbarIsHidden = !s_ToolBarIsDisplayed();
                // if a unity tool is selected or cmToolbar is hidden, unselect our tools.
                if (s_ActiveExclusiveTool != null && (Tools.current != Tool.None || cmToolbarIsHidden))
                {
                    SetTool(true, null);
                }

                if (!cmToolbarIsHidden)
                {
                    // only display cm tools that are relevant for the current selection
                    foreach (var toolHandle in s_ExclusiveTools)
                    {
                        toolHandle.Value.IsDisplayedSetter(s_RequiredTools.ContainsKey(toolHandle.Key));
                    }
                    foreach (var toolHandle in s_Tools)
                    {
                        toolHandle.Value.IsDisplayedSetter(s_RequiredTools.ContainsKey(toolHandle.Key));
                    }
                }
            };
        }
#endif
    }
    
    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<Texture2D>(ScriptableObjectUtility.
                    CinemachineRealativeInstallPath + "/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;
        }

        /// <summary>
        /// Calculate delta and discard imprecision.
        /// </summary>
        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.FinalPosition;
            var camRot = vcam.State.FinalOrientation;
            var camForward = camRot * Vector3.forward;
                
            EditorGUI.BeginChangeCheck();
#if UNITY_2022_2_OR_NEWER
            var fovHandleId = GUIUtility.GetControlID(s_ScaleSliderHash, FocusType.Passive);
            var newFov = Handles.ScaleSlider(fovHandleId, s_FOVAfterLastToolModification, 
                camPos, camForward, camRot, HandleUtility.GetHandleSize(camPos), 0.1f);
#else
            var fovHandleId = GUIUtility.GetControlID(s_ScaleSliderHash, FocusType.Passive) + 1;
            var newFov = Handles.ScaleSlider(
                s_FOVAfterLastToolModification, camPos, camForward, camRot, HandleUtility.GetHandleSize(camPos), 0.1f);
#endif
            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.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.FinalPosition;
            var camRot = vcamState.FinalOrientation;
            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, "Near Clip Plane (" + nearClipPlane.floatValue.ToString("F1") + ")");
            }
            if (GUIUtility.hotControl == fcHandleId || HandleUtility.nearestControl == fcHandleId)
            {
                DrawPreFrustum(vcamLocalToWorld, vcamLens);
                DrawLabel(farClipPos, "Far Clip Plane (" + 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(
            CinemachineComponentBase cmComponent, SerializedProperty trackedObjectOffset)
        {
            var originalColor = Handles.color;
            
            var lookAtPos = cmComponent.LookAtTargetPosition;
            var lookAtRot = cmComponent.LookAtTargetRotation;
            var trackedObjectPos = lookAtPos + lookAtRot * trackedObjectOffset.vector3Value;

            EditorGUI.BeginChangeCheck();
#if UNITY_2022_2_OR_NEWER
            var tooHandleIds = Handles.PositionHandleIds.@default;
            var newTrackedObjectPos = Handles.PositionHandle(tooHandleIds, trackedObjectPos, lookAtRot);
            var tooHandleMinId = tooHandleIds.x - 1;
            var tooHandleMaxId = tooHandleIds.xyz + 1;
#else
            var tooHandleMinId = GUIUtility.GetControlID(FocusType.Passive);
            var newTrackedObjectPos = Handles.PositionHandle(trackedObjectPos, lookAtRot);
            var tooHandleMaxId = GUIUtility.GetControlID(FocusType.Passive);
#endif
            if (EditorGUI.EndChangeCheck())
            {
                trackedObjectOffset.vector3Value += 
                    PositionHandleDelta(lookAtRot, newTrackedObjectPos, trackedObjectPos);
                trackedObjectOffset.serializedObject.ApplyModifiedProperties();
            }

            var trackedObjectOffsetHandleIsDragged = 
                tooHandleMinId < GUIUtility.hotControl && GUIUtility.hotControl < tooHandleMaxId;
            var trackedObjectOffsetHandleIsUsedOrHovered = trackedObjectOffsetHandleIsDragged || 
                tooHandleMinId < HandleUtility.nearestControl && HandleUtility.nearestControl < tooHandleMaxId;
            if (trackedObjectOffsetHandleIsUsedOrHovered)
            {
                DrawLabel(trackedObjectPos, "(" + cmComponent.Stage + ") Tracked Object Offset " 
                    + trackedObjectOffset.vector3Value.ToString("F1"));
            }
            
            Handles.color = trackedObjectOffsetHandleIsUsedOrHovered ? 
                Handles.selectedColor : HelperLineDefaultColor;
            Handles.DrawDottedLine(lookAtPos, trackedObjectPos, k_DottedLineSpacing);
            Handles.DrawLine(trackedObjectPos, cmComponent.VcamState.FinalPosition);

            SoloOnDrag(trackedObjectOffsetHandleIsDragged, cmComponent.VirtualCamera, tooHandleMaxId);
            
            Handles.color = originalColor;
        }

        public static void TransposerFollowOffsetTool(CinemachineTransposer cmComponent)
        {
            var originalColor = Handles.color;
            
            var brain = CinemachineCore.Instance.FindPotentialTargetBrain(cmComponent.VirtualCamera);
            var up = brain != null ? brain.DefaultWorldUp : Vector3.up;
            var camPos = cmComponent.GetTargetCameraPosition(up);
            var camRot = cmComponent.GetReferenceOrientation(up);

            EditorGUI.BeginChangeCheck();
#if UNITY_2022_2_OR_NEWER
            var foHandleIds = Handles.PositionHandleIds.@default;
            var newPos = Handles.PositionHandle(foHandleIds, camPos, camRot);
            var foHandleMinId = foHandleIds.x - 1;
            var foHandleMaxId = foHandleIds.xyz + 1;
#else
            var foHandleMinId = GUIUtility.GetControlID(FocusType.Passive);
            var newPos = Handles.PositionHandle(camPos, camRot);
            var foHandleMaxId = GUIUtility.GetControlID(FocusType.Passive);
#endif
            if (EditorGUI.EndChangeCheck())
            {
                var so = new SerializedObject(cmComponent);
                var followOffset = so.FindProperty(() => cmComponent.m_FollowOffset);
                followOffset.vector3Value += PositionHandleDelta(camRot, newPos, camPos);
                so.ApplyModifiedProperties();
                followOffset.vector3Value = cmComponent.EffectiveOffset;
                so.ApplyModifiedProperties();
            }

            var followOffsetHandleIsDragged = 
                foHandleMinId < GUIUtility.hotControl && GUIUtility.hotControl < foHandleMaxId;
            var followOffsetHandleIsDraggedOrHovered = followOffsetHandleIsDragged || 
                foHandleMinId < HandleUtility.nearestControl && HandleUtility.nearestControl < foHandleMaxId;
            if (followOffsetHandleIsDraggedOrHovered)
            {
                DrawLabel(camPos, "Follow offset " + cmComponent.m_FollowOffset.ToString("F1"));
            }

            Handles.color = followOffsetHandleIsDraggedOrHovered ? Handles.selectedColor : HelperLineDefaultColor;
            Handles.DrawDottedLine(cmComponent.FollowTargetPosition, camPos, k_DottedLineSpacing);
            
            SoloOnDrag(followOffsetHandleIsDragged, cmComponent.VirtualCamera, foHandleMaxId);
            
            Handles.color = originalColor;
        }
        
        /// <summary>
        /// Draws Orbit handles (e.g. for freelook)
        /// </summary>
        /// <returns>Index of the rig being edited, or -1 if none</returns>
        public static int OrbitControlHandle(
            CinemachineVirtualCameraBase vcam, 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");
                Handles.color = Handles.preselectionColor;
                EditorGUI.BeginChangeCheck();
            
                var heightHandleId = GUIUtility.GetControlID(FocusType.Passive);
                var heightHandlePos = followPos + Vector3.up * orbitHeight.floatValue;
                var newHeightHandlePos = Handles.Slider(heightHandleId, heightHandlePos, Vector3.up, 
                    CubeHandleCapSize(heightHandlePos), Handles.CubeHandleCap, 0.5f);
                
                var radiusHandleOffset = Vector3.right;
                var radiusHandleId = GUIUtility.GetControlID(FocusType.Passive);
                var radiusHandlePos = followPos + Vector3.up * orbitHeight.floatValue
                    + radiusHandleOffset * orbitRadius.floatValue;
                var newRadiusHandlePos = Handles.Slider(radiusHandleId, radiusHandlePos, radiusHandleOffset, 
                    CubeHandleCapSize(radiusHandlePos), Handles.CubeHandleCap, 0.5f);

                if (EditorGUI.EndChangeCheck())
                {
                    orbitHeight.floatValue += 
                        SliderHandleDelta(newHeightHandlePos, heightHandlePos, Vector3.up);
                    orbitRadius.floatValue += 
                        SliderHandleDelta(newRadiusHandlePos, radiusHandlePos, radiusHandleOffset);
                    orbits.serializedObject.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(heightHandlePos, "Height: " + orbitHeight.floatValue);
                }
                if (GUIUtility.hotControl == radiusHandleId || HandleUtility.nearestControl == radiusHandleId)
                {
                    DrawLabel(radiusHandlePos, "Radius: " + orbitRadius.floatValue);
                }

                Handles.DrawWireDisc(newHeightHandlePos, Vector3.up, orbitRadius.floatValue);
                if (isDragged)
                {
                    draggedRig = rigIndex;
                    minIndex = Mathf.Min(Mathf.Min(heightHandleId), radiusHandleId);
                }
            }
            SoloOnDrag(draggedRig != -1, vcam, minIndex);

            Handles.color = originalColor;
            return draggedRig;
        }
        
        static bool s_IsDragging;
        static ICinemachineCamera s_UserSolo;
        public static void SoloOnDrag(bool isDragged, ICinemachineCamera vcam, int handleMaxId)
        {
            if (isDragged)
            {
                if (!s_IsDragging)
                {
                    s_UserSolo = CinemachineBrain.SoloCamera;
                    s_IsDragging = true;
                }
                CinemachineBrain.SoloCamera = vcam;
            }
            else if (s_IsDragging && handleMaxId != -1) // Handles sometimes return -1 as id, ignore those frames
            {
                CinemachineBrain.SoloCamera = s_UserSolo;
                InspectorUtility.RepaintGameView();
                s_IsDragging = false;
                s_UserSolo = null;
            }
        }
    } 
}
#endif