using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Barotrauma
{
    class AIObjectiveIdle : AIObjective
    {
        public override Identifier Identifier { get; set; } = "idle".ToIdentifier();
        public override bool AllowAutomaticItemUnequipping => true;
        public override bool AllowInAnySub => true;

        private BehaviorType behavior;
        public BehaviorType Behavior
        {
            get { return behavior; }
            set
            {
                behavior = value;
                switch (behavior)
                {
                    case BehaviorType.Passive:
                    case BehaviorType.StayInHull:
                        newTargetIntervalMin = 60;
                        newTargetIntervalMax = 120;
                        standStillMin = 30;
                        standStillMax = 60;
                        break;
                    case BehaviorType.Active:
                        newTargetIntervalMin = 40;
                        newTargetIntervalMax = 60;
                        standStillMin = 20;
                        standStillMax = 40;
                        break;
                    case BehaviorType.Patrol:
                        newTargetIntervalMin = 15;
                        newTargetIntervalMax = 30;
                        standStillMin = 5;
                        standStillMax = 10;
                        break;
                }
            }
        }

        private float newTargetIntervalMin;
        private float newTargetIntervalMax;
        private float standStillMin;
        private float standStillMax;
        private readonly float walkDurationMin = 5;
        private readonly float walkDurationMax = 10;

        public enum BehaviorType
        {
            Patrol,
            Passive,
            StayInHull,
            Active
        }
        public Hull TargetHull { get; set; }
        private Hull currentTarget;
        private float newTargetTimer;

        private bool searchingNewHull;

        private float standStillTimer;
        private float walkDuration;

        private Character tooCloseCharacter;

        const float chairCheckInterval = 5.0f;
        private float chairCheckTimer;

        private float autonomousObjectiveRetryTimer = 10;

        private readonly List<Hull> targetHulls = new List<Hull>(20);
        private readonly List<float> hullWeights = new List<float>(20);

        public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
        {
            Behavior = BehaviorType.Passive;
            standStillTimer = Rand.Range(-10.0f, 10.0f);
            walkDuration = Rand.Range(0.0f, 10.0f);
            chairCheckTimer = Rand.Range(0.0f, chairCheckInterval);
            CalculatePriority();
        }

        protected override bool CheckObjectiveSpecific() => false;
        public override bool CanBeCompleted => true;

        public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }

        public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();

        public void CalculatePriority(float max = 0)
        {
            //Random = Rand.Range(0.5f, 1.5f);
            //randomTimer = randomUpdateInterval;
            //max = max > 0 ? max : Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
            //float initiative = character.GetSkillLevel("initiative");
            //Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
            Priority = 1;
        }

        protected override float GetPriority() => Priority;

        public override void Update(float deltaTime)
        {
            // Do nothing. Overrides the inherited devotion calculations.
        }

        private float timerMargin;

        private void SetTargetTimerLow()
        {
            // Increases the margin each time the method is called -> takes longer between the path finding calls.
            // The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
            timerMargin += 0.5f;
            timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
            newTargetTimer = Math.Min(newTargetTimer, timerMargin);
        }

        private void SetTargetTimerHigh()
        {
            // This method is used to the timer between the current value and the min so that it never reaches 0.
            // Prevents pathfinder calls.
            newTargetTimer = Math.Max(newTargetTimer, newTargetIntervalMin);
            timerMargin = 0;
        }

        private void SetTargetTimerNormal()
        {
            newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
            timerMargin = 0;
        }

        private bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);

        protected override void Act(float deltaTime)
        {
            if (PathSteering == null) { return; }

            if (objectiveManager.FailedAutonomousObjectives)
            {
                if (autonomousObjectiveRetryTimer > 0)
                {
                    autonomousObjectiveRetryTimer -= deltaTime;
                }
                else
                {
                    objectiveManager.CreateAutonomousObjectives();
                }
            }

            //don't keep dragging others when idling
            if (character.SelectedCharacter != null)
            {
                character.DeselectCharacter();
            }

            character.SelectedItem = null;

            if (!character.IsClimbing)
            {
                CleanupItems(deltaTime);
            }

            if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
            {
                TargetHull = character.CurrentHull;
            }

            bool currentTargetIsInvalid = 
                currentTarget == null || 
                IsForbidden(currentTarget) ||
                (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));

            if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
            {
                currentTarget = TargetHull;
                bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
                if (stayInHull)
                {
                    Wander(deltaTime);
                }
                else if (currentTarget != null)
                {
                    PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
                }
                else
                {
                    PathSteering.ResetPath();
                    PathSteering.Reset();
                }
            }
            else
            {
                if (currentTarget != null && !currentTargetIsInvalid)
                {
                    if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
                    {
                        if (currentTarget.Submarine.TeamID != character.TeamID)
                        {
                            currentTargetIsInvalid = true;
                        }
                    }
                    else
                    {
                        if (currentTarget.Submarine != character.Submarine)
                        {
                            currentTargetIsInvalid = true;
                        }
                    }
                }

                if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
                {
                    if (newTargetTimer > timerMargin)
                    {
                        //don't reset to zero, otherwise the character will keep calling FindTargetHulls 
                        //almost constantly when there's a small number of potential hulls to move to
                        SetTargetTimerLow();
                    }
                }
                else if (character.IsClimbing)
                {
                    if (currentTarget == null)
                    {
                        SetTargetTimerLow();
                    }
                    else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
                    {
                        // Don't allow new targets when climbing straight up or down
                        SetTargetTimerHigh();
                    }
                }
                else if (character.AnimController.InWater)
                {
                    if (currentTarget == null)
                    {
                        SetTargetTimerLow();
                    }
                }
                if (newTargetTimer <= 0.0f)
                {
                    if (!searchingNewHull)
                    {
                        //find all available hulls first
                        searchingNewHull = true;
                        FindTargetHulls();
                    }
                    else if (targetHulls.Any())
                    {
                        //choose a random available hull
                        currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
                        bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
                        bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
                        var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node =>
                        {
                            if (node.Waypoint.CurrentHull == null) { return false; }
                            // Check that there is no unsafe hulls on the way to the target
                            if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
                            return true;
                            //don't stop at ladders when idling
                        }, endNodeFilter: node => node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
                        if (path.Unreachable)
                        {
                            //can't go to this room, remove it from the list and try another room
                            int index = targetHulls.IndexOf(currentTarget);
                            targetHulls.RemoveAt(index);
                            hullWeights.RemoveAt(index);
                            PathSteering.Reset();
                            currentTarget = null;
                            SetTargetTimerLow();
                            return;
                        }
                        character.AIController.SelectTarget(currentTarget.AiTarget);
                        PathSteering.SetPath(path);
                        SetTargetTimerNormal();
                        searchingNewHull = false;
                    }
                    else
                    {
                        // Couldn't find a valid hull
                        SetTargetTimerHigh();
                        searchingNewHull = false;
                    }
                }
                newTargetTimer -= deltaTime;
                if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
                {
                    Wander(deltaTime);
                }
                else if (currentTarget != null)
                {
                    PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, 
                        nodeFilter: node => node.Waypoint.CurrentHull != null, 
                        endNodeFilter: node => node.Waypoint.Ladders == null);
                }
                else
                {
                    PathSteering.ResetPath();
                    PathSteering.Reset();
                }
            }
        }

        public void Wander(float deltaTime)
        {
            if (character.IsClimbing)
            {
                PathSteering.Reset();
                character.StopClimbing();
            }
            var currentHull = character.CurrentHull;
            if (!character.AnimController.InWater && currentHull != null)
            {
                standStillTimer -= deltaTime;
                if (standStillTimer > 0.0f)
                {
                    walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
                    if (currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
                    {
                        foreach (Character c in Character.CharacterList)
                        {
                            if (c == character || !c.IsBot || c.CurrentHull != currentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
                            if (Vector2.DistanceSquared(c.WorldPosition, character.WorldPosition) > 60.0f * 60.0f) { continue; }
                            if ((humanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective && idleObjective.standStillTimer > 0.0f) ||
                                (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveGoTo gotoObjective && gotoObjective.IsCloseEnough))
                            {
                                //if there are characters too close on both sides, don't try to steer away from them
                                //because it'll cause the character to spaz out trying to avoid both
                                if (tooCloseCharacter != null &&
                                    Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
                                {
                                    tooCloseCharacter = null;
                                    break;
                                }
                                tooCloseCharacter = c;
                            }
                            HumanAIController.FaceTarget(c);
                        }
                    }

                    if (tooCloseCharacter != null && !tooCloseCharacter.Removed && Vector2.DistanceSquared(tooCloseCharacter.WorldPosition, character.WorldPosition) < 50.0f * 50.0f)
                    {
                        Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
                        if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
                        if (Math.Abs(diff.X) > 0 &&
                            (character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
                        {
                            // Between a wall and a character -> move away
                            tooCloseCharacter = null;
                            PathSteering.Reset();
                            standStillTimer = 0;
                            walkDuration = Math.Min(walkDuration, walkDurationMin);
                            if (Behavior != BehaviorType.StayInHull && (currentHull.Size.X < IndoorsSteeringManager.smallRoomSize || currentHull.Size.X < (IndoorsSteeringManager.smallRoomSize / 2 * Character.CharacterList.Count(c => c.CurrentHull == currentHull))))
                            {
                                // Small room -> find another
                                newTargetTimer = Math.Min(newTargetTimer, 1);
                            }
                        }
                        else
                        {
                            character.ReleaseSecondaryItem();
                            PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
                        }
                        return;
                    }
                    else
                    {
                        PathSteering.Reset();
                        tooCloseCharacter = null;
                    }

                    chairCheckTimer -= deltaTime;
                    if (chairCheckTimer <= 0.0f && character.SelectedSecondaryItem == null)
                    {
                        foreach (Item item in Item.ItemList)
                        {
                            if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; }
                            var controller = item.GetComponent<Controller>();
                            if (controller == null || controller.User != null) { continue; }
                            item.TryInteract(character, forceSelectKey: true);
                        }
                        chairCheckTimer = chairCheckInterval;
                    }
                    return;
                }
                if (standStillTimer < -walkDuration)
                {
                    standStillTimer = Rand.Range(standStillMin, standStillMax);
                }
            }

            PathSteering.Wander(deltaTime);
        }

        public void FaceTargetAndWait(ISpatialEntity target, float waitTime)
        {
            standStillTimer = waitTime;
            HumanAIController.FaceTarget(target);
            currentTarget = null;
            SetTargetTimerHigh();
        }

        private void FindTargetHulls()
        {
            targetHulls.Clear();
            hullWeights.Clear();
            foreach (var hull in Hull.HullList)
            {
                if (character.Submarine == null) { break; }
                if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
                if (hull.Submarine == null) { continue; }
                if (hull.Submarine.Info.IsRuin || hull.Submarine.Info.IsWreck) { continue; }
                if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
                {
                    if (hull.Submarine.TeamID != character.TeamID)
                    {
                        // Don't allow npcs to idle in a sub that's not in their team (like the player sub)
                        continue;
                    }
                }
                else
                {
                    if (hull.Submarine.TeamID != character.Submarine.TeamID)
                    {
                        // Don't allow to idle in the subs that are not in the same team as the current sub
                        // -> the crew ai bots can't change the sub from outpost to main sub or vice versa on their own
                        continue;
                    }
                }
                if (IsForbidden(hull)) { continue; }
                // Check that the hull is linked
                if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
                // Ignore very narrow hulls.
                if (hull.RectWidth < 200) { continue; }
                // Ignore hulls that are too low to stand inside.
                if (character.AnimController is HumanoidAnimController animController)
                {
                    if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
                    {
                        continue;
                    }
                }
                if (!targetHulls.Contains(hull))
                {
                    targetHulls.Add(hull);
                    float weight = hull.RectWidth;
                    // Prefer rooms that are closer. Avoid rooms that are not in the same level.
                    // If the behavior is active, prefer rooms that are not close.
                    float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
                    yDist = yDist > 100 ? yDist * 5 : 0;
                    float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
                    float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
                    float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
                    weight *= distanceFactor * waterFactor;
                    hullWeights.Add(weight);
                }
            }

            if (PreferredOutpostModuleTypes.Any() && character.CurrentHull != null)
            {
                for (int i = 0; i < targetHulls.Count; i++)
                {
                    if (targetHulls[i].OutpostModuleTags.Any(t => PreferredOutpostModuleTypes.Contains(t)))
                    {
                        hullWeights[i] *= Rand.Range(10.0f, 100.0f);
                    }
                }
            }
        }

        #region Cleaning
        private readonly float checkItemsInterval = 1;
        private float checkItemsTimer;
        private readonly List<Item> itemsToClean = new List<Item>();
        private readonly List<Item> ignoredItems = new List<Item>();

        private void CleanupItems(float deltaTime)
        {
            if (checkItemsTimer <= 0)
            {
                checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
                var hull = character.CurrentHull;
                if (hull != null)
                {
                    itemsToClean.Clear();
                    foreach (Item item in Item.CleanableItems)
                    {
                        if (item.CurrentHull != hull) { continue; }
                        if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
                        {
                            itemsToClean.Add(item);
                        }
                    }
                    if (itemsToClean.Any())
                    {
                        var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault();
                        if (targetItem != null)
                        {
                            var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
                            cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
                            subObjectives.Add(cleanupObjective);
                        }
                    }
                }
            }
            else
            {
                checkItemsTimer -= deltaTime;
            }
        }
        #endregion

        public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying;

        public override void Reset()
        {
            base.Reset();
            currentTarget = null;
            searchingNewHull = false;
            tooCloseCharacter = null;
            targetHulls.Clear();
            hullWeights.Clear();
            checkItemsTimer = 0;;
            itemsToClean.Clear();
            ignoredItems.Clear();
            autonomousObjectiveRetryTimer = 10;
        }

        public override void OnDeselected()
        {
            base.OnDeselected();
            foreach (var subObjective in SubObjectives)
            {
                if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
                {
                    cleanUpObjective.DropTarget();
                }
            }
        }
    }
}
