using KitsuneCafe.Extension;
using KitsuneCafe.ItemSystem;
using KitsuneCafe.Sys;
using KitsuneCafe.Sys.Attributes;
using R3;
using UnityEngine;

namespace KitsuneCafe.Entities
{
    public class EntityAnimator : MonoBehaviour
    {
        private const float epsilon = 0.05f;

        [SerializeField]
        private Animator animator;

        [SerializeField]
        private new Rigidbody rigidbody;

        [Header("Movement")]
        [SerializeField]
        private Motor motor;

        [SerializeField]
        private SprintFeature sprint;

        [SerializeField, AnimatorParam("animator")]
        private string movementSpeedParameter;

        [SerializeField]
        private float movementSmoothTime = 0.1f;

        [SerializeField, AnimatorParam("animator")]
        private string sprintingParameter;

        [SerializeField, Range(0f, 1f)]
        private float walkAnimationParamMax = 0.5f;

        [SerializeField, Range(0f, 1f)]
        private float sprintAnimationParamMax = 1.0f;

        [Header("Combat")]
        [SerializeField, AnimatorParam("animator")]
        private string readyParam;

        [SerializeField]
        private EquipmentInstance equipment;

        private void OnValidate()
        {
            if (animator == null)
            {
                animator = GetComponent<Animator>();
            }

            if (rigidbody == null)
            {
                rigidbody = GetComponent<Rigidbody>();
            }

            if (motor == null)
            {
                motor = GetComponent<Motor>();
            }

            if (sprint == null)
            {
                sprint = GetComponent<SprintFeature>();
            }

            if (equipment == null)
            {
                equipment = GetComponent<EquipmentInstance>();
            }
        }

        private void Awake()
        {
            var d = Disposable.CreateBuilder();

            CurrentVelocity()
                .Compose(CurrentAnimationSpeed)
                .SmoothDamp(
                    () => animator.GetFloat(movementSpeedParameter),
                    movementSmoothTime,
                    UnityTimeProvider.FixedUpdate
                )
                .Subscribe(speed =>
                {
                    animator.SetFloat(movementSpeedParameter, speed);
                })
                .AddTo(ref d);

            sprint.IsSprinting
                .DistinctUntilChanged()
                .Subscribe(isSprinting =>
                {
                    animator.SetBool(sprintingParameter, isSprinting);
                })
                .AddTo(ref d);


            Observable.FromEventHandler<bool>(
                e => equipment.Readied += e,
                e => equipment.Readied -= e
            )
                .Select(eh => eh.e)
                .Subscribe(ready => animator.SetBool(readyParam, ready))
                .AddTo(ref d);

            d.RegisterTo(destroyCancellationToken);
        }

        private Observable<float> CurrentVelocity()
        {
            return Observable.EveryUpdate(UnityFrameProvider.FixedUpdate)
                .Select(_ => rigidbody.linearVelocity.sqrMagnitude)
                .DistinctUntilChanged()
                .Select(Mathf.Sqrt);
        }

        private Observable<float> CurrentAnimationSpeed(Observable<float> speedSource)
        {
            var scaleFactor = motor.MaxSpeedSource.Select(maxSpeed => NormalizationFactor(maxSpeed, motor.DefaultMaxSpeed));

            return speedSource
                .CombineLatest(motor.MaxSpeedSource, scaleFactor, NormalizeSpeed)
                .DistinctUntilChanged(FEqualityComparer<float>.Create(KMath.IsApproxEqual));
        }

        private float NormalizationFactor(float maxSpeed, float defaultMaxSpeed)
        {
            return maxSpeed > defaultMaxSpeed ? sprintAnimationParamMax : walkAnimationParamMax;
        }

        private float NormalizeSpeed(float currentSpeed, float maxSpeed, float factor)
        {
            if (currentSpeed > maxSpeed && maxSpeed > epsilon)
            {
                return factor;
            }
            else
            {
                return Mathf.InverseLerp(0, maxSpeed, currentSpeed) * factor;
            }
        }
    }
}
