using System.Collections.Generic;
using System.Linq;
using Schema;
using UnityEngine;
using UnityEngine.AI;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Util;

namespace VOID.Generic.AI.Schema.Actions
{
    [Description("Gets unit by selected filters and save it as blackboard variable")]
    [Category(SchemaCategory.CombatThisUnit)]
    public class GetUnit : Action
    {
        private enum UnitType
        {
            Any,
            Enemy,
            Neutral,
            Friendly
        }

        private enum SearchType
        {
            ClosestByDistance,
            FarthestByDistance,
            Random,
            LeastHp,
            MostHp,
            ClosestByPath,
            FarthestByPath
        }

        [SerializeField] private UnitType _findUnitBy;
        [SerializeField] private SearchType _searchBy;
        [SerializeField, Space(15)] private BlackboardEntrySelector<UnitObject> _saveTo;

        public override NodeStatus Tick(object nodeMemory, SchemaAgent agent)
        {
            var thisUnit = ((UnitObjectSchemaAgent)agent).unit;
            if (!thisUnit.basicState.turnManager) return NodeStatus.Failure;

            // Get all participants in combat except current unit
            var allUnits = thisUnit.basicState.turnManager.allUnitsInQueue.Where(otherUnit => otherUnit != thisUnit)
                .ToList();

            // Filter by UnitType
            if (_findUnitBy is UnitType.Enemy) allUnits = GetEnemy(thisUnit, allUnits);
            if (_findUnitBy is UnitType.Neutral) allUnits = GetNeutral(thisUnit, allUnits);
            if (_findUnitBy is UnitType.Friendly) allUnits = GetFriendly(thisUnit, allUnits);

            // Filter by SearchType
            if (_searchBy is SearchType.ClosestByDistance) _saveTo.value = GetClosestByDistance(thisUnit, allUnits);
            if (_searchBy is SearchType.FarthestByDistance) _saveTo.value = GetFarthestByDistance(thisUnit, allUnits);
            if (_searchBy is SearchType.Random) _saveTo.value = RandomUtil.RandomElement(allUnits);
            if (_searchBy is SearchType.LeastHp) _saveTo.value = GetByLeastHp(allUnits);
            if (_searchBy is SearchType.MostHp) _saveTo.value = GetByMostHp(allUnits);
            if (_searchBy is SearchType.ClosestByPath) _saveTo.value = GetClosestByPath(thisUnit, allUnits);
            if (_searchBy is SearchType.FarthestByPath) _saveTo.value = GetFarthestByPath(thisUnit, allUnits);

            // Could not find anything
            if (!_saveTo.value) return NodeStatus.Failure;

            return NodeStatus.Success;
        }

        private List<UnitObject> GetEnemy(UnitObject thisUnit, List<UnitObject> units)
        {
            return units.Where(otherUnit => thisUnit.unitAttitude.GetAttitudeWith(otherUnit) < AttitudeType.Neutral)
                .ToList();
        }

        private List<UnitObject> GetNeutral(UnitObject thisUnit, List<UnitObject> units)
        {
            return units.Where(otherUnit => thisUnit.unitAttitude.GetAttitudeWith(otherUnit) == AttitudeType.Neutral)
                .ToList();
        }

        private List<UnitObject> GetFriendly(UnitObject thisUnit, List<UnitObject> units)
        {
            return units.Where(otherUnit => thisUnit.unitAttitude.GetAttitudeWith(otherUnit) > AttitudeType.Neutral)
                .ToList();
        }

        private UnitObject GetClosestByDistance(UnitObject thisUnit, List<UnitObject> units)
        {
            var closestDistance = float.MaxValue;
            var closestUnit = (UnitObject)null;
            units.ForEach(otherUnit =>
            {
                var distance = RangeUtil.GetDistance(thisUnit, otherUnit);
                if (distance >= closestDistance) return;
                closestDistance = distance;
                closestUnit = otherUnit;
            });
            return closestUnit;
        }

        private UnitObject GetFarthestByDistance(UnitObject thisUnit, List<UnitObject> units)
        {
            var farthestDistance = float.MinValue;
            var farthestUnit = (UnitObject)null;
            units.ForEach(otherUnit =>
            {
                var distance = RangeUtil.GetDistance(thisUnit, otherUnit);
                if (distance <= farthestDistance) return;
                farthestDistance = distance;
                farthestUnit = otherUnit;
            });
            return farthestUnit;
        }

        private UnitObject GetClosestByPath(UnitObject thisUnit, List<UnitObject> units)
        {
            var closestPathLength = float.MaxValue;
            var closestUnit = (UnitObject)null;
            units.ForEach(otherUnit =>
            {
                thisUnit.unitNavAgent.CalculatePathTo(otherUnit.transform.position);
                var path = thisUnit.unitNavAgent.GetPath();
                var pathLength = NavMeshPathUtil.GetPathLength(path);
                if (path.status is not NavMeshPathStatus.PathInvalid)
                    pathLength += Vector3.Distance(path.corners.Last(), otherUnit.transform.position);
                if (pathLength >= closestPathLength) return;
                closestPathLength = pathLength;
                closestUnit = otherUnit;
            });
            return closestUnit;
        }

        private UnitObject GetFarthestByPath(UnitObject thisUnit, List<UnitObject> units)
        {
            var farthestPathLength = float.MinValue;
            var farthestUnit = (UnitObject)null;
            units.ForEach(otherUnit =>
            {
                thisUnit.unitNavAgent.CalculatePathTo(otherUnit.transform.position);
                var path = thisUnit.unitNavAgent.GetPath();
                var pathLength = NavMeshPathUtil.GetPathLength(path);
                if (path.status is not NavMeshPathStatus.PathInvalid)
                    pathLength += Vector3.Distance(path.corners.Last(), otherUnit.transform.position);
                if (pathLength <= farthestPathLength) return;
                farthestPathLength = pathLength;
                farthestUnit = otherUnit;
            });
            return farthestUnit;
        }

        private UnitObject GetByLeastHp(List<UnitObject> units)
        {
            var leastHp = float.MinValue;
            var leastHpUnit = (UnitObject)null;
            units.ForEach(otherUnit =>
            {
                var hp = otherUnit.basicData.currentResources.health;
                if (hp >= leastHp) return;
                leastHp = hp;
                leastHpUnit = otherUnit;
            });
            return leastHpUnit;
        }

        private UnitObject GetByMostHp(List<UnitObject> units)
        {
            var mostHp = float.MaxValue;
            var mostHpUnit = (UnitObject)null;
            units.ForEach(otherUnit =>
            {
                var hp = otherUnit.basicData.currentResources.health;
                if (hp <= mostHp) return;
                mostHp = hp;
                mostHpUnit = otherUnit;
            });
            return mostHpUnit;
        }
    }
}