using PkmnLib.Dynamic.Models;
using PkmnLib.Static.Moves;

namespace PkmnLib.Dynamic.BattleFlow;

/// <summary>
/// Helper class for resolving the targets of a move.
/// </summary>
public static class TargetResolver
{
    /// <summary>
    /// Get the targets of a move based on the target type, and the selected side and position to target.
    /// </summary>
    public static IReadOnlyList<IPokemon?> ResolveTargets(IBattle battle, byte side, byte position, MoveTarget target)
    {
        return target switch
        {
            MoveTarget.Adjacent or MoveTarget.AdjacentAlly or MoveTarget.AdjacentAllySelf or MoveTarget.AdjacentOpponent
                or MoveTarget.Any or MoveTarget.RandomOpponent or MoveTarget.SelfUse =>
                [battle.GetPokemon(side, position)],
            MoveTarget.All => GetAllTargets(battle),
            MoveTarget.AllAdjacentOpponent => GetAllAdjacentAndOpponent(battle, side, position),
            MoveTarget.AllAdjacent => GetAllAdjacent(battle, side, position),
            MoveTarget.AllAlly or MoveTarget.AllOpponent => battle.Sides[side].Pokemon.ToList(),
            _ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
        };
    }

    /// <summary>
    /// Validates whether a given target is valid for a move choice. Returns true if the target is valid.
    /// </summary>
    public static bool IsValidTarget(byte side, byte position, MoveTarget target, IPokemon user)
    {
        var userBattleData = user.BattleData;
        if (userBattleData == null)
            throw new ArgumentNullException(nameof(user.BattleData));
        var userSide = userBattleData.SideIndex;
        var userPosition = userBattleData.Position;

        switch (target)
        {
            case MoveTarget.Adjacent:
            case MoveTarget.AllAdjacent:
            {
                var diff = Math.Abs(position - userPosition);
                if (diff == 0)
                    return userSide == side;
                return diff <= 1;
            }
            case MoveTarget.AdjacentAlly:
            {
                if (userSide != side)
                    return false;
                return Math.Abs(position - userPosition) == 1;
            }
            case MoveTarget.AdjacentAllySelf:
            {
                if (userSide != side)
                    return false;
                return Math.Abs(position - userPosition) <= 1;
            }
            case MoveTarget.AdjacentOpponent:
            case MoveTarget.AllAdjacentOpponent:
            {
                if (userSide == side)
                    return false;
                return Math.Abs(position - userPosition) <= 1;
            }
            case MoveTarget.All:
            case MoveTarget.Any:
            case MoveTarget.RandomOpponent:
                return true;
            case MoveTarget.AllAlly:
                return userSide == side;
            case MoveTarget.AllOpponent:
                return userSide != side;
            case MoveTarget.SelfUse:
                return userSide == side && userPosition == position;
        }
        throw new ArgumentOutOfRangeException(nameof(target), target, null);
    }

    private static IReadOnlyList<IPokemon?> GetAllTargets(IBattle battle) =>
        battle.Sides.SelectMany(x => x.Pokemon).ToList();

    private static byte GetOppositeSide(byte side) => side == 0 ? (byte)1 : (byte)0;

    /// <summary>
    /// Gets all Pokémon that are adjacent to of directly opposite of a Pokémon. This means the target,
    /// the Pokémon left of it, the Pokémon right of it, and the Pokémon opposite of it.
    /// </summary>
    private static IReadOnlyList<IPokemon?> GetAllAdjacentAndOpponent(IBattle battle, byte side, byte position)
    {
        var left = position - 1;
        var right = position + 1;
        if (left < 0 && right >= battle.PositionsPerSide)
        {
            return [battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position)];
        }

        if (left < 0)
        {
            return
            [
                battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position),
                battle.GetPokemon(side, (byte)right),
            ];
        }

        if (right >= battle.PositionsPerSide)
        {
            return
            [
                battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position),
                battle.GetPokemon(side, (byte)left),
            ];
        }

        return
        [
            battle.GetPokemon(side, position), battle.GetPokemon(GetOppositeSide(side), position),
            battle.GetPokemon(side, (byte)left), battle.GetPokemon(side, (byte)right),
        ];
    }

    private static IReadOnlyList<IPokemon?> GetAllAdjacent(IBattle battle, byte side, byte position)
    {
        var left = position - 1;
        var right = position + 1;
        if (left < 0 && right >= battle.PositionsPerSide)
        {
            return [battle.GetPokemon(side, position)];
        }

        if (left < 0)
        {
            return [battle.GetPokemon(side, position), battle.GetPokemon(side, (byte)right)];
        }

        if (right >= battle.PositionsPerSide)
        {
            return [battle.GetPokemon(side, position), battle.GetPokemon(side, (byte)left)];
        }

        return
        [
            battle.GetPokemon(side, position), battle.GetPokemon(side, (byte)left),
            battle.GetPokemon(side, (byte)right),
        ];
    }
}