using KitsuneCafe.Extension;
using KitsuneCafe.SOAP;
using R3;
using UnityEngine;

namespace KitsuneCafe.Entity
{
    public class Motor : MonoBehaviour
    {
        [SerializeField]
        private new Rigidbody rigidbody;

        [SerializeField]
        private ReactiveSource<Vector2> direction;

        [SerializeField]
        private float maxSpeed = 25f;
        public float DefaultMaxSpeed => maxSpeed;

        [SerializeField]
        private float accelerationTime = 1f;
        public float AccelerationTime => accelerationTime;

        [SerializeField]
        private AnimationCurve accelerationCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);

        [SerializeField]
        private float decelerationTime = 0.5f;
        public float Decelerationtime => decelerationTime;

        [SerializeField]
        private AnimationCurve decelerationCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);

        [SerializeField]
        private float rotationSpeed = 25f;
        public float RotationSpeed => rotationSpeed;

        [SerializeField]
        private float epsilon = 0.001f;
        private float sqEpsilon;

        private ReactiveProperty<float> currentMaxSpeed = new ReactiveProperty<float>();
        public float MaxSpeed => currentMaxSpeed.Value;
        public ReactiveProperty<float> MaxSpeedSource => currentMaxSpeed;

        //private readonly ReactiveProperty<Vector3> direction = new ReactiveProperty<Vector3>(Vector3.zero);

        private void Reset()
        {
            rigidbody = GetComponent<Rigidbody>();
        }

        private void Awake()
        {
            var directionSource = direction.AsObservable().Select(v2 => new Vector3(v2.x, 0, v2.y));

            currentMaxSpeed.Value = maxSpeed;

            sqEpsilon = epsilon * epsilon;

            var d = Disposable.CreateBuilder();

            CalculateVelocityStream(directionSource, currentMaxSpeed, UnityFrameProvider.FixedUpdate)
                .Subscribe(force =>
                {
                    rigidbody.AddForce(force, ForceMode.Acceleration);
                })
                .AddTo(ref d);

            CalculateRotationStream(directionSource, UnityFrameProvider.FixedUpdate)
                .Subscribe(targetRotation =>
                {
                    if (Quaternion.Angle(transform.rotation, targetRotation) > 0.1f)
                    {
                        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.fixedDeltaTime * rotationSpeed);
                    }
                })
                .AddTo(ref d);

            d.RegisterTo(destroyCancellationToken);
        }

        private Observable<Quaternion> CalculateRotationStream(Observable<Vector3> directionSource, FrameProvider provider)
        {
            var targetRotationStream = directionSource
            .Select(inputDir =>
            {
                if (inputDir.sqrMagnitude > sqEpsilon)
                {
                    return Quaternion.LookRotation(inputDir.normalized);
                }
                else
                {
                    return transform.rotation;
                }
            })
            .DistinctUntilChanged();

            return Observable.EveryUpdate(provider)
                .WithLatestFrom(targetRotationStream, (_, targetRotation) => targetRotation);
        }

        private Observable<Vector3> CalculateVelocityStream(Observable<Vector3> directionSource, Observable<float> maxSpeedSource, FrameProvider provider)
        {
            return directionSource
                .DistinctUntilChanged()
                .CombineLatest(maxSpeedSource, (direction, currentMaxSpeed) => (direction, currentMaxSpeed))
                .Select(data =>
                {
                    var (direction, maxSpeed) = data;
                    var start = rigidbody.linearVelocity;
                    var targetMagnitude = direction.sqrMagnitude > sqEpsilon ? maxSpeed : 0f;
                    var targetVelocity = direction.normalized * targetMagnitude;

                    if (start.SqrDistance(targetVelocity) < sqEpsilon)
                    {
                        if (targetVelocity.sqrMagnitude < sqEpsilon && start.sqrMagnitude < sqEpsilon)
                        {
                            return Observable.Return(Vector3.zero);
                        }
                        else
                        {
                            return Observable.Return((targetVelocity - start) / Time.fixedDeltaTime);
                        }
                    }

                    float lerpDuration;
                    AnimationCurve curve;

                    if (targetMagnitude > epsilon)
                    {
                        var ratio = start.magnitude / maxSpeed;
                        lerpDuration = accelerationTime * (1f - ratio);
                        lerpDuration = Mathf.Max(lerpDuration, 0.05f);
                        curve = accelerationCurve;
                    }
                    else
                    {
                        lerpDuration = decelerationTime;
                        curve = decelerationCurve;
                    }

                    return Interpolate(start, targetVelocity, lerpDuration, provider, curve)
                        .Select(targetVelocity =>
                        {
                            var currentVelocity = rigidbody.linearVelocity;
                            return (targetVelocity - currentVelocity) / Time.fixedDeltaTime;
                        })
                        .Where(_ => targetVelocity.sqrMagnitude > sqEpsilon || rigidbody.linearVelocity.sqrMagnitude > sqEpsilon)
                        .DefaultIfEmpty(Vector3.zero);
                })
                .Switch();
        }

        private static Observable<Vector3> Interpolate(Vector3 start, Vector3 target, float duration)
        {
            return Interpolate(start, target, duration, UnityFrameProvider.FixedUpdate, AnimationCurve.Linear(0, 0, 1, 1));
        }

        private static Observable<Vector3> Interpolate(Vector3 start, Vector3 target, float duration, AnimationCurve curve)
        {
            return Interpolate(start, target, duration, UnityFrameProvider.FixedUpdate, curve);
        }

        private static Observable<Vector3> Interpolate(Vector3 start, Vector3 target, float duration, FrameProvider provider)
        {
            return Interpolate(start, target, duration, provider, AnimationCurve.Linear(0, 0, 1, 1));
        }

        private static Observable<Vector3> Interpolate(Vector3 start, Vector3 target, float duration, FrameProvider provider, AnimationCurve curve)
        {
            return Observable.EveryUpdate(provider)
                .Scan(0f, (elapsedTime, _) => elapsedTime + Time.fixedDeltaTime)
                .Select(elapsedTime =>
                {
                    var factor = 0f;
                    if (duration > float.Epsilon)
                    {
                        factor = Mathf.Clamp01(elapsedTime / duration);
                    }
                    else
                    {
                        factor = 1f;
                    }

                    var evaluatedFactor = curve.Evaluate(factor);

                    return Vector3.Lerp(start, target, evaluatedFactor);
                });
        }


        public void ChangeMaxSpeed(float value)
        {
            currentMaxSpeed.Value = value;
        }

        public void ResetMaxSpeed()
        {
            currentMaxSpeed.Value = maxSpeed;
        }
    }
}
