using System;
using Spine;
using Spine.Unity;
using UnityEngine;
using UnityEngine.InputSystem;

namespace CharacterController
{
    [RequireComponent(typeof(Rigidbody2D), typeof(PlayerInput))]
    public class PlatformingController : MonoBehaviour
    {
        [SerializeField] private PlayerStats stats;
        [SerializeField, SpineAnimation] private string run;
        [SerializeField, SpineAnimation] private string idle;
        [SerializeField, SpineAnimation] private string faceLeft;
        [SerializeField, SpineAnimation] private string faceRight;
        
        [Header("Hack to fix a bug with new RigidBody2D.Slide method")]
        [SerializeField] private Vector2 ceilingCheckOffset;
        [SerializeField] private float ceilingCheckSize;
        [SerializeField] private LayerMask checkForCeiling;
        
        private Rigidbody2D.SlideMovement _slideSettings;
        
        private Rigidbody2D _rbody;
        private PlayerInput _input;
        private SkeletonAnimation _anim;
        
        private Vector2 _currentVelocity;
        private float _currentVelocityX;

        private Vector2 _moveInput;

        private bool _grounded;
        private float _lastGroundedTime;
        private float _jumpInputTime;
        
        private bool _isJumping;
        private float _jumpStartTime;

        private Collider2D[] _wallJumpOverlap = new Collider2D[2];
        private ContactFilter2D _wallJumpContactFilter;
        private float _wallSide;

        private bool _onWall;
        private float _lastOnWallTime;

        private bool _wallJumping;

        private Vector2 _surfaceAnchor;
        
        private bool _touchingCeiling; 
        private Collider2D[] _ceilingOverlap = new Collider2D[2];
        private ContactFilter2D _ceilingContactFilter;
        
        private void Awake()
        {
            _anim = GetComponentInChildren<SkeletonAnimation>();
            
            _surfaceAnchor = Vector2.down / 4f;
            _wallJumpContactFilter = new ContactFilter2D
            {
                layerMask = stats.WallCheckLayers,
                useLayerMask = true
            };
            _ceilingContactFilter = new ContactFilter2D
            {
                layerMask = checkForCeiling,
                useLayerMask = true
            };
            
            _rbody = GetComponent<Rigidbody2D>();
            _slideSettings = new Rigidbody2D.SlideMovement
            {
                gravity = Vector2.zero,
                surfaceAnchor = _surfaceAnchor
                // using the calculated gravity:
                // this makes slopes work properly
                // jumping does not work, the player sticks to the floor
                //gravity = new Vector2(0, stats.Gravity),
                
                // multiplying it by Time.fixedDeltaTime
                // this makes jumping work properly
                // slopes do not work, the player bounces down them
                //gravity = new Vector2(0, stats.Gravity * Time.fixedDeltaTime),
            };
        }

        private void OnEnable()
        {
            _input = GetComponent<PlayerInput>();
            _input.actions["Move"].performed += Move;
            _input.actions["Move"].canceled += Move;

            _input.actions["Jump"].performed += Jump;
            _input.actions["Jump"].canceled += Jump;
        }

        private void OnDisable()
        {
            _input.actions["Move"].performed -= Move;
            _input.actions["Move"].canceled -= Move;
            
            _input.actions["Jump"].performed -= Jump;
            _input.actions["Jump"].canceled -= Jump;
        }

        private void Move(InputAction.CallbackContext ctx)
        {
            _moveInput = ctx.ReadValue<Vector2>();
        }
        
        private void Jump(InputAction.CallbackContext ctx)
        {
            if (ctx.ReadValueAsButton() && (_grounded || Time.time - _lastGroundedTime < stats.InputBuffer))
                StartJump();
            else if (ctx.ReadValueAsButton() && !_grounded)
                _jumpInputTime = Time.time;
            else if (!ctx.ReadValueAsButton() && _isJumping)
                EndJump();

            if (!_grounded && ctx.ReadValueAsButton() && (_onWall || Time.time - _lastOnWallTime < stats.WallStickTime))
                WallJump();
        }

        private async void WallJump()
        {
            _lastOnWallTime = -1;
            if (Mathf.Approximately(Mathf.Sign(_moveInput.x), _wallSide))
                _currentVelocity = stats.WallJumpForce;
            else
                _currentVelocity = stats.WallLeapForce;
            
            _wallJumping = true;
            await Awaitable.WaitForSecondsAsync(stats.WallJumpPlayerInputTime);
            _wallJumping = false;
        }

        private void StartJump()
        {
            _grounded = false;
            _isJumping = true;
            _jumpStartTime = Time.time;
            _currentVelocity.y = stats.JumpForce;
        }

        private void EndJump()
        {
            _isJumping = false;
            
            if (Time.time - _jumpStartTime < stats.TimeForMaxJump)
            {
                // Calculate jump force for min jump height
                float minJumpForce = Mathf.Sqrt(2 * Mathf.Abs(stats.Gravity) * stats.JumpHeightMin);
                _currentVelocity.y = Mathf.Min(_currentVelocity.y, minJumpForce);
            }
        }

        private void FixedUpdate()
        {
            _touchingCeiling = Physics2D.OverlapCircle(_rbody.position + ceilingCheckOffset, ceilingCheckSize,
                _ceilingContactFilter, _ceilingOverlap) > 0;
            
            if (Time.time - _jumpInputTime < stats.InputBuffer && _grounded)
                StartJump();
            
            _currentVelocity.x = GetHorizontalVelocity();

            if (!_grounded || _isJumping)
                _currentVelocity.y += stats.Gravity * GetGravityModifier() * Time.fixedDeltaTime;
            else if (_currentVelocity.y < 0)
                _currentVelocity.y = 0;
            
            if (_isJumping && !_grounded)
                if (Time.time - _jumpStartTime >= stats.TimeForMaxJump)
                    _isJumping = false; // Stop jumping after reaching max jump time

            if (_currentVelocity.y > 0)
                _slideSettings.surfaceAnchor = Vector2.zero;
            else
                _slideSettings.surfaceAnchor = _surfaceAnchor;

            if (!_touchingCeiling)
            {
                var results = _rbody.Slide(_currentVelocity, Time.fixedDeltaTime, _slideSettings);
                _grounded = results.surfaceHit.transform != null;
            }
            else
            {
                _currentVelocity.y = -1f;
                _isJumping = false;
                _rbody.MovePosition(_rbody.position + _currentVelocity * Time.fixedDeltaTime);
            }

            if (_grounded)
            {
                _jumpInputTime = -1;
                _lastGroundedTime = Time.time;
            }

            _onWall = Physics2D.OverlapCircle(_rbody.position + stats.WallJumpCheckOffset * Mathf.Sign(_currentVelocity.x), stats.WallJumpCheckSize,
                _wallJumpContactFilter, _wallJumpOverlap) > 0;

            if (_onWall)
            {
                _lastOnWallTime = Time.time;
                _wallSide = Mathf.Sign(_currentVelocity.x);
            }

            if (_onWall && MovingAndInputMatch() && _currentVelocity.y < 0)
                _currentVelocity.y = Mathf.Max(_currentVelocity.y, -stats.WallSlideSpeed);

            if (Mathf.Abs(_currentVelocity.x) > 0.1f && _anim.state.GetCurrent(0)?.Animation?.Name != run)
                _anim.state.SetAnimation(0, run, true);
            else if (Mathf.Abs(_currentVelocity.x) < 0.1f && _anim.state.GetCurrent(0)?.Animation?.Name != idle)
                _anim.state.SetAnimation(0, idle, true);

            if (Mathf.Sign(_currentVelocity.x)>0) _anim.state.SetAnimation(1, faceRight, true);
            else if (Mathf.Sign(_currentVelocity.x)<0) _anim.state.SetAnimation(1, faceLeft, true);
        }

        private float GetGravityModifier()
        {
            if (Mathf.Abs(_currentVelocity.y) < stats.JumpPeakThreshold)
                return stats.JumpPeakGravity;
            if (_currentVelocity.y < 0)
                return stats.FallGravity;

            return 1;
        }

        private bool MovingAndInputMatch()
        {
            return Mathf.Approximately(Mathf.Sign(_currentVelocity.x), Mathf.Sign(_moveInput.x));
        }
        
        private float GetHorizontalVelocity()
        {
            if (_wallJumping)
                return _currentVelocity.x;
            
            var targetSpeed = _moveInput.x * stats.TargetRunSpeed;

            return Mathf.SmoothDamp(_currentVelocity.x, targetSpeed, ref _currentVelocityX, 
                GetAcceleration(targetSpeed));
        }

        private float GetAcceleration(float targetSpeed)
        {
            // we're trying to turn
            if (!Mathf.Approximately(Mathf.Sign(targetSpeed), Mathf.Sign(_currentVelocity.x)))
                return _grounded ? stats.TurnSpeed : stats.TurnSpeedAir;
            
            if (Mathf.Abs(targetSpeed) > Mathf.Epsilon) // we're trying to move
            {
                if (_grounded)
                    return stats.Acceleration;
                if (Mathf.Abs(_currentVelocity.y) < stats.JumpPeakThreshold) // we're at the peak of the jump
                    return stats.PeakAcceleration;
                return stats.AirAcceleration;
            }
            else // we're trying to stop
            {
                if (_grounded)
                    return stats.Deceleration;
                if (Mathf.Abs(_currentVelocity.y) < stats.JumpPeakThreshold) // we're at the peak of the jump
                    return stats.PeakDeceleration;
                return stats.AirDeceleration;
            }
        }
        
        private void OnValidate()
        {
            _rbody = GetComponent<Rigidbody2D>();
            stats.CalculateForces();
        }

        private void OnDrawGizmos()
        {
            Gizmos.color = Color.green;
            Gizmos.DrawWireSphere(_rbody.position + stats.WallJumpCheckOffset, stats.WallJumpCheckSize);
            Gizmos.DrawWireSphere(_rbody.position + ceilingCheckOffset, ceilingCheckSize);
        }
    }
}