// Object Follow|Utilities|90110
namespace VRTK
{
    using UnityEngine;

    /// <summary>
    /// Abstract class that allows to change one game object's properties to follow another game object.
    /// </summary>
    public abstract class VRTK_ObjectFollow : MonoBehaviour
    {
        [Header("Object Settings")]

        [Tooltip("The game object to follow. The followed property values will be taken from this one.")]
        public GameObject gameObjectToFollow;
        [Tooltip("The game object to change the property values of. If left empty the game object this script is attached to will be changed.")]
        public GameObject gameObjectToChange;

        [Header("Position Settings")]

        [Tooltip("Whether to follow the position of the given game object.")]
        public bool followsPosition = true;
        [Tooltip("Whether to smooth the position when following `gameObjectToFollow`.")]
        public bool smoothsPosition;
        [Tooltip("The maximum allowed distance between the unsmoothed source and the smoothed target per frame to use for smoothing.")]
        public float maxAllowedPerFrameDistanceDifference = 0.003f;
        /// <summary>
        /// The position that results by following `gameObjectToFollow`.
        /// </summary>
        public Vector3 targetPosition { get; private set; }

        [Header("Rotation Settings")]

        [Tooltip("Whether to follow the rotation of the given game object.")]
        public bool followsRotation = true;
        [Tooltip("Whether to smooth the rotation when following `gameObjectToFollow`.")]
        public bool smoothsRotation;
        [Tooltip("The maximum allowed angle between the unsmoothed source and the smoothed target per frame to use for smoothing.")]
        public float maxAllowedPerFrameAngleDifference = 1.5f;
        /// <summary>
        /// The rotation that results by following `gameObjectToFollow`.
        /// </summary>
        public Quaternion targetRotation { get; private set; }

        [Header("Scale Settings")]

        [Tooltip("Whether to follow the scale of the given game object.")]
        public bool followsScale = true;
        [Tooltip("Whether to smooth the scale when following `gameObjectToFollow`.")]
        public bool smoothsScale;
        [Tooltip("The maximum allowed size between the unsmoothed source and the smoothed target per frame to use for smoothing.")]
        public float maxAllowedPerFrameSizeDifference = 0.003f;
        /// <summary>
        /// The scale that results by following `gameObjectToFollow`.
        /// </summary>
        public Vector3 targetScale { get; private set; }

        /// <summary>
        /// Follow `gameObjectToFollow` using the current settings.
        /// </summary>
        public virtual void Follow()
        {
            if (gameObjectToFollow == null)
            {
                return;
            }

            if (followsPosition)
            {
                FollowPosition();
            }

            if (followsRotation)
            {
                FollowRotation();
            }

            if (followsScale)
            {
                FollowScale();
            }
        }

        protected virtual void OnEnable()
        {
            gameObjectToChange = gameObjectToChange != null ? gameObjectToChange : gameObject;
        }

        protected virtual void OnValidate()
        {
            maxAllowedPerFrameDistanceDifference = Mathf.Max(0.0001f, maxAllowedPerFrameDistanceDifference);
            maxAllowedPerFrameAngleDifference = Mathf.Max(0.0001f, maxAllowedPerFrameAngleDifference);
            maxAllowedPerFrameSizeDifference = Mathf.Max(0.0001f, maxAllowedPerFrameSizeDifference);
        }

        protected abstract Vector3 GetPositionToFollow();

        protected abstract void SetPositionOnGameObject(Vector3 newPosition);

        protected abstract Quaternion GetRotationToFollow();

        protected abstract void SetRotationOnGameObject(Quaternion newRotation);

        protected virtual Vector3 GetScaleToFollow()
        {
            return gameObjectToFollow.transform.localScale;
        }

        protected virtual void SetScaleOnGameObject(Vector3 newScale)
        {
            gameObjectToChange.transform.localScale = newScale;
        }

        protected virtual void FollowPosition()
        {
            Vector3 positionToFollow = GetPositionToFollow();
            Vector3 newPosition;

            if (smoothsPosition)
            {
                float alpha = Mathf.Clamp01(Vector3.Distance(targetPosition, positionToFollow) / maxAllowedPerFrameDistanceDifference);
                newPosition = Vector3.Lerp(targetPosition, positionToFollow, alpha);
            }
            else
            {
                newPosition = positionToFollow;
            }

            targetPosition = newPosition;
            SetPositionOnGameObject(newPosition);
        }

        protected virtual void FollowRotation()
        {
            Quaternion rotationToFollow = GetRotationToFollow();
            Quaternion newRotation;

            if (smoothsRotation)
            {
                float alpha = Mathf.Clamp01(Quaternion.Angle(targetRotation, rotationToFollow) / maxAllowedPerFrameAngleDifference);
                newRotation = Quaternion.Lerp(targetRotation, rotationToFollow, alpha);
            }
            else
            {
                newRotation = rotationToFollow;
            }

            targetRotation = newRotation;
            SetRotationOnGameObject(newRotation);
        }

        protected virtual void FollowScale()
        {
            Vector3 scaleToFollow = GetScaleToFollow();
            Vector3 newScale;

            if (smoothsScale)
            {
                float alpha = Mathf.Clamp01(Vector3.Distance(targetScale, scaleToFollow) / maxAllowedPerFrameSizeDifference);
                newScale = Vector3.Lerp(targetScale, scaleToFollow, alpha);
            }
            else
            {
                newScale = scaleToFollow;
            }

            targetScale = newScale;
            SetScaleOnGameObject(newScale);
        }
    }
}