using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Barotrauma.Extensions;

namespace Barotrauma.Items.Components
{
    partial class TriggerComponent : ItemComponent 
    {
        [Editable, Serialize(0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
        public float Force { get; set; }

        [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The maximum amount of directional force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
        public Vector2 DirectionalForce { get; set; }

        [Editable, Serialize(false, IsPropertySaveable.Yes, $"If true, {nameof(DirectionalForce)} is relative to the angle between the target and the item, Similar to {nameof(Force)}.\nIf false, it always pushes in the same direction, with respect to the item's rotation.", alwaysUseInstanceValues: true)]
        public bool RelativeDirectionalForce { get; set; }

        [Editable, Serialize(true, IsPropertySaveable.Yes, "If false, no vertical force will be applied.", alwaysUseInstanceValues: true)]
        public bool VerticalForce { get; set; }

        [Editable, Serialize(true, IsPropertySaveable.Yes, "If false, no horizontal force will be applied.", alwaysUseInstanceValues: true)]
        public bool HorizontalForce { get; set; }

        [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)]
        public bool DistanceBasedForce { get; set; }

        [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
        public bool ForceFluctuation { get; set; }

        [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)]
        public float ForceFluctuationStrength
        {
            get
            {
                return forceFluctuationStrength;
            }
            set
            {
                forceFluctuationStrength = Math.Clamp(value, 0.0f, 1.0f);
            }
        }
        [Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)]
        public float ForceFluctuationFrequency
        {
            get
            {
                return forceFluctuationFrequency;
            }
            set
            {
                forceFluctuationFrequency = Math.Max(value, 0.01f);
            }
        }
        [Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)]
        public float ForceFluctuationInterval
        {
            get
            {
                return forceFluctuationInterval;
            }
            set
            {
                forceFluctuationInterval = Math.Max(value, 0.01f);
            }
        }

        public PhysicsBody PhysicsBody { get; private set; }

        private float radius;
        [Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
        public float Radius
        {
            get => radius;
            
            set
            {
                if (radius == value) { return; }
                radius = value;
                if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
            }
        }

        private float width;
        [Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
        public float Width
        {
            get => width;

            set
            {
                if (width == value) { return; }
                width = value;
                if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
            }
        }

        private float height;
        [Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
        public float Height
        {
            get => height;

            set
            {
                if (height == value) { return; }
                height = value;
                if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
            }
        }

        private float currentRadius, currentWidth, currentHeight;

        private Vector2 bodyOffset;
        [Editable, Serialize("0,0", IsPropertySaveable.Yes)]
        public Vector2 BodyOffset
        {
            get => bodyOffset;

            set
            {
                if (bodyOffset == value) { return; }
                bodyOffset = value;
                if (PhysicsBody != null) { SetPhysicsBodyPosition(); }
            }
        }

        private float RadiusInDisplayUnits { get; set; }
        private bool TriggeredOnce { get; set; }
        private float CurrentForceFluctuation { get; set; } = 1.0f;
        public bool TriggerActive { get; private set; }
        private float ForceFluctuationTimer { get; set; }
        private static float TimeInLevel
        {
            get
            {
                return GameMain.GameSession?.RoundDuration ?? 0.0f;
            }
        }

        [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
        public bool ApplyEffectsToCharactersInsideSub { get; set; }

        [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
        public bool MoveOutsideSub { get; set; }

        public override bool IsActive 
        {
            get => base.IsActive;
            set
            {
                bool wasActive = base.IsActive;

                base.IsActive = value;
                if (!IsActive)
                {
                    TriggerActive = false;
                    triggerers.Clear();
                }
                else if (!wasActive && PhysicsBody?.FarseerBody != null)
                {
                    //when the trigger becomes active, we need to check which entities are inside it
                    ContactEdge ce = PhysicsBody.FarseerBody.ContactList;
                    while (ce != null && ce.Contact != null)
                    {
                        if (ce.Contact.Enabled)
                        {
                            var thisFixture = ce.Contact.FixtureA.Body == PhysicsBody.FarseerBody ? ce.Contact.FixtureA : ce.Contact.FixtureB;
                            var otherFixture = ce.Contact.FixtureA.Body == PhysicsBody.FarseerBody ? ce.Contact.FixtureB : ce.Contact.FixtureA;
                            OnCollision(thisFixture, otherFixture, ce.Contact);
                        }
                        ce = ce.Next;
                    }
                }
            }
        }

        private readonly LevelTrigger.TriggererType triggeredBy;
        private readonly Identifier triggerSpeciesOrGroup;
        private readonly PropertyConditional.LogicalComparison conditionals;
        
        private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
        private readonly bool triggerOnce;
        private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
        /// <summary>
        /// Effects applied to entities inside the trigger
        /// </summary>
        private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
        /// <summary>
        /// Attacks applied to entities inside the trigger
        /// </summary>
        private readonly List<Attack> attacks = new List<Attack>();

        private float forceFluctuationStrength;
        private float forceFluctuationFrequency;
        private float forceFluctuationInterval;

        public TriggerComponent(Item item, ContentXElement element) : base(item, element)
        {
            string triggeredByString = element.GetAttributeString("triggeredby", "Character");
            if (!Enum.TryParse(triggeredByString, out triggeredBy))
            {
                Identifier speciesOrGroup = triggeredByString.ToIdentifier();
                if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup)))
                {
                    triggerSpeciesOrGroup = speciesOrGroup;
                    triggeredBy = LevelTrigger.TriggererType.Character;
                }
                else
                {
                    DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByString}\" is not a valid triggerer type.",
                        contentPackage: element.ContentPackage);
                }
            }
            triggerOnce = element.GetAttributeBool("triggeronce", false);
            string parentDebugName = $"TriggerComponent in {item.Name}";
            foreach (var subElement in element.Elements())
            {
                switch (subElement.Name.ToString().ToLowerInvariant())
                {
                    case "statuseffect":
                        LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName);
                        break;
                    case "attack":
                    case "damage":
                        LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks);
                        break;
                }
            }
            conditionals = PropertyConditional.LoadConditionals(element);
            IsActive = true;
        }

        public override void OnItemLoaded()
        {
            RefreshPhysicsBodySize();
        }

        private void RefreshPhysicsBodySize()
        {
            PhysicsBody?.Remove();

            currentWidth = ConvertUnits.ToSimUnits(Width * item.Scale);
            currentHeight = ConvertUnits.ToSimUnits(Height * item.Scale);
            if (currentWidth > 0 && currentHeight > 0)
            {
                PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
                {
                    UserData = item
                };
            }
            else
            {
                currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f);
                PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
                {
                    UserData = item
                };
            }

            SetPhysicsBodyPosition();
            PhysicsBody.FarseerBody.SetIsSensor(originalElement.GetAttributeBool("sensor", true));
            PhysicsBody.FarseerBody.OnCollision += OnCollision;
            PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
            RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius);
        }

        public void SetPhysicsBodyPosition(bool ignoreContacts = true)
        {
            if (PhysicsBody == null) { return; }

            Vector2 offset = ConvertUnits.ToSimUnits(BodyOffset * item.Scale);
            if (item.FlippedX)
            {
                offset.X = -offset.X;
            }
            if (item.FlippedY)
            {
                offset.Y = -offset.Y;
            }
            if (!MathUtils.NearlyEqual(item.RotationRad, 0))
            {
                Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
                offset = Vector2.Transform(offset, transform);
            }
            if (ignoreContacts)
            {
                PhysicsBody.SetTransformIgnoreContacts(item.SimPosition + offset, -item.RotationRad);
            }
            else
            {
                PhysicsBody.SetTransform(item.SimPosition + offset, -item.RotationRad);
            }
            PhysicsBody.UpdateDrawPosition();
        }

        public override void FlipX(bool relativeToSub)
        {
            SetPhysicsBodyPosition();
        }
        public override void FlipY(bool relativeToSub)
        {
            SetPhysicsBodyPosition();
        }

        public override void OnMapLoaded()
        {
            base.OnMapLoaded();
            SetPhysicsBodyPosition(true);
            PhysicsBody.Submarine = item.Submarine;
        }

        private bool OnCollision(Fixture sender, Fixture other, Contact contact)
        {
            if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; }
            if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
            triggerers.Add(entity);
            return true;
        }

        private void OnSeparation(Fixture sender, Fixture other, Contact contact)
        {
            if (LevelTrigger.GetEntity(other) is not Entity entity)
            {
                return;
            }
            if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity))
            {
                triggerers.Remove(entity);
                return;
            }
            if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity))
            {
                return;
            }
            triggerers.Remove(entity);
        }

        public override void Update(float deltaTime, Camera cam)
        {
            if (item.Submarine != null && MoveOutsideSub)
            {                
                item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation);
                item.CurrentHull = null;
                item.Submarine = null;
                SetPhysicsBodyPosition();
                PhysicsBody.Submarine = item.Submarine;
            }
            else if (item.body is { BodyType: BodyType.Dynamic })
            {
                SetPhysicsBodyPosition();
                PhysicsBody.Submarine = item.Submarine;
            }

            LevelTrigger.RemoveInActiveTriggerers(PhysicsBody, triggerers);

            if (triggerOnce)
            {
                if (TriggeredOnce) { return; }
                if (triggerers.Count > 0)
                {
                    TriggeredOnce = true;
                    IsActive = false;
                }
            }

            TriggerActive = triggerers.Any();
            if (TriggerActive && conditionals != null)
            {
                switch (conditionals.LogicalOperator)
                {
                    case PropertyConditional.LogicalOperatorType.And:
                    {
                        if (triggerers.Any(t => !PropertyConditional.CheckConditionals((ISerializableEntity)t, conditionals.Conditionals, conditionals.LogicalOperator)))
                        {
                            // Some of the conditionals doesn't match
                            IsActive = false;
                        }
                        break;
                    }
                    case PropertyConditional.LogicalOperatorType.Or:
                    {
                        if (triggerers.None(t => !PropertyConditional.CheckConditionals((ISerializableEntity)t, conditionals.Conditionals, conditionals.LogicalOperator)))
                        {
                            // None of the conditionals match
                            IsActive = false;
                        }
                        break;
                    }
                }
            }

            if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
            {
                ForceFluctuationTimer += deltaTime;
                if (ForceFluctuationTimer >= ForceFluctuationInterval)
                {
                    float v = MathF.Sin(2 * MathF.PI * ForceFluctuationFrequency * TimeInLevel);
                    float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v);
                    CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount);
                    ForceFluctuationTimer = 0.0f;
#if SERVER
                    item.CreateServerEvent(this);
#endif
                }
            }

            foreach (Entity triggerer in triggerers)
            {
                LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets, targetItem: Item);

                if (triggerer is IDamageable damageable)
                {
                    LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime);
                }
                else if (triggerer is Submarine submarine)
                {
                    LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime);
                    foreach (Character c2 in Character.CharacterList)
                    {
                        if (c2.Submarine == submarine)
                        {
                            LevelTrigger.ApplyAttacks(attacks, c2, item.WorldPosition, deltaTime);
                        }
                    }
                }

                if (Force < 0.01f && DirectionalForce.LengthSquared() < 0.0001f)
                {
                    // Just ignore very minimal forces
                    continue;
                }
                else if (triggerer is Character c)
                {
                    if (c.AnimController.Collider.BodyType == BodyType.Dynamic)
                    {
                        if (c.AnimController.Collider.Enabled)
                        {
                            ApplyForce(c.AnimController.Collider);
                        }
                        foreach (var limb in c.AnimController.Limbs)
                        {
                            ApplyForce(limb.body, multiplier: limb.Mass * c.AnimController.Collider.Mass / c.AnimController.Mass);
                        }
                    }
                }
                else if (triggerer is Submarine s)
                {
                    ApplyForce(s.SubBody.Body);
                }
                else if (triggerer is Item i && i.body != null)
                {
                    ApplyForce(i.body);
                }
            }

            item.SendSignal(IsActive ? "1" : "0", "state_out");
        }

        private void ApplyForce(PhysicsBody body, float multiplier = 1.0f)
        {
            Vector2 diff = ConvertUnits.ToDisplayUnits(item.SimPosition - body.SimPosition);
            if (diff.LengthSquared() < 0.0001f) { return; }
            float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
            if (distanceFactor <= 0.0f) { return; }
            Vector2 radialForce = Force * Vector2.Normalize(diff);
            Vector2 directionalForce;
            if (RelativeDirectionalForce)
            {
                directionalForce = DirectionalForce * new Vector2(Math.Sign(diff.X), Math.Sign(diff.Y));
            }
            else
            {
                Vector2 flippedForce = DirectionalForce;
                if (item.FlippedX) { flippedForce.X = -flippedForce.X; }
                if (item.FlippedY) { flippedForce.Y = -flippedForce.Y; }
                directionalForce = MathUtils.RotatePoint(flippedForce, -item.RotationRad);
            }

            Vector2 force = (radialForce + directionalForce) * CurrentForceFluctuation * distanceFactor * multiplier;

            if (!HorizontalForce) { force.Y = 0.0f; }
            if (!VerticalForce) { force.Y = 0.0f; }

            if (force.LengthSquared() < 0.01f) { return; }
            if (body.Mass < 1)
            {
                //restrict the force if the body is very light, otherwise it can end up moving at a speed that breaks physics
                force *= body.Mass;
            }
            body.ApplyForce(force);
        }

        public override void Move(Vector2 amount, bool ignoreContacts = false)
        {
            if (PhysicsBody != null)
            {
                SetPhysicsBodyPosition(ignoreContacts);
                PhysicsBody.Submarine = item.Submarine;
            }
        }

        protected override void RemoveComponentSpecific()
        {
            if (PhysicsBody != null)
            {
                PhysicsBody.Remove();
                PhysicsBody = null;
            }
        }

        public override void ReceiveSignal(Signal signal, Connection connection)
        {
            base.ReceiveSignal(signal, connection);
            switch (connection.Name)
            {
                case "set_force":
                    if (!FloatTryParse(signal, out float force)) { break; }
                    Force = force;
                    break;
                case "set_distancebasedforce":
                    if (!bool.TryParse(signal.value, out bool distanceBasedForce)) { break; }
                    DistanceBasedForce = distanceBasedForce;
                    break;
                case "set_forcefluctuation":
                    if (!bool.TryParse(signal.value, out bool forceFluctuation)) { break; }
                    ForceFluctuation = forceFluctuation;
                    break;
                case "set_forcefluctuationstrength":
                    if (!FloatTryParse(signal, out float forceFluctuationStrength)) { break; }
                    ForceFluctuationStrength = forceFluctuationStrength;
                    break;
                case "set_forcefluctuationfrequency":
                    if (!FloatTryParse(signal, out float forceFluctuationFrequency)) { break; }
                    ForceFluctuationFrequency = forceFluctuationFrequency;
                    break;
                case "set_forcefluctuationinterval":
                    if (!FloatTryParse(signal, out float forceFluctuationInterval)) { break; }
                    ForceFluctuationInterval = forceFluctuationInterval;
                    break;
            }

            static bool FloatTryParse(Signal signal, out float value)
            {
                return float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
            }
        }
    }
}
 