package mage.player.ai.util;

import mage.abilities.keyword.DoubleStrikeAbility;
import mage.abilities.keyword.InfectAbility;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.combat.Combat;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.turn.CombatDamageStep;
import mage.game.turn.EndOfCombatStep;
import mage.game.turn.FirstCombatDamageStep;
import mage.game.turn.Step;
import mage.players.Player;
import org.apache.log4j.Logger;

import java.util.*;

/**
 * Base helper methods for combat.
 *
 * @author noxx
 */
public final class CombatUtil {

    private static final List<Permanent> emptyList = Collections.unmodifiableList(new ArrayList<>());

    private static final Logger log = Logger.getLogger(CombatUtil.class);

    private CombatUtil() {
    }

    public static List<Permanent> canKillOpponent(Game game, List<Permanent> attackersList, List<Permanent> blockersList,
                                                  Player defender) {
        List<Permanent> blockableAttackers = new ArrayList<>(attackersList);
        List<Permanent> unblockableAttackers = new ArrayList<>();
        for (Permanent attacker : attackersList) {
            if (!canBeBlocked(game, attacker, blockersList)) {
                unblockableAttackers.add(attacker);
                blockableAttackers.remove(attacker);
            }
        }

        sortByPower(blockableAttackers, true);

        // imagine that most powerful will be blocked as 1-vs-1
        List<Permanent> attackersThatWontBeBlocked = new ArrayList<>(blockableAttackers);
        for (int i = 0; (i < blockersList.size() && i < blockableAttackers.size()); i++) {
            attackersThatWontBeBlocked.remove(blockableAttackers.get(i));
        }
        attackersThatWontBeBlocked.addAll(unblockableAttackers);

        // now count if it is possible to win the game by this attack using unblockable attackers and
        // those attackers that won't be blocked for sure (as player will block other creatures)
        if (sumDamage(attackersThatWontBeBlocked, defender) >= defender.getLife() && defender.isLifeTotalCanChange()
                && defender.canLose(game) && defender.getLife() > 0) {
            blockableAttackers.addAll(unblockableAttackers);
            return blockableAttackers;
        }

        if (sumPoisonDamage(attackersThatWontBeBlocked, defender) >= 10 - defender.getCounters().getCount(CounterType.POISON)) {
            blockableAttackers.addAll(unblockableAttackers);
            return blockableAttackers;
        }

        return emptyList;
    }

    /**
     * Checks that attacker can be blocked by any blocker from the list.
     *
     * @param game         Game
     * @param attacker     Attacker to check
     * @param blockersList Blockers to try to block the attacker with.
     * @return true if attacker can be blocked by any blocker
     */
    public static boolean canBeBlocked(Game game, Permanent attacker, List<Permanent> blockersList) {
        for (Permanent blocker : blockersList) {
            if (blocker.canBlock(attacker.getId(), game)) {
                return true;
            }
        }
        return false;
    }

    public static void sortByPower(List<Permanent> permanents, final boolean ascending) {
        Collections.sort(permanents, new Comparator<Permanent>() {
            @Override
            public int compare(Permanent o1, Permanent o2) {
                if (ascending) {
                    return o2.getPower().getValue() - o1.getPower().getValue();
                } else {
                    return o1.getPower().getValue() - o2.getPower().getValue();
                }
            }
        });
    }

    public static Permanent getWorstCreature(List<Permanent> creatures) {
        if (creatures.isEmpty()) {
            return null;
        }
        Collections.sort(creatures, new Comparator<Permanent>() {
            @Override
            public int compare(Permanent o1, Permanent o2) {
                return o2.getPower().getValue() - o1.getPower().getValue();
            }
        });
        return creatures.get(0);
    }

    private static int sumDamage(List<Permanent> attackersThatWontBeBlocked, Player defender) {
        int damage = 0;
        for (Permanent attacker : attackersThatWontBeBlocked) {
            if (!attacker.getAbilities().contains(InfectAbility.getInstance())) {
                damage += attacker.getPower().getValue();
                if (attacker.getAbilities().contains(DoubleStrikeAbility.getInstance())) {
                    damage += attacker.getPower().getValue();
                }
            }
        }
        return damage;
    }

    private static int sumPoisonDamage(List<Permanent> attackersThatWontBeBlocked, Player defender) {
        int damage = 0;
        for (Permanent attacker : attackersThatWontBeBlocked) {
            if (attacker.getAbilities().contains(InfectAbility.getInstance())) {
                damage += attacker.getPower().getValue();
                if (attacker.getAbilities().contains(DoubleStrikeAbility.getInstance())) {
                    damage += attacker.getPower().getValue();
                }
            }
        }
        return damage;
    }

    public static void handleExalted() {
    }

    /**
     * Determines what blockers from the list can block the attacker
     *
     * @param game         Game
     * @param attacker     Attacker to check
     * @param blockersList Blockers to try to block the attacker with.
     * @return true if attacker can be blocked by any blocker
     */
    public static List<Permanent> getPossibleBlockers(Game game, Permanent attacker, List<Permanent> blockersList) {
        List<Permanent> canBlock = new ArrayList<>();
        for (Permanent blocker : blockersList) {
            if (blocker.canBlock(attacker.getId(), game)) {
                canBlock.add(blocker);
            }
        }
        return canBlock;
    }

    public static CombatInfo blockWithGoodTrade(Game game, List<Permanent> attackers, List<Permanent> blockers) {

        UUID attackerId = game.getCombat().getAttackingPlayerId();
        UUID defenderId = game.getCombat().getDefenders().iterator().next();
        if (attackerId == null || defenderId == null) {
            log.warn("Couldn't find attacker or defender: " + attackerId + ' ' + defenderId);
            return new CombatInfo();
        }

        CombatInfo combatInfo = new CombatInfo();
        for (Permanent attacker : attackers) {
            //TODO: handle attackers with "can't be blocked except"
            List<Permanent> possibleBlockers = getPossibleBlockers(game, attacker, blockers);
            List<Permanent> survivedBlockers = getBlockersThatWillSurvive(game, attackerId, defenderId, attacker, possibleBlockers);
            if (!survivedBlockers.isEmpty()) {
                Permanent blocker = getWorstCreature(survivedBlockers);
                combatInfo.addPair(attacker, blocker);
                blockers.remove(blocker);
            }
            if (blockers.isEmpty()) {
                break;
            }
        }

        return combatInfo;
    }

    private static List<Permanent> getBlockersThatWillSurvive(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
        List<Permanent> blockers = new ArrayList<>();
        for (Permanent blocker : possibleBlockers) {
            SurviveInfo info = willItSurvive(game, attackerId, defenderId, attacker, blocker);
            //if (info.isAttackerDied() && !info.isBlockerDied()) {
            if (info != null) {
                if (info.isAttackerDied()) {
                    blockers.add(blocker);
                } else if (!info.isBlockerDied()) {
                    blockers.add(blocker);
                }
            }
        }
        return blockers;
    }

    public static SurviveInfo willItSurvive(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
        Game sim = game.copy();

        Combat combat = sim.getCombat();
        combat.setAttacker(attackingPlayerId);
        combat.setDefenders(sim);

        if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) {
            return null;
        }

        sim.getPlayer(defendingPlayerId).declareBlocker(defendingPlayerId, blocker.getId(), attacker.getId(), sim);
        sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defendingPlayerId, defendingPlayerId));

        sim.checkStateAndTriggered();
        while (!sim.getStack().isEmpty()) {
            sim.getStack().resolve(sim);
            sim.applyEffects();
        }
        sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId()));

        simulateStep(sim, new FirstCombatDamageStep());
        simulateStep(sim, new CombatDamageStep());
        simulateStep(sim, new EndOfCombatStep());
        // The following commented out call produces random freezes.
        //sim.checkStateAndTriggered();
        while (!sim.getStack().isEmpty()) {
            sim.getStack().resolve(sim);
            sim.applyEffects();
        }

        return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), !sim.getBattlefield().containsPermanent(blocker.getId()));
    }

    public static SurviveInfo getCombatInfo(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker) {
        Game sim = game.copy();

        Combat combat = sim.getCombat();
        combat.setAttacker(attackingPlayerId);
        combat.setDefenders(sim);

        UUID defenderId = sim.getCombat().getDefenders().iterator().next();
        boolean triggered = false;

        sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defendingPlayerId, defendingPlayerId));

        sim.checkStateAndTriggered();
        while (!sim.getStack().isEmpty()) {
            triggered = true;
            sim.getStack().resolve(sim);
            sim.applyEffects();
        }
        sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId()));

        simulateStep(sim, new FirstCombatDamageStep());
        simulateStep(sim, new CombatDamageStep());
        simulateStep(sim, new EndOfCombatStep());
        // The following commented out call produces random freezes.
        //sim.checkStateAndTriggered();
        while (!sim.getStack().isEmpty()) {
            triggered = true;
            sim.getStack().resolve(sim);
            sim.applyEffects();
        }

        return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), false, sim.getPlayer(defenderId), triggered);
    }

    protected static void simulateStep(Game game, Step step) {
        game.getPhase().setStep(step);
        if (!step.skipStep(game, game.getActivePlayerId())) {
            step.beginStep(game, game.getActivePlayerId());
            // The following commented out call produces random freezes.
            //game.checkStateAndTriggered();
            while (!game.getStack().isEmpty()) {
                game.getStack().resolve(game);
                game.applyEffects();
            }
            step.endStep(game, game.getActivePlayerId());
        }
    }

    public static boolean canBlock(Game game, Permanent blocker) {
        boolean canBlock = true;
        if (!blocker.isTapped()) {
            try {
                canBlock = blocker.canBlock(null, game);
            } catch (Exception e) {
                //e.printStackTrace();
            }
        }
        return canBlock;
    }

    public static CombatInfo blockWithGoodTrade2(Game game, List<Permanent> attackers, List<Permanent> blockers) {

        UUID attackerId = game.getCombat().getAttackingPlayerId();
        UUID defenderId = game.getCombat().getDefenders().iterator().next();
        if (attackerId == null || defenderId == null) {
            log.warn("Couldn't find attacker or defender: " + attackerId + ' ' + defenderId);
            return new CombatInfo();
        }

        CombatInfo combatInfo = new CombatInfo();
        for (Permanent attacker : attackers) {
            //TODO: handle attackers with "can't be blocked except"
            List<Permanent> possibleBlockers = getPossibleBlockers(game, attacker, blockers);
            List<Permanent> survivedBlockers = getBlockersThatWillSurvive2(game, attackerId, defenderId, attacker, possibleBlockers);
            if (!survivedBlockers.isEmpty()) {
                Permanent blocker = getWorstCreature(survivedBlockers);
                combatInfo.addPair(attacker, blocker);
                blockers.remove(blocker);
            }
            if (blockers.isEmpty()) {
                break;
            }
        }

        return combatInfo;
    }

    private static List<Permanent> getBlockersThatWillSurvive2(Game game, UUID attackerId, UUID defenderId, Permanent attacker, List<Permanent> possibleBlockers) {
        List<Permanent> blockers = new ArrayList<>();
        for (Permanent blocker : possibleBlockers) {
            SurviveInfo info = willItSurvive2(game, attackerId, defenderId, attacker, blocker);
            //if (info.isAttackerDied() && !info.isBlockerDied()) {
            if (info != null) {
                if (info.isAttackerDied()) {
                    blockers.add(blocker);
                } else if (!info.isBlockerDied()) {
                    blockers.add(blocker);
                }
            }
        }
        return blockers;
    }

    public static SurviveInfo willItSurvive2(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {

        Game sim = game.copy();

        Combat combat = sim.getCombat();
        combat.setAttacker(attackingPlayerId);
        combat.setDefenders(sim);

        if (blocker == null || attacker == null || sim.getPlayer(defendingPlayerId) == null) {
            return null;
        }

        if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) {
            sim.getBattlefield().removePermanent(blocker.getId());
        }
        if (attacker.getToughness().getValue() <= blocker.getPower().getValue()) {
            sim.getBattlefield().removePermanent(attacker.getId());
        }

        return new SurviveInfo(!sim.getBattlefield().containsPermanent(attacker.getId()), !sim.getBattlefield().containsPermanent(blocker.getId()));
    }

}
