using System;
using UnityEngine;
using UnityEngine.Rendering;
using Object = UnityEngine.Object;

namespace UnityEditor.Splines
{
    struct ColorScope : IDisposable
    {
        readonly Color m_PrevColor;

        public ColorScope(Color color)
        {
            m_PrevColor = Handles.color;
            Handles.color = color;
        }

        public void Dispose()
        {
            Handles.color = m_PrevColor;
        }
    }
    
    struct ZTestScope : IDisposable
    {
        readonly CompareFunction m_Original;

        public ZTestScope(CompareFunction function)
        {
            m_Original = Handles.zTest;
            Handles.zTest = function;
        }
        
        public void Dispose()
        {
            Handles.zTest = m_Original;
        }
    }
        
    static class SplineHandleUtility
    {
        const int k_MaxDecimals = 15;
        const int k_SegmentsPointCount = 30;
        static readonly Vector3[] s_ClosestPointArray = new Vector3[k_SegmentsPointCount];
        const float k_KnotPickingDistance = 18f;
        
        static readonly Vector3[] s_LineBuffer = new Vector3[2]; 
        
        internal static Ray TransformRay(Ray ray, Matrix4x4 matrix)
        {
            return new Ray(matrix.MultiplyPoint3x4(ray.origin), matrix.MultiplyVector(ray.direction));
        }

        internal static Vector3 DoIncrementSnap(Vector3 position, Vector3 previousPosition)
        {
            var delta = position - previousPosition;

            var right = Tools.handleRotation * Vector3.right;
            var up = Tools.handleRotation * Vector3.up;
            var forward = Tools.handleRotation * Vector3.forward;
            
            var snappedDelta = 
                Snapping.Snap(Vector3.Dot(delta, right), EditorSnapSettings.move[0]) * right + 
                Snapping.Snap(Vector3.Dot(delta, up), EditorSnapSettings.move[1]) * up + 
                Snapping.Snap(Vector3.Dot(delta, forward), EditorSnapSettings.move[2]) * forward;
            return previousPosition + snappedDelta;
        }
        
        static Vector3 SnapToGrid(Vector3 position)
        {
            //todo Temporary version, waiting for a trunk PR to land to move to the commented version:
//#if UNITY_2022_2_OR_NEWER
            // if(EditorSnapSettings.gridSnapActive)
            //     return Snapping.Snap(position, EditorSnapSettings.gridSize, SnapAxis.All);
//#else
            GameObject tmp = new GameObject();
            tmp.hideFlags = HideFlags.HideAndDontSave;
            var trs = tmp.transform;
            trs.position = position;
            Handles.SnapToGrid(new []{trs});
            var snapped = trs.position;
            Object.DestroyImmediate(tmp);

            return snapped;
//#endif
        }
        
        internal static bool GetPointOnSurfaces(Vector2 mousePosition, out Vector3 point, out Vector3 normal)
        {
#if UNITY_2020_1_OR_NEWER
            if(HandleUtility.PlaceObject(mousePosition, out point, out normal))
            {
                if(EditorSnapSettings.gridSnapEnabled)
                    point = SnapToGrid(point);
                return true;
            }
#endif

            var ray = HandleUtility.GUIPointToWorldRay(mousePosition);

#if !UNITY_2020_1_OR_NEWER
            if (Physics.Raycast(ray, out RaycastHit hit))
            {
                point = hit.point;
                normal = hit.normal;
                return true;
            }
#endif

            //Backup if couldn't find a surface
            var constraint = new Plane(Vector3.up, Vector3.zero); //This should be in the direction of the current grid
            if (constraint.Raycast(ray, out float distance))
            {
                normal = constraint.normal;
                point = ray.origin + ray.direction * distance;
                
                if(EditorSnapSettings.gridSnapEnabled)
                    point = SnapToGrid(point);
                
                return true;
            }

            point = normal = Vector3.zero;
            return false;
        }

        internal static void DrawLineWithWidth(Vector3 a, Vector3 b, float width, Texture2D lineAATex = null)
        {
            s_LineBuffer[0] = a;
            s_LineBuffer[1] = b;
         
            Handles.DrawAAPolyLine(lineAATex, width, s_LineBuffer);
        }

        public static float DistanceToKnot(Vector3 position)
        {
            return DistanceToCircle(position, k_KnotPickingDistance); 
        }
        
        public static float DistanceToCircle(Vector3 point, float radius)
        {
            Vector3 screenPos = HandleUtility.WorldToGUIPointWithDepth(point);
            if (screenPos.z < 0)
                return float.MaxValue;

            return Mathf.Max(0, Vector2.Distance(screenPos, Event.current.mousePosition) - radius);
        }
        
        internal static Vector3 RoundBasedOnMinimumDifference(Vector3 position)
        {
            var minDiff = GetMinDifference(position);
            position.x = RoundBasedOnMinimumDifference(position.x, minDiff.x);
            position.y = RoundBasedOnMinimumDifference(position.y, minDiff.y);
            position.z = RoundBasedOnMinimumDifference(position.z, minDiff.z);
            return position;
        }
        
        internal static Vector3 GetMinDifference(Vector3 position)
        {
            return Vector3.one * (HandleUtility.GetHandleSize(position) / 80f);
        }
        
        internal static float RoundBasedOnMinimumDifference(float valueToRound, float minDifference)
        {
            var numberOfDecimals = Mathf.Clamp(-Mathf.FloorToInt(Mathf.Log10(Mathf.Abs(minDifference))), 0, k_MaxDecimals);
            return (float)Math.Round(valueToRound, numberOfDecimals, MidpointRounding.AwayFromZero);
        }
        
        public static void GetNearestPointOnCurve(CurveData curve, out Vector3 position, out float t)
        {
            Vector3 closestA = Vector3.zero;
            Vector3 closestB = Vector3.zero;
            float closestDist = float.MaxValue;
            int closestSegmentFirstPoint = -1;

            GetCurveSegments(curve, s_ClosestPointArray);
            for (int j = 0; j < s_ClosestPointArray.Length - 1; ++j)
            {
                Vector3 a = s_ClosestPointArray[j];
                Vector3 b = s_ClosestPointArray[j + 1];
                float dist = HandleUtility.DistanceToLine(a, b);

                if (dist < closestDist)
                {
                    closestA = a;
                    closestB = b;
                    closestDist = dist;
                    closestSegmentFirstPoint = j;
                }
            }

            //Calculate position
            Vector2 screenPosA = HandleUtility.WorldToGUIPoint(closestA);
            Vector2 screenPosB = HandleUtility.WorldToGUIPoint(closestB);
            Vector2 relativePoint = Event.current.mousePosition - screenPosA;
            Vector2 lineDirection = screenPosB - screenPosA;
            float length = lineDirection.magnitude;
            float dot = Vector3.Dot(lineDirection, relativePoint);
            if (length > .000001f)
                dot /= length * length;
            dot = Mathf.Clamp01(dot);
            position = Vector3.Lerp(closestA, closestB, dot);

            //Calculate percent on curve's segment
            float percentPerSegment = 1.0f / (k_SegmentsPointCount - 1);
            float percentA = closestSegmentFirstPoint * percentPerSegment;
            float lengthAB = (closestB - closestA).magnitude;
            float lengthAToClosest = (position - closestA).magnitude;
            t = percentA + percentPerSegment * (lengthAToClosest / lengthAB);
        }
        
        internal static void GetCurveSegments(CurveData curve, Vector3[] results)
        {
            if (!curve.IsValid())
                throw new ArgumentException(nameof(curve));

            if (results == null)
                throw new ArgumentNullException(nameof(results));

            if (results.Length < 2)
                throw new ArgumentException("Get curve segments requires a results array of at least two points", nameof(results));

            var segmentCount = results.Length - 1;
            float segmentPercentage = 1f / segmentCount;
            var path = curve.a.spline;
            for (int i = 0; i <= segmentCount; ++i)
            {
                results[i] = path.GetPointOnCurve(curve, i * segmentPercentage);
            }
        }
    }
}