using Intrepid.Utilities;
using UnityEngine;

namespace Intrepid.Core.TimeControl
{
    internal enum TargetTimeScale
    {
        NONE,
        VERY_SLOW_MOTION,
        SLOW_MOTION,
        BREATH_MOTION,
        NORMAL,
        FAST_MOTION,
    }

    [DefaultExecutionOrder(GonConstants.ExecutionOrder.TimeSimulation)]
    public class TimeSimulation : SingletonMonoBehaviour<TimeSimulation>
    {
        [GroupConfigurations, SerializeField, Range(0.001f, 0.05f)] private float baseFixedDeltaTime = 0.02f;
        [GroupConfigurations, SerializeField, Range(0.01f, 1f)] private float verySlowMotionScale = 0.25f;
        [GroupConfigurations, SerializeField, Range(0.01f, 1f)] private float slowMotionScale = 0.5f;
        [GroupConfigurations, SerializeField, Range(0.01f, 1f)] private float breathMotionScale = 0.75f;
        [GroupConfigurations, SerializeField, Range(1f, 2f)] private float fastMotionScale = 1.25f;

        public float UnscaledElapsedTime { get; private set; }
        public float ScaledElapsedTime { get; private set; }

        public bool IsPaused { get; private set; }

        private TargetTimeScale _currentTargetTimeScale = TargetTimeScale.NORMAL;
        private TargetTimeScale _targetTimeScale = TargetTimeScale.NONE;

        private float _blendStartedAt;
        private float _blendDuration;
        private float _blendStartScale;
        private float _blendTargetScale;

        private float _timeScaleBeforePause = 1f;

        private float TimeScale
        {
            get => Time.timeScale;
            set => Time.timeScale = Mathf.Clamp(value, verySlowMotionScale, fastMotionScale);
        }

        private void Awake()
        {
            SingletonInstantiation(false, s =>
            {
                s.ResetSimulation();
            });
        }

        public void ResetSimulation()
        {
            IsPaused = false;

            TimeScale = 1f;
            Time.fixedDeltaTime = baseFixedDeltaTime;

            UnscaledElapsedTime = 0f;
            ScaledElapsedTime = 0f;

            _currentTargetTimeScale = TargetTimeScale.NORMAL;
            _targetTimeScale = TargetTimeScale.NONE;

            _blendStartedAt = 0f;
            _blendDuration = 0f;
            _blendStartScale = 1f;
            _blendTargetScale = 1f;

            _timeScaleBeforePause = 1f;
        }

        private void Update()
        {
            if (!IsPaused)
            {
                UpdateTimeScaleBlend();
            }

            UnscaledElapsedTime += Time.unscaledDeltaTime;
            ScaledElapsedTime += Time.deltaTime;
        }

        public void Pause()
        {
            if (IsPaused)
            {
                return;
            }

            _timeScaleBeforePause = Time.timeScale;

            IsPaused = true;
            Time.timeScale = 0f;
        }

        public void Unpause()
        {
            if (!IsPaused)
            {
                return;
            }

            IsPaused = false;
            Time.timeScale = _timeScaleBeforePause;
        }

        private void UpdateTimeScaleBlend()
        {
            if (_targetTimeScale == TargetTimeScale.NONE)
            {
                return;
            }

            if (_blendDuration <= 0f)
            {
                CompleteBlend();
                return;
            }

            var elapsed = UnscaledElapsedTime - _blendStartedAt;
            var t = Mathf.Clamp01(elapsed / _blendDuration);
            
            var easedT = t * t * (3f - 2f * t);

            TimeScale = Mathf.Lerp(
                _blendStartScale,
                _blendTargetScale,
                easedT
            );

            if (t >= 1f)
            {
                CompleteBlend();
            }
        }

        private void CompleteBlend()
        {
            TimeScale = _blendTargetScale;
            _currentTargetTimeScale = _targetTimeScale;
            _targetTimeScale = TargetTimeScale.NONE;
        }

        public void AsBreathMotion(float blendTimeInSeconds = 0f)
        {
            BlendTo(
                TargetTimeScale.BREATH_MOTION,
                breathMotionScale,
                blendTimeInSeconds
            );
        }

        public void AsVerySlowMotion(float blendTimeInSeconds = 0f)
        {
            BlendTo(
                TargetTimeScale.VERY_SLOW_MOTION,
                verySlowMotionScale,
                blendTimeInSeconds
            );
        }

        public void AsSlowMotion(float blendTimeInSeconds = 0f)
        {
            BlendTo(
                TargetTimeScale.SLOW_MOTION,
                slowMotionScale,
                blendTimeInSeconds
            );
        }

        public void AsFastMotion(float blendTimeInSeconds = 0f)
        {
            BlendTo(
                TargetTimeScale.FAST_MOTION,
                fastMotionScale,
                blendTimeInSeconds
            );
        }

        public void AsNormalMotion(float blendTimeInSeconds = 0f)
        {
            BlendTo(
                TargetTimeScale.NORMAL,
                1f,
                blendTimeInSeconds
            );
        }

        private void BlendTo(
            TargetTimeScale target,
            float targetScale,
            float blendTimeInSeconds
        )
        {
            if (IsPaused)
            {
                return;
            }

            if (_currentTargetTimeScale == target &&
                _targetTimeScale == TargetTimeScale.NONE)
            {
                return;
            }

            targetScale = Mathf.Clamp(targetScale, 0f, 2f);

            if (blendTimeInSeconds <= 0f)
            {
                TimeScale = targetScale;

                _currentTargetTimeScale = target;
                _targetTimeScale = TargetTimeScale.NONE;

                return;
            }

            _targetTimeScale = target;

            _blendStartedAt = UnscaledElapsedTime;
            _blendDuration = blendTimeInSeconds;

            _blendStartScale = TimeScale;
            _blendTargetScale = targetScale;
        }
    }
}