using FishNet.Utility.Extension;
using FishNet.Object;
using System.Runtime.CompilerServices;
using UnityEngine;

namespace FishNet.Component.Prediction
{
    internal class PredictedObjectOwnerSmoother
    {

        #region Serialized.
        /// <summary>
        /// Transform which holds the graphical features of this object. This transform will be smoothed when desynchronizations occur.
        /// </summary>
        private Transform _graphicalObject;
        /// <summary>
        /// Sets GraphicalObject.
        /// </summary>
        /// <param name="value"></param>
        public void SetGraphicalObject(Transform value)
        {
            _graphicalObject = value;
            _networkBehaviour.transform.SetTransformOffsets(value, ref _graphicalInstantiatedOffsetPosition, ref _graphicalInstantiatedOffsetRotation);
        }
        /// <summary>
        /// NetworkBehaviour which is using this object.
        /// </summary>
        private NetworkBehaviour _networkBehaviour;
        /// <summary>
        /// How far the transform must travel in a single update to cause a teleport rather than smoothing. Using 0f will teleport every update.
        /// </summary>
        private float _teleportThreshold = 1f;
        /// <summary>
        /// How far in the past to keep the graphical object when owner.
        /// </summary>
        private byte _interpolation = 1;
        /// <summary>
        /// Sets the interpolation value to use when the owner of this object.
        /// </summary>
        /// <param name="value"></param>
        public void SetInterpolation(byte value) => _interpolation = value;
        #endregion

        #region Private.
        /// <summary>
        /// World position before transform was predicted or reset.
        /// </summary>
        private Vector3 _graphicalStartPosition;
        /// <summary>
        /// World rotation before transform was predicted or reset.
        /// </summary>
        private Quaternion _graphicalStartRotation;
        /// <summary>
        /// GraphicalObject position difference from the PredictedObject when this is initialized.
        /// </summary>
        private Vector3 _graphicalInstantiatedOffsetPosition;
        /// <summary>
        /// How quickly to move towards TargetPosition.
        /// </summary>
        private float _positionMoveRate = -2;
        /// <summary>
        /// GraphicalObject rotation difference from the PredictedObject when this is initialized.
        /// </summary>
        private Quaternion _graphicalInstantiatedOffsetRotation;
        /// <summary>
        /// How quickly to move towards TargetRotation.
        /// </summary>
        private float _rotationMoveRate = -2;
        /// <summary>
        /// True if OnPreTick was received this frame.
        /// </summary>
        private bool _preTickReceived;
        /// <summary>
        /// True to move towards position goals.
        /// </summary>
        private bool _smoothPosition;
        /// <summary>
        /// True to move towards rotation goals.
        /// </summary>
        private bool _smoothRotation;
        #endregion

        /// <summary>
        /// Initializes this script for use.
        /// </summary>
        public void Initialize(NetworkBehaviour nb, Vector3 instantiatedOffsetPosition, Quaternion instantiatedOffsetRotation, Transform graphicalObject
              , bool smoothPosition, bool smoothRotation, byte interpolation, float teleportThreshold)
        {
            _networkBehaviour = nb;
            _graphicalInstantiatedOffsetPosition = instantiatedOffsetPosition;
            _graphicalInstantiatedOffsetRotation = instantiatedOffsetRotation;
            _graphicalObject = graphicalObject;

            _smoothPosition = smoothPosition;
            _smoothRotation = smoothRotation;

            _interpolation = interpolation;
            _teleportThreshold = teleportThreshold;
        }

        /// <summary>
        /// Called every frame.
        /// </summary>
        public void ManualUpdate()
        {
            MoveToTarget();
        }

        /// <summary>
        /// Called when the TimeManager invokes OnPreTick.
        /// </summary>
        public void OnPreTick()
        {
            if (CanSmooth())
            {
                _preTickReceived = true;
                /* Only snap to destination if interpolation is 1.
                 * This ensures the graphics will be at the proper location
                 * before the next movement rates are calculated. */
                if (_interpolation == 1)
                    ResetGraphicalToInstantiatedProperties(true, true);

                SetGraphicalPreviousProperties();
            }
        }

        public void OnPostTick()
        {
            if (CanSmooth() && _preTickReceived)
            {
                _preTickReceived = false;
                ResetGraphicalToPreviousProperties();
                SetGraphicalMoveRates();
            }
        }

        /// <summary>
        /// Returns if prediction can be used on this rigidbody.
        /// </summary>
        /// <returns></returns>
        private bool CanSmooth()
        {
            if (_interpolation == 0)
                return false;
            //Only owner needs smoothing.
            if (!_networkBehaviour.IsOwner && !_networkBehaviour.IsHost)
                return false;

            return true;
        }

        /// <summary>
        /// Moves transform to target values.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void MoveToTarget()
        {
            //Not set, meaning movement doesnt need to happen or completed.
            if (_positionMoveRate == -2f && _rotationMoveRate == -2f)
                return;

            Vector3 posGoal = GetGraphicalGoalPosition();
            Quaternion rotGoal = GetGraphicalGoalRotation();

            /* Only try to update properties if they have a valid move rate.
             * Properties may have 0f move rate if they did not change. */
            Transform t = _graphicalObject;
            float delta = Time.deltaTime;

            //Position.
            if (SmoothPosition())
            {
                if (_positionMoveRate == -1f)
                    ResetGraphicalToInstantiatedProperties(true, false);
                else if (_positionMoveRate > 0f)
                    t.position = Vector3.MoveTowards(t.position, posGoal, _positionMoveRate * delta);
            }

            //Rotation.
            if (SmoothRotation())
            {
                if (_rotationMoveRate == -1f)
                    ResetGraphicalToInstantiatedProperties(false, true);
                else if (_rotationMoveRate > 0f)
                    t.rotation = Quaternion.RotateTowards(t.rotation, rotGoal, _rotationMoveRate * delta);
            }

            if (GraphicalObjectMatches(posGoal, rotGoal))
            {
                _positionMoveRate = -2f;
                _rotationMoveRate = -2f;
            } 
        }

        /// <summary>
        /// Returns if this transform matches arguments.
        /// </summary>
        /// <returns></returns>
        private bool GraphicalObjectMatches(Vector3 position, Quaternion rotation)
        {
            bool positionMatches = (!_smoothPosition || (_graphicalObject.position == position));
            bool rotationMatches = (!_smoothRotation || (_graphicalObject.rotation == rotation));
            return (positionMatches && rotationMatches);
        }

        /// <summary>
        /// True to smooth position. When false the graphicalObjects property will not be updated.
        /// </summary>
        /// <returns></returns>
        private bool SmoothPosition() => (_smoothPosition && (_networkBehaviour.IsOwner || _networkBehaviour.IsHost));
        /// <summary>
        /// True to smooth rotation. When false the graphicalObjects property will not be updated.
        /// </summary>
        /// <returns></returns>
        private bool SmoothRotation() => (_smoothRotation && (_networkBehaviour.IsOwner || _networkBehaviour.IsHost));

        /// <summary>
        /// Sets Position and Rotation move rates to reach Target datas.
        /// </summary>
        private void SetGraphicalMoveRates()
        {
            float delta = ((float)_networkBehaviour.TimeManager.TickDelta * _interpolation);

            float distance;
            distance = Vector3.Distance(_graphicalObject.position, GetGraphicalGoalPosition());
            //If qualifies for teleporting.
            if (_teleportThreshold != -1f && distance >= _teleportThreshold)
            {
                _positionMoveRate = -1f;
                _rotationMoveRate = -1f;
            }
            //Smoothing.
            else
            {
                _positionMoveRate = (distance / delta);
                distance = Quaternion.Angle(_graphicalObject.rotation, GetGraphicalGoalRotation());
                if (distance > 0f)
                    _rotationMoveRate = (distance / delta);
            }
        }

        /// <summary>
        /// Gets a goal position for the graphical object.
        /// </summary>
        /// <returns></returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private Vector3 GetGraphicalGoalPosition()
        {
            if (SmoothPosition())
                return (_networkBehaviour.transform.position + _graphicalInstantiatedOffsetPosition);
            else
                return _graphicalObject.position;
        }

        /// <summary>
        /// Gets a goal rotation for the graphical object.
        /// </summary>
        /// <returns></returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private Quaternion GetGraphicalGoalRotation()
        {
            if (SmoothRotation())
                return (_graphicalInstantiatedOffsetRotation * _networkBehaviour.transform.rotation);
            else
                return _graphicalObject.rotation;
        }
        /// <summary>
        /// Caches the graphical object' current position and rotation.
        /// </summary>
        private void SetGraphicalPreviousProperties()
        {
            _graphicalStartPosition = _graphicalObject.position;
            _graphicalStartRotation = _graphicalObject.rotation;
        }

        /// <summary>
        /// Resets the graphical object to cached position and rotation of the transform.
        /// </summary>
        private void ResetGraphicalToPreviousProperties()
        {
            _graphicalObject.SetPositionAndRotation(_graphicalStartPosition, _graphicalStartRotation);
        }

        /// <summary>
        /// Resets the graphical object to it's transform offsets during instantiation.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void ResetGraphicalToInstantiatedProperties(bool position, bool rotation)
        {
            if (position)
                _graphicalObject.position = GetGraphicalGoalPosition();
            if (rotation)
                _graphicalObject.rotation = GetGraphicalGoalRotation();
        }

    }


}