using UnityEngine;
using UnityEngine.Events;

namespace Dreamteck.Splines
{
    public delegate void SplineReachHandler();
    [AddComponentMenu("Dreamteck/Splines/Users/Spline Follower")]
    public class SplineFollower : SplineTracer
    {
        public enum FollowMode { Uniform, Time }
        public enum Wrap { Default, Loop, PingPong }
        [HideInInspector]
        public Wrap wrapMode = Wrap.Default;
        [HideInInspector]
        public FollowMode followMode = FollowMode.Uniform;

        [HideInInspector]
        public bool autoStartPosition = false;

        [SerializeField]
        [HideInInspector]
        [UnityEngine.Serialization.FormerlySerializedAs("follow")]
        private bool _follow = true;

        [SerializeField]
        [HideInInspector]
        [Range(0f, 1f)]
        private double _startPosition;

        /// <summary>
        /// If the follow mode is set to Uniform and there is an added offset in the motion panel, this will presserve the uniformity of the follow speed
        /// </summary>
        [HideInInspector]
        public bool preserveUniformSpeedWithOffset = false;

        /// <summary>
        /// Used when follow mode is set to Uniform. Defines the speed of the follower
        /// </summary>
        public float followSpeed
        {
            get { return _followSpeed; }
            set
            {
                if (_followSpeed != value)
                {
                    _followSpeed = value;
                    Spline.Direction lastDirection = _direction;
                    if (_followSpeed < 0f)
                    {
                        direction = Spline.Direction.Backward;
                    }
                    if(_followSpeed > 0f)
                    {
                        direction = Spline.Direction.Forward;
                    }
                }
            }
        }

        public override Spline.Direction direction {
            get { 
                return base.direction; 
            }
            set { 
                base.direction = value; 
                if(_direction == Spline.Direction.Forward)
                {
                    if(_followSpeed < 0f)
                    {
                        _followSpeed = -_followSpeed;
                    }
                } else
                {
                    if (_followSpeed > 0f)
                    {
                        _followSpeed = -_followSpeed;
                    }
                }
            }
        }

        /// <summary>
        /// Used when follow mode is set to Time. Defines how much time it takes for the follower to travel through the path
        /// </summary>
        public float followDuration
        {
            get { return _followDuration; }
            set
            {
                if (_followDuration != value)
                {
                    if (value < 0f) value = 0f;
                    _followDuration = value;
                }
            }
        }

        public bool follow
        {
            get { return _follow; }
            set
            {
                if(_follow != value)
                {
                    if (autoStartPosition)
                    {
                        Project(GetTransform().position, ref evalResult);
                        SetPercent(evalResult.percent);
                    }
                    _follow = value;
                }
            }
        }

        public event System.Action<double> onEndReached;
        public event System.Action<double> onBeginningReached;

        public FollowerSpeedModifier speedModifier
        {
            get
            {
                return _speedModifier;
            }
        }

        [SerializeField]
        [HideInInspector]
        private float _followSpeed = 1f;
        [SerializeField]
        [HideInInspector]
        private float _followDuration = 1f;

        [SerializeField]
        [HideInInspector]
        private FollowerSpeedModifier _speedModifier = new FollowerSpeedModifier();

        [SerializeField]
        [HideInInspector]
        private FloatEvent _unityOnEndReached = null;
        [SerializeField]
        [HideInInspector]
        private FloatEvent _unityOnBeginningReached = null;

        private double lastClippedPercent = -1.0;

        protected override void Start()
        {
            base.Start();
            if (_follow && autoStartPosition)
            {
                SetPercent(spline.Project(GetTransform().position).percent);
            }
        }

        protected override void LateRun()
        {
            base.LateRun();
#if UNITY_EDITOR
            if (!Application.isPlaying) return;
#endif
            if (_follow)
            {
                Follow();
            }
        }

        protected override void PostBuild()
        {
            base.PostBuild();
            Evaluate(_result.percent, ref _result);
            if (sampleCount > 0)
            {
                if (_follow && !autoStartPosition) ApplyMotion();
            }
        }

        private void Follow()
        {
            switch (followMode)
            {
                case FollowMode.Uniform:
                    double percent = result.percent;
                    if (!_speedModifier.useClippedPercent)
                    {
                        UnclipPercent(ref percent);
                    }
                    float speed = _speedModifier.GetSpeed(Mathf.Abs(_followSpeed), percent);
                    Move(Time.deltaTime * speed); break;
                case FollowMode.Time:
                    if (_followDuration == 0.0) Move(0.0);
                    else Move((double)Time.deltaTime / _followDuration);
                    break;
            }
        }

        public void Restart(double startPosition = 0.0)
        {
            SetPercent(startPosition);
        }

        public override void SetPercent(double percent, bool checkTriggers = false, bool handleJuncitons = false)
        {
            base.SetPercent(percent, checkTriggers, handleJuncitons);
            lastClippedPercent = percent;
        }

        public override void SetDistance(float distance, bool checkTriggers = false, bool handleJuncitons = false)
        {
            base.SetDistance(distance, checkTriggers, handleJuncitons);
            lastClippedPercent = ClipPercent(_result.percent);
            if (samplesAreLooped && clipFrom == clipTo && distance > 0f && lastClippedPercent == 0.0) lastClippedPercent = 1.0;
        }

        public void Move(double percent)
        {
            if (percent == 0.0) return;
            if (sampleCount <= 1)
            {
                if (sampleCount == 1)
                {
                    GetSampleRaw(0, ref _result);
                    ApplyMotion();
                }
                return;
            }
            Evaluate(_result.percent, ref _result);
            double startPercent = _result.percent;
            if (wrapMode == Wrap.Default && lastClippedPercent >= 1.0 && startPercent == 0.0) startPercent = 1.0;
            double p = startPercent + (_direction == Spline.Direction.Forward ? percent : -percent);
            bool callOnEndReached = false, callOnBeginningReached = false;
            lastClippedPercent = p;
            if (_direction == Spline.Direction.Forward && p >= 1.0)
            {
                if (startPercent < 1.0)
                {
                    callOnEndReached = true;
                }
                switch (wrapMode)
                {
                    case Wrap.Default:
                        p = 1.0;
                        break;
                    case Wrap.Loop:
                        CheckTriggers(startPercent, 1.0);
                        CheckNodes(startPercent, 1.0);
                        while (p > 1.0) p -= 1.0;
                        startPercent = 0.0;
                        break;
                    case Wrap.PingPong:
                        p = DMath.Clamp01(1.0 - (p - 1.0));
                        startPercent = 1.0;
                        _direction = Spline.Direction.Backward;
                        break;
                }
            }
            else if (_direction == Spline.Direction.Backward && p <= 0.0)
            {
                if (startPercent > 0.0)
                {
                    callOnBeginningReached = true;
                }
                switch (wrapMode)
                {
                    case Wrap.Default:
                        p = 0.0;
                        break;
                    case Wrap.Loop:
                        CheckTriggers(startPercent, 0.0);
                        CheckNodes(startPercent, 0.0);
                        while (p < 0.0) p += 1.0;
                        startPercent = 1.0;
                        break;
                    case Wrap.PingPong:
                        p = DMath.Clamp01(-p);
                        startPercent = 0.0;
                        _direction = Spline.Direction.Forward;
                        break;
                }
            }
            CheckTriggers(startPercent, p);
            CheckNodes(startPercent, p);
            Evaluate(p, ref _result);
            ApplyMotion();
            if (callOnEndReached)
            {
                if (onEndReached != null)
                {
                    onEndReached(startPercent);
                }
                if (_unityOnEndReached != null)
                {
                    _unityOnEndReached.Invoke((float)startPercent);
                }
            }
            else if (callOnBeginningReached)
            {
                if (onBeginningReached != null)
                {
                    onBeginningReached(startPercent);
                }
                if (_unityOnBeginningReached != null)
                {
                    _unityOnBeginningReached.Invoke((float)startPercent);
                }
            }
            InvokeTriggers();
            InvokeNodes();
        }

        public void Move(float distance)
        {
            bool endReached = false, beginningReached = false;
            float moved = 0f;
            double startPercent = _result.percent;

            double travelPercent = DoTravel(_result.percent, distance, out moved);
            if (startPercent != travelPercent)
            {
                CheckTriggers(startPercent, travelPercent);
                CheckNodes(startPercent, travelPercent);
            }

            if (direction == Spline.Direction.Forward)
            {
                if (travelPercent >= 1.0)
                {
                    if (startPercent < 1.0)
                    {
                        endReached = true;
                    }
                    switch (wrapMode)
                    {
                        case Wrap.Loop:
                            travelPercent = DoTravel(0.0, distance - moved, out moved);
                            CheckTriggers(0.0, travelPercent);
                            CheckNodes(0.0, travelPercent);
                            break;
                        case Wrap.PingPong:
                            _direction = Spline.Direction.Backward;
                            travelPercent = DoTravel(1.0, distance - moved, out moved);
                            CheckTriggers(1.0, travelPercent);
                            CheckNodes(1.0, travelPercent);
                            break;
                    }
                }
            } else
            {
                if (travelPercent <= 0.0)
                {
                    if (startPercent > 0.0)
                    {
                        beginningReached = true;
                    }
                    switch (wrapMode)
                    {
                        case Wrap.Loop:
                            travelPercent = DoTravel(1.0, distance - moved, out moved);
                            CheckTriggers(1.0, travelPercent);
                            CheckNodes(1.0, travelPercent);
                            break;
                        case Wrap.PingPong:
                            _direction = Spline.Direction.Forward;
                            travelPercent = DoTravel(0.0, distance - moved, out moved);
                            CheckTriggers(0.0, travelPercent);
                            CheckNodes(0.0, travelPercent);
                            break;
                    }
                }
            }

            Evaluate(travelPercent, ref _result);
            ApplyMotion();
            if (endReached)
            {
                if (onEndReached != null)
                {
                    onEndReached(startPercent);
                }
                if (_unityOnEndReached != null)
                {
                    _unityOnEndReached.Invoke((float)startPercent);
                }
            }
            else if (beginningReached)
            {
                if (onBeginningReached != null)
                {
                    onBeginningReached(startPercent);
                }
                if (_unityOnBeginningReached != null)
                {
                    _unityOnBeginningReached.Invoke((float)startPercent);
                }
            }
            InvokeTriggers();
            InvokeNodes();
        }

        protected virtual double DoTravel(double start, float distance, out float moved)
        {
            moved = 0f;
            double result = 0.0;
            if (preserveUniformSpeedWithOffset && _motion.hasOffset)
            {
                result = TravelWithOffset(start, distance, _direction, _motion.offset, out moved);
            } else
            {
                result = Travel(start, distance, _direction, out moved);
            }
            return result;
        }

        [System.Serializable]
        public class FloatEvent : UnityEvent<float> { }
    }
}