using UnityEngine;

namespace Flexalon
{
    /// <summary>
    /// The curve animator applies a curve the the position, rotation, and scale
    /// of the object. The curve is restarted each time the layout position changes.
    /// This is ideal for scenarios in which the layout position does not change often.
    /// </summary>
    [AddComponentMenu("Flexalon/Flexalon Curve Animator"), HelpURL("https://www.flexalon.com/docs/animators")]
    public class FlexalonCurveAnimator : MonoBehaviour, TransformUpdater
    {
        private FlexalonNode _node;
        private RectTransform _rectTransform;

        private bool _animateInWorldSpace = true;
        /// <summary> Determines if the animation should be performed in world space. </summary>
        public bool AnimateInWorldSpace
        {
            get => _animateInWorldSpace;
            set { _animateInWorldSpace = value; }
        }

        [SerializeField]
        private AnimationCurve _curve = AnimationCurve.Linear(0, 0, 1, 1);
        /// <summary> The curve to apply. Should begin at 0 and end at 1. </summary>
        public AnimationCurve Curve
        {
            get => _curve;
            set { _curve = value; }
        }

        [SerializeField]
        private bool _animatePosition = true;
        /// <summary> Determines if the position should be animated. </summary>
        public bool AnimatePosition
        {
            get => _animatePosition;
            set { _animatePosition = value; }
        }

        [SerializeField]
        private bool _animateRotation = true;
        /// <summary> Determines if the rotation should be animated. </summary>
        public bool AnimateRotation
        {
            get => _animateRotation;
            set { _animateRotation = value; }
        }

        [SerializeField]
        private bool _animateScale = true;
        /// <summary> Determines if the scale should be animated. </summary>
        public bool AnimateScale
        {
            get => _animateScale;
            set { _animateScale = value; }
        }

        private Vector3 _startPosition;
        private Quaternion _startRotation;
        private Vector3 _startScale;
        private Vector2 _startRectSize;

        private Vector3 _endPosition;
        private Quaternion _endRotation;
        private Vector3 _endScale;
        private Vector2 _endRectSize;

        private float _positionTime;
        private float _rotationTime;
        private float _scaleTime;
        private float _rectSizeTime;

        private Vector3 _fromPosition;
        private Quaternion _fromRotation;
        private Vector3 _fromScale;
        private Vector2 _fromRectSize;

        void OnEnable()
        {
            _startPosition = _endPosition = new Vector3(float.NaN, float.NaN, float.NaN);
            _startRotation = _endRotation = new Quaternion(float.NaN, float.NaN, float.NaN, float.NaN);
            _startScale = _endScale = new Vector3(float.NaN, float.NaN, float.NaN);
            _positionTime = _rotationTime = _scaleTime = 0;
            _rectTransform = (transform is RectTransform) ? (RectTransform)transform : null;

            _node = Flexalon.GetOrCreateNode(gameObject);
            _node.SetTransformUpdater(this);
        }

        void OnDisable()
        {
            _node?.SetTransformUpdater(null);
            _node = null;
        }

        /// <inheritdoc />
        public void PreUpdate(FlexalonNode node)
        {
            _fromPosition = transform.position;
            _fromRotation = transform.rotation;
            _fromScale = transform.lossyScale;
            _fromRectSize = _rectTransform?.rect.size ?? Vector2.zero;
        }

        /// <inheritdoc />
        public bool UpdatePosition(FlexalonNode node, Vector3 position)
        {
            var newEndPosition = position;
            var newStartPosition = transform.localPosition;

            if (_animateInWorldSpace)
            {
                newEndPosition = transform.parent ? transform.parent.localToWorldMatrix.MultiplyPoint(position) : position;;
                newStartPosition = _fromPosition;
            }

            if (newEndPosition != _endPosition)
            {
                _startPosition = newStartPosition;
                _endPosition = newEndPosition;
                _positionTime = 0;
            }

            _positionTime += Time.smoothDeltaTime;

            if (!_animatePosition || _positionTime > _curve.keys[_curve.keys.Length - 1].time)
            {
                transform.localPosition = position;
                _endPosition = new Vector3(float.NaN, float.NaN, float.NaN);
                return true;
            }
            else
            {
                var newPosition = Vector3.Lerp(_startPosition, _endPosition, _curve.Evaluate(_positionTime));
                if (_animateInWorldSpace)
                {
                    transform.position = newPosition;
                }
                else
                {
                    transform.localPosition = newPosition;
                }

                return false;
            }
        }

        /// <inheritdoc />
        public bool UpdateRotation(FlexalonNode node, Quaternion rotation)
        {
            var newEndRotation = rotation;
            var newStartRotation = transform.localRotation;

            if (_animateInWorldSpace)
            {
                newEndRotation = transform.parent ? transform.parent.rotation * rotation : rotation;;
                newStartRotation = _fromRotation;
            }

            if (newEndRotation != _endRotation)
            {
                _startRotation = newStartRotation;
                _endRotation = newEndRotation;
                _rotationTime = 0;
            }

            _rotationTime += Time.smoothDeltaTime;

            if (!_animateRotation || _rotationTime > _curve.keys[_curve.keys.Length - 1].time)
            {
                transform.localRotation = rotation;
                _endRotation = new Quaternion(float.NaN, float.NaN, float.NaN, float.NaN);
                return true;
            }
            else
            {
                var newRotation = Quaternion.Slerp(_startRotation, _endRotation, _curve.Evaluate(_rotationTime));
                if (_animateInWorldSpace)
                {
                    transform.rotation = newRotation;
                }
                else
                {
                    transform.localRotation = newRotation;
                }

                return false;
            }
        }

        /// <inheritdoc />
        public bool UpdateScale(FlexalonNode node, Vector3 scale)
        {
            var newEndScale = scale;
            var newStartScale = transform.localScale;

            if (_animateInWorldSpace)
            {
                newEndScale = transform.parent ? Math.Mul(scale, transform.parent.lossyScale) : scale;
                newStartScale = _fromScale;
            }

            if (newEndScale != _endScale)
            {
                _startScale = newStartScale;
                _endScale = newEndScale;
                _scaleTime = 0;
            }

            _scaleTime += Time.smoothDeltaTime;

            if (!_animateScale || _scaleTime > _curve.keys[_curve.keys.Length - 1].time)
            {
                transform.localScale = scale;
                _endScale = new Vector3(float.NaN, float.NaN, float.NaN);
                return true;
            }
            else
            {
                var newScale = Vector3.Lerp(_startScale, _endScale, _curve.Evaluate(_scaleTime));

                if (_animateInWorldSpace)
                {
                    transform.localScale = transform.parent ? Math.Div(newScale, transform.parent.lossyScale) : newScale;
                }
                else
                {
                    transform.localScale = newScale;
                }

                return false;
            }
        }

        /// <inheritdoc />
        public bool UpdateRectSize(FlexalonNode node, Vector2 size)
        {
            if (size != _endRectSize)
            {
                _startRectSize = _fromRectSize;
                _endRectSize = size;
                _rectSizeTime = 0;
            }

            _rectSizeTime += Time.smoothDeltaTime;
            bool done = !_animateScale || _rectSizeTime > _curve.keys[_curve.keys.Length - 1].time;
            var newSize = done ? size : Vector2.Lerp(_startRectSize, _endRectSize, _curve.Evaluate(_rectSizeTime));
            _rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, newSize.x);
            _rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, newSize.y);

            if (done)
            {
                _endRectSize = new Vector2(float.NaN, float.NaN);
            }

            return done;
        }
    }
}