#if PRIME_TWEEN_SAFETY_CHECKS && UNITY_ASSERTIONS
#define SAFETY_CHECKS
#endif
using System;
using JetBrains.Annotations;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace PrimeTween {
    [Serializable]
    internal class ReusableTween {
        #if UNITY_EDITOR
        [SerializeField, HideInInspector] internal string debugDescription;
        [SerializeField, CanBeNull, UsedImplicitly] internal UnityEngine.Object unityTarget;
        #endif
        internal long id = -1;
        /// Holds a reference to tween's target. If the target is UnityEngine.Object, the tween will gracefully stop when the target is destroyed. That is, destroying object with running tweens is perfectly ok.
        /// Keep in mind: when animating plain C# objects (not derived from UnityEngine.Object), the plugin will hold a strong reference to the object for the entire tween duration.
        ///     If plain C# target holds a reference to UnityEngine.Object and animates its properties, then it's user's responsibility to ensure that UnityEngine.Object still exists.
        [CanBeNull] internal object target;
        [SerializeField] internal bool _isPaused;
        internal bool _isAlive;
        [SerializeField] internal float elapsedTimeTotal;
        [SerializeField] internal float easedInterpolationFactor;
        internal float cycleDuration;

        [SerializeField] internal ValueContainerStartEnd startEndValue;

        internal PropType propType => Utils.TweenTypeToTweenData(startEndValue.tweenType).Item1;
        internal ref TweenType tweenType => ref startEndValue.tweenType;
        internal ref ValueContainer startValue => ref startEndValue.startValue;
        internal ref ValueContainer endValue => ref startEndValue.endValue;
        internal ValueContainer diff;
        internal bool isAdditive;
        internal ValueContainer prevVal;
        [SerializeField] internal TweenSettings settings;
        [SerializeField] int cyclesDone;
        const int iniCyclesDone = -1;

        internal object customOnValueChange;
        internal long longParam;
        internal int intParam {
            get => (int)longParam;
            set => longParam = value;
        }
        Action<ReusableTween> onValueChange;

        [CanBeNull] Action<ReusableTween> onComplete;
        [CanBeNull] object onCompleteCallback;
        [CanBeNull] object onCompleteTarget;

        internal float waitDelay;
        internal Sequence sequence;
        internal Tween prev;
        internal Tween next;
        internal Tween prevSibling;
        internal Tween nextSibling;

        internal Func<ReusableTween, ValueContainer> getter;
        internal ref bool startFromCurrent => ref startEndValue.startFromCurrent;

        bool stoppedEmergently;
        internal readonly TweenCoroutineEnumerator coroutineEnumerator = new TweenCoroutineEnumerator();
        internal float timeScale = 1f;
        bool warnIgnoredOnCompleteIfTargetDestroyed = true;
        internal ShakeData shakeData;
        State state;
        bool warnEndValueEqualsCurrent;

        internal bool updateAndCheckIfRunning(float dt) {
            if (!_isAlive) {
                return sequence.IsCreated; // don't release a tween until sequence.releaseTweens()
            }
            if (!_isPaused) {
                SetElapsedTimeTotal(elapsedTimeTotal + dt * timeScale);
            } else if (isUnityTargetDestroyed()) {
                EmergencyStop(true);
                return false;
            }
            return _isAlive;
        }

        bool isUpdating; // todo place this check only on calls that come from Tween.Custom()? no, then it would not be possible to call .Complete() on custom tweens

        internal void SetElapsedTimeTotal(float newElapsedTimeTotal, bool earlyExitSequenceIfPaused = true) {
            if (isUpdating) {
                Debug.LogError(Constants.recursiveCallError);
                return;
            }
            isUpdating = true;
            if (!sequence.IsCreated) {
                setElapsedTimeTotal(newElapsedTimeTotal, out int cyclesDiff);
                if (!stoppedEmergently && _isAlive && isDone(cyclesDiff)) {
                    if (!_isPaused) {
                        kill();
                    }
                    ReportOnComplete();
                }
            } else {
                Assert.IsTrue(sequence.isAlive, id);
                if (isMainSequenceRoot()) {
                    Assert.IsTrue(sequence.root.id == id, id);
                    updateSequence(newElapsedTimeTotal, false, earlyExitSequenceIfPaused);
                }
            }
            isUpdating = false;
        }

        internal void updateSequence(float _elapsedTimeTotal, bool isRestart, bool earlyExitSequenceIfPaused = true, bool allowSkipChildrenUpdate = true) {
            Assert.IsTrue(isSequenceRoot());
            float prevEasedT = easedInterpolationFactor;
            if (!setElapsedTimeTotal(_elapsedTimeTotal, out int cyclesDiff) && allowSkipChildrenUpdate) { // update sequence root
                return;
            }

            bool isRestartToBeginning = isRestart && cyclesDiff < 0;
            Assert.IsTrue(!isRestartToBeginning || cyclesDone == 0 || cyclesDone == iniCyclesDone);
            if (cyclesDiff != 0 && !isRestartToBeginning) {
                // print($"           sequence cyclesDiff: {cyclesDiff}");
                if (isRestart) {
                    Assert.IsTrue(cyclesDiff > 0 && cyclesDone == settings.cycles);
                    cyclesDiff = 1;
                }
                int cyclesDiffAbs = Mathf.Abs(cyclesDiff);
                int newCyclesDone = cyclesDone;
                cyclesDone -= cyclesDiff;
                int cyclesDelta = cyclesDiff > 0 ? 1 : -1;
                var interpolationFactor = cyclesDelta > 0 ? 1f : 0f;
                for (int i = 0; i < cyclesDiffAbs; i++) {
                    Assert.IsTrue(!isRestart || i == 0);
                    if (cyclesDone == settings.cycles || cyclesDone == iniCyclesDone) {
                        // do nothing when moving backward from the last cycle or forward from the -1 cycle
                        cyclesDone += cyclesDelta;
                        continue;
                    }

                    var easedT = calcEasedT(interpolationFactor, cyclesDone);
                    var isForwardCycle = easedT > 0.5f;
                    const float negativeElapsedTime = -1000f;
                    if (!forceChildrenToPos()) {
                        return;
                    }
                    bool forceChildrenToPos() {
                        // complete the previous cycles by forcing all children tweens to 0f or 1f
                        // print($" (i:{i}) force to pos: {isForwardCycle}");
                        var simulatedSequenceElapsedTime = isForwardCycle ? float.MaxValue : negativeElapsedTime;
                        foreach (var t in getSequenceSelfChildren(isForwardCycle)) {
                            var tween = t.tween;
                            tween.updateSequenceChild(simulatedSequenceElapsedTime, isRestart);
                            if (isEarlyExitAfterChildUpdate()) {
                                return false;
                            }
                        }
                        return true;
                    }

                    cyclesDone += cyclesDelta;
                    var sequenceCycleMode = settings.cycleMode;
                    if (sequenceCycleMode == CycleMode.Restart && cyclesDone != settings.cycles && cyclesDone != iniCyclesDone) { // '&& cyclesDone != 0' check is wrong because we should do the restart when moving from 1 to 0 cyclesDone
                        if (!restartChildren()) {
                            return;
                        }
                        bool restartChildren() {
                            // print($"restart to pos: {!isForwardCycle}");
                            var simulatedSequenceElapsedTime = !isForwardCycle ? float.MaxValue : negativeElapsedTime;
                            prevEasedT = simulatedSequenceElapsedTime;
                            foreach (var t in getSequenceSelfChildren(!isForwardCycle)) {
                                var tween = t.tween;
                                tween.updateSequenceChild(simulatedSequenceElapsedTime, true);
                                if (isEarlyExitAfterChildUpdate()) {
                                    return false;
                                }
                                Assert.IsTrue(isForwardCycle || tween.cyclesDone == tween.settings.cycles, id);
                                Assert.IsTrue(!isForwardCycle || tween.cyclesDone <= 0, id);
                                Assert.IsTrue(isForwardCycle || tween.state == State.After, id);
                                Assert.IsTrue(!isForwardCycle || tween.state == State.Before, id);
                            }
                            return true;
                        }
                    }
                }
                Assert.IsTrue(newCyclesDone == cyclesDone, id);
                if (isDone(cyclesDiff)) {
                    if (isMainSequenceRoot() && !_isPaused) {
                        sequence.releaseTweens();
                    }
                    ReportOnComplete();
                    return;
                }
            }

            easedInterpolationFactor = Mathf.Clamp01(easedInterpolationFactor);
            bool isForward = easedInterpolationFactor > prevEasedT;
            float sequenceElapsedTime = easedInterpolationFactor * cycleDuration;
            foreach (var t in getSequenceSelfChildren(isForward)) {
                t.tween.updateSequenceChild(sequenceElapsedTime, isRestart);
                if (isEarlyExitAfterChildUpdate()) {
                    return;
                }
            }

            bool isEarlyExitAfterChildUpdate() {
                if (!sequence.isAlive) {
                    return true;
                }
                return earlyExitSequenceIfPaused && sequence.root.tween._isPaused; // access isPaused via root tween to bypass the cantManipulateNested check
            }
        }

        Sequence.SequenceDirectEnumerator getSequenceSelfChildren(bool isForward) {
            Assert.IsTrue(sequence.isAlive, id);
            return sequence.getSelfChildren(isForward);
        }

        bool isDone(int cyclesDiff) {
            Assert.IsTrue(settings.cycles == -1 || cyclesDone <= settings.cycles);
            if (timeScale > 0f) {
                return cyclesDiff > 0 && cyclesDone == settings.cycles;
            }
            return cyclesDiff < 0 && cyclesDone == iniCyclesDone;
        }

        void updateSequenceChild(float encompassingElapsedTime, bool isRestart) {
            if (isSequenceRoot()) {
                updateSequence(encompassingElapsedTime, isRestart);
            } else {
                setElapsedTimeTotal(encompassingElapsedTime, out var cyclesDiff);
                if (!stoppedEmergently && _isAlive && isDone(cyclesDiff)) {
                    ReportOnComplete();
                }
            }
        }

        internal bool isMainSequenceRoot() => tweenType == TweenType.MainSequence;
        internal bool isSequenceRoot() => tweenType == TweenType.MainSequence || tweenType == TweenType.NestedSequence;

        bool setElapsedTimeTotal(float _elapsedTimeTotal, out int cyclesDiff) {
            elapsedTimeTotal = _elapsedTimeTotal;
            int oldCyclesDone = cyclesDone;
            float t = calcTFromElapsedTimeTotal(_elapsedTimeTotal, out var newState);
            cyclesDiff = cyclesDone - oldCyclesDone;
            if (newState == State.Running || state != newState) {
                if (isUnityTargetDestroyed()) {
                    EmergencyStop(true);
                    return false;
                }
                float easedT = calcEasedT(t, cyclesDone);
                // print($"state: {state}/{newState}, cycles: {cyclesDone}/{settings.cycles} (diff: {cyclesDiff}), elapsedTimeTotal: {elapsedTimeTotal}, interpolation: {t}/{easedT}");
                state = newState;
                ReportOnValueChange(easedT);
                return true;
            }
            return false;
        }

        float calcTFromElapsedTimeTotal(float _elapsedTimeTotal, out State newState) {
            // key timeline points: 0 | startDelay | duration | 1 | endDelay | onComplete
            var cyclesTotal = settings.cycles;
            // ReSharper disable once CompareOfFloatsByEqualityOperator
            if (_elapsedTimeTotal == float.MaxValue) {
                Assert.AreNotEqual(-1, cyclesTotal);
                Assert.IsTrue(cyclesDone <= cyclesTotal);
                cyclesDone = cyclesTotal;
                newState = State.After;
                return 1f;
            }
            _elapsedTimeTotal -= waitDelay; // waitDelay is applied before calculating cycles
            if (_elapsedTimeTotal < 0f) {
                cyclesDone = iniCyclesDone;
                newState = State.Before;
                return 0f;
            }
            Assert.IsTrue(_elapsedTimeTotal >= 0f);
            Assert.AreNotEqual(float.MaxValue, _elapsedTimeTotal);
            var duration = settings.duration;
            if (duration == 0f) {
                if (cyclesTotal == -1) {
                    // add max one cycle per frame
                    if (timeScale > 0f) {
                        if (cyclesDone == iniCyclesDone) {
                            cyclesDone = 1;
                        } else {
                            cyclesDone++;
                        }
                    } else if (timeScale != 0f) {
                        cyclesDone--;
                        if (cyclesDone == iniCyclesDone) {
                            newState = State.Before;
                            return 0f;
                        }
                    }
                    newState = State.Running;
                    return 1f;
                }
                Assert.AreNotEqual(-1, cyclesTotal);
                if (_elapsedTimeTotal == 0f) {
                    cyclesDone = iniCyclesDone;
                    newState = State.Before;
                    return 0f;
                }
                Assert.IsTrue(cyclesDone <= cyclesTotal);
                cyclesDone = cyclesTotal;
                newState = State.After;
                return 1f;
            }
            Assert.AreNotEqual(0f, cycleDuration);
            cyclesDone = (int) (_elapsedTimeTotal / cycleDuration);
            if (cyclesTotal != -1 && cyclesDone > cyclesTotal) {
                cyclesDone = cyclesTotal;
            }
            if (cyclesTotal != -1 && cyclesDone == cyclesTotal) {
                newState = State.After;
                return 1f;
            }
            var elapsedTimeInCycle = _elapsedTimeTotal - cycleDuration * cyclesDone - settings.startDelay;
            if (elapsedTimeInCycle < 0f) {
                newState = State.Before;
                return 0f;
            }
            Assert.IsTrue(elapsedTimeInCycle >= 0f);
            Assert.AreNotEqual(0f, duration);
            var result = elapsedTimeInCycle / duration;
            if (result > 1f) {
                newState = State.After;
                return 1f;
            }
            newState = State.Running;
            Assert.IsTrue(result >= 0f);
            return result;
        }

        // void print(string msg) => Debug.Log($"[{Time.frameCount}]  id {id}  {msg}");

        internal void Reset() {
            Assert.IsFalse(isUpdating);
            Assert.IsFalse(_isAlive);
            Assert.IsFalse(sequence.IsCreated);
            Assert.IsFalse(prev.IsCreated);
            Assert.IsFalse(next.IsCreated);
            Assert.IsFalse(prevSibling.IsCreated);
            Assert.IsFalse(nextSibling.IsCreated);
            Assert.IsFalse(IsInSequence());
            if (shakeData.isAlive) {
                shakeData.Reset(this);
            }
            #if UNITY_EDITOR
            debugDescription = null;
            unityTarget = null;
            #endif
            id = -1;
            target = null;
            settings.customEase = null;
            customOnValueChange = null;
            onValueChange = null;
            onComplete = null;
            onCompleteCallback = null;
            onCompleteTarget = null;
            getter = null;
            stoppedEmergently = false;
            waitDelay = 0f;
            coroutineEnumerator.resetEnumerator();
            tweenType = TweenType.None;
            timeScale = 1f;
            warnIgnoredOnCompleteIfTargetDestroyed = true;
            clearOnUpdate();
        }

        /// <param name="warnIfTargetDestroyed">https://github.com/KyryloKuzyk/PrimeTween/discussions/4</param>
        internal void OnComplete([NotNull] Action _onComplete, bool warnIfTargetDestroyed) {
            Assert.IsNotNull(_onComplete);
            validateOnCompleteAssignment();
            warnIgnoredOnCompleteIfTargetDestroyed = warnIfTargetDestroyed;
            onCompleteCallback = _onComplete;
            onComplete = tween => {
                var callback = tween.onCompleteCallback as Action;
                Assert.IsNotNull(callback);
                try {
                    callback();
                } catch (Exception e) {
                    tween.handleOnCompleteException(e);
                }
            };
        }

        internal void OnComplete<T>([CanBeNull] T _target, [NotNull] Action<T> _onComplete, bool warnIfTargetDestroyed) where T : class {
            if (_target == null || isDestroyedUnityObject(_target)) {
                Debug.LogError($"{nameof(_target)} is null or has been destroyed. {Constants.onCompleteCallbackIgnored}");
                return;
            }
            Assert.IsNotNull(_onComplete);
            validateOnCompleteAssignment();
            warnIgnoredOnCompleteIfTargetDestroyed = warnIfTargetDestroyed;
            onCompleteTarget = _target;
            onCompleteCallback = _onComplete;
            onComplete = tween => {
                var callback = tween.onCompleteCallback as Action<T>;
                Assert.IsNotNull(callback);
                var _onCompleteTarget = tween.onCompleteTarget as T;
                if (isDestroyedUnityObject(_onCompleteTarget)) {
                    tween.warnOnCompleteIgnored(true);
                    return;
                }
                try {
                    callback(_onCompleteTarget);
                } catch (Exception e) {
                    tween.handleOnCompleteException(e);
                }
            };
        }

        void handleOnCompleteException(Exception e) {
            // Design decision: if a tween is inside a Sequence and user's tween.OnComplete() throws an exception, the Sequence should continue
            Assert.LogError($"Tween's onComplete callback raised exception, tween: {GetDescription()}, exception:\n{e}\n", id, target as UnityEngine.Object);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")]
        static bool isDestroyedUnityObject<T>(T obj) where T: class => obj is UnityEngine.Object unityObject && unityObject == null;

        void validateOnCompleteAssignment() {
            const string msg = "Tween already has an onComplete callback. Adding more callbacks is not allowed.\n" +
                               "Workaround: wrap a tween in a Sequence by calling Sequence.Create(tween) and use multiple ChainCallback().\n";
            Assert.IsNull(onCompleteTarget, msg);
            Assert.IsNull(onCompleteCallback, msg);
            Assert.IsNull(onComplete, msg);
        }

        /// _getter is null for custom tweens
        internal void Setup([CanBeNull] object _target, ref TweenSettings _settings, [NotNull] Action<ReusableTween> _onValueChange, [CanBeNull] Func<ReusableTween, ValueContainer> _getter, bool _startFromCurrent, TweenType _tweenType) {
            Assert.IsTrue(_settings.cycles >= -1);
            Assert.IsNotNull(_onValueChange);
            Assert.IsNull(getter);
            tweenType = _tweenType;
            var propertyType = propType;
            Assert.AreNotEqual(PropType.None, propertyType);
            #if UNITY_EDITOR
            if (Constants.noInstance) {
                return;
            }
            #endif
            if (_settings.ease == Ease.Default) {
                _settings.ease = PrimeTweenManager.Instance.defaultEase;
            } else if (_settings.ease == Ease.Custom && _settings.parametricEase == ParametricEase.None) {
                if (_settings.customEase == null || !TweenSettings.ValidateCustomCurveKeyframes(_settings.customEase)) {
                    Debug.LogError($"Ease type is Ease.Custom, but {nameof(TweenSettings.customEase)} is not configured correctly.");
                    _settings.ease = PrimeTweenManager.Instance.defaultEase;
                }
            }
            state = State.Before;
            target = _target;
            setUnityTarget(_target);
            elapsedTimeTotal = 0f;
            easedInterpolationFactor = float.MinValue;
            _isPaused = false;
            revive();

            cyclesDone = iniCyclesDone;
            _settings.SetValidValues();
            settings.CopyFrom(ref _settings);
            recalculateTotalDuration();
            Assert.IsTrue(cycleDuration >= 0);
            onValueChange = _onValueChange;
            Assert.IsFalse(_startFromCurrent && _getter == null);
            startFromCurrent = _startFromCurrent;
            getter = _getter;
            if (!_startFromCurrent) {
                cacheDiff();
            }
            if (propertyType == PropType.Quaternion) {
                prevVal.QuaternionVal = Quaternion.identity;
            } else {
                prevVal.Reset();
            }
            warnEndValueEqualsCurrent = PrimeTweenManager.Instance.warnEndValueEqualsCurrent;
        }

        internal void setUnityTarget(object _target) {
            #if UNITY_EDITOR
            unityTarget = _target as UnityEngine.Object;
            #endif
        }

        /// Tween.Custom and Tween.ShakeCustom try-catch the <see cref="onValueChange"/> and calls <see cref="ReusableTween.EmergencyStop"/> if an exception occurs.
        /// <see cref="ReusableTween.EmergencyStop"/> sets <see cref="stoppedEmergently"/> to true.
        internal void ReportOnValueChange(float _easedInterpolationFactor) {
            // Debug.Log($"id {id}, ReportOnValueChange {_easedInterpolationFactor}");
            Assert.IsFalse(isUnityTargetDestroyed());
            if (startFromCurrent) {
                startFromCurrent = false;
                if (!ShakeData.TryTakeStartValueFromOtherShake(this)) {
                    startValue = getter(this);
                }
                if (startValue.Vector4Val == endValue.Vector4Val && warnEndValueEqualsCurrent && !shakeData.isAlive) {
                    Assert.LogWarning($"Tween's 'endValue' equals to the current animated value: {startValue.Vector4Val}, tween: {GetDescription()}.\n" +
                                      $"{Constants.buildWarningCanBeDisabledMessage(nameof(PrimeTweenConfig.warnEndValueEqualsCurrent))}\n", id);
                }
                cacheDiff();
            }
            easedInterpolationFactor = _easedInterpolationFactor;
            onValueChange(this);
            if (stoppedEmergently || !_isAlive) {
                return;
            }
            onUpdate?.Invoke(this);
        }

        void ReportOnComplete() {
            // Debug.Log($"[{Time.frameCount}] id {id} ReportOnComplete() {easedInterpolationFactor}");
            Assert.IsFalse(startFromCurrent);
            Assert.IsTrue(timeScale < 0 || cyclesDone == settings.cycles);
            Assert.IsTrue(timeScale >= 0 || cyclesDone == iniCyclesDone);
            onComplete?.Invoke(this);
        }

        internal bool isUnityTargetDestroyed() {
            // must use target here instead of unityTarget
            // unityTarget has the SerializeField attribute, so if ReferenceEquals(unityTarget, null), then Unity will populate the field with non-null UnityEngine.Object when a new scene is loaded in the Editor
            // https://github.com/KyryloKuzyk/PrimeTween/issues/32
            return isDestroyedUnityObject(target);
        }

        internal bool HasOnComplete => onComplete != null;

        [NotNull]
        internal string GetDescription() {
            string result = "";
            if (!_isAlive) {
                result += " - ";
            }
            if (target != PrimeTweenManager.dummyTarget) {
                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
                result += $"{(target is UnityEngine.Object unityObject && unityObject != null ? unityObject.name : target?.GetType().Name)} / ";
            }
            var duration = settings.duration;
            if (tweenType == TweenType.Delay) {
                if (duration == 0f && onComplete != null) {
                    result += "Callback";
                } else {
                    result += $"Delay / duration {duration}";
                }
            } else {
                if (tweenType == TweenType.MainSequence) {
                    result += $"Sequence {id}";
                } else if (tweenType == TweenType.NestedSequence) {
                    result += $"Sequence {id} (nested)";
                } else {
                    result += tweenType.ToString() ;
                }
                result += " / duration ";
                /*if (waitDelay != 0f) {
                    result += $"{waitDelay}+";
                }*/
                result += $"{duration}";
            }
            result += $" / id {id}";
            if (sequence.IsCreated && tweenType != TweenType.MainSequence) {
                result += $" / sequence {sequence.root.id}";
            }
            return result;
        }

        internal float calcDurationWithWaitDependencies() {
            var cycles = settings.cycles;
            Assert.AreNotEqual(-1, cycles, "It's impossible to calculate the duration of an infinite tween (cycles == -1).");
            Assert.AreNotEqual(0, cycles);
            return waitDelay + cycleDuration * cycles;
        }

        internal void recalculateTotalDuration() {
            cycleDuration = settings.startDelay + settings.duration + settings.endDelay;
        }

        internal float FloatVal => startValue.x + diff.x * easedInterpolationFactor;
        internal double DoubleVal => startValue.DoubleVal + diff.DoubleVal * easedInterpolationFactor;
        internal Vector2 Vector2Val {
            get {
                var easedT = easedInterpolationFactor;
                return new Vector2(
                    startValue.x + diff.x * easedT,
                    startValue.y + diff.y * easedT);
            }
        }
        internal Vector3 Vector3Val {
            get {
                var easedT = easedInterpolationFactor;
                return new Vector3(
                    startValue.x + diff.x * easedT,
                    startValue.y + diff.y * easedT,
                    startValue.z + diff.z * easedT);
            }
        }
        internal Vector4 Vector4Val {
            get {
                var easedT = easedInterpolationFactor;
                return new Vector4(
                    startValue.x + diff.x * easedT,
                    startValue.y + diff.y * easedT,
                    startValue.z + diff.z * easedT,
                    startValue.w + diff.w * easedT);
            }
        }
        internal Color ColorVal {
            get {
                var easedT = easedInterpolationFactor;
                return new Color(
                    startValue.x + diff.x * easedT,
                    startValue.y + diff.y * easedT,
                    startValue.z + diff.z * easedT,
                    startValue.w + diff.w * easedT);
            }
        }
        internal Rect RectVal {
            get {
                var easedT = easedInterpolationFactor;
                return new Rect(
                    startValue.x + diff.x * easedT,
                    startValue.y + diff.y * easedT,
                    startValue.z + diff.z * easedT,
                    startValue.w + diff.w * easedT);
            }
        }
        internal Quaternion QuaternionVal => Quaternion.SlerpUnclamped(startValue.QuaternionVal, endValue.QuaternionVal, easedInterpolationFactor);

        float calcEasedT(float t, int cyclesDone) {
            switch (settings.cycleMode) {
                case CycleMode.Restart:
                    return evaluate(t);
                case CycleMode.Incremental:
                    return evaluate(t) + clampCyclesDone();
                case CycleMode.Yoyo: {
                    var isForwardCycle = clampCyclesDone() % 2 == 0;
                    return isForwardCycle ? evaluate(t) : 1 - evaluate(t);
                }
                case CycleMode.Rewind: {
                    var isForwardCycle = clampCyclesDone() % 2 == 0;
                    return isForwardCycle ? evaluate(t) : evaluate(1 - t);
                }
                default:
                    throw new Exception();
            }

            int clampCyclesDone() {
                if (cyclesDone == iniCyclesDone) {
                    return 0;
                }
                int cyclesTotal = settings.cycles;
                if (cyclesDone == cyclesTotal) {
                    Assert.AreNotEqual(-1, cyclesTotal);
                    return cyclesTotal - 1;
                }
                return cyclesDone;
            }
        }

        float evaluate(float t) {
            if (settings.ease == Ease.Custom) {
                if (settings.parametricEase != ParametricEase.None) {
                    return Easing.Evaluate(t, this);
                }
                return settings.customEase.Evaluate(t);
            }
            return StandardEasing.Evaluate(t, settings.ease);
        }

        internal void cacheDiff() {
            Assert.IsFalse(startFromCurrent);
            var propertyType = propType;
            Assert.AreNotEqual(PropType.None, propertyType);
            switch (propertyType) {
                case PropType.Quaternion:
                    startValue.QuaternionVal.Normalize();
                    endValue.QuaternionVal.Normalize();
                    break;
                case PropType.Double:
                    diff.DoubleVal = endValue.DoubleVal - startValue.DoubleVal;
                    diff.z = 0;
                    diff.w = 0;
                    break;
                default:
                    diff.x = endValue.x - startValue.x;
                    diff.y = endValue.y - startValue.y;
                    diff.z = endValue.z - startValue.z;
                    diff.w = endValue.w - startValue.w;
                    break;
            }
        }

        internal void ForceComplete() {
            Assert.IsFalse(sequence.IsCreated);
            kill(); // protects from recursive call
            if (isUnityTargetDestroyed()) {
                warnOnCompleteIgnored(true);
                return;
            }
            var cyclesTotal = settings.cycles;
            if (cyclesTotal == -1) {
                // same as SetRemainingCycles(1)
                cyclesTotal = getCyclesDone() + 1;
                settings.cycles = cyclesTotal;
            }
            cyclesDone = cyclesTotal;
            ReportOnValueChange(calcEasedT(1f, cyclesTotal));
            if (stoppedEmergently) {
                return;
            }
            ReportOnComplete();
            Assert.IsFalse(_isAlive);
        }

        internal void warnOnCompleteIgnored(bool isTargetDestroyed) {
            if (HasOnComplete && warnIgnoredOnCompleteIfTargetDestroyed) {
                onComplete = null;
                var msg = $"{Constants.onCompleteCallbackIgnored} Tween: {GetDescription()}.\n";
                if (isTargetDestroyed) {
                    msg += "\nIf you use tween.OnComplete(), Tween.Delay(), or sequence.ChainDelay() only for cosmetic purposes, you can turn off this error by passing 'warnIfTargetDestroyed: false' to the method.\n" +
                           "More info: https://github.com/KyryloKuzyk/PrimeTween/discussions/4\n";
                }
                Assert.LogError(msg, id, target as UnityEngine.Object);
            }
        }

        internal void EmergencyStop(bool isTargetDestroyed = false) {
            if (sequence.IsCreated) {
                var mainSequence = sequence;
                while (true) {
                    var _prev = mainSequence.root.tween.prev;
                    if (!_prev.IsCreated) {
                        break;
                    }
                    var parent = _prev.tween.sequence;
                    if (!parent.IsCreated) {
                        break;
                    }
                    mainSequence = parent;
                }
                Assert.IsTrue(mainSequence.isAlive);
                Assert.IsTrue(mainSequence.root.tween.isMainSequenceRoot());
                mainSequence.emergencyStop();
            } else if (_isAlive) {
                // EmergencyStop() can be called after ForceComplete() and a caught exception in Tween.Custom()
                kill();
            }
            stoppedEmergently = true;
            warnOnCompleteIgnored(isTargetDestroyed);
            Assert.IsFalse(_isAlive);
            Assert.IsFalse(sequence.isAlive);
        }

        internal void kill() {
            // print($"kill {GetDescription()}");
            Assert.IsTrue(_isAlive);
            _isAlive = false;
            #if UNITY_EDITOR
            debugDescription = null;
            #endif
        }

        void revive() {
            // print($"revive {GetDescription()}");
            Assert.IsFalse(_isAlive);
            _isAlive = true;
            #if UNITY_EDITOR
            debugDescription = null;
            #endif
        }

        internal bool IsInSequence() {
            var result = sequence.IsCreated;
            Assert.IsTrue(result || !nextSibling.IsCreated);
            return result;
        }

        internal bool canManipulate() => !IsInSequence() || isMainSequenceRoot();

        internal bool trySetPause(bool isPaused) {
            if (_isPaused == isPaused) {
                return false;
            }
            _isPaused = isPaused;
            return true;
        }

        [CanBeNull] object onUpdateTarget;
        object onUpdateCallback;
        Action<ReusableTween> onUpdate;

        internal void SetOnUpdate<T>(T _target, [NotNull] Action<T, Tween> _onUpdate) where T : class {
            Assert.IsNull(onUpdate, "Only one OnUpdate() is allowed for one tween.");
            Assert.IsNotNull(_onUpdate, nameof(_onUpdate) + " is null!");
            onUpdateTarget = _target;
            onUpdateCallback = _onUpdate;
            onUpdate = reusableTween => reusableTween.invokeOnUpdate<T>();
        }

        void invokeOnUpdate<T>() where T : class {
            var callback = onUpdateCallback as Action<T, Tween>;
            Assert.IsNotNull(callback);
            var _onUpdateTarget = onUpdateTarget as T;
            if (isDestroyedUnityObject(_onUpdateTarget)) {
                Assert.LogError($"OnUpdate() will not be called again because OnUpdate()'s target has been destroyed, tween: {GetDescription()}", id, target as UnityEngine.Object);
                clearOnUpdate();
                return;
            }
            try {
                callback(_onUpdateTarget, new Tween(this));
            } catch (Exception e) {
                Assert.LogError($"OnUpdate() will not be called again because it thrown exception, tween: {GetDescription()}, exception:\n{e}", id, target as UnityEngine.Object);
                clearOnUpdate();
            }
        }

        void clearOnUpdate() {
            onUpdateTarget = null;
            onUpdateCallback = null;
            onUpdate = null;
        }

        public override string ToString() {
            return GetDescription();
        }

        enum State : byte {
            Before, Running, After
        }

        internal float getElapsedTimeTotal() {
            var result = elapsedTimeTotal;
            var durationTotal = getDurationTotal();
            // ReSharper disable once CompareOfFloatsByEqualityOperator
            if (result == float.MaxValue) {
                return durationTotal;
            }
            return Mathf.Clamp(result, 0f, durationTotal);
        }

        internal float getDurationTotal() {
            var cyclesTotal = settings.cycles;
            if (cyclesTotal == -1) {
                return float.PositiveInfinity;
            }
            Assert.AreNotEqual(0, cyclesTotal);
            return cycleDuration * cyclesTotal;
        }

        internal int getCyclesDone() {
            int result = cyclesDone;
            if (result == iniCyclesDone) {
                return 0;
            }
            Assert.IsTrue(result >= 0);
            return result;
        }
    }
}
