using Barotrauma.Extensions;
using FarseerPhysics;
using FarseerPhysics.Common;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;

namespace Barotrauma.Particles
{
    class Particle
    {
        private ParticlePrefab prefab;
        
        private string debugName = "Particle (uninitialized)";

        public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull);
        public OnChangeHullHandler OnChangeHull;

        public OnChangeHullHandler OnCollision;

        private Vector2 position;
        private Vector2 prevPosition;

        private Vector2 velocity;

        private float rotation;
        private float prevRotation;

        private float angularVelocity;

        private float collisionIgnoreTimer = 0;

        private Vector2 size;
        private Vector2 sizeChange;

        private Color color;
        private bool changeColor;
        private bool UseMiddleColor;

        private int spriteIndex;

        private float totalLifeTime;
        private float lifeTime;

        private float startDelay;

        private Vector2 velocityChange;
        private Vector2 velocityChangeWater;

        private Vector2 drawPosition;
        private float drawRotation;

        private Vector2 colliderRadius;
        
        private Hull currentHull;

        private List<Gap> hullGaps;

        private bool hasSubEmitters;
        private readonly List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();

        private float animState;
        private int animFrame;

        private float collisionUpdateTimer;

        private bool changesSize;

        public bool HighQualityCollisionDetection;

        public Vector4 ColorMultiplier;

        public float VelocityChangeMultiplier;

        public ParticleDrawOrder DrawOrder { get; private set; }

        public ParticlePrefab.DrawTargetType DrawTarget
        {
            get { return prefab.DrawTarget; }        
        }

        public ParticleBlendState BlendState
        {
            get { return prefab.BlendState; }
        }

        public float StartDelay
        {
            get { return startDelay; }
            set { startDelay = Math.Max(value, 0.0f); }
        }
        
        public Vector2 Size
        {
            get { return size; }
            set { size = value; }
        }
        
        public Hull CurrentHull
        {
            get { return currentHull; }
        }

        public ParticlePrefab Prefab
        {
            get { return prefab; }
        }

        public override string ToString()
        {
            return debugName;
        }
        public void Init(ParticlePrefab prefab, Vector2 spawnPosition, Vector2 speed, float spawnRotation, Hull hullGuess = null, ParticleDrawOrder drawOrder = ParticleDrawOrder.Default, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple<Vector2, Vector2> tracerPoints = null)
        {
            this.prefab = prefab;

            System.Diagnostics.Debug.Assert(position.IsValid(), "Attempted to spawn a particle at an invalid position.");

#if DEBUG
            debugName = $"Particle ({prefab.Name})";
#else
            //don't instantiate new string objects in release builds
            debugName = prefab.Name;
#endif
            spriteIndex = Rand.Int(prefab.Sprites.Count);

            animState = 0;
            animFrame = 0;

            currentHull = prefab.CanEnterSubs ? Hull.FindHull(spawnPosition, hullGuess) : null;

            size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f);

            if (tracerPoints != null)
            {
                size = new Vector2(Vector2.Distance(tracerPoints.Item1, tracerPoints.Item2), size.Y);
                spawnPosition = (tracerPoints.Item1 + tracerPoints.Item2) / 2;
            }

            RefreshColliderSize();

            sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f);
            changesSize = !sizeChange.NearlyEquals(Vector2.Zero);

            if (currentHull?.Submarine != null)
            {
                //convert to the sub's coordinate space
                spawnPosition -= currentHull.Submarine.Position;
            }
            
            position = prevPosition = drawPosition = spawnPosition;
            velocity = MathUtils.IsValid(speed) ? speed : Vector2.Zero;

            rotation = spawnRotation + Rand.Range(prefab.StartRotationMinRad, prefab.StartRotationMaxRad);    
            prevRotation = spawnRotation;

            angularVelocity = Rand.Range(prefab.AngularVelocityMinRad, prefab.AngularVelocityMaxRad);
          
            if (prefab.LifeTimeMin <= 0.0f)
            {
                totalLifeTime = prefab.LifeTime * lifeTimeMultiplier;
                lifeTime = prefab.LifeTime * lifeTimeMultiplier;
            }
            else
            {
                totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime) * lifeTimeMultiplier;
                lifeTime = totalLifeTime * lifeTimeMultiplier;
            }

            startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax);

            UseMiddleColor = prefab.UseMiddleColor;
            color = prefab.StartColor;
            changeColor = prefab.StartColor != prefab.EndColor;
            ColorMultiplier = Vector4.One;

            velocityChange = prefab.VelocityChangeDisplay;
            velocityChangeWater = prefab.VelocityChangeWaterDisplay;

            HighQualityCollisionDetection = false;

            VelocityChangeMultiplier = 1.0f;

            OnChangeHull = null;
            OnCollision = null;

            subEmitters.Clear();
            hasSubEmitters = false;
            foreach (ParticleEmitterPrefab emitterPrefab in prefab.SubEmitters)
            {
                subEmitters.Add(new ParticleEmitter(emitterPrefab));
                hasSubEmitters = true;
            }

            if (prefab.UseCollision)
            {
                hullGaps = currentHull == null ? new List<Gap>() : currentHull.ConnectedGaps;
            }

            if (prefab.RotateToDirection)
            {
                this.rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y));

                prevRotation = spawnRotation;
            }

            DrawOrder = drawOrder;

            this.collisionIgnoreTimer = collisionIgnoreTimer;
        }
        
        public enum UpdateResult
        {
            Normal,
            Delete
        }

        public UpdateResult Update(float deltaTime)
        {
            if (startDelay > 0.0f)
            {
                startDelay -= deltaTime;
                return UpdateResult.Normal;
            }

            prevPosition = position;
            prevRotation = rotation;

            //over 3 times faster than position += velocity * deltatime
            position.X += velocity.X * deltaTime;
            position.Y += velocity.Y * deltaTime;

            if (prefab.RotateToDirection)
            {
                if (velocityChange != Vector2.Zero || angularVelocity != 0.0f)
                {
                    Vector2 relativeVel = velocity;
                    if (currentHull?.Submarine != null)
                    {
                        relativeVel -= ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity);
                    }
                    rotation = MathUtils.VectorToAngle(new Vector2(relativeVel.X, -relativeVel.Y));
                }
            }
            else
            {
                rotation += angularVelocity * deltaTime;
            }

            bool inWater = (currentHull == null || (currentHull.Submarine != null && position.Y < currentHull.Surface));
            if (inWater)
            {
                velocity.X += velocityChangeWater.X * VelocityChangeMultiplier * deltaTime;
                velocity.Y += velocityChangeWater.Y * VelocityChangeMultiplier * deltaTime;
                if (prefab.WaterDrag > 0.0f)
                {
                    ApplyDrag(prefab.WaterDrag, deltaTime);
                }
            }
            else
            {
                velocity.X += velocityChange.X * VelocityChangeMultiplier * deltaTime;
                velocity.Y += velocityChange.Y * VelocityChangeMultiplier * deltaTime; 
                if (prefab.Drag > 0.0f)
                {
                    ApplyDrag(prefab.Drag, deltaTime);
                }
            }

            if (changesSize)
            {
                size.X += sizeChange.X * deltaTime;
                size.Y += sizeChange.Y * deltaTime; 
                RefreshColliderSize();
            }

            if (UseMiddleColor)
            {
                if (lifeTime > totalLifeTime * 0.5f)
                {
                    color = Color.Lerp(prefab.MiddleColor, prefab.StartColor, (lifeTime / totalLifeTime - 0.5f) * 2.0f);
                }
                else
                {
                    color = Color.Lerp(prefab.EndColor, prefab.MiddleColor, lifeTime / totalLifeTime * 2.0f);
                }
            }
            else
            {
                if (changeColor)
                {
                    color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / totalLifeTime);
                }
            }

            if (prefab.Sprites[spriteIndex] is SpriteSheet)
            {
                animState += deltaTime;
                int frameCount = ((SpriteSheet)prefab.Sprites[spriteIndex]).FrameCount;
                if (prefab.LoopAnim)
                {
                    animFrame = (int)(Math.Floor(animState / prefab.AnimDuration * frameCount) % frameCount);
                }
                else
                {
                    animFrame = (int)Math.Min(Math.Floor(animState / prefab.AnimDuration * frameCount), frameCount - 1);
                }
            }
            
            lifeTime -= deltaTime;
            if (lifeTime <= 0.0f || color.A <= 0 || size.X <= 0.0f || size.Y <= 0.0f) { return UpdateResult.Delete; }

            if (hasSubEmitters)
            {
                foreach (ParticleEmitter emitter in subEmitters)
                {
                    emitter.Emit(deltaTime, position, currentHull, particleRotation: rotation, 
                        sizeMultiplier: emitter.Prefab.Properties.CopyParentParticleScale ? Math.Max(size.X, size.Y) : 1.0f);
                }
            }

            if (collisionIgnoreTimer > 0f)
            {
                collisionIgnoreTimer -= deltaTime;
                if (collisionIgnoreTimer <= 0f) { currentHull ??= Hull.FindHull(position, guess: currentHull, useWorldCoordinates: false); }
                return UpdateResult.Normal;
            }
            if (!prefab.UseCollision) { return UpdateResult.Normal; }

            if (HighQualityCollisionDetection)
            {
                return CollisionUpdate();
            }
            else
            {
                collisionUpdateTimer -= deltaTime;
                if (collisionUpdateTimer <= 0.0f)
                {
                    //more frequent collision updates if the particle is moving fast
                    collisionUpdateTimer = 0.5f - Math.Min((Math.Abs(velocity.X) + Math.Abs(velocity.Y)) * 0.01f, 0.45f);
                    return CollisionUpdate();
                }
            }

            return UpdateResult.Normal;
        }

        private UpdateResult CollisionUpdate()
        {
            if (currentHull == null)
            {
                Hull collidedHull = Hull.FindHull(position, useWorldCoordinates: true);
                if (collidedHull != null)
                {
                    if (prefab.DeleteOnCollision) { return UpdateResult.Delete; }
                    OnWallCollisionOutside(collidedHull);
                }
            }
            else
            {
                Rectangle hullRect = currentHull.Rect;
                Vector2 collisionNormal = Vector2.Zero;
                if (velocity.Y < 0.0f && position.Y - colliderRadius.Y < hullRect.Y - hullRect.Height)
                {
                    collisionNormal = new Vector2(0.0f, 1.0f);
                }
                else if (velocity.Y > 0.0f && position.Y + colliderRadius.Y > hullRect.Y)
                {
                    collisionNormal = new Vector2(0.0f, -1.0f);
                }

                if (collisionNormal != Vector2.Zero)
                {
                    bool gapFound = false;
                    foreach (Gap gap in hullGaps)
                    {
                        if (gap.Open <= 0.9f || gap.IsHorizontal) { continue; }

                        if (gap.Rect.X > position.X || gap.Rect.Right < position.X) { continue; }
                        float hullCenterY = currentHull.Rect.Y - currentHull.Rect.Height / 2;
                        int gapDir = Math.Sign(gap.Rect.Y - hullCenterY);
                        if (Math.Sign(velocity.Y) != gapDir || Math.Sign(position.Y - hullCenterY) != gapDir) { continue; }

                        gapFound = true;
                        break;
                    }

                    if (prefab.DeleteOnCollision && !gapFound)
                    {
                        OnCollision?.Invoke(position, currentHull);
                        return UpdateResult.Delete; 
                    }
                    handleCollision(gapFound, collisionNormal);
                }

                collisionNormal = Vector2.Zero;
                if (velocity.X < 0.0f && position.X - colliderRadius.X < hullRect.X)
                {
                    collisionNormal = new Vector2(1.0f, 0.0f);
                }
                else if (velocity.X > 0.0f && position.X + colliderRadius.X > hullRect.Right)
                {
                    collisionNormal = new Vector2(-1.0f, 0.0f);
                }

                if (collisionNormal != Vector2.Zero)
                {
                    bool gapFound = false;
                    foreach (Gap gap in hullGaps)
                    {
                        if (gap.Open <= 0.9f || !gap.IsHorizontal) { continue; }

                        if (gap.Rect.Y < position.Y || gap.WorldRect.Y - gap.Rect.Height > position.Y) { continue; }
                        int gapDir = Math.Sign(gap.Rect.Center.X - currentHull.Rect.Center.X);
                        if (Math.Sign(velocity.X) != gapDir || Math.Sign(position.X - currentHull.Rect.Center.X) != gapDir) { continue; }

                        gapFound = true;
                        break;
                    }
                    if (prefab.DeleteOnCollision && !gapFound)
                    {
                        OnCollision?.Invoke(position, currentHull);
                        return UpdateResult.Delete; 
                    }
                    handleCollision(gapFound, collisionNormal);
                }

                void handleCollision(bool gapFound, Vector2 collisionNormal)
                {
                    if (!gapFound)
                    {
                        OnWallCollisionInside(currentHull, collisionNormal);
                    }
                    else
                    {
                        Hull newHull = Hull.FindHull(position, currentHull, useWorldCoordinates: false);
                        if (newHull != currentHull)
                        {
                            currentHull = newHull;
                            hullGaps = currentHull == null ? new List<Gap>() : currentHull.ConnectedGaps;
                            OnChangeHull?.Invoke(position, currentHull);
                        }
                    }
                }
            }

            return UpdateResult.Normal;
        }

        private void RefreshColliderSize()
        {
            if (!prefab.UseCollision) { return; }
            colliderRadius = new Vector2(prefab.CollisionRadius);
            if (!prefab.InvariantCollisionSize) { colliderRadius *= size; }
        }

        private void ApplyDrag(float dragCoefficient, float deltaTime)
        {
            Vector2 newVel = velocity;
            float speed = velocity.Length();

            if (speed < 0.01f) { return; }
            
            newVel /= speed;

            float drag = speed * speed * dragCoefficient * 0.01f * deltaTime;
            if (drag > speed)
            {
                newVel = Vector2.Zero;
            }
            else
            {
                speed -= drag;
                newVel *= speed;
            }

            velocity = newVel;        
        }


        private void OnWallCollisionInside(Hull prevHull, Vector2 collisionNormal)
        {
            if (prevHull == null) { return; }

            Rectangle prevHullRect = prevHull.Rect;

            if (Math.Abs(collisionNormal.X) > Math.Abs(collisionNormal.Y))
            {
                if (collisionNormal.X > 0.0f)
                {
                    position.X = Math.Max(position.X, prevHullRect.X + colliderRadius.X);
                }
                else
                {
                    position.X = Math.Min(position.X, prevHullRect.Right - colliderRadius.X);
                }
                velocity.X = Math.Sign(collisionNormal.X) * Math.Abs(velocity.X) * prefab.Restitution;
                velocity.Y *= (1.0f - prefab.Friction);
            }
            else
            {
                if (collisionNormal.Y > 0.0f)
                {
                    position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + colliderRadius.Y);
                }
                else
                {
                    position.Y = Math.Min(position.Y, prevHullRect.Y - colliderRadius.Y);

                }
                velocity.X *= (1.0f - prefab.Friction);
                velocity.Y = Math.Sign(collisionNormal.Y) * Math.Abs(velocity.Y) * prefab.Restitution;
            }

            OnCollision?.Invoke(position, currentHull);
        }


        private void OnWallCollisionOutside(Hull collisionHull)
        {
            Rectangle hullRect = collisionHull.Rect;

            Vector2 center = new Vector2(hullRect.X + hullRect.Width / 2, hullRect.Y - hullRect.Height / 2);

            if (position.Y < center.Y)
            {
                position.Y = hullRect.Y - hullRect.Height - colliderRadius.Y;
                velocity.X *= (1.0f - prefab.Friction);
                velocity.Y = -velocity.Y * prefab.Restitution;
            }
            else if (position.Y > center.Y)
            {
                position.Y = hullRect.Y + colliderRadius.Y;
                velocity.X *= (1.0f - prefab.Friction);
                velocity.Y = -velocity.Y * prefab.Restitution;
            }

            if (position.X < center.X)
            {
                position.X = hullRect.X - colliderRadius.X;
                velocity.X = -velocity.X * prefab.Restitution;
                velocity.Y *= (1.0f - prefab.Friction);
            }
            else if (position.X > center.X)
            {
                position.X = hullRect.X + hullRect.Width + colliderRadius.X;
                velocity.X = -velocity.X * prefab.Restitution;
                velocity.Y *= (1.0f - prefab.Friction);
            }

            OnCollision?.Invoke(position, currentHull);

            velocity *= prefab.Restitution;
        }

        public void UpdateDrawPos()
        {
            drawPosition = Timing.Interpolate(prevPosition, position);
            drawRotation = Timing.Interpolate(prevRotation, rotation);
        }

        public void Draw(SpriteBatch spriteBatch)
        {
            if (startDelay > 0.0f) { return; }

            Vector2 drawSize = size;
            if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime)
            {
                drawSize *= MathUtils.SmoothStep((totalLifeTime - lifeTime) / prefab.GrowTime);
            }

            Color currColor = new Color(color.ToVector4() * ColorMultiplier);

            Vector2 drawPos = drawPosition;
            if (currentHull?.Submarine is Submarine sub)
            {
                drawPos += sub.DrawPosition;
            }

            drawPos = new Vector2(drawPos.X, -drawPos.Y);
            if (prefab.Sprites[spriteIndex] is SpriteSheet sheet)
            {
                sheet.Draw(
                    spriteBatch, animFrame,
                    drawPos,
                    currColor * (currColor.A / 255.0f),
                    prefab.Sprites[spriteIndex].Origin, drawRotation,
                    drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
            }
            else
            {
                prefab.Sprites[spriteIndex].Draw(spriteBatch,
                    drawPos,
                    currColor * (currColor.A / 255.0f),
                    prefab.Sprites[spriteIndex].Origin, drawRotation,
                    drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
            }

            /*if (GameMain.DebugDraw && prefab.UseCollision)
            {
                GUI.DrawLine(spriteBatch, 
                    drawPos - Vector2.UnitX * colliderRadius.X, 
                    drawPos + Vector2.UnitX * colliderRadius.X,
                    Color.Gray);
                GUI.DrawLine(spriteBatch, 
                    drawPos - Vector2.UnitY * colliderRadius.Y, 
                    drawPos + Vector2.UnitY * colliderRadius.Y, 
                    Color.Gray);
            }*/
        }
    }
}
