using System.Runtime.CompilerServices;

namespace FishNet.Managing.Timing
{
    public class EstimatedTick
    {
        #region Types.
        /// <summary>
        /// How to handle old ticks, specifically related to EstimatedTick.
        /// </summary>
        public enum OldTickOption : byte
        {
            /// <summary>
            /// Completely ignore old ticks.
            /// </summary>
            Discard = 0,
            /// <summary>
            /// Set LastRemoteTick but do not update RemoteTick.
            /// </summary>
            SetLastRemoteTick = 1,
            /// <summary>
            /// Set LastRemoteTick and RemoteTick.
            /// </summary>
            SetRemoteTick = 2
        }
        #endregion

        /// <summary>
        /// Local tick when this was last updated.
        /// </summary>
        public uint LocalTick { get; private set; } = TimeManager.UNSET_TICK;
        /// <summary>
        /// Last remote tick this was updated with that was not out of order or a duplicate.
        /// </summary>
        public uint RemoteTick { get; private set; } = TimeManager.UNSET_TICK;
        /// <summary>
        /// Last remote tick received regardless if it was out of order or a duplicate.
        /// </summary>
        public uint LastRemoteTick { get; private set; } = TimeManager.UNSET_TICK;
        /// <summary>
        /// True if LastRemoteTick is equal to RemoteTick.
        /// This would indicate that the LastRemoteTick did not arrive out of order.
        /// </summary>
        public bool IsLastRemoteTickOrdered => LastRemoteTick == RemoteTick;
        /// <summary>
        /// True if value is unset.
        /// </summary>
        // Only need to check one value for unset as they all would be if not set.
        public bool IsUnset => LocalTick == TimeManager.UNSET_TICK;
        /// <summary>
        /// Last TimeManager specified during an Update call.
        /// </summary>
        private TimeManager _updateTimeManager;
        /// <summary>
        /// LocalTick when Value was last reset.
        /// </summary>
        private uint _valueLocalTick = TimeManager.UNSET_TICK;

        /// <summary>
        /// Number of ticks LocalTick is being current LocalTick.
        /// </summary>
        public uint LocalTickDifference(TimeManager tm = null)
        {
            if (!TryAssignTimeManager(ref tm))
                return TimeManager.UNSET_TICK;

            long value = tm.LocalTick - LocalTick;
            // Shouldn't be possible to be less than 0.
            if (value < 0)
                return TimeManager.UNSET_TICK;
            else if (value > uint.MaxValue)
                value = uint.MaxValue;

            return (uint)value;
        }

        /// <summary>
        /// True if values were updated this tick.
        /// </summary>
        public bool IsCurrent(TimeManager tm = null)
        {
            if (!TryAssignTimeManager(ref tm))
                return false;

            return !IsUnset && LocalTick == tm.LocalTick;
        }

        /// <summary>
        /// Current estimated value.
        /// </summary>
        /// <param name = "nm">NetworkManager to use. When null default value will be returned.</param>
        public uint Value(TimeManager tm = null)
        {
            if (!TryAssignTimeManager(ref tm))
                return TimeManager.UNSET_TICK;

            return Value(out _, tm);
        }

        /// <summary>
        /// Current estimated value. Outputs if value is current.
        /// </summary>
        /// <param name = "nm">NetworkManager to use. When null default value will be returned.</param>
        /// <param name = "isCurrent">True if the value was updated this local tick.</param>
        public uint Value(out bool isCurrent, TimeManager tm = null)
        {
            // Default value.
            isCurrent = false;

            if (!TryAssignTimeManager(ref tm))
                return TimeManager.UNSET_TICK;
            if (IsUnset)
                return TimeManager.UNSET_TICK;

            isCurrent = IsCurrent(tm);

            uint diff = tm.LocalTick - _valueLocalTick;
            return diff + RemoteTick;
        }

        /// <summary>
        /// Initializes this EstimatedTick with values.
        /// </summary>
        public void Initialize(TimeManager tm, uint remoteTick = 0, uint lastRemoteTick = 0, uint localTick = 0)
        {
            _updateTimeManager = tm;
            RemoteTick = remoteTick;
            LastRemoteTick = lastRemoteTick;
            LocalTick = localTick;
        }

        /// <summary>
        /// Updates values.
        /// </summary>
        /// <param name = "tm">TimeManager to use.</param>
        /// <param name = "remoteTick">Remote tick being updated.</param>
        /// <param name = "oldTickOption">How to handle remoteTick if it is old.</param>
        /// ///
        /// <param name = "resetValue">True to reset Value based on this information. False will allow Value to continue to to estimate tick based on the last reset.</param>
        /// <returns>True if was able to update values.</returns>
        public bool Update(TimeManager tm, uint remoteTick, OldTickOption oldTickOption = OldTickOption.Discard, bool resetValue = true)
        {
            _updateTimeManager = tm;
            // Always set LastRemoteTick even if out of order.
            LastRemoteTick = remoteTick;
            // If cannot update with old values return.
            if (oldTickOption != OldTickOption.SetRemoteTick && remoteTick <= RemoteTick)
                return false;

            // nm is assumed set here.
            LocalTick = tm.LocalTick;
            if (resetValue)
                _valueLocalTick = LocalTick;
            RemoteTick = remoteTick;

            return true;
        }

        /// <summary>
        /// Updates values.
        /// </summary>
        /// <param name = "remoteTick">Remote tick being updated.</param>
        /// <param name = "oldTickOption">How to handle remoteTick if it is old.</param>
        /// <param name = "resetValue">True to reset Value based on this information. False will allow Value to continue to to estimate tick based on the last reset.</param>
        /// <returns>True if was able to update values.</returns>
        public bool Update(uint remoteTick, OldTickOption oldTickOption = OldTickOption.Discard, bool resetValue = true)
        {
            TimeManager tm = null;
            if (!TryAssignTimeManager(ref tm))
                return false;

            return Update(tm, remoteTick, oldTickOption);
        }

        /// <summary>
        /// Updates Value based on current ticks.
        /// This is typically used when you want to control when Value is reset through the Update methods.
        /// </summary>
        public void UpdateValue()
        {
            _valueLocalTick = LocalTick;
        }

        /// <summary>
        /// Assigns a TimeManager reference to UpdateTimeManager if was null.
        /// </summary>
        /// <returns>True if the reference has value or was assigned value. False if the reference remains null.</returns>
        private bool TryAssignTimeManager(ref TimeManager tm)
        {
            if (tm == null)
                tm = _updateTimeManager;

            return tm != null;
        }

        /// <summary>
        /// Resets values to unset and clears the NetworkManager.
        /// </summary>
        public void Reset()
        {
            ResetTicks();
            _updateTimeManager = null;
        }

        /// <summary>
        /// Resets only tick values, leaving type references.
        /// </summary>
        public void ResetTicks()
        {
            LocalTick = TimeManager.UNSET_TICK;
            RemoteTick = TimeManager.UNSET_TICK;
            LastRemoteTick = TimeManager.UNSET_TICK;
            _valueLocalTick = TimeManager.UNSET_TICK;
        }
    }
}