using System;
using UnityEngine;
using UnityEditor.ShortcutManagement;
using UnityEngine.UIElements;

namespace UnityEditor.Rendering.LookDev
{
    class CameraController : Manipulator
    {
        [Flags]
        enum Direction
        {
            None = 0,
            Up = 1 << 0,
            Down = 1 << 1,
            Left = 1 << 2,
            Right = 1 << 3,
            Forward = 1 << 4,
            Backward = 1 << 5,
            All = Up | Down | Left | Right | Forward | Backward
        }
        Direction m_DirectionKeyPressed = Direction.None;

        float m_StartZoom = 0.0f;
        float m_ZoomSpeed = 0.0f;

        float m_TotalMotion = 0.0f;
        Vector3 m_MotionDirection = new Vector3();

        float m_FlySpeedNormalized = .5f;
        float m_FlySpeed = 1f;
        float m_FlySpeedAccelerated = 0f;
        const float m_FlySpeedMin = .01f;
        const float m_FlySpeedMax = 2f;
        //[TODO: check if necessary to add hability to deactivate acceleration]
        const float k_FlyAcceleration = 1.1f;
        bool m_ShiftBoostedFly = false;
        bool m_InFlyMotion;

        bool m_IsDragging;
        ViewTool m_BehaviorState;
        static TimeHelper s_Timer = new TimeHelper();

        protected CameraState m_CameraState;
        DisplayWindow m_Window;
        protected Action m_Focused;

        Rect screen => target.contentRect;

        bool inFlyMotion
        {
            get => m_InFlyMotion;
            set
            {
                if (value ^ m_InFlyMotion)
                {
                    if (value)
                    {
                        s_Timer.Begin();
                        EditorApplication.update += UpdateFPSMotion;
                    }
                    else
                    {
                        m_FlySpeedAccelerated = 0f;
                        m_MotionDirection = Vector3.zero;
                        m_ShiftBoostedFly = false;
                        EditorApplication.update -= UpdateFPSMotion;
                    }
                    m_InFlyMotion = value;
                }
            }
        }

        float flySpeedNormalized
        {
            get => m_FlySpeedNormalized;
            set
            {
                m_FlySpeedNormalized = Mathf.Clamp01(value);
                float speed = Mathf.Lerp(m_FlySpeedMin, m_FlySpeedMax, m_FlySpeedNormalized);
                // Round to nearest decimal: 2 decimal points when between [0.01, 0.1]; 1 decimal point when between [0.1, 10]; integral between [10, 99]
                speed = (float)(System.Math.Round((double)speed, speed < 0.1f ? 2 : speed < 10f ? 1 : 0));
                m_FlySpeed = Mathf.Clamp(speed, m_FlySpeedMin, m_FlySpeedMax);
            }
        }
        float flySpeed
        {
            get => m_FlySpeed;
            set => flySpeedNormalized = Mathf.InverseLerp(m_FlySpeedMin, m_FlySpeedMax, value);
        }

        virtual protected bool isDragging
        {
            get => m_IsDragging;
            set
            {
                //As in scene view, stop dragging as first button is release in case of multiple button down
                if (value ^ m_IsDragging)
                {
                    if (value)
                    {
                        target.RegisterCallback<MouseMoveEvent>(OnMouseDrag);
                        target.CaptureMouse();
                        EditorGUIUtility.SetWantsMouseJumping(1); //through screen edges
                    }
                    else
                    {
                        EditorGUIUtility.SetWantsMouseJumping(0);
                        target.ReleaseMouse();
                        target.UnregisterCallback<MouseMoveEvent>(OnMouseDrag);
                    }
                    m_IsDragging = value;
                }
            }
        }

        public CameraController(CameraState cameraState, DisplayWindow window, Action focused)
        {
            m_CameraState = cameraState;
            m_Window = window;
            m_Focused = focused;
        }

        private void ResetCameraControl()
        {
            isDragging = false;
            inFlyMotion = false;
            m_BehaviorState = ViewTool.None;
        }

        protected virtual void OnScrollWheel(WheelEvent evt)
        {
            // See UnityEditor.SceneViewMotion.HandleScrollWheel
            switch (m_BehaviorState)
            {
                case ViewTool.FPS: OnChangeFPSCameraSpeed(evt); break;
                default: OnZoom(evt); break;
            }
        }

        void OnMouseDrag(MouseMoveEvent evt)
        {
            switch (m_BehaviorState)
            {
                case ViewTool.Orbit: OnMouseDragOrbit(evt); break;
                case ViewTool.FPS: OnMouseDragFPS(evt); break;
                case ViewTool.Pan: OnMouseDragPan(evt); break;
                case ViewTool.Zoom: OnMouseDragZoom(evt); break;
                default: break;
            }
        }

        void OnKeyDown(KeyDownEvent evt)
        {
            OnKeyUpOrDownFPS(evt);
            OnKeyDownReset(evt);
        }

        void OnChangeFPSCameraSpeed(WheelEvent evt)
        {
            float scrollWheelDelta = evt.delta.y;
            flySpeedNormalized -= scrollWheelDelta * .01f;
            string cameraSpeedDisplayValue = flySpeed.ToString(flySpeed < 0.1f ? "F2" : flySpeed < 10f ? "F1" : "F0");
            if (flySpeed < 0.1f)
                cameraSpeedDisplayValue = cameraSpeedDisplayValue.TrimStart(new Char[] { '0' });
            GUIContent cameraSpeedContent = EditorGUIUtility.TrTempContent(
                $"{cameraSpeedDisplayValue}x");
            m_Window.ShowNotification(cameraSpeedContent, .5f);
            evt.StopPropagation();
        }

        void OnZoom(WheelEvent evt)
        {
            const float deltaCutoff = .3f;
            const float minZoom = .003f;
            float scrollWheelDelta = evt.delta.y;
            float relativeDelta = m_CameraState.viewSize * scrollWheelDelta * .015f;
            if (relativeDelta > 0 && relativeDelta < deltaCutoff)
                relativeDelta = deltaCutoff;
            else if (relativeDelta <= 0 && relativeDelta > -deltaCutoff)
                relativeDelta = -deltaCutoff;
            m_CameraState.viewSize += relativeDelta;
            if (m_CameraState.viewSize < minZoom)
                m_CameraState.viewSize = minZoom;
            evt.StopPropagation();
        }

        void OnMouseDragOrbit(MouseMoveEvent evt)
        {
            Quaternion rotation = m_CameraState.rotation;
            rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
            rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
            m_CameraState.rotation = rotation;
            evt.StopPropagation();
        }

        void OnMouseDragFPS(MouseMoveEvent evt)
        {
            Vector3 camPos = m_CameraState.pivot - m_CameraState.rotation * Vector3.forward * m_CameraState.distanceFromPivot;
            Quaternion rotation = m_CameraState.rotation;
            rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
            rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
            m_CameraState.rotation = rotation;
            m_CameraState.pivot = camPos + rotation * Vector3.forward * m_CameraState.distanceFromPivot;
            evt.StopPropagation();
        }

        void OnMouseDragPan(MouseMoveEvent evt)
        {
            //[TODO: fix WorldToScreenPoint and ScreenToWorldPoint
            var screenPos = m_CameraState.QuickProjectPivotInScreen(screen);
            screenPos += new Vector3(evt.mouseDelta.x, -evt.mouseDelta.y, 0);
            //Vector3 newWorldPos = m_CameraState.ScreenToWorldPoint(screen, screenPos);
            Vector3 newWorldPos = m_CameraState.QuickReprojectionWithFixedFOVOnPivotPlane(screen, screenPos);
            Vector3 worldDelta = newWorldPos - m_CameraState.pivot;
            worldDelta *= EditorGUIUtility.pixelsPerPoint;
            if (evt.shiftKey)
                worldDelta *= 4;
            m_CameraState.pivot += worldDelta;
            evt.StopPropagation();
        }

        void OnMouseDragZoom(MouseMoveEvent evt)
        {
            float zoomDelta = HandleUtility.niceMouseDeltaZoom * (evt.shiftKey ? 9 : 3);
            m_TotalMotion += zoomDelta;
            if (m_TotalMotion < 0)
                m_CameraState.viewSize = m_StartZoom * (1 + m_TotalMotion * .001f);
            else
                m_CameraState.viewSize = m_CameraState.viewSize + zoomDelta * m_ZoomSpeed * .003f;
            evt.StopPropagation();
        }

        void OnKeyDownReset(KeyDownEvent evt)
        {
            if (evt.keyCode == KeyCode.Escape)
                ResetCameraControl();
            evt.StopPropagation();
        }

        void OnKeyUpOrDownFPS<T>(KeyboardEventBase<T> evt)
            where T : KeyboardEventBase<T>, new()
        {
            if (m_BehaviorState != ViewTool.FPS)
                return;

            //Note: Keydown is called in loop but between first occurence of the
            // loop and laters, there is a small pause. To deal with this, we
            // need to register the UpdateMovement function to the Editor update
            KeyCombination combination;
            if (GetKeyCombinationByID("3D Viewport/Fly Mode Forward", out combination) && combination.Match(evt))
                RegisterMotionChange(Direction.Forward, evt);
            if (GetKeyCombinationByID("3D Viewport/Fly Mode Backward", out combination) && combination.Match(evt))
                RegisterMotionChange(Direction.Backward, evt);
            if (GetKeyCombinationByID("3D Viewport/Fly Mode Left", out combination) && combination.Match(evt))
                RegisterMotionChange(Direction.Left, evt);
            if (GetKeyCombinationByID("3D Viewport/Fly Mode Right", out combination) && combination.Match(evt))
                RegisterMotionChange(Direction.Right, evt);
            if (GetKeyCombinationByID("3D Viewport/Fly Mode Up", out combination) && combination.Match(evt))
                RegisterMotionChange(Direction.Up, evt);
            if (GetKeyCombinationByID("3D Viewport/Fly Mode Down", out combination) && combination.Match(evt))
                RegisterMotionChange(Direction.Down, evt);
        }
        
        void RegisterMotionChange<T>(Direction direction, KeyboardEventBase<T> evt)
            where T : KeyboardEventBase<T>, new()
        {
            m_ShiftBoostedFly = evt.shiftKey;
            Direction formerDirection = m_DirectionKeyPressed;
            bool keyUp = evt is KeyUpEvent;
            bool keyDown = evt is KeyDownEvent;
            if (keyDown)
                m_DirectionKeyPressed |= direction;
            else if (keyUp)
                m_DirectionKeyPressed &= (Direction.All & ~direction);
            if (formerDirection != m_DirectionKeyPressed)
            {
                m_MotionDirection = new Vector3(
                    ((m_DirectionKeyPressed & Direction.Right) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Left) > 0 ? 1 : 0),
                    ((m_DirectionKeyPressed & Direction.Up) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Down) > 0 ? 1 : 0),
                    ((m_DirectionKeyPressed & Direction.Forward) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Backward) > 0 ? 1 : 0));

                inFlyMotion = m_DirectionKeyPressed != Direction.None;
            }
            evt.StopPropagation();
        }

        Vector3 GetMotionDirection()
        {
            var deltaTime = s_Timer.Update();
            Vector3 result;
            float speed = (m_ShiftBoostedFly ? 5 * flySpeed : flySpeed);
            if (m_FlySpeedAccelerated == 0)
                m_FlySpeedAccelerated = 9;
            else
                m_FlySpeedAccelerated *= Mathf.Pow(k_FlyAcceleration, deltaTime);
            result = m_MotionDirection.normalized * m_FlySpeedAccelerated * speed * deltaTime;
            return result;
        }

        void UpdateFPSMotion()
        {
            m_CameraState.pivot += m_CameraState.rotation * GetMotionDirection();
            m_Window.Repaint(); //this prevent hich on key down as in CameraFlyModeContext.cs
        }

        bool GetKeyCombinationByID(string ID, out KeyCombination combination)
        {
            var sequence = ShortcutManager.instance.GetShortcutBinding(ID).keyCombinationSequence.GetEnumerator();
            if (sequence.MoveNext()) //have a first entry
            {
                combination = new KeyCombination(sequence.Current);
                return true;
            }
            else
            {
                combination = default;
                return false;
            }
        }
        
        void OnMouseUp(MouseUpEvent evt)
        {
            ResetCameraControl();
            evt.StopPropagation();
        }

        void OnMouseDown(MouseDownEvent evt)
        {
            bool onMac = Application.platform == RuntimePlatform.OSXEditor;

            if (evt.button == 2)
                m_BehaviorState = ViewTool.Pan;
            else if (evt.button == 0 && evt.ctrlKey && onMac || evt.button == 1 && evt.altKey)
            {
                m_BehaviorState = ViewTool.Zoom;
                m_StartZoom = m_CameraState.viewSize;
                m_ZoomSpeed = Mathf.Max(Mathf.Abs(m_StartZoom), .3f);
                m_TotalMotion = 0;
            }
            else if (evt.button == 0)
                m_BehaviorState = ViewTool.Orbit;
            else if (evt.button == 1 && !evt.altKey)
                m_BehaviorState = ViewTool.FPS;

            // see also SceneView.HandleClickAndDragToFocus()
            if (evt.button == 1 && onMac)
                m_Window.Focus();

            target.Focus(); //required for keyboard event
            isDragging = true;
            evt.StopPropagation();

            m_Focused?.Invoke();
        }

        protected override void RegisterCallbacksOnTarget()
        {
            target.focusable = true; //prerequisite for being focusable and recerive keydown events
            target.RegisterCallback<MouseUpEvent>(OnMouseUp);
            target.RegisterCallback<MouseDownEvent>(OnMouseDown);
            target.RegisterCallback<WheelEvent>(OnScrollWheel);
            target.RegisterCallback<KeyDownEvent>(OnKeyDown);
            target.RegisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
        }

        protected override void UnregisterCallbacksFromTarget()
        {
            target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
            target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
            target.UnregisterCallback<WheelEvent>(OnScrollWheel);
            target.UnregisterCallback<KeyDownEvent>(OnKeyDown);
            target.UnregisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
        }

        struct KeyCombination
        {
            KeyCode key;
            EventModifiers modifier;
            public bool shiftOnLastMatch;

            public KeyCombination(UnityEditor.ShortcutManagement.KeyCombination shortcutCombination)
            {
                key = shortcutCombination.keyCode;
                modifier = EventModifiers.None;
                if ((shortcutCombination.modifiers & ShortcutModifiers.Shift) != 0)
                    modifier |= EventModifiers.Shift;
                if ((shortcutCombination.modifiers & ShortcutModifiers.Alt) != 0)
                    modifier |= EventModifiers.Alt;
                if ((shortcutCombination.modifiers & ShortcutModifiers.Action) != 0)
                {
                    if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer)
                        modifier |= EventModifiers.Command;
                    else
                        modifier |= EventModifiers.Control;
                }
                shiftOnLastMatch = false;
            }

            //atLeastModifier allow case were A is required but event provide shift+A
            public bool Match(IKeyboardEvent evt, bool atLeastForModifier = true)
            {
                shiftOnLastMatch = evt.shiftKey;
                if (atLeastForModifier)
                    return key == evt.keyCode && modifier == (evt.modifiers & modifier);
                else
                    return key == evt.keyCode && modifier == evt.modifiers;
            }
        }

        struct TimeHelper
        {
            long lastTime;

            public void Begin() => lastTime = System.DateTime.Now.Ticks;

            public float Update()
            {
                float deltaTime = (System.DateTime.Now.Ticks - lastTime) / 10000000.0f;
                lastTime = System.DateTime.Now.Ticks;
                return deltaTime;
            }
        }
    }

    class SwitchableCameraController : CameraController
    {
        CameraState m_FirstView;
        CameraState m_SecondView;
        ViewIndex m_CurrentViewIndex;

        bool switchedDrag = false;
        bool switchedWheel = false;

        public SwitchableCameraController(CameraState cameraStateFirstView, CameraState cameraStateSecondView, DisplayWindow window, Action<ViewIndex> focused)
            : base(cameraStateFirstView, window, null)
        {
            m_FirstView = cameraStateFirstView;
            m_SecondView = cameraStateSecondView;
            m_CurrentViewIndex = ViewIndex.First;

            m_Focused = () => focused?.Invoke(m_CurrentViewIndex);
        }

        void SwitchTo(ViewIndex index)
        {
            CameraState stateToSwitch;
            switch (index)
            {
                case ViewIndex.First:
                    stateToSwitch = m_FirstView;
                    break;
                case ViewIndex.Second:
                    stateToSwitch = m_SecondView;
                    break;
                default:
                    throw new ArgumentException("Unknown ViewIndex");
            }

            if (stateToSwitch != m_CameraState)
                m_CameraState = stateToSwitch;

            m_CurrentViewIndex = index;
        }

        public void SwitchUntilNextEndOfDrag()
        {
            switchedDrag = true;
            SwitchTo(ViewIndex.Second);
        }

        override protected bool isDragging
        {
            get => base.isDragging;
            set
            {
                bool switchBack = false;
                if (switchedDrag && base.isDragging && !value)
                    switchBack = true;
                base.isDragging = value;
                if (switchBack)
                    SwitchTo(ViewIndex.First);
            }
        }

        public void SwitchUntilNextWheelEvent()
        {
            switchedWheel = true;
            SwitchTo(ViewIndex.Second);
        }

        protected override void OnScrollWheel(WheelEvent evt)
        {
            base.OnScrollWheel(evt);
            if (switchedWheel)
                SwitchTo(ViewIndex.First);
        }
    }
}
