using Barotrauma.SpriteDeformations;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Xml.Linq;

namespace Barotrauma
{

    class BackgroundCreature : ISteerable
    {
        const float MaxDepth = 10000.0f;

        const float CheckWallsInterval = 5.0f;

        public bool Visible;

        public readonly BackgroundCreaturePrefab Prefab;

        private readonly List<SpriteDeformation> uniqueSpriteDeformations = new List<SpriteDeformation>();
        private readonly List<SpriteDeformation> spriteDeformations = new List<SpriteDeformation>();
        private readonly List<SpriteDeformation> lightSpriteDeformations = new List<SpriteDeformation>();
        
        private Vector2 position;

        private Vector3 velocity;

        public float Depth { get; private set; }

        private float alpha = 1.0f;
        
        private readonly SteeringManager steeringManager;

        private float checkWallsTimer, flashTimer;

        private float wanderZPhase;
        private Vector2 obstacleDiff;
        private float obstacleDist;

        public Swarm Swarm;

        Vector2 drawPosition;

        private bool flippedHorizontally;

        public Vector2[,] CurrentSpriteDeformation
        {
            get;
            private set;
        }
        public Vector2[,] CurrentLightSpriteDeformation
        {
            get;
            private set;
        }

        public Vector2 SimPosition
        {
            get { return FarseerPhysics.ConvertUnits.ToSimUnits(position); }
        }

        public Vector2 WorldPosition
        {
            get { return position; }
        }

        public Vector2 Velocity
        {
            get { return new Vector2(velocity.X, velocity.Y); }
        }

        public Vector2 Steering
        {
            get;
            set;
        }

        public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position)
        {
            this.Prefab = prefab;
            this.position = position;

            drawPosition = position;

            steeringManager = new SteeringManager(this);

            velocity = new Vector3(
                Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly),
                Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly),
                Rand.Range(0.0f, prefab.WanderZAmount, Rand.RandSync.ClientOnly));

            Depth = Rand.Range(prefab.MinDepth, prefab.MaxDepth, Rand.RandSync.ClientOnly);

            checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval, Rand.RandSync.ClientOnly);

            foreach (var subElement in prefab.Config.Elements())
            {
                List<SpriteDeformation> deformationList = null;
                switch (subElement.Name.ToString().ToLowerInvariant())
                {
                    case "deformablesprite":
                        deformationList = spriteDeformations;
                        break;
                    case "deformablelightsprite":
                        deformationList = lightSpriteDeformations;
                        break;
                    default:
                        continue;
                }
                int j = 0;
                foreach (XElement animationElement in subElement.Elements())
                {
                    SpriteDeformation deformation = null;
                    int sync = animationElement.GetAttributeInt("sync", -1);
                    if (sync > -1)
                    {
                        string typeName = animationElement.GetAttributeString("type", "").ToLowerInvariant();
                        deformation = uniqueSpriteDeformations.Find(d => d.TypeName == typeName && d.Sync == sync);
                    }
                    if (deformation == null)
                    {
                        deformation = SpriteDeformation.Load(animationElement, prefab.Name);
                        if (deformation != null)
                        {
                            deformation.Params = Prefab.SpriteDeformations[j].Params;
                            uniqueSpriteDeformations.Add(deformation);
                            if (prefab.DeformableSprite != null)
                            {
                                if (deformation.Resolution.X > prefab.DeformableSprite.Subdivisions.X ||
                                    deformation.Resolution.Y > prefab.DeformableSprite.Subdivisions.Y)
                                {
                                    DebugConsole.AddWarning(
                                        $"Potential error in background creature {Prefab.Identifier}: deformation {deformation.GetType()} has a larger resolution ({deformation.Resolution})"+
                                        $" than the amount of subdivisions on the deformable sprite ({prefab.DeformableSprite.Subdivisions}). Should the sprite be subdivided further to make full use of the deformation?",
                                        contentPackage: Prefab.ContentPackage);
                                }
                            }

                            j++;
                        }
                    }
                    if (deformation != null)
                    {
                        deformationList.Add(deformation);
                    }
                }
            }

            flashTimer = Rand.Range(0.0f, prefab.FlashInterval, Rand.RandSync.Unsynced);
        }
        
        public void Update(float deltaTime)
        {
            position += new Vector2(velocity.X, velocity.Y) * deltaTime;
            Depth = MathHelper.Clamp(Depth + velocity.Z * deltaTime, Prefab.MinDepth, Prefab.MaxDepth);

            if (Prefab.FlashInterval > 0.0f)
            {
                flashTimer -= deltaTime;
                if (flashTimer > 0.0f)
                {
                    alpha = 0.0f;
                }
                else
                {
                    //value goes from 0 to 1 and back to 0 during the flash
                    alpha = (float)Math.Sin(-flashTimer / Prefab.FlashDuration * MathHelper.Pi) * PerlinNoise.GetPerlin((float)Timing.TotalTime, (float)Timing.TotalTime * 0.5f);
                    if (flashTimer < -Prefab.FlashDuration)
                    {
                        flashTimer = Prefab.FlashInterval;
                    }
                }
            }

            checkWallsTimer -= deltaTime;
            if (checkWallsTimer <= 0.0f && Level.Loaded != null)
            {
                checkWallsTimer = CheckWallsInterval;

                obstacleDiff = Vector2.Zero;
                if (position.Y > Level.Loaded.Size.Y)
                {
                    obstacleDiff = Vector2.UnitY;
                }
                else if (position.Y < 0.0f)
                {
                    obstacleDiff = -Vector2.UnitY;
                }
                else if (position.X < 0.0f)
                {
                    obstacleDiff = -Vector2.UnitX;
                }
                else if (position.X > Level.Loaded.Size.X)
                {
                    obstacleDiff = Vector2.UnitX;
                }
                else
                {
                    var cells = Level.Loaded.GetCells(position, 1);
                    if (cells.Count > 0)
                    {
                        int cellCount = 0;
                        foreach (Voronoi2.VoronoiCell cell in cells)
                        {
                            Vector2 diff = cell.Center - position;
                            if (diff.LengthSquared() > 5000.0f * 5000.0f) { continue; }
                            obstacleDiff += diff;
                            cellCount++;
                        }
                        if (cellCount > 0)
                        {
                            obstacleDiff /= cellCount;
                            obstacleDist = obstacleDiff.Length();
                            obstacleDiff = Vector2.Normalize(obstacleDiff);
                        }
                    }
                }
            }

            if (Swarm != null)
            {
                Vector2 midPoint = Swarm.MidPoint();
                float midPointDist = Vector2.Distance(SimPosition, midPoint) * 100.0f;
                if (midPointDist > Swarm.MaxDistance)
                {
                    steeringManager.SteeringSeek(midPoint, ((midPointDist / Swarm.MaxDistance) - 1.0f) * Prefab.Speed);
                }
                steeringManager.SteeringManual(deltaTime, Swarm.AvgVelocity() * Swarm.Cohesion);
            }

            if (Prefab.WanderAmount > 0.0f)
            {
                steeringManager.SteeringWander(Prefab.Speed);
            }

            if (obstacleDiff != Vector2.Zero)
            {
                steeringManager.SteeringManual(deltaTime, -obstacleDiff * (1.0f - obstacleDist / 5000.0f) * Prefab.Speed);
            }

            steeringManager.Update(Prefab.Speed);

            if (Prefab.WanderZAmount > 0.0f)
            {
                wanderZPhase += Rand.Range(-Prefab.WanderZAmount, Prefab.WanderZAmount);
                velocity.Z = (float)Math.Sin(wanderZPhase) * Prefab.Speed;
            }

            velocity = Vector3.Lerp(velocity, new Vector3(Steering.X, Steering.Y, velocity.Z), deltaTime);

            //only flip if there's some horizontal movement speed (10% of the creature's speed)
            //otherwise a creature swimming roughly up/down can flip around very frequently when the horizontal speed fluctuates around 0
            if (Math.Abs(velocity.X) > Prefab.Speed * 0.1f)
            {
                flippedHorizontally = !Prefab.DisableFlipping && velocity.X < 0.0f;
            }
            UpdateDeformations(deltaTime, flippedHorizontally);            
        }

        public void DrawLightSprite(SpriteBatch spriteBatch, Camera cam)
        {
            Draw(spriteBatch, cam, Prefab.LightSprite, Prefab.DeformableLightSprite, CurrentLightSpriteDeformation, Color.White * alpha);
        }

        public void Draw(SpriteBatch spriteBatch, Camera cam)
        {
            Color color =
                Prefab.FadeOut ?
                Color.Lerp(Color.White, Level.Loaded.BackgroundColor, Depth / Prefab.FadeOutDepth) * alpha :
                Color.White * alpha;

            Draw(spriteBatch,
                cam,
                Prefab.Sprite,
                Prefab.DeformableSprite,
                CurrentSpriteDeformation, 
                color);
        }

        private void Draw(SpriteBatch spriteBatch, Camera cam, Sprite sprite, DeformableSprite deformableSprite, Vector2[,] currentSpriteDeformation, Color color)
        {
            if (sprite == null && deformableSprite == null) { return; }
            if (color.A == 0) { return; }

            float rotation = 0.0f;
            if (!Prefab.DisableRotation)
            {
                rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y));
                if (flippedHorizontally) { rotation -= MathHelper.Pi; }
            }

            drawPosition = GetDrawPosition(cam);

            float scale = GetScale();
            sprite?.Draw(spriteBatch,
                new Vector2(drawPosition.X, -drawPosition.Y),
                color,
                rotation, 
                scale,
                flippedHorizontally ? SpriteEffects.FlipHorizontally : SpriteEffects.None,
                Math.Min(Depth / MaxDepth, 1.0f));

            if (deformableSprite != null)
            {
                if (currentSpriteDeformation != null)
                {
                    deformableSprite.Deform(currentSpriteDeformation);
                }
                else
                {
                    deformableSprite.Reset();
                }
                deformableSprite?.Draw(cam,
                    new Vector3(drawPosition.X, drawPosition.Y, Math.Min(Depth / 10000.0f, 1.0f)),
                    deformableSprite.Origin,
                    rotation,
                    Vector2.One * scale,
                    color,
                    mirror: flippedHorizontally);
            }
        }

        public Vector2 GetDrawPosition(Camera cam)
        {
            Vector2 drawPosition = WorldPosition;
            if (Depth >= 0)
            {
                Vector2 camOffset = drawPosition - cam.WorldViewCenter;
                drawPosition -= camOffset * Depth / MaxDepth;
            }
            return drawPosition;
        }

        public float GetScale()
        {
            return Math.Max(1.0f - Depth / MaxDepth, 0.05f) * Prefab.Scale;
        }

        public Rectangle GetExtents(Camera cam)
        {
            Vector2 min = GetDrawPosition(cam);
            Vector2 max = min;

            float scale = GetScale();
            GetSpriteExtents(Prefab.Sprite, ref min, ref max);
            GetSpriteExtents(Prefab.LightSprite, ref min, ref max);
            GetSpriteExtents(Prefab.DeformableSprite?.Sprite, ref min, ref max);
            GetSpriteExtents(Prefab.DeformableLightSprite?.Sprite, ref min, ref max);

            return new Rectangle(min.ToPoint(), (max - min).ToPoint());

            void GetSpriteExtents(Sprite sprite, ref Vector2 min, ref Vector2 max)
            {
                if (sprite == null) { return; }
                min.X = Math.Min(min.X, min.X - sprite.size.X * sprite.RelativeOrigin.X * scale);
                min.Y = Math.Min(min.Y, min.Y - sprite.size.Y * sprite.RelativeOrigin.Y * scale);
                max.X = Math.Max(max.X, max.X + sprite.size.X * (1.0f - sprite.RelativeOrigin.X) * scale);
                max.Y = Math.Max(max.Y, max.Y + sprite.size.Y * (1.0f - sprite.RelativeOrigin.Y) * scale);
            }
        }

        private void UpdateDeformations(float deltaTime, bool flippedHorizontally)
        {
            foreach (SpriteDeformation deformation in uniqueSpriteDeformations)
            {
                deformation.Update(deltaTime);
            }
            if (spriteDeformations.Count > 0)
            {
                CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, Prefab.DeformableSprite.Size, 
                    flippedHorizontally: flippedHorizontally);
            }
            if (lightSpriteDeformations.Count > 0)
            {
                CurrentLightSpriteDeformation = SpriteDeformation.GetDeformation(lightSpriteDeformations, Prefab.DeformableLightSprite.Size, 
                    flippedHorizontally: flippedHorizontally);
            }
        }
    }

    class Swarm
    {
        public List<BackgroundCreature> Members;

        public readonly float MaxDistance;
        public readonly float Cohesion;

        public Vector2 MidPoint()
        {
            if (Members.Count == 0) return Vector2.Zero;

            Vector2 midPoint = Vector2.Zero;

            foreach (BackgroundCreature member in Members)
            {
                midPoint += member.SimPosition;
            }

            midPoint /= Members.Count;

            return midPoint;
        }

        public Vector2 AvgVelocity()
        {
            if (Members.Count == 0) return Vector2.Zero;

            Vector2 avgVel = Vector2.Zero;
            foreach (BackgroundCreature member in Members)
            {
                avgVel += member.Velocity;
            }
            avgVel /= Members.Count;
            return avgVel;
        }

        public Swarm(List<BackgroundCreature> members, float maxDistance, float cohesion)
        {
            Members = members;
            MaxDistance = maxDistance;
            Cohesion = cohesion;
            foreach (BackgroundCreature bgSprite in members)
            {
                bgSprite.Swarm = this;
            }
        }
    }
}
