using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
using static Barotrauma.AIObjectiveFindSafety;
using System.Collections.Immutable;

namespace Barotrauma
{
    class AIObjectiveCombat : AIObjective
    {
        public override Identifier Identifier { get; set; } = "combat".ToIdentifier();

        public override bool KeepDivingGearOn => true;
        public override bool IgnoreUnsafeHulls => true;
        public override bool AllowOutsideSubmarine => true;
        public override bool AllowInAnySub => true;

        private readonly CombatMode initialMode;

        private float checkWeaponsTimer;
        private readonly float checkWeaponsInterval = 1;
        private float ignoreWeaponTimer;
        private readonly float ignoredWeaponsClearTime = 10;

        private readonly float goodWeaponPriority = 30;

        private readonly float arrestHoldFireTime = 8;
        private float holdFireTimer;
        private bool hasAimed;
        private bool isLethalWeapon;
        private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;

        public Character Enemy { get; private set; }
        public bool HoldPosition { get; set; }

        private Item _weapon;
        private Item Weapon
        {
            get { return _weapon; }
            set
            {
                _weapon = value;
                _weaponComponent = null;
                hasAimed = false;
                RemoveSubObjective(ref seekAmmunitionObjective);
            }
        }
        private ItemComponent _weaponComponent;
        private ItemComponent WeaponComponent
        {
            get
            {
                if (Weapon == null) { return null; }
                if (_weaponComponent == null)
                {
                    _weaponComponent =
                        Weapon.GetComponent<RangedWeapon>() ??
                        Weapon.GetComponent<MeleeWeapon>() ??
                        Weapon.GetComponent<RepairTool>() as ItemComponent;
                }
                return _weaponComponent;
            }
        }

        public override bool ConcurrentObjectives => true;
        public override bool AbandonWhenCannotCompleteSubjectives => false;

        private readonly AIObjectiveFindSafety findSafety;
        private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
        private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();

        private AIObjectiveContainItem seekAmmunitionObjective;
        private AIObjectiveGoTo retreatObjective;
        private AIObjectiveGoTo followTargetObjective;
        private AIObjectiveGetItem seekWeaponObjective;

        private Hull retreatTarget;
        private float coolDownTimer;
        private IEnumerable<Body> myBodies;
        private float aimTimer;
        private float reloadTimer;
        private float spreadTimer;

        private bool canSeeTarget;
        private float visibilityCheckTimer;
        private readonly float visibilityCheckInterval = 0.2f;

        private float sqrDistance;
        private readonly float maxDistance = 2000;
        private readonly float distanceCheckInterval = 0.2f;
        private float distanceTimer;

        public bool allowHoldFire;

        /// <summary>
        /// Don't start using a weapon if this condition is true
        /// </summary>
        public Func<bool> holdFireCondition;

        public enum CombatMode
        {
            Defensive,  // Use weapons against the enemy, but try to retreat to a safe place
            Offensive,  // Engage the enemy and keep attacking it
            Arrest,     // Try to arrest the enemy without using lethal weapons (stunning + handcuffs)
            Retreat,    // Run to a safe place without attacking the target
            None        // Don't use
        }

        public CombatMode Mode { get; private set; }

        private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
        private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
        private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;

        private float AimSpeed => HumanAIController.AimSpeed;
        private float AimAccuracy => HumanAIController.AimAccuracy;

        private bool IsEnemyCloserThan(float margin) =>
            Enemy != null && Enemy.CurrentHull != null &&
            character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin ||
            HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin;

        public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) 
            : base(character, objectiveManager, priorityModifier)
        {
            if (mode == CombatMode.None)
            {
#if DEBUG
                DebugConsole.ThrowError("Combat mode == None");
#endif
                return;
            }
            Enemy = enemy;
            coolDownTimer = coolDown;
            findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
            if (findSafety != null)
            {
                findSafety.Priority = 0;
                HumanAIController.UnreachableHulls.Clear();
            }
            Mode = mode;
            initialMode = Mode;
            if (Enemy == null)
            {
                Mode = CombatMode.Retreat;
            }
            spreadTimer = Rand.Range(-10f, 10f);
            SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
            HumanAIController.SortTimer = 0;
        }

        protected override float GetPriority()
        {
            if (Enemy == null)
            {
                Priority = 0;
                Abandon = true;
                return Priority;
            }
            if (character.TeamID == CharacterTeamType.FriendlyNPC)
            {
                if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
                {
                    Priority = 0;
                    Abandon = true;
                    return Priority;
                }
            }
            if (TargetEliminated)
            {
                Priority = 0;
            }
            else
            {
                // 91-100
                float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
                float maxPriority = AIObjectiveManager.MaxObjectivePriority;
                float priorityScale = maxPriority - minPriority;
                float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
                float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
                if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
                {
                    xDist /= 2;
                    yDist /= 2;
                }
                float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
                float devotion = CumulatedDevotion / 100;
                float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
                Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
                if (Priority > 0)
                {
                    if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
                    {
                        Priority = 0;
                    }
                }
            }
            return Priority;
        }

        public override void Update(float deltaTime)
        {
            base.Update(deltaTime);
            ignoreWeaponTimer -= deltaTime;
            checkWeaponsTimer -= deltaTime;
            if (reloadTimer > 0)
            {
                reloadTimer -= deltaTime;
            }
            if (ignoreWeaponTimer < 0)
            {
                ignoredWeapons.Clear();
                ignoreWeaponTimer = ignoredWeaponsClearTime;
            }
            bool isCurrentObjective = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
            if (findSafety != null && isCurrentObjective)
            {
                findSafety.Priority = 0;
            }
            if (!AllowCoolDown && !character.IsOnPlayerTeam && !isCurrentObjective)
            {
                distanceTimer -= deltaTime;
                if (distanceTimer < 0)
                {
                    distanceTimer = distanceCheckInterval;
                    sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
                }
            }
        }

        protected override bool CheckObjectiveSpecific()
        {
            if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
            {
                // Can't lose the target in friendly outposts.
                if (sqrDistance > maxDistance * maxDistance)
                {
                    // The target escaped from us.
                    return true;
                }
            }
            return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
        }

        protected override void Act(float deltaTime)
        {
            if (IsEnemyDisabled)
            {
                IsCompleted = true;
                return;
            }
            if (AllowCoolDown)
            {
                coolDownTimer -= deltaTime;
            }
            if (seekAmmunitionObjective == null && seekWeaponObjective == null)
            {
                if (Mode != CombatMode.Retreat && TryArm())
                {
                    OperateWeapon(deltaTime);
                }
                if (HoldPosition)
                {
                    SteeringManager.Reset();
                }
                else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
                {
                    Move(deltaTime);
                }
                switch (Mode)
                {
                    case CombatMode.Offensive:
                        if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
                        {
                            character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
                        }
                        break;
                    case CombatMode.Arrest:
                        if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true))
                        {
                            IsCompleted = true;
                        }
                        else if (Enemy.IsKnockedDown && 
                            !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() && 
                            !HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false))
                        {
                            IsCompleted = true;
                        }
                        break;
                }
            }
        }

        private void Move(float deltaTime)
        {
            switch (Mode)
            {
                case CombatMode.Offensive:
                case CombatMode.Arrest:
                    Engage(deltaTime);
                    break;
                case CombatMode.Defensive:
                    if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
                    {
                        if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
                        {
                            Engage(deltaTime);
                        }
                        else
                        {
                            // Keep following the goto target
                            var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
                            if (gotoObjective != null)
                            {
                                gotoObjective.ForceAct(deltaTime);
                                if (!character.AnimController.InWater)
                                {
                                    HumanAIController.FaceTarget(Enemy);
                                    ForceWalk = true;
                                    HumanAIController.AutoFaceMovement = false;
                                }
                            }
                            else
                            {
                                SteeringManager.Reset();
                            }
                        }
                    }
                    else
                    {
                        Retreat(deltaTime);
                    }
                    break;
                case CombatMode.Retreat:
                    Retreat(deltaTime);
                    break;
                default:
                    throw new NotImplementedException();
            }
        }

        private bool TryArm()
        {
            if (character.LockHands || Enemy == null)
            {
                Weapon = null;
                return false;
            }
            if (checkWeaponsTimer < 0)
            {
                checkWeaponsTimer = checkWeaponsInterval;
                // First go through all weapons and try to reload without seeking ammunition
                var allWeapons = FindWeaponsFromInventory();
                while (allWeapons.Any())
                {
                    Weapon = GetWeapon(allWeapons, out _weaponComponent);
                    if (Weapon == null)
                    {
                        // No weapons
                        break;
                    }
                    if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
                    {
                        // Not in the inventory anymore or cannot find the weapon component
                        allWeapons.Remove(WeaponComponent);
                        Weapon = null;
                        continue;
                    }
                    if (!WeaponComponent.IsEmpty(character))
                    {
                        // All good, the weapon is loaded
                        break;
                    }
                    if (Reload(seekAmmo: false))
                    {
                        // All good, we can use the weapon.
                        break;
                    }
                    else
                    {
                        // No ammo.
                        allWeapons.Remove(WeaponComponent);
                        Weapon = null;
                    }
                }
                if (Weapon == null)
                {
                    // No weapon found with the conditions above. Try again, now let's try to seek ammunition too
                    Weapon = FindWeapon(out _weaponComponent);
                    if (Weapon != null)
                    {
                        if (!CheckWeapon(seekAmmo: true))
                        {
                            if (seekAmmunitionObjective != null)
                            {
                                // No loaded weapon, but we are trying to seek ammunition.
                                return false;
                            }
                            else
                            {
                                Weapon = null;
                            }
                        }
                    }
                }
                bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
                if (!isAllowedToSeekWeapons)
                {
                    if (WeaponComponent == null)
                    {
                        SpeakNoWeapons();
                        Mode = CombatMode.Retreat;
                    }
                }
                else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority))
                {
                    // Poor weapon equipped -> try to find better.
                    RemoveSubObjective(ref seekAmmunitionObjective);
                    RemoveSubObjective(ref retreatObjective);
                    RemoveSubObjective(ref followTargetObjective);
                    TryAddSubObjective(ref seekWeaponObjective,
                        constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
                        {
                            AllowStealing = HumanAIController.IsMentallyUnstable,
                            EvaluateCombatPriority = false,  // Use a custom formula instead
                            GetItemPriority = i =>
                            {
                                if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
                                if (i.IsOwnedBy(character)) { return 0; }
                                var mw = i.GetComponent<MeleeWeapon>();
                                var rw = i.GetComponent<RangedWeapon>();
                                float priority = 0;
                                if (mw != null)
                                {
                                    priority = mw.CombatPriority / 100;
                                }
                                else if (rw != null)
                                {
                                    priority = rw.CombatPriority / 100;
                                }
                                if (i.HasTag("stunner"))
                                {
                                    if (Mode == CombatMode.Arrest)
                                    {
                                        priority *= 2;
                                    }
                                    else
                                    {
                                        priority /= 2;
                                    }
                                }
                                return priority;
                            }
                        },
                        onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
                        onAbandon: () =>
                        {
                            RemoveSubObjective(ref seekWeaponObjective);
                            if (Weapon == null)
                            {
                                SpeakNoWeapons();
                                Mode = CombatMode.Retreat;
                            }
                            else
                            {
                                Mode = CombatMode.Defensive;
                            }
                        });
                }
            }
            else
            {
                if (!CheckWeapon(seekAmmo: false))
                {
                    Weapon = null;
                }
            }
            return Weapon != null;

            bool CheckWeapon(bool seekAmmo)
            {
                if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
                {
                    // Not in the inventory anymore or cannot find the weapon component
                    return false;
                }
                if (WeaponComponent.IsEmpty(character))
                {
                    // Try reloading (and seek ammo)
                    if (!Reload(seekAmmo))
                    {
                        return false;
                    }
                }
                return true;
            };
        }

        private void OperateWeapon(float deltaTime)
        {
            switch (Mode)
            {
                case CombatMode.Offensive:
                case CombatMode.Defensive:
                case CombatMode.Arrest:
                    if (Equip())
                    {
                        Attack(deltaTime);
                    }
                    break;
                case CombatMode.Retreat:
                    break;
                default:
                    throw new NotImplementedException();
            }
        }

        private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);

        private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
        {
            weaponComponent = null;
            float bestPriority = 0;
            float lethalDmg = -1;
            bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300);
            bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
            foreach (var weapon in weaponList)
            {
                float priority = weapon.CombatPriority;
                if (weapon is RepairTool repairTool)
                {
                    switch (repairTool.UsableIn)
                    {
                        case RepairTool.UseEnvironment.Air:
                            if (character.InWater) { continue; }
                            break;
                        case RepairTool.UseEnvironment.Water:
                            if (!character.InWater) { continue; }
                            break;
                        case RepairTool.UseEnvironment.None:
                            continue;
                        case RepairTool.UseEnvironment.Both:
                        default:
                            break;
                    }
                }
                if (prioritizeMelee)
                {
                    if (weapon is MeleeWeapon)
                    {
                        priority *= 5;
                    }
                    else
                    {
                        priority /= 2;
                    }
                }
                if (weapon.IsEmpty(character))
                {
                    if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
                    {
                        // Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
                        continue;
                    }
                    else
                    {
                        // Halve the priority for weapons that don't have proper ammunition loaded.
                        priority /= 2;
                    }
                }
                if (Enemy.Params.Health.StunImmunity)
                {
                    if (weapon.Item.HasTag("stunner"))
                    {
                        priority /= 2;
                    }
                }
                else if (Enemy.IsKnockedDown)
                {
                    // Enemy is stunned, reduce the priority of stunner weapons.
                    Attack attack = GetAttackDefinition(weapon);
                    if (attack != null)
                    {
                        lethalDmg = attack.GetTotalDamage();
                        float max = lethalDmg + 1;
                        if (weapon.Item.HasTag("stunner"))
                        {
                            priority = max;
                        }
                        else
                        {
                            float stunDmg = ApproximateStunDamage(weapon, attack);
                            float diff = stunDmg - lethalDmg;
                            priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
                        }
                    }
                }
                else if (Mode == CombatMode.Arrest)
                {
                    // Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
                    if (weapon.Item.HasTag("stunner"))
                    {
                        priority *= 2;
                    }
                    else
                    {
                        Attack attack = GetAttackDefinition(weapon);
                        if (attack != null)
                        {
                            lethalDmg = attack.GetTotalDamage();
                            float stunDmg = ApproximateStunDamage(weapon, attack);
                            float diff = stunDmg - lethalDmg;
                            if (diff < 0)
                            {
                                priority /= 2;
                            }
                        }
                    }
                }
                else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon))
                {
                    Attack attack = GetAttackDefinition(weapon);
                    priority = attack?.GetTotalDamage() ?? priority / 2;
                }
                if (priority > bestPriority)
                {
                    weaponComponent = weapon;
                    bestPriority = priority;
                }
            }
            if (weaponComponent == null) { return null; }
            if (bestPriority < 1) { return null; }
            if (Mode == CombatMode.Arrest)
            {
                if (weaponComponent.Item.HasTag("stunner"))
                {
                    isLethalWeapon = false;
                }
                else
                {
                    if (lethalDmg < 0)
                    {
                        lethalDmg = GetLethalDamage(weaponComponent);
                    }
                    isLethalWeapon = lethalDmg > 1;
                }
                if (allowHoldFire && !hasAimed && holdFireTimer <= 0)
                {
                    holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
                }
            }
            return weaponComponent.Item;

            float ApproximateStunDamage(ItemComponent weapon, Attack attack)
            {
                // Try to reduce the priority using the actual damage values and status effects.
                // This is an approximation, because we can't check the status effect conditions here.
                // The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
                var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
                if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
                {
                    statusEffects = statusEffects.Concat(hitEffects);
                }
                float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
                float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
                {
                    float stunAmount = 0;
                    var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
                    if (stunAffliction != null)
                    {
                        stunAmount = stunAffliction.Strength;
                    }
                    return stunAmount;
                });
                return attack.Stun + afflictionsStun + effectsStun;
            }

            bool CanMeleeStunnerStun(ItemComponent weapon)
            {
                // If there's an item container that takes a battery,
                // assume that it's required for the stun effect
                // as we can't check the status effect conditions here.
                var mobileBatteryTag = "mobilebattery".ToIdentifier();
                var containers = weapon.Item.Components.Where(ic => 
                    ic is ItemContainer container &&
                    container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
                // If there's no such container, assume that the melee weapon can stun without a battery.
                return containers.None() || containers.Any(container =>
                    (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
            }
        }

        public static float GetLethalDamage(ItemComponent weapon)
        {
            float lethalDmg = 0;
            Attack attack = GetAttackDefinition(weapon);
            if (attack != null)
            {
                lethalDmg = attack.GetTotalDamage();
            }
            return lethalDmg;
        }

        private static Attack GetAttackDefinition(ItemComponent weapon)
        {
            Attack attack = null;
            if (weapon is MeleeWeapon meleeWeapon)
            {
                attack = meleeWeapon.Attack;
            }
            else if (weapon is RangedWeapon rangedWeapon)
            {
                attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
            }
            return attack;
        }

        private HashSet<ItemComponent> FindWeaponsFromInventory()
        {
            weapons.Clear();
            foreach (var item in character.Inventory.AllItems)
            {
                if (ignoredWeapons.Contains(item)) { continue; }
                GetWeapons(item, weapons);
                if (item.OwnInventory != null)
                {
                    item.OwnInventory.AllItems.ForEach(i => GetWeapons(i, weapons));
                }
            }
            return weapons;
        }

        private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
        {
            if (item == null) { return; }
            foreach (var component in item.Components)
            {
                if (component.CombatPriority > 0)
                {
                    weaponList.Add(component);
                }
            }
        }

        private void Unequip()
        {
            if (!character.LockHands && character.HeldItems.Contains(Weapon))
            {
                if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
                {
                    if (Weapon.AllowedSlots.Contains(InvSlotType.Bag))
                    {
                        if (character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Bag }))
                        {
                            return;
                        }
                    }
                    Weapon.Drop(character);
                }
            }
        }

        private bool Equip()
        {
            if (character.LockHands) { return false; }
            if (WeaponComponent.IsEmpty(character))
            {
                return false;
            }
            if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
            {
                //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
                character.ClearInput(InputType.Aim);
                character.ClearInput(InputType.Shoot);
                Weapon.TryInteract(character, forceSelectKey: true);
                var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
                if (character.Inventory.TryPutItem(Weapon, character, slots))
                {
                    SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
                }
                else
                {
                    SpeakNoWeapons();
                    Weapon = null;
                    Mode = CombatMode.Retreat;
                    return false;
                }
            }
            return true;

            static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
        }

        private float findHullTimer;
        private readonly float findHullInterval = 1.0f;

        private void Retreat(float deltaTime)
        {
            if (!Enemy.IsHuman)
            {
                SpeakRetreating();
            }
            RemoveFollowTarget();
            RemoveSubObjective(ref seekAmmunitionObjective);
            if (retreatObjective != null && retreatObjective.Target != retreatTarget)
            {
                RemoveSubObjective(ref retreatObjective);
            }
            if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance))
            {
                // Swim away
                SteeringManager.Reset();
                character.ReleaseSecondaryItem();
                SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
                SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
                return;
            }
            if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
            {
                if (findHullTimer > 0)
                {
                    findHullTimer -= deltaTime;
                }
                else
                {
                    HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
                    if (hullSearchStatus != HullSearchStatus.Finished)
                    {
                        findSafety.UpdateSimpleEscape(deltaTime);
                        return;
                    }
                    retreatTarget = potentialSafeHull;
                    findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
                }
            }
            if (retreatTarget != null && character.CurrentHull != retreatTarget)
            {
                TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
                {
                    UsePathingOutside = false,
                    SpeakIfFails = false
                },
                onAbandon: () =>
                {
                    if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
                    {
                        // If in the same room with an enemy -> don't try to escape because we'd want to fight it
                        SteeringManager.Reset();
                        RemoveSubObjective(ref retreatObjective);
                    }
                    else
                    {
                        // else abandon and fall back to find safety mode
                        Abandon = true;
                    }
                }, 
                onCompleted: () => RemoveSubObjective(ref retreatObjective));
            }
        }

        private void Engage(float deltaTime)
        {
            if (WeaponComponent == null)
            {
                RemoveFollowTarget();
                SteeringManager.Reset();
                return;
            }
            if (character.LockHands || Enemy == null)
            {
                Mode = CombatMode.Retreat;
                SteeringManager.Reset();
                return;
            }
            retreatTarget = null;
            RemoveSubObjective(ref retreatObjective);
            RemoveSubObjective(ref seekAmmunitionObjective);
            RemoveSubObjective(ref seekWeaponObjective);
            if (character.Submarine == null && WeaponComponent is MeleeWeapon meleeWeapon)
            {
                if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
                {
                    character.ReleaseSecondaryItem();
                    // Swim towards the target
                    SteeringManager.Reset();
                    SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
                    SteeringManager.SteeringAvoid(deltaTime, 5, weight: 15);
                }
                else
                {
                    SteeringManager.Reset();
                }
                return;
            }
            if (followTargetObjective != null && followTargetObjective.Target != Enemy)
            {
                RemoveFollowTarget();
            }
            TryAddSubObjective(ref followTargetObjective,
                constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50)
                {
                    UsePathingOutside = false,
                    IgnoreIfTargetDead = true,
                    TargetName = Enemy.DisplayName,
                    AlwaysUseEuclideanDistance = false,
                    SpeakIfFails = false
                },
                onAbandon: () =>
                {
                    if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
                    {
                        // If in the same room with an enemy -> don't try to escape because we'd want to fight it
                        SteeringManager.Reset();
                        RemoveSubObjective(ref followTargetObjective);
                    }
                    else
                    {
                        // else abandon and fall back to find safety mode
                        Abandon = true;
                    }
                });
            if (followTargetObjective == null) { return; }
            if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
            {
                if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _))
                {
                    if (!arrestingRegistered)
                    {
                        arrestingRegistered = true;
                        followTargetObjective.Completed += OnArrestTargetReached;
                        followTargetObjective.CloseEnough = 100;
                    }
                }
                else
                {
                    if (character.TeamID == CharacterTeamType.FriendlyNPC)
                    {
                        ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
                        if (prefab != null)
                        {
                            Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
                        }
                    }
                    RemoveFollowTarget();
                    SteeringManager.Reset();
                }
            }
            if (!arrestingRegistered && followTargetObjective != null)
            {
                followTargetObjective.CloseEnough =
                    WeaponComponent is RangedWeapon ? 1000 :
                    WeaponComponent is MeleeWeapon mw ? mw.Range :
                    WeaponComponent is RepairTool rt ? rt.Range : 50;
            }
        }

        private bool arrestingRegistered;

        private void RemoveFollowTarget()
        {
            if (followTargetObjective != null)
            {
                if (arrestingRegistered)
                {
                    followTargetObjective.Completed -= OnArrestTargetReached;
                }
                RemoveSubObjective(ref followTargetObjective);
            }
            arrestingRegistered = false;
        }

        private void OnArrestTargetReached()
        {
            if (!Enemy.IsKnockedDown)
            {
                RemoveFollowTarget();
                return;
            }
            if (character.TeamID == CharacterTeamType.FriendlyNPC)
            {
                // Confiscate stolen goods and all weapons
                foreach (var item in Enemy.Inventory.AllItemsMod)
                {
                    if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
                        item.HasTag("weapon") ||
                        item.GetComponent<MeleeWeapon>() != null ||
                        item.GetComponent<RangedWeapon>() != null)
                    {
                        item.Drop(character);
                        character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
                    }
                }
            }
            if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
            {
                var handCuffs = matchingItems.First();
                if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
                {
#if DEBUG
                    DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
#endif
                    if (objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
                    {
                        Abandon = true;
                        return;
                    }
                }
                character.Speak(TextManager.Get("DialogTargetArrested").Value, null, 3.0f, "targetarrested".ToIdentifier(), 30.0f);
            }
            if (!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
            {
                IsCompleted = true;
            }
        }

        /// <summary>
        /// Seeks for more ammunition. Creates a new subobjective.
        /// </summary>
        private void SeekAmmunition(ImmutableHashSet<Identifier> ammunitionIdentifiers)
        {
            retreatTarget = null;
            RemoveSubObjective(ref retreatObjective);
            RemoveSubObjective(ref seekWeaponObjective);
            RemoveFollowTarget();
            TryAddSubObjective(ref seekAmmunitionObjective,
                constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
                {
                    ItemCount = Weapon.GetComponent<ItemContainer>().Capacity * Weapon.GetComponent<ItemContainer>().MaxStackSize,
                    checkInventory = false,
                    MoveWholeStack = true
                },
                onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
                onAbandon: () =>
                {
                    SteeringManager.Reset();
                    RemoveSubObjective(ref seekAmmunitionObjective);
                    ignoredWeapons.Add(Weapon);
                    Weapon = null;
                });
        }
        
        /// <summary>
        /// Reloads the ammunition found in the inventory.
        /// If seekAmmo is true, tries to get find the ammo elsewhere.
        /// </summary>
        private bool Reload(bool seekAmmo)
        {
            if (WeaponComponent == null) { return false; }        
            if (Weapon.OwnInventory == null) { return true; }
            // Eject empty ammo
            HumanAIController.UnequipEmptyItems(Weapon);
            ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
            if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
            {
                foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
                {
                    if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
                    ammunitionIdentifiers = requiredItem.Identifiers;
                    break;
                }
            }
            else if (WeaponComponent is MeleeWeapon meleeWeapon)
            {
                ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
            }
            // No ammo
            if (ammunitionIdentifiers != null)
            {
                // Try reload ammunition from inventory
                static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
                Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
                if (ammunition != null)
                {
                    var container = Weapon.GetComponent<ItemContainer>();
                    if (!container.Inventory.TryPutItem(ammunition, user: character))
                    {
                        if (ammunition.ParentInventory == character.Inventory)
                        {
                            ammunition.Drop(character);
                        }
                    }
                }
            }
            if (!WeaponComponent.IsEmpty(character))
            {
                return true;
            }
            else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
            {
                SeekAmmunition(ammunitionIdentifiers);
            }
            return false;
        }

        private void Attack(float deltaTime)
        {
            character.CursorPosition = Enemy.WorldPosition;
            if (AimAccuracy < 1)
            {
                spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
                float shake = Rand.Range(0.95f, 1.05f);
                float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
                float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
                float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
                character.CursorPosition += new Vector2(0, offset);
            }
            if (character.Submarine != null)
            {
                character.CursorPosition -= character.Submarine.Position;
            }
            visibilityCheckTimer -= deltaTime;
            if (visibilityCheckTimer <= 0.0f)
            {
                canSeeTarget = character.CanSeeTarget(Enemy);
                visibilityCheckTimer = visibilityCheckInterval;
            }
            if (!canSeeTarget)
            {
                SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
                return;
            }
            if (Weapon.RequireAimToUse)
            {
                character.SetInput(InputType.Aim, false, true);
            }
            hasAimed = true;
            if (holdFireTimer > 0)
            {
                holdFireTimer -= deltaTime;
                return;
            }
            if (aimTimer > 0)
            {
                aimTimer -= deltaTime;
                return;
            }
            if (reloadTimer > 0) { return; }
            if (holdFireCondition != null && holdFireCondition()) { return; }
            sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
            distanceTimer = distanceCheckInterval;
            if (WeaponComponent is MeleeWeapon meleeWeapon)
            {
                bool closeEnough = true;
                float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
                if (character.AnimController.InWater)
                {
                    if (sqrDistance > sqrRange) 
                    {
                        closeEnough = false;
                    }
                }
                else
                {
                    // It's possible that the center point of the creature is out of reach, but we could still hit the character.
                    float xDiff = Math.Abs(Enemy.WorldPosition.X - character.WorldPosition.X);
                    if (xDiff > meleeWeapon.Range)
                    {
                        closeEnough = false;
                    }
                    float yDiff = Math.Abs(Enemy.WorldPosition.Y - character.WorldPosition.Y);
                    if (yDiff > Math.Max(meleeWeapon.Range, 100))
                    {
                        closeEnough = false;
                    }
                    if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
                    {
                        // The target is probably knocked down? -> try to reach it by crouching.
                        HumanAIController.AnimController.Crouching = true;
                    }
                }
                if (closeEnough)
                {
                    UseWeapon(deltaTime);
                    character.AIController.SteeringManager.Reset();
                }
                else if (!character.IsFacing(Enemy.WorldPosition))
                {
                    // Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
                    SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
                }
            }
            else
            {
                if (WeaponComponent is RepairTool repairTool)
                {
                    if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
                }
                float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
                if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
                {
                    if (myBodies == null)
                    {
                        myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
                    }
                    // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
                    var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
                    foreach (var body in pickedBodies)
                    {
                        Character target = null;
                        if (body.UserData is Character c)
                        {
                            target = c;
                        }
                        else if (body.UserData is Limb limb)
                        {
                            target = limb.character;
                        }
                        if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
                        {
                            return;
                        }
                    }
                    UseWeapon(deltaTime);
                }
            }
        }

        private void UseWeapon(float deltaTime)
        {
            // Never allow to attack characters with deadly weapons while trying to arrest.
            if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
            float reloadTime = 0;
            if (WeaponComponent is RangedWeapon rangedWeapon)
            {
                // If the weapon is just equipped, we can't shoot just yet.
                if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
                {
                    reloadTime = rangedWeapon.Reload;
                }
            }
            if (WeaponComponent is MeleeWeapon mw)
            {
                if (!((HumanoidAnimController)character.AnimController).Crouching)
                {
                    reloadTime = mw.Reload;
                }
            }
            character.SetInput(InputType.Shoot, false, true);
            Weapon.Use(deltaTime, character);
            reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
        }

        private bool ShouldUnequipWeapon =>
            Weapon != null &&
            character.Submarine != null &&
            character.Submarine.TeamID == character.TeamID &&
            Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));

        protected override void OnCompleted()
        {
            base.OnCompleted();
            if (ShouldUnequipWeapon)
            {
                Unequip();
            }
            SteeringManager.Reset();
        }

        protected override void OnAbandon()
        {
            base.OnAbandon();
            if (ShouldUnequipWeapon)
            {
                Unequip();
            }
            SteeringManager.Reset();
        }

        public override void Reset()
        {
            base.Reset();
            hasAimed = false;
            isLethalWeapon = false;
            canSeeTarget = false;
            seekWeaponObjective = null;
            seekAmmunitionObjective = null;
            retreatObjective = null;
            followTargetObjective = null;
            retreatTarget = null;
        }

        private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
        private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);

        private void Speak(Identifier textIdentifier, float delay, float minDuration)
        {
            if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
            {
                LocalizedString msg = TextManager.Get(textIdentifier);
                if (!msg.IsNullOrEmpty())
                {
                    character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
                }
            }
        }

        private void SetAimTimer(float newTimer) => aimTimer = Math.Max(aimTimer, newTimer);
    }
}
