using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using LimbParams = Barotrauma.RagdollParams.LimbParams;
using ColliderParams = Barotrauma.RagdollParams.ColliderParams;

namespace Barotrauma
{
    class PosInfo
    {
        public Vector2 Position
        {
            get;
            private set;
        }

        public float? Rotation
        {
            get;
            private set;
        }

        public Vector2 LinearVelocity
        {
            get;
            private set;
        }

        public float? AngularVelocity
        {
            get;
            private set;
        }

        public readonly float Timestamp;
        public readonly UInt16 ID;


        public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, float time)
            : this(pos, rotation, linearVelocity, angularVelocity, 0, time)
        {
        }

        public PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID)
            : this(pos, rotation, linearVelocity, angularVelocity, ID, 0.0f)
        {
        }

        protected PosInfo(Vector2 pos, float? rotation, Vector2 linearVelocity, float? angularVelocity, UInt16 ID, float time)
        {
            Position = pos;
            Rotation = rotation;
            LinearVelocity = linearVelocity;
            AngularVelocity = angularVelocity;

            this.ID = ID;            
            Timestamp = time;
        }

        public void TransformOutToInside(Submarine submarine)
        {
            //transform outside coordinates to in-sub coordinates
            Position -= ConvertUnits.ToSimUnits(submarine.Position);         
        }

        public void TransformInToOutside()
        {
            var sub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(Position));
            if (sub != null)
            {
                Position += ConvertUnits.ToSimUnits(sub.Position);
            }         
        }

        public void Translate(Vector2 posAmount,float rotationAmount)
        {
            Position += posAmount; Rotation += rotationAmount;
        }
    }

    partial class PhysicsBody
    {
        public enum Shape
        {
            Circle, Rectangle, Capsule, HorizontalCapsule
        };

        public const float MinDensity = 0.01f;
        public const float DefaultAngularDamping = 5.0f;

        private static readonly List<PhysicsBody> list = new List<PhysicsBody>();
        public static List<PhysicsBody> List
        {
            get { return list; }
        }

        protected Vector2 prevPosition;
        protected float prevRotation;

        protected Vector2? targetPosition;
        protected float? targetRotation;

        private Vector2 drawPosition;
        private float drawRotation;
        
        public bool Removed
        {
            get;
            private set;
        }

        public Vector2 LastSentPosition
        {
            get;
            private set;
        }

        private Shape bodyShape;
        public float Height { get; private set; }
        public float Width { get; private set; }
        public float Radius { get; private set; }
        
        private readonly float density;

        //the direction the item is facing (for example, a gun has to be 
        //flipped horizontally if the Character holding it turns around)
        float dir = 1.0f;

        private Vector2 drawOffset;
        private float rotationOffset;

        private float lastProcessedNetworkState;

        public float? PositionSmoothingFactor;

        public Shape BodyShape
        {
            get { return bodyShape; }
        }

        public Vector2? TargetPosition
        {
            get { return targetPosition; }
            set
            {
                if (value == null)
                {
                    targetPosition = null;
                }
                else
                {
                    if (!IsValidValue(value.Value, "target position", -1e5f, 1e5f)) return;
                    targetPosition = new Vector2(
                        MathHelper.Clamp(((Vector2)value).X, -10000.0f, 10000.0f),
                        MathHelper.Clamp(((Vector2)value).Y, -10000.0f, 10000.0f));
                }
            }
        }
        
        public float? TargetRotation
        {
            get { return targetRotation; }
            set 
            {
                if (value == null)
                {
                    targetRotation = null;
                }
                else
                {
                    if (!IsValidValue(value.Value, "target rotation")) return;
                    targetRotation = value;
                }
 
            }
        }

        public Vector2 DrawPosition
        {
            get { return Submarine == null ? drawPosition : drawPosition + Submarine.DrawPosition; }
        }

        public float DrawRotation
        {
            get { return drawRotation; }
        }

        public Submarine Submarine;

        public float Dir
        {
            get { return dir; }
            set { dir = value; }
        }

        private bool isEnabled = true;
        private bool isPhysEnabled = true;

        public bool Enabled
        {
            get { return isEnabled; }
            set
            {
                isEnabled = value;
                try
                {
                    FarseerBody.Enabled = isEnabled && isPhysEnabled;
                }
                catch (Exception e)
                {
                    DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = " + value + " (" + isPhysEnabled + ")", e);
                    if (UserData != null)
                    {
                        DebugConsole.NewMessage("PhysicsBody UserData: " + UserData.GetType(), Color.Red);
                    }
                    if (GameMain.World.ContactManager == null)
                    {
                        DebugConsole.NewMessage("ContactManager is null!", Color.Red);
                    }
                    else if (GameMain.World.ContactManager.BroadPhase == null)
                    {
                        DebugConsole.NewMessage("Broadphase is null!", Color.Red);
                    }
                    if (FarseerBody.FixtureList == null)
                    {
                        DebugConsole.NewMessage("FixtureList is null!", Color.Red);
                    }
                    if (UserData is Entity entity)
                    {
                        DebugConsole.NewMessage("Entity \"" + entity + "\" removed!", Color.Red);
                    }
                }
            }
        }

        public bool PhysEnabled
        {
            get { return FarseerBody.Enabled; }
            set
            {
                isPhysEnabled = value;
                if (Enabled)
                {
                    FarseerBody.Enabled = value;
                }
            }
        }

        public Vector2 SimPosition
        {
            get { return FarseerBody.Position; }
        }

        public Vector2 Position
        {
            get { return ConvertUnits.ToDisplayUnits(FarseerBody.Position); }
        }

        /// <summary>
        /// Offset of the DrawPosition from the Position (i.e. how much the interpolated draw position is offset from the "actual position"). In display units.
        /// </summary>
        public Vector2 DrawPositionOffset => DrawPosition - Position;

        public Vector2 PrevPosition
        {
            get { return prevPosition; }
        }

        public float Rotation
        {
            get { return FarseerBody.Rotation; }
        }

        /// <summary>
        /// Takes flipping (Dir) into account.
        /// </summary>
        public float TransformedRotation => TransformRotation(Rotation, Dir);

        public float TransformRotation(float rotation) => TransformRotation(rotation, dir);

        public static float TransformRotation(float rot, float dir) => dir < 0 ? rot - MathHelper.Pi : rot;

        public Vector2 LinearVelocity
        {
            get { return FarseerBody.LinearVelocity; }
            set
            {
                if (!IsValidValue(value, "velocity", -1000.0f, 1000.0f)) return;
                FarseerBody.LinearVelocity = value;
            }
        }

        public float AngularVelocity
        {
            get { return FarseerBody.AngularVelocity; }
            set
            {
                if (!IsValidValue(value, "angular velocity", -1000f, 1000f)) return;
                FarseerBody.AngularVelocity = value;
            }
        }

        public float Mass
        {
            get { return FarseerBody.Mass; }
        }

        public float Density
        {
            get { return density; }
        }

        public Body FarseerBody { get; private set; }

        public object UserData
        {
            get { return FarseerBody.UserData; }
            set { FarseerBody.UserData = value; }
        }

        public float Friction
        {
            set { FarseerBody.Friction = value; }
        }

        public BodyType BodyType
        {
            get { return FarseerBody.BodyType; }
            set { FarseerBody.BodyType = value; }
        }

        private Category _collisionCategories;

        public Category CollisionCategories
        {
            set
            {
                _collisionCategories = value;
                FarseerBody.CollisionCategories = value;
            }
            get
            {
                return _collisionCategories;
            }
        }

        private Category _collidesWith;
        public Category CollidesWith
        {
            set
            {
                _collidesWith = value;
                FarseerBody.CollidesWith = value;
            }
            get
            {
                return _collidesWith;
            }
        }

        /// <summary>
        /// Ignore rotation calls for the rest of this and the next update. Automatically disabled after that. Used for temporarily suppressing the SmoothRotate calls to prevent conflicting or unitentionally amplified rotations.
        /// </summary>
        public bool SuppressSmoothRotationCalls
        {
            get => _suppressSmoothRotationCalls;
            set
            {
                _suppressSmoothRotationCalls = value;
                smoothRotationSuppressionCounter = 0;
            }
        }

        private bool _suppressSmoothRotationCalls;
        private int smoothRotationSuppressionCounter;

        public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { }
        public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { }
        public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { }

        public PhysicsBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true)
        {
            density = Math.Max(density, MinDensity);
            CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts);
            LastSentPosition = FarseerBody.Position;
            list.Add(this);
        }

        public PhysicsBody(Body farseerBody)
        {
            FarseerBody = farseerBody;
            if (FarseerBody.UserData == null) { FarseerBody.UserData = this; }
            LastSentPosition = FarseerBody.Position;
            list.Add(this);
        }

        public PhysicsBody(ColliderParams colliderParams, Vector2 position, bool findNewContacts = true)
        {
            float radius = ConvertUnits.ToSimUnits(colliderParams.Radius) * colliderParams.Ragdoll.LimbScale;
            float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale;
            float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale;
            density = Physics.NeutralDensity;
            CreateBody(width, height, radius, density, colliderParams.BodyType,
                Physics.CollisionCharacter,
                Physics.CollisionWall | Physics.CollisionLevel, 
                findNewContacts);
            FarseerBody.AngularDamping = DefaultAngularDamping;
            FarseerBody.FixedRotation = true;
            FarseerBody.Friction = 0.05f;
            FarseerBody.Restitution = 0.05f;
            SetTransformIgnoreContacts(position, 0.0f);
            LastSentPosition = position;
            list.Add(this);
        }

        public PhysicsBody(LimbParams limbParams, Vector2 position, bool findNewContacts = true)
        {
            float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale;
            float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale;
            float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale;
            density = Math.Max(limbParams.Density, MinDensity);

            Category collisionCategory =  Physics.CollisionCharacter;
            Category collidesWith =  Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
            if (limbParams.IgnoreCollisions)
            {
                collisionCategory = Category.None;
                collidesWith = Category.None;
            }
            CreateBody(width, height, radius, density, BodyType.Dynamic,
                collisionCategory: collisionCategory,
                collidesWith: collidesWith,
                findNewContacts: findNewContacts);
            FarseerBody.Friction = limbParams.Friction;
            FarseerBody.Restitution = limbParams.Restitution;
            FarseerBody.AngularDamping = limbParams.AngularDamping;
            FarseerBody.UserData = this;
            _collisionCategories = collisionCategory;
            _collidesWith = collidesWith;
            SetTransformIgnoreContacts(position, 0.0f);
            LastSentPosition = position;
            list.Add(this);
        }
        
        public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f, float? forceDensity = null, Category collisionCategory = Physics.CollisionItem, Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform, bool findNewContacts = true)
        {
            float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale;
            float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale;
            float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale;
            density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity);
            Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType);
            if (element.GetAttributeBool("ignorecollision", false))
            {
                _collisionCategories = Category.None;
                _collidesWith = Category.None;
            }
            else
            {
                _collisionCategories = collisionCategory;
                _collidesWith = collidesWith;
            }
            CreateBody(width, height, radius, density, bodyType, _collisionCategories, _collidesWith, findNewContacts);
            FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f);
            FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f);
            FarseerBody.GravityScale = element.GetAttributeFloat("gravityscale", 1.0f);
            FarseerBody.UserData = this;
            SetTransformIgnoreContacts(position, 0.0f);
            LastSentPosition = position;      
            list.Add(this);
        }

        private void CreateBody(float width, float height, float radius, float density, BodyType bodyType, Category collisionCategory, Category collidesWith, bool findNewContacts = true)
        {
            if (IsValidShape(radius, height, width))
            {
                bodyShape = DefineBodyShape(radius, width, height);
                switch (bodyShape)
                {
                    case Shape.Capsule:
                        FarseerBody = GameMain.World.CreateCapsule(height, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts); ;
                        break;
                    case Shape.HorizontalCapsule:
                        FarseerBody = GameMain.World.CreateCapsuleHorizontal(width, radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts);
                        break;
                    case Shape.Circle:
                        FarseerBody = GameMain.World.CreateCircle(radius, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts);
                        break;
                    case Shape.Rectangle:
                        FarseerBody = GameMain.World.CreateRectangle(width, height, density, bodyType: bodyType, collisionCategory: collisionCategory, collidesWith: collidesWith, findNewContacts: findNewContacts);
                        break;
                    default:
                        throw new NotImplementedException(bodyShape.ToString());
                }
            }
            else
            {
                DebugConsole.ThrowError("Invalid physics body dimensions (width: " + width + ", height: " + height + ", radius: " + radius + ")");
            }
            Width = width;
            Height = height;
            Radius = radius;
            _collisionCategories = collisionCategory;
            _collidesWith = collidesWith;
        }

        /// <summary>
        /// Returns the farthest point towards the forward of the body.
        /// For capsules and circles, the front is at the top.
        /// For horizontal capsules, the front is at the right-most point.
        /// For rectangles, the front is either at the top or at the right, depending on which one of the two is greater: width or height.
        /// The rotation is in radians.
        /// </summary>
        public Vector2 GetLocalFront(float? spritesheetRotation = null)
        {
            Vector2 pos;
            switch (bodyShape)
            {
                case Shape.Capsule:
                    pos = new Vector2(0.0f, Height / 2 + Radius);
                    break;
                case Shape.HorizontalCapsule:
                    pos = new Vector2(Width / 2 + Radius, 0.0f);
                    break;
                case Shape.Circle:
                    pos = new Vector2(0.0f, Radius);
                    break;
                case Shape.Rectangle:
                    pos = Height > Width ? new Vector2(0, Height / 2) : new Vector2(Width / 2, 0);
                    break;
                default:
                    throw new NotImplementedException();
            }
            return spritesheetRotation.HasValue ? Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation.Value)) : pos;
        }

        public float GetMaxExtent()
        {
            switch (bodyShape)
            {
                case Shape.Capsule:
                    return Height / 2 + Radius;
                case Shape.HorizontalCapsule:
                    return Width / 2 + Radius;
                case Shape.Circle:
                    return Radius;
                case Shape.Rectangle:
                    return new Vector2(Width * 0.5f, Height * 0.5f).Length();
                default:
                    throw new NotImplementedException();
            }
        }

        public Vector2 GetSize()
        {
            switch (bodyShape)
            {
                case Shape.Capsule:
                    return new Vector2(Radius * 2, Height + Radius * 2);
                case Shape.HorizontalCapsule:
                    return new Vector2(Width + Radius * 2, Radius * 2);
                case Shape.Circle:
                    return new Vector2(Radius * 2);
                case Shape.Rectangle:
                    return new Vector2(Width, Height);
                default:
                    throw new NotImplementedException();
            }
        }

        public void SetSize(Vector2 size)
        {
            switch (bodyShape)
            {
                case Shape.Capsule:
                    Radius = Math.Max(size.X / 2, 0);
                    Height = Math.Max(size.Y - size.X, 0);
                    Width = 0;
                    break;
                case Shape.HorizontalCapsule:
                    Radius = Math.Max(size.Y / 2, 0);
                    Width = Math.Max(size.X - size.Y, 0);
                    Height = 0;
                    break;
                case Shape.Circle:
                    Radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0);
                    Width = 0;
                    Height = 0;
                    break;
                case Shape.Rectangle:
                    Width = Math.Max(size.X, 0);
                    Height = Math.Max(size.Y, 0);
                    Radius = 0;
                    break;
                default:
                    throw new NotImplementedException();
            }
        }
        
        public bool IsValidValue(float value, string valueName, float minValue = float.MinValue, float maxValue = float.MaxValue)
        {
            if (!MathUtils.IsValid(value) || value < minValue || value > maxValue)
            {
                string userData = UserData == null ? "null" : UserData.ToString();
                string errorMsg =
                    "Attempted to apply invalid " + valueName +
                    " to a physics body (userdata: " + userData +
                    "), value: " + value;
                if (GameMain.NetworkMember != null)
                {
                    errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
                }
                errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace();

                if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
                GameAnalyticsManager.AddErrorEventOnce(
                    "PhysicsBody.SetPosition:InvalidPosition" + userData,
                    GameAnalyticsManager.ErrorSeverity.Error,
                    errorMsg);
                return false;
            }
            return true;
        }

        private bool IsValidValue(Vector2 value, string valueName, float minValue = float.MinValue, float maxValue = float.MaxValue)
        {
            if (!MathUtils.IsValid(value) ||
                (value.X < minValue || value.Y < minValue) ||
                (value.X > maxValue || value.Y > maxValue))
            {
                string userData = UserData == null ? "null" : UserData.ToString();
                string errorMsg =
                    "Attempted to apply invalid " + valueName +
                    " to a physics body (userdata: " + userData +
                    "), value: " + value;
                if (GameMain.NetworkMember != null)
                {
                    errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
                }
                errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace();

                if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
                GameAnalyticsManager.AddErrorEventOnce(
                    "PhysicsBody.SetPosition:InvalidPosition" + userData,
                    GameAnalyticsManager.ErrorSeverity.Error,
                    errorMsg);
                return false;
            }
            return true;
        }

        public void ResetDynamics()
        {
            FarseerBody.ResetDynamics();
        }

        public void ApplyLinearImpulse(Vector2 impulse)
        {
            if (!IsValidValue(impulse / FarseerBody.Mass, "new velocity", -1000f, 1000f)) return;
            if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;

            FarseerBody.ApplyLinearImpulse(impulse);
        }

        /// <summary>
        /// Apply an impulse to the body without increasing it's velocity above a specific limit.
        /// </summary>
        public void ApplyLinearImpulse(Vector2 impulse, float maxVelocity)
        {
            if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
            if (!IsValidValue(maxVelocity, "max velocity")) return;
            
            Vector2 velocityAddition = impulse / Mass;
            Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition;
            float newSpeedSqr = newVelocity.LengthSquared();
            if (newSpeedSqr > maxVelocity * maxVelocity)
            {
                newVelocity = newVelocity.ClampLength(maxVelocity);
            }

            if (!IsValidValue((newVelocity - FarseerBody.LinearVelocity), "new velocity", -1000.0f, 1000.0f)) return;

            FarseerBody.ApplyLinearImpulse((newVelocity - FarseerBody.LinearVelocity) * Mass);
        }

        public void ApplyLinearImpulse(Vector2 impulse, Vector2 point)
        {
            if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
            if (!IsValidValue(point, "point")) return;
            if (!IsValidValue(impulse / FarseerBody.Mass, "new velocity", -1000.0f, 1000.0f)) return;
            FarseerBody.ApplyLinearImpulse(impulse, point);
        }

        /// <summary>
        /// Apply an impulse to the body without increasing it's velocity above a specific limit.
        /// </summary>
        public void ApplyLinearImpulse(Vector2 impulse, Vector2 point, float maxVelocity)
        {
            if (!IsValidValue(impulse, "impulse", -1e10f, 1e10f)) return;
            if (!IsValidValue(point, "point")) return;
            if (!IsValidValue(maxVelocity, "max velocity")) return;

            Vector2 velocityAddition = impulse / Mass;
            Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition;
            float newSpeedSqr = newVelocity.LengthSquared();
            if (newSpeedSqr > maxVelocity * maxVelocity)
            {
                newVelocity = newVelocity.ClampLength(maxVelocity);
            }

            if (!IsValidValue((newVelocity - FarseerBody.LinearVelocity), "new velocity", -1000.0f, 1000.0f)) return;

            FarseerBody.ApplyLinearImpulse((newVelocity - FarseerBody.LinearVelocity) * Mass, point);
            FarseerBody.AngularVelocity = MathHelper.Clamp(
                FarseerBody.AngularVelocity, 
                -NetConfig.MaxPhysicsBodyAngularVelocity, 
                NetConfig.MaxPhysicsBodyAngularVelocity);
        }

        public void ApplyForce(Vector2 force, float maxVelocity = NetConfig.MaxPhysicsBodyVelocity)
        {
            if (!IsValidValue(maxVelocity, "max velocity")) { return; }

            if (force.LengthSquared() < 0.01f) { return; }

            Vector2 velocityAddition = force / Mass * (float)Timing.Step;
            Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition;

            float newSpeedSqr = newVelocity.LengthSquared();
            if (newSpeedSqr > maxVelocity * maxVelocity)            
            {
                float currSpeed = FarseerBody.LinearVelocity.Length();
                //limit velocity if the force is increasing the velocity in the current or new direction of travel
                // = we don't want to limit it if the force is slowing down the movement: if a projectile is moving at 50 m/s
                //   and we apply a force that can only impart a maximum velocity of 1 m/s, we don't want to clamp that projectile to 1 m/s!

                //force is acting in the same direction as the current velocity
                // -> the maximum allowed change is the difference from current to max speed
                if (Vector2.Dot(FarseerBody.LinearVelocity, force) > 0.0f)
                {
                    force = velocityAddition.ClampLength(Math.Max(maxVelocity - currSpeed, 0)) * Mass / (float)Timing.Step;
                }
                //the new velocity will be in the direction of the force
                // -> make sure it's not too much, maximum allowed change is current speed plus the max speed
                else if (Vector2.Dot(newVelocity, force) > 0.0f)
                {
                    force = velocityAddition.ClampLength(maxVelocity + currSpeed) * Mass / (float)Timing.Step;
                }
            }

            if (!IsValidValue(force, "clamped force", -1e10f, 1e10f)) { return; }
            FarseerBody.ApplyForce(force);
        }

        public void ApplyForce(Vector2 force, Vector2 point)
        {
            if (!IsValidValue(force, "force", -1e10f, 1e10f)) { return; }
            if (!IsValidValue(point, "point")) { return; }
            FarseerBody.ApplyForce(force, point);
        }

        public void ApplyTorque(float torque)
        {
            if (!IsValidValue(torque, "torque")) { return; }
            FarseerBody.ApplyTorque(torque);
        }

        public bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform = true)
        {
            System.Diagnostics.Debug.Assert(MathUtils.IsValid(simPosition));
            System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f);
            System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f);

            if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; }
            if (!IsValidValue(rotation, "rotation")) { return false; }

            FarseerBody.SetTransform(simPosition, rotation);
            if (setPrevTransform) { SetPrevTransform(simPosition, rotation); }
            return true;
        }

        public bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform = true)
        {
            System.Diagnostics.Debug.Assert(MathUtils.IsValid(simPosition));
            System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f);
            System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f);

            if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; }
            if (!IsValidValue(rotation, "rotation")) { return false; }

            FarseerBody.SetTransformIgnoreContacts(ref simPosition, rotation);
            if (setPrevTransform) { SetPrevTransform(simPosition, rotation); }
            return true;
        }

        public void SetPrevTransform(Vector2 simPosition, float rotation)
        {
#if DEBUG
            if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; }
            if (!IsValidValue(rotation, "rotation")) { return; }
#endif
            prevPosition = simPosition;
            prevRotation = rotation;
        }

        public void MoveToTargetPosition(bool lerp = true)
        {
            if (targetPosition == null) { return; }

            if (lerp)
            {
                if (Vector2.DistanceSquared((Vector2)targetPosition, FarseerBody.Position) < 10.0f * 10.0f)
                {
                    drawOffset = -((Vector2)targetPosition - (FarseerBody.Position + drawOffset));
                    prevPosition = (Vector2)targetPosition;
                }
                else
                {
                    drawOffset = Vector2.Zero;
                }
                if (targetRotation.HasValue)
                {
                    rotationOffset = -MathUtils.GetShortestAngle(FarseerBody.Rotation + rotationOffset, targetRotation.Value);
                }
            }

            SetTransformIgnoreContacts((Vector2)targetPosition, targetRotation == null ? FarseerBody.Rotation : (float)targetRotation);
            targetPosition = null;
            targetRotation = null;
        }

        public void MoveToPos(Vector2 simPosition, float force, Vector2? pullPos = null)
        {
            if (pullPos == null) { pullPos = FarseerBody.Position; }

            if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; }
            if (!IsValidValue(force, "force")) { return; }

            Vector2 vel = FarseerBody.LinearVelocity;
            Vector2 deltaPos = simPosition - (Vector2)pullPos;
            if (deltaPos.LengthSquared() > 100.0f * 100.0f)
            {
#if DEBUG
                DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
                return;
            }
            deltaPos *= force;
            ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos);
        }

        /// <summary>
        /// Applies buoyancy, drag and angular drag caused by water
        /// </summary>
        public void ApplyWaterForces()
        {
            //buoyancy
            Vector2 buoyancy = new Vector2(0, Mass * 9.6f);

            Vector2 dragForce = Vector2.Zero;

            float speedSqr = LinearVelocity.LengthSquared();
            if (speedSqr > 0.00001f)
            {
                //drag
                float speed = (float)Math.Sqrt(speedSqr);
                Vector2 velDir = LinearVelocity / speed;

                float vel = speed * 2.0f;
                float drag = vel * vel * Math.Max(Height + Radius * 2, Height);
                dragForce = Math.Min(drag, Mass * 500.0f) * -velDir;
            }

            ApplyForce(dragForce + buoyancy);
            ApplyTorque(FarseerBody.AngularVelocity * FarseerBody.Mass * -0.08f);
        }

        public void Update()
        {
            if (drawOffset.LengthSquared() < 0.01f)
            {
                PositionSmoothingFactor = null;
            }
            drawOffset = NetConfig.InterpolateSimPositionError(drawOffset, PositionSmoothingFactor);
            rotationOffset = NetConfig.InterpolateRotationError(rotationOffset);
            if (SuppressSmoothRotationCalls)
            {
                if (smoothRotationSuppressionCounter > 0)
                {
                    SuppressSmoothRotationCalls = false;
                }
                else
                {
                    smoothRotationSuppressionCounter++;
                }
            }
        }

        public void UpdateDrawPosition(bool interpolate = true)
        {
            if (interpolate)
            {
                drawPosition = Timing.Interpolate(prevPosition, FarseerBody.Position);
                drawPosition = ConvertUnits.ToDisplayUnits(drawPosition + drawOffset);
                drawRotation = Timing.InterpolateRotation(prevRotation, FarseerBody.Rotation) + rotationOffset;
            }
            else
            {
                prevPosition = FarseerBody.Position;
                drawPosition = ConvertUnits.ToDisplayUnits(FarseerBody.Position);
                drawRotation = prevRotation = FarseerBody.Rotation;
                drawOffset = Vector2.Zero;
                rotationOffset = 0.0f;
            }
        }
        
        public void CorrectPosition<T>(List<T> positionBuffer,
            out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity) where T : PosInfo
        {
            newVelocity = LinearVelocity;
            newPosition = SimPosition;
            newRotation = Rotation;
            newAngularVelocity = AngularVelocity;

            while (positionBuffer.Count > 0 && positionBuffer[0].Timestamp < lastProcessedNetworkState)
            {
                positionBuffer.RemoveAt(0);
            }

            if (positionBuffer.Count == 0) { return; }

            lastProcessedNetworkState = positionBuffer[0].Timestamp;

            newVelocity = positionBuffer[0].LinearVelocity;
            newPosition = positionBuffer[0].Position;
            newRotation = positionBuffer[0].Rotation ?? Rotation;
            newAngularVelocity = positionBuffer[0].AngularVelocity ?? AngularVelocity;
            
            positionBuffer.RemoveAt(0);            
        }

        /// <summary>
        /// Rotate the body towards the target rotation in the "shortest direction", taking into account the current angular velocity to prevent overshooting.
        /// </summary>
        /// <param name="targetRotation">Desired rotation in radians</param>
        /// <param name="force">How fast the body should be rotated. Does not represent any real unit, you may want to experiment with different values to get the desired effect.</param>
        /// <param name="wrapAngle">Should the angles be wrapped. Set to false if it makes a difference whether the angle of the body is 0.0f or 360.0f.</param>
        public void SmoothRotate(float targetRotation, float force = 10.0f, bool wrapAngle = true)
        {
            if (SuppressSmoothRotationCalls) { return; }
            float nextAngle = FarseerBody.Rotation + FarseerBody.AngularVelocity * (float)Timing.Step;
            float angle = wrapAngle ? 
                MathUtils.GetShortestAngle(nextAngle, targetRotation) : 
                MathHelper.Clamp(targetRotation - nextAngle, -MathHelper.Pi, MathHelper.Pi);
            float torque = angle * 60.0f * (force / 100.0f);

            if (FarseerBody.BodyType == BodyType.Kinematic)
            {
                if (!IsValidValue(torque, "torque")) { return; }
                FarseerBody.AngularVelocity = torque;
            }
            else
            {
                ApplyTorque(FarseerBody.Mass * torque);
            }
        }

        /// <summary>
        /// Wraps the angle so it has "has the same number of revolutions" as this body, i.e. that the angles are at most 180 degrees apart.
        /// For example, if the angle of this body was 720, an angle of 5 would get wrapped to 725.
        /// </summary>
        public float WrapAngleToSameNumberOfRevolutions(float angle)
        {
            if (float.IsInfinity(angle)) { return angle; }
            while (Rotation - angle > MathHelper.TwoPi)
            {
                angle += MathHelper.TwoPi;
            }
            while (Rotation - angle < -MathHelper.TwoPi)
            {
                angle -= MathHelper.TwoPi;
            }
            return angle;
        }
        
        public void Remove()
        {
            list.Remove(this);
            GameMain.World.Remove(FarseerBody);

            Removed = true;

            DisposeProjSpecific();
        }

        public static void RemoveAll()
        {
            for (int i = list.Count - 1; i >= 0; i--)
            {
                list[i].Remove();
            }
            System.Diagnostics.Debug.Assert(list.Count == 0);
        }

        public static bool IsValidShape(float radius, float height, float width) => radius > 0 || (height > 0 && width > 0);

        public static Shape DefineBodyShape(float radius, float width, float height)
        {
            Shape bodyShape;
            if (width <= 0 && height <= 0 && radius > 0)
            {
                bodyShape = Shape.Circle;
            }
            else if (radius > 0)
            {
                if (width > height)
                {
                    bodyShape = Shape.HorizontalCapsule;
                }
                else
                {
                    bodyShape = Shape.Capsule;
                }
            }
            else
            {
                bodyShape = Shape.Rectangle;
            }
            return bodyShape;
        }

        partial void DisposeProjSpecific();

    }
}
