using System;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
using Object = UnityEngine.Object;

namespace UnityEditor.Splines
{
    static class TransformOperation
    {
        [Flags]
        public enum PivotFreeze
        {
            None = 0, 
            Position = 1,
            Rotation = 2,
            All = Position | Rotation
        }

        struct TransformData
        {
            internal float3 position;
            internal float3 inTangentDirection;
            internal float3 outTangentDirection;

            internal static TransformData GetData(ISplineElement element)
            {
                var tData = new TransformData();
                tData.position = new float3(element.position);
                if (element is BezierEditableKnot knot)
                {
                    tData.inTangentDirection = knot.tangentIn.direction;
                    tData.outTangentDirection = knot.tangentOut.direction;
                }

                return tData;
            }
        }
        
        struct RotationSyncData
        {
            quaternion m_RotationDelta;
            float m_MagnitudeDelta;
            float m_ScaleMultiplier; // Only used for scale operation
            bool m_Initialized;
            
            public bool initialized => m_Initialized;
            public quaternion rotationDelta => m_RotationDelta;
            public float magnitudeDelta => m_MagnitudeDelta;
            public float scaleMultiplier => m_ScaleMultiplier;

            public void Initialize(quaternion rotationDelta, float magnitudeDelta, float scaleMultiplier)
            {
                m_RotationDelta = rotationDelta;
                m_MagnitudeDelta = magnitudeDelta;
                m_ScaleMultiplier = scaleMultiplier;
                m_Initialized = true;
            }

            public void Clear()
            {
                m_RotationDelta = quaternion.identity;
                m_MagnitudeDelta = 0f;
                m_ScaleMultiplier = 1f;
                m_Initialized = false;
            }
        }

        static List<ISplineElement> s_ElementSelection = new List<ISplineElement>(32);
        
        public static IReadOnlyList<ISplineElement> elementSelection => s_ElementSelection;

        static int s_ElementSelectionCount = 0;

        public static bool canManipulate => s_ElementSelectionCount > 0;

        public static ISplineElement currentElementSelected
            => canManipulate ? s_ElementSelection[0] : null;

        static Vector3 s_PivotPosition;
        public static Vector3 pivotPosition => s_PivotPosition;

        static quaternion s_HandleRotation;
        public static quaternion handleRotation => s_HandleRotation;

        //Caching rotation inverse for rotate and scale operations
        static quaternion s_HandleRotationInv;
        
        public static PivotFreeze pivotFreeze { get; set; }
        
        static TransformData[] s_MouseDownData;
        
        // Used to prevent same knot being rotated multiple times during a transform operation in Rotation Sync mode.
        static HashSet<EditableKnot> s_RotatedKnotCache = new HashSet<EditableKnot>();
        static RotationSyncData s_RotationSyncData = new RotationSyncData();

        internal static void UpdateSelection(IEnumerable<Object> selection)
        {
            SplineSelection.GetSelectedElements(selection, s_ElementSelection);
            s_ElementSelectionCount = s_ElementSelection.Count;
            if (s_ElementSelectionCount > 0)
            {
                UpdatePivotPosition();
                UpdateHandleRotation();
            }
        }
        
        internal static void UpdatePivotPosition(bool useKnotPositionForTangents = false)
        {
            if ((pivotFreeze & PivotFreeze.Position) != 0)
                return;

            switch (Tools.pivotMode)
            {
                case PivotMode.Center:
                    s_PivotPosition = EditableSplineUtility.GetBounds(s_ElementSelection, useKnotPositionForTangents).center;
                    break;

                case PivotMode.Pivot:
                    if (s_ElementSelectionCount == 0)
                        goto default;

                    var element = s_ElementSelection[0];
                    if (useKnotPositionForTangents && element is EditableTangent tangent)
                        s_PivotPosition = tangent.owner.position;
                    else
                        s_PivotPosition = element.position;
                    break;

                default:
                    s_PivotPosition = Vector3.positiveInfinity;
                    break;
            }
        }

        // A way to set pivot position for situations, when by design, pivot position does
        // not necessarily match the pivot of selected elements.
        internal static void ForcePivotPosition(float3 position)
        {
            s_PivotPosition = position;
        }

        internal static void UpdateHandleRotation()
        {
            if ((pivotFreeze & PivotFreeze.Rotation) != 0)
                return;

            var handleRotation = Tools.handleRotation;
            if (canManipulate && (SplineTool.handleOrientation == HandleOrientation.Element || SplineTool.handleOrientation == HandleOrientation.Parent))
            {
                var curElement = TransformOperation.currentElementSelected;

                if (SplineTool.handleOrientation == HandleOrientation.Element)
                    handleRotation = CalculateElementSpaceHandleRotation(curElement);
                else if (curElement is EditableTangent editableTangent)
                    handleRotation = CalculateElementSpaceHandleRotation(editableTangent.owner);
            }

            s_HandleRotation = handleRotation;
            s_HandleRotationInv = math.inverse(s_HandleRotation);
        }

        public static void ApplyTranslation(Vector3 delta)
        {
            s_RotatedKnotCache.Clear();

            foreach (var element in s_ElementSelection)
            {
                if (element is EditableKnot knot) 
                {
                    knot.position += (float3)delta;
                    if (!s_RotationSyncData.initialized)
                        s_RotationSyncData.Initialize(quaternion.identity, 0f, 1f);
                }
                else if (element is EditableTangent tangent)
                {
                    //Do nothing on the tangent if the knot is also in the selection
                    if (s_ElementSelection.Contains(tangent.owner))
                        continue;

                    if (tangent.owner is BezierEditableKnot owner)
                    {
                        if (OppositeTangentSelected(tangent))
                            owner.SetMode(BezierEditableKnot.Mode.Broken);

                        if (owner.mode == BezierEditableKnot.Mode.Broken)
                            tangent.position = tangent.owner.position + tangent.direction + (float3) delta;
                        else
                        {
                            if (s_RotatedKnotCache.Contains(tangent.owner))
                                continue;

                            if (tangent.owner is BezierEditableKnot tangentOwner)
                            {
                                var targetDirection = tangent.direction + (float3) delta;

                                // Build rotation sync data based on active selection's transformation
                                if (!s_RotationSyncData.initialized)
                                {
                                    var rotationDelta = Quaternion.FromToRotation(tangent.direction, targetDirection);
                                    var magnitudeDelta = math.length(targetDirection) - math.length(tangent.direction);

                                    s_RotationSyncData.Initialize(rotationDelta, magnitudeDelta, 1f);
                                }

                                ApplyTangentRotationSyncTransform(tangent);
                            }
                        }
                    }
                }
            }

            s_RotationSyncData.Clear();
        }
        
        public static void ApplyRotation(Quaternion deltaRotation, Vector3 rotationCenter)
        {
            s_RotatedKnotCache.Clear();
            
            foreach (var element in s_ElementSelection)
            {
                if (element is EditableKnot knot)
                {
                    var knotRotation = knot.rotation;
                    RotateKnot(knot, deltaRotation, rotationCenter);
                    if (!s_RotationSyncData.initialized)
                        s_RotationSyncData.Initialize(math.mul(math.inverse(knotRotation), knot.rotation), 0f, 1f);                    
                }
                else if (element is EditableTangent tangent && !s_ElementSelection.Contains(tangent.owner))
                {
                    if (tangent.owner is BezierEditableKnot tangentOwner)
                    {
                        if (tangentOwner.mode == BezierEditableKnot.Mode.Broken)
                        {
                            if (Tools.pivotMode == PivotMode.Pivot)
                                rotationCenter = tangent.owner.position;
            
                            var mode = tangentOwner.mode;

                            var deltaPos = math.rotate(deltaRotation, tangent.position - (float3)rotationCenter);
                            tangent.position = deltaPos + (float3)rotationCenter;

                            tangentOwner.TangentChanged(tangent, mode);
                        }
                        else
                        {
                            if (s_RotatedKnotCache.Contains(tangent.owner))
                                continue;
                            
                            deltaRotation.ToAngleAxis(out var deltaRotationAngle, out var deltaRotationAxis);

                            if (math.abs(deltaRotationAngle) > 0f)
                            {
                                if (tangentOwner.mode != BezierEditableKnot.Mode.Broken)
                                {
                                    // If we're in center pivotMode and both tangents of the same knot are in selection, enter Broken mode under these conditions:
                                    if (Tools.pivotMode == PivotMode.Center && OppositeTangentSelected(tangent))
                                    {
                                        var knotToCenter = (float3) rotationCenter - tangentOwner.position;
                                        // 1) Rotation center does not match owner knot's position
                                        if (!Mathf.Approximately(math.length(knotToCenter), 0f))
                                        {
                                            var similarity = Math.Abs(Vector3.Dot(math.normalize(deltaRotationAxis), math.normalize(knotToCenter)));
                                            // 2) Both rotation center and knot, are not on rotation delta's axis
                                            if (!Mathf.Approximately(similarity, 1f))
                                                tangentOwner.SetMode(BezierEditableKnot.Mode.Broken);
                                        }
                                    }
                                }

                                // Build rotation sync data based on active selection's transformation
                                if (!s_RotationSyncData.initialized)
                                {
                                    if (Tools.pivotMode == PivotMode.Pivot)
                                        s_RotationSyncData.Initialize(deltaRotation, 0f, 1f);
                                    else
                                    {
                                        var deltaPos = math.rotate(deltaRotation, tangent.position - (float3) rotationCenter);
                                        var knotToRotationCenter = (float3) rotationCenter - tangent.owner.position;
                                        var targetDirection = knotToRotationCenter + deltaPos;
                                        var tangentNorm = math.normalize(tangent.direction);
                                        var axisDotTangent = math.dot(math.normalize(deltaRotationAxis), tangentNorm);
                                        var toRotCenterDotTangent = math.length(knotToRotationCenter) > 0f ? math.dot(math.normalize(knotToRotationCenter), tangentNorm) : 1f;
                                        quaternion knotRotationDelta;
                                        // In center pivotMode, use handle delta only if our handle delta rotation's axis
                                        // matches knot's active selection tangent direction and rotation center is on the tangent's axis.
                                        // This makes knot roll possible when element selection list only contains one or both tangents of a single knot.
                                        if (Mathf.Approximately(math.abs(axisDotTangent), 1f) && Mathf.Approximately(math.abs(toRotCenterDotTangent), 1f))
                                            knotRotationDelta = deltaRotation;
                                        else
                                            knotRotationDelta = Quaternion.FromToRotation(tangent.direction, targetDirection);

                                        var scaleMultiplier = math.length(targetDirection) / math.length(tangent.direction);

                                        s_RotationSyncData.Initialize(knotRotationDelta, 0f, scaleMultiplier);
                                    }
                                }

                                ApplyTangentRotationSyncTransform(tangent, false);
                            }
                        }
                    }
                }
            }
            
            s_RotationSyncData.Clear();
        }

        static bool OppositeTangentSelected(EditableTangent tangent)
        {
            if (tangent.owner is BezierEditableKnot tangentOwner && tangentOwner.mode != BezierEditableKnot.Mode.Broken)
                if (tangentOwner.TryGetOppositeTangent(tangent, out var oppositeTangent) && s_ElementSelection.Contains(oppositeTangent))
                    return true;

            return false;
        }

        static void RotateKnot(EditableKnot knot, quaternion deltaRotation, float3 rotationCenter, bool allowTranslation = true)
        {
            var knotInBrokenMode = (knot is BezierEditableKnot bezierKnot && bezierKnot.mode == BezierEditableKnot.Mode.Broken);
            if (!knotInBrokenMode && s_RotatedKnotCache.Contains(knot))
                return;

            if (allowTranslation && Tools.pivotMode == PivotMode.Center)
            {
                var dir = knot.position - rotationCenter;

                if (SplineTool.handleOrientation == HandleOrientation.Element || SplineTool.handleOrientation == HandleOrientation.Parent)
                    knot.position = math.rotate(deltaRotation, dir) + rotationCenter;
                else
                    knot.position = math.rotate(s_HandleRotation, math.rotate(deltaRotation, math.rotate(s_HandleRotationInv, dir))) + rotationCenter;
            }
            
            if (SplineTool.handleOrientation == HandleOrientation.Element || SplineTool.handleOrientation == HandleOrientation.Parent)
            {
                if (Tools.pivotMode == PivotMode.Center)
                    knot.rotation = math.mul(deltaRotation, knot.rotation);
                else
                {
                    var handlePivotModeRot = math.mul(GetCurrentSelectionKnot().rotation, math.inverse(knot.rotation));
                    knot.rotation = math.mul(math.inverse(handlePivotModeRot), math.mul(deltaRotation, math.mul(handlePivotModeRot, knot.rotation)));
                }
            }
            else
                knot.rotation = math.mul(s_HandleRotation, math.mul(deltaRotation, math.mul(s_HandleRotationInv, knot.rotation)));

            s_RotatedKnotCache.Add(knot);
        }

        public static void ApplyScale(float3 scale)
        {
            s_RotatedKnotCache.Clear();
            ISplineElement[] scaledElements = new ISplineElement[s_ElementSelectionCount];
            
            for(int elementIndex = 0; elementIndex<s_ElementSelectionCount; elementIndex++)
            {
                var element = s_ElementSelection[elementIndex];
                if (element is EditableKnot knot)
                {
                    ScaleKnot(knot, elementIndex, scale);
                    
                    if (!s_RotationSyncData.initialized)
                        s_RotationSyncData.Initialize(quaternion.identity, 0f, 1f);
                } 
                else if(element is EditableTangent tangent && !s_ElementSelection.Contains(tangent.owner))
                {
                    if(tangent.owner is BezierEditableKnot tangentOwner)
                    {
                        var restoreMode = false;
                        var mode = tangentOwner.mode;
                        var scaleDelta = scale - new float3(1f, 1f, 1f);
                        if (tangentOwner.mode != BezierEditableKnot.Mode.Broken && math.length(scaleDelta) > 0f)
                        {
                            // If we're in center pivotMode and both tangents of the same knot are in selection
                            if (Tools.pivotMode == PivotMode.Center && OppositeTangentSelected(tangent))
                            {
                                var knotToCenter = (float3)pivotPosition - tangentOwner.position;
                                //  Enter broken mode if scale operation center does not match owner knot's position
                                if (!Mathf.Approximately(math.length(knotToCenter), 0f))
                                {
                                    tangentOwner.SetMode(BezierEditableKnot.Mode.Broken);
                                    var similarity = Math.Abs(Vector3.Dot(math.normalize(scaleDelta), math.normalize(knotToCenter)));
                                    // If scale center and knot are both on an axis that's orthogonal to scale operation's axis,
                                    // mark knot for mode restore so that mirrored/continous modes can be restored
                                    if (Mathf.Approximately(similarity, 0f))
                                        restoreMode = true;
                                }
                            }
                        }

                        var index = Array.IndexOf(scaledElements, element); 
                        if (index == -1) //element not scaled yet
                        {
                            if (tangentOwner.mode == BezierEditableKnot.Mode.Broken)
                                tangent.position = ScaleTangent(tangent, s_MouseDownData[elementIndex].position, scale);
                            else
                            {
                                // Build rotation sync data based on active selection's transformation
                                if (!s_RotationSyncData.initialized)
                                {
                                    var targetDirection = ScaleTangent(tangent, s_MouseDownData[elementIndex].position, scale) - tangent.owner.position;
                                    var rotationDelta = Quaternion.FromToRotation(tangent.direction, targetDirection);
                                    var scaleMultiplier = math.length(targetDirection) / math.length(tangent.direction);

                                    s_RotationSyncData.Initialize(rotationDelta, 0f, scaleMultiplier);
                                }

                                if (tangentOwner.mode == BezierEditableKnot.Mode.Mirrored && s_RotatedKnotCache.Contains(tangentOwner))
                                    continue;

                                ApplyTangentRotationSyncTransform(tangent, false);
                            }

                            if (restoreMode)
                                tangentOwner.SetMode(mode);
                        }
                    }
                }
                scaledElements[elementIndex] = element;
            }

            s_RotationSyncData.Clear();
        }

        static void ScaleKnot(EditableKnot knot, int dataIndex, float3 scale)
        {
            if(Tools.pivotMode == PivotMode.Center)
            {
                var deltaPos = math.rotate(s_HandleRotationInv ,s_MouseDownData[dataIndex].position - (float3) pivotPosition);
                var deltaPosKnot =  deltaPos * scale;
                knot.position = math.rotate(s_HandleRotation, deltaPosKnot) + (float3)pivotPosition;
            }

            using(new BezierEditableKnot.TangentSafeEditScope(knot))
            {
                if(knot is BezierEditableKnot bezierKnot)
                {
                    var tangent = bezierKnot.tangentIn;
                    tangent.direction = math.rotate(s_HandleRotation, math.rotate(s_HandleRotationInv,s_MouseDownData[dataIndex].inTangentDirection) * scale);
                    tangent = bezierKnot.tangentOut;
                    tangent.direction = math.rotate(s_HandleRotation, math.rotate(s_HandleRotationInv,s_MouseDownData[dataIndex].outTangentDirection) * scale);
                }
            }
        }

        static float3 ScaleTangent(EditableTangent tangent, float3 originalPosition, float3 scale)
        {
            var scaleCenter = Tools.pivotMode == PivotMode.Center ? (float3) pivotPosition : tangent.owner.position;
            
            var deltaPos = math.rotate(s_HandleRotationInv, originalPosition - scaleCenter) * scale;
            return math.rotate(s_HandleRotation, deltaPos) + scaleCenter;
        }

        static void ApplyTangentRotationSyncTransform(EditableTangent tangent, bool absoluteScale = true)
        {
            if (tangent.owner is BezierEditableKnot tangentOwner)
            {
                // Apply scale only if tangent is active selection or it's part of multi select and its knot is mirrored
                if (tangent == currentElementSelected || 
                    tangentOwner.mode == BezierEditableKnot.Mode.Mirrored || 
                    (!absoluteScale && tangentOwner.mode == BezierEditableKnot.Mode.Continuous))
                {
                    if (absoluteScale)
                        tangent.direction += math.normalize(tangent.direction) * s_RotationSyncData.magnitudeDelta;
                    else 
                        tangent.direction *= s_RotationSyncData.scaleMultiplier;
                }
            }

            RotateKnot(tangent.owner, s_RotationSyncData.rotationDelta, tangent.owner.position, false);
        }

        internal static quaternion CalculateElementSpaceHandleRotation(ISplineElement element)
        {
            quaternion handleRotation = quaternion.identity;
            if (element is EditableTangent editableTangent && editableTangent.owner is BezierEditableKnot tangentKnot)
            {
                float3 forward;
                var knotUp = math.rotate(tangentKnot.rotation, math.up());
                    
                if (math.length(editableTangent.direction) > 0)
                    forward = math.normalize(editableTangent.direction);
                else // Treat zero length tangent same way as when it's parallel to knot's up vector
                    forward = knotUp;

                float3 right;
                var dotForwardKnotUp = math.dot(forward, knotUp);
                if (Mathf.Approximately(math.abs(dotForwardKnotUp), 1f))
                    right = math.rotate(tangentKnot.rotation, math.right()) * math.sign(dotForwardKnotUp);
                else
                    right = math.cross(forward, knotUp);

                handleRotation = quaternion.LookRotationSafe(forward, math.cross(right, forward));
            }
            else if (element is EditableKnot editableKnot)
                handleRotation = editableKnot.rotation;

            return handleRotation;
        }

        static EditableKnot GetCurrentSelectionKnot()
        {
            if (currentElementSelected == null)
                return null;

            if (currentElementSelected is EditableTangent tangent)
                return tangent.owner;

            if (currentElementSelected is EditableKnot knot)
                return knot;

            return null;
        }

        public static void RecordMouseDownState()
        {
            s_MouseDownData = new TransformData[s_ElementSelectionCount];
            for (int i = 0; i < s_ElementSelectionCount; i++)
            {
                s_MouseDownData[i] = TransformData.GetData(s_ElementSelection[i]);
            }
        }

        public static void ClearMouseDownState()
        {
            s_MouseDownData = null;
        }
        
        public static Bounds GetSelectionBounds(bool useKnotPositionForTangents = false)
        {
            return EditableSplineUtility.GetBounds(s_ElementSelection, useKnotPositionForTangents);
        }
    }
}