using Sirenix.OdinInspector;
using UnityEngine;
using VOID.Generic.Player.Input;

namespace VOID.Generic.Player
{
    /// <summary>
    /// Manager with full control of camera with movement by player.
    /// </summary>
    [DefaultExecutionOrder(-1)]
    public class CameraManager : MonoBehaviour
    {
        public static CameraManager current {get; private set;}

        // Camera Movement Settings
        [Title("Camera Horizontal Movement")]
        [SerializeField, MinValue(1f), MaxValue(10f), SuffixLabel("m/s")]
        private float moveSpeed = 5f;
        [SerializeField, MinValue(1f), MaxValue(5f)]
        private float moveSpeedFastMultiplier = 2.5f;
        
        // Camera's target elevating
        [Title("Camera Vertical Movement")]
        [SerializeField, MinValue(1f), MaxValue(20f), SuffixLabel("m/s")]
        private float elevateSpeed = 1f;
        
        // Camera Rotate and Tilt Settings
        [Title("Camera Rotate and Tilt")]
        [SerializeField, MinValue(0.01f), MaxValue(1f)]
        private float rotateSensitivity = 0.1f;
        [SerializeField, MinValue(0.01f), MaxValue(1f)]
        private float tiltSensitivity = 0.06f;
        [SerializeField, MinMaxSlider(10f, 90f, true)]
        private Vector2 tiltLimit = new(20f, 85f);
        
        // Camera Zoom Settings
        [Title("Camera Zoom")]
        [SerializeField, MinValue(0.05f), MaxValue(1f)]
        private float zoomSensitivity = 0.3f;
        [SerializeField, MinMaxSlider(1f, 30f, true)]
        private Vector2 zoomLimit = new(2f, 15f);
        [SerializeField]
        private float zoomCurrent = 8f;
        
        // Camera Offset
        [Title("Camera Offset")]
        [SerializeField, MinValue(0f), MaxValue(3f)]
        private float targetVerticalOffset = 1.0f;
        [SerializeField]
        private Vector3 cameraOffset = new(0f, 0f, -1f);

        // Camera and Target
        private Camera _camera;
        private GameObject _target;

        // Stick properties
        [Title("Sticking to transform")]
        public Transform stickTo;
        
        // Aditional Components
        private CameraInputManager _cameraInputManager;

        private void Awake()
        {
            current = this;
        }

        private void Start()
        {
            _cameraInputManager = CameraInputManager.current;
            
            _camera = Camera.main;
            _target = CameraTarget.InstantiateNew().gameObject;
        }

        private void FixedUpdate()
        {
            MoveHorizontally();
            MoveVertically();
            RotateAndTilt();
            Zoom();
            Offset();
            CameraLookAtTarget();
        }

        /// <summary>
        /// Moves camera's target to given position.
        /// </summary>
        public void MoveTo(Vector3 position)
        {
            _target.transform.position = position;
            stickTo = null;
        }

        /// <summary>
        /// Moving camera's target horizontally.
        /// </summary>
        private void MoveHorizontally()
        {
            // No camera movement from player, but it can be sticked to something
            if (!_cameraInputManager.isMoving) {
                if (stickTo) _target.transform.position = stickTo.position + new Vector3(0f, 1.5f, 0f);
                return;
            }
            
            // Player moved camera - unstick camera
            stickTo = null;

            // Calculate camera's target next relative position
            var moveVector = _cameraInputManager.movementDelta * (moveSpeed * Time.deltaTime);
            if (_cameraInputManager.isMovingFaster) moveVector *= moveSpeedFastMultiplier;

            var position = _target.transform.TransformDirection(moveVector.x, 0, moveVector.y);
            _target.transform.position += position;
        }

        /// <summary>
        /// Moving camera's target vertically.
        /// </summary>
        private void MoveVertically()
        {
            // IMPORTANT - target's pos already include offset!
            var targetPos = _target.transform.position;

            // Find point where camera's target can stick to
            Physics.Raycast(
                new Ray(targetPos + Vector3.up*50f, Vector3.down),
                out var rayCastHit,
                100f,
                LayerManager.Static);

            // Nothing - seems like camera floating and there is nothing above or below
            if (!rayCastHit.collider) return;

            // IMPORTANT - target include offset by default, but not raycast - we need add offset to raycast point!
            var rayCastPos = rayCastHit.point + new Vector3(0, targetVerticalOffset, 0);
            
            // If distance is positive we go UP, otherwise we go DOWN
            var distance = rayCastPos.y - targetPos.y;

            // Move camera's target towards new position, if really close then move remaining distance to prevent jiggling
            var speed = Mathf.Sign(distance) * Mathf.Min(Mathf.Abs(distance), elevateSpeed * Time.fixedDeltaTime);
            _target.transform.position += new Vector3(0f, speed, 0f);
        }

        /// <summary>
        /// Rotating and tilting camera.<br/>
        /// Rotating camera's target.
        /// </summary>
        private void RotateAndTilt()
        {
            if (_cameraInputManager.isRotatingAndTilting == false) return;

            var delta = _cameraInputManager.rotateAndTiltDelta;
            var tiltDelta = delta.y * tiltSensitivity * -1;
            var rotateDelta = delta.x * rotateSensitivity;

            var previousEuler = _camera.transform.rotation.eulerAngles;
            var nextEuler = previousEuler + new Vector3(tiltDelta, rotateDelta, 0);

            if (nextEuler.x < tiltLimit.x) nextEuler.x = tiltLimit.x; 
            if (nextEuler.x > tiltLimit.y) nextEuler.x = tiltLimit.y; 
                
            _camera.transform.rotation = Quaternion.Euler(nextEuler);
            _target.transform.rotation = Quaternion.Euler(0f, nextEuler.y, 0f);
        }

        /// <summary>
        /// Updating camera's zoom.
        /// </summary>
        private void Zoom()
        {
            if (_cameraInputManager.isZooming == false) return;

            zoomCurrent -= _cameraInputManager.zoomDelta * zoomSensitivity;

            if (zoomCurrent > zoomLimit.y) zoomCurrent = zoomLimit.y;
            if (zoomCurrent < zoomLimit.x) zoomCurrent = zoomLimit.x;
        }

        /// <summary>
        /// Updating camera's offset from target.
        /// </summary>
        private void Offset()
        {
            var offset = _camera.transform.TransformDirection(Vector3.back * zoomCurrent + cameraOffset);
            _camera.transform.position = _target.transform.position + offset;
        }

        private void CameraLookAtTarget()
        {
            _camera.transform.LookAt(_target.transform);
        }
    }
}
