// FlexiMotion // https://kybernetik.com.au/flexi-motion // Copyright 2023-2025 Kybernetik //

using System.Text;
using UnityEngine;

namespace FlexiMotion.Modifiers
{
    /// <summary>
    /// A <see cref="FlexiMotionModifier"/> which modifies the <see cref="FlexiMotionRuntime.UpdatesPerSecond"/> based
    /// on distance to a target object.
    /// </summary>
    /// https://kybernetik.com.au/flexi-motion/api/FlexiMotion.Modifiers/DistanceBasedUpdateRateModifier
    /// 
    [AddComponentMenu(FMStrings.ModifiersMenuPrefix + "Distance Based Update Rate Modifier")]
    [HelpURL(FMStrings.DocsURLs.ModifiersAPIDocumentation + "/" + nameof(DistanceBasedUpdateRateModifier))]
    [DefaultExecutionOrder(-1000)]// LateUpdate before JobScheduller.
    public class DistanceBasedUpdateRateModifier : FlexiMotionModifier
    {
        /************************************************************************************************************************/

        [SerializeField]
        [Tooltip("The object used to calculate the distance. If not set, the Camera.main will be used.")]
        private Transform _DistanceTo;

        /// <summary>The object used to calculate the distance.</summary>
        /// <remarks>If not set, the <see cref="Camera.main"/> will be used.</remarks>
        public ref Transform DistanceTo => ref _DistanceTo;

        [SerializeField]
        [Tooltip("When the distance is closer than this value, the full Updates Per Second will be used.")]
        private float _NearRange = 3;

        /// <summary>
        /// When the distance is closer than this value, the full <see cref="FlexiMotionRuntime.UpdatesPerSecond"/>
        /// will be used.
        /// </summary>
        public ref float NearRange => ref _NearRange;

        [SerializeField]
        [Tooltip("When the distance is farther than this value, the " + nameof(Target) + " will be disabled.")]
        private float _FarRange = 15;

        /// <summary>
        /// When the distance is farther than this value, the <see cref="FlexiMotionModifier.Target"/> will be
        /// disabled.
        /// </summary>
        public ref float FarRange => ref _FarRange;

        [SerializeField, Range(0.01f, 1f)]
        [Tooltip("As the distance approaches the Far Range, the Updates Per Second will approach" +
            " the base value multiplied by this value")]
        private float _MinimumUpdateRateMultiplyer = 0.1f;

        /// <summary>As the distance approaches the <see cref="FarRange"/>, the
        /// <see cref="FlexiMotionRuntime.UpdatesPerSecond"/> will approach the
        /// <see cref="FlexiMotionDefinition.UpdatesPerSecond"/> multiplied by this value.
        /// </summary>
        public ref float MinimumUpdateRateMultiplyer => ref _MinimumUpdateRateMultiplyer;

        /************************************************************************************************************************/

        /// <summary>Sets the <see cref="Camera.main"/> as the default <see cref="DistanceTo"/>.</summary>
        protected virtual void Reset()
        {
            // Not in OnValidate in case you want to clear it.
            var camera = Camera.main;
            if (camera != null)
                _DistanceTo = camera.transform;
        }

        /************************************************************************************************************************/

        /// <inheritdoc/>
        protected override void OnValidate()
        {
            base.OnValidate();

            if (_NearRange < 0)
                _NearRange = 0;

            if (_FarRange < 0)
                _FarRange = 0;

            if (_NearRange > _FarRange)
            {
                if (float.IsNaN(_NearRange))
                {
                    if (float.IsNaN(_FarRange))
                    {
                        _NearRange = 5;
                        _FarRange = 20;
                    }
                    else
                    {
                        _NearRange = _FarRange * 0.5f;
                    }
                }
                else
                {
                    if (float.IsNaN(_FarRange))
                    {
                        _FarRange = _NearRange * 2;
                    }
                    else
                    {
                        _NearRange = _FarRange = (_NearRange + _FarRange) * 0.5f;
                    }
                }
            }
        }

        /************************************************************************************************************************/

        /// <inheritdoc/>
        protected override bool Disable(JobModifierGroup modifiers)
        {
            if (Target != null &&
                Target.Runtime != null)
                Target.Runtime.UpdatesPerSecond = Target.Definition.UpdatesPerSecond;

            return base.Disable(modifiers);
        }

        /************************************************************************************************************************/

        /// <summary>Updates the <see cref="FlexiMotionRuntime.UpdatesPerSecond"/> based on the distance.</summary>
        protected virtual void LateUpdate()
        {
            if (_DistanceTo == null)
            {
                var camera = Camera.main;
                if (camera == null)
                    return;

                _DistanceTo = camera.transform;
            }

            var squaredDistance = (_DistanceTo.position - Target.transform.position).sqrMagnitude;
            if (squaredDistance <= _NearRange * _NearRange)// Near = Full Rate.
            {
                Target.enabled = true;
                Target.Runtime.UpdatesPerSecond = Target.Definition.UpdatesPerSecond;
            }
            else if (squaredDistance >= _FarRange * _FarRange)// Far = Disabled.
            {
                Target.enabled = false;
            }
            else// Between = Reduced Rate.
            {
                Target.enabled = true;
                Target.Runtime.UpdatesPerSecond = CalculateUpdatesPerSecond(Mathf.Sqrt(squaredDistance));
            }
        }

        /************************************************************************************************************************/

        /// <summary>Calculates the number of updates per second for the given `distance`.</summary>
        public float CalculateUpdatesPerSecond(float distance)
        {
            distance = Mathf.InverseLerp(_NearRange, _FarRange, distance);
            distance = Mathf.Lerp(1, _MinimumUpdateRateMultiplyer, distance);
            return Target.Definition.UpdatesPerSecond * distance;
        }

        /************************************************************************************************************************/
#if UNITY_EDITOR
        /************************************************************************************************************************/

        /// <summary>A custom editor for <see cref="DistanceBasedUpdateRateModifier"/>.</summary>
        [UnityEditor.CustomEditor(typeof(DistanceBasedUpdateRateModifier), true)]
        public class Editor : UnityEditor.Editor
        {
            /************************************************************************************************************************/

            /// <summary>This editor needs to be repainted constantly in case the distance changes.</summary>
            public override bool RequiresConstantRepaint()
                => true;

            /************************************************************************************************************************/

            private static readonly StringBuilder
                StringBuilder = new StringBuilder();

            /// <summary>Draws the default Inspector followed by an info box listing the current details.</summary>
            public override void OnInspectorGUI()
            {
                base.OnInspectorGUI();

                StringBuilder.Length = 0;
                var target = (DistanceBasedUpdateRateModifier)this.target;
                if (target._DistanceTo != null && target.Target != null)
                {
                    var distance = (target._DistanceTo.position - target.Target.transform.position).magnitude;
                    StringBuilder
                        .Append(FMStrings.Bullet + " Distance: ")
                        .Append(distance);

                    if (target.Target != null && target.Target.Definition != null)
                    {
                        StringBuilder
                            .Append(FMStrings.NewBullet + "Updates Per Second: ")
                            .Append(distance < target.FarRange
                                ? target.CalculateUpdatesPerSecond(distance).ToString()
                                : "Disabled");
                    }

                    UnityEditor.EditorGUILayout.HelpBox(
                        StringBuilder.ToString(),
                        UnityEditor.MessageType.Info);
                }
            }

            /************************************************************************************************************************/
        }

        /************************************************************************************************************************/
#endif
        /************************************************************************************************************************/
    }
}

