package mage.game.stack;

import mage.MageItem;
import mage.MageObject;
import mage.abilities.Abilities;
import mage.abilities.AbilitiesImpl;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.choices.Choice;
import mage.choices.ChoiceHintType;
import mage.choices.ChoiceImpl;
import mage.constants.Outcome;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game;
import mage.game.events.CopyStackObjectEvent;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetAmount;
import mage.util.CardUtil;
import mage.util.functions.StackObjectCopyApplier;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author LevelX2
 */
public abstract class StackObjectImpl implements StackObject {

    protected boolean targetChanged; // for Psychic Battle

    @Override
    public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
        createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1);
    }

    @Override
    public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) {
        createCopyOnStack(game, source, newControllerId, chooseNewTargets, amount, null);
    }

    /**
     * Copy logic:
     * - multiple copies allows
     * - new targets for each copy allows
     * - each next copy can have new target type/filter
     * - player can choose copies order on stack
     * <p>
     * Code logic:
     * - find all possible target types (any target or custom)
     * - user can choose next target type to put on stack
     * - put all copies with that target type and request next choice
     * - implemented by iterator mechanic (target type -> target filter)
     */
    private static final class NewTargetTypeIterator implements Iterator<MageObjectReferencePredicate> {

        private final StackObjectCopyApplier applier;
        private final Player player;
        private final int amount;
        private final Game game;
        private Map<String, MageObjectReferencePredicate> newTargetTypes = null;
        private Iterator<MageObjectReferencePredicate> currentNewTargetType = null;
        private Choice newTargetTypeChoiceDialog = null;

        private NewTargetTypeIterator(Game game, UUID newControllerId, int amount, StackObjectCopyApplier applier) {
            this.applier = applier;
            this.player = game.getPlayer(newControllerId);
            this.amount = amount;
            this.game = game;
        }

        @Override
        public boolean hasNext() {
            return true;
        }

        private void prepareNewTargetTypes() {
            if (newTargetTypes != null) {
                // already prepared
                return;
            }

            int currentAnyTargetNumber = 0;
            int currentFilteredTargetNumber = 0;

            newTargetTypes = new HashMap<>();
            for (int i = 0; i < amount; i++) {
                MageObjectReferencePredicate newTargetType = applier.getNextNewTargetType();
                if (newTargetType == null) {
                    currentAnyTargetNumber++;
                    String message = "Any target";
                    if (currentAnyTargetNumber > 1) {
                        message += " (" + currentAnyTargetNumber + ")";
                    }
                    newTargetTypes.put(message, null);
                    continue;
                }
                currentFilteredTargetNumber++;
                newTargetTypes.put(newTargetType.getName(game), newTargetType);
            }

            // if only one target type then choose it by default
            if ((currentFilteredTargetNumber == 1 && currentAnyTargetNumber == 0) || currentFilteredTargetNumber == 0) {
                currentNewTargetType = newTargetTypes.values().stream().collect(Collectors.toList()).iterator();
            }
        }

        private void prepareFilterChooseDialog() {
            // used choices removes from the dialog
            if (newTargetTypeChoiceDialog != null) {
                // already prepared
                return;
            }
            newTargetTypeChoiceDialog = new ChoiceImpl(false, ChoiceHintType.CARD);
            newTargetTypeChoiceDialog.setMessage("Choose the order of copies to go on the stack");
            newTargetTypeChoiceDialog.setSubMessage("Press cancel to put the rest in any order");
            newTargetTypeChoiceDialog.setChoices(new HashSet<>(newTargetTypes.keySet()));
        }

        @Override
        public MageObjectReferencePredicate next() {
            if (player == null || applier == null) {
                return null;
            }

            prepareNewTargetTypes();
            if (currentNewTargetType != null) {
                // target type already selected
                return currentNewTargetType.hasNext() ? currentNewTargetType.next() : null;
            }

            prepareFilterChooseDialog();
            if (newTargetTypeChoiceDialog.getChoices().size() < 2) {
                // no more unused target types - select the last one
                currentNewTargetType = newTargetTypeChoiceDialog.getChoices().stream().map(newTargetTypes::get).iterator();
                return next();
            }

            // choose next target type for usage
            newTargetTypeChoiceDialog.clearChoice();
            player.choose(Outcome.AIDontUseIt, newTargetTypeChoiceDialog, game);
            String chosen = newTargetTypeChoiceDialog.getChoice();
            if (chosen == null) {
                currentNewTargetType = newTargetTypeChoiceDialog.getChoices().stream().map(newTargetTypes::get).iterator();
                return next();
            }
            newTargetTypeChoiceDialog.getChoices().remove(chosen);
            return newTargetTypes.get(chosen);
        }
    }

    @Override
    public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount, StackObjectCopyApplier applier) {
        GameEvent gameEvent = new CopyStackObjectEvent(source, this, newControllerId, amount);
        if (game.replaceEvent(gameEvent)) {
            return;
        }
        Iterator<MageObjectReferencePredicate> newTargetTypeIterator = new NewTargetTypeIterator(game, newControllerId, gameEvent.getAmount(), applier);
        for (int i = 0; i < gameEvent.getAmount(); i++) {
            createSingleCopy(newControllerId, applier, newTargetTypeIterator.next(), game, source, chooseNewTargets);
        }
        Player player = game.getPlayer(newControllerId);
        if (player == null) {
            return;
        }
        game.informPlayers(
                player.getName() + " created " + CardUtil.numberToText(gameEvent.getAmount(), "a")
                        + " cop" + (gameEvent.getAmount() == 1 ? "y" : "ies") + " of " + getIdName()
        );
    }

    /**
     * Choose new targets for a stack Object
     *
     * @param game
     * @param playerId Player UUID who changes the targets.
     * @return
     */
    public boolean chooseNewTargets(Game game, UUID playerId) {
        return chooseNewTargets(game, playerId, false, false, null);
    }

    /**
     * 114.6. Some effects allow a player to change the target(s) of a spell or
     * ability, and other effects allow a player to choose new targets for a
     * spell or ability.
     * <p>
     * 114.6a If an effect allows a player to "change the target(s)" of a spell
     * or ability, each target can be changed only to another legal target. If a
     * target can't be changed to another legal target, the original target is
     * unchanged, even if the original target is itself illegal by then. If all
     * the targets aren't changed to other legal targets, none of them are
     * changed.
     * <p>
     * 114.6b If an effect allows a player to "change a target" of a spell or
     * ability, the process described in rule 114.6a is followed, except that
     * only one of those targets may be changed (rather than all of them or none
     * of them).
     * <p>
     * 114.6c If an effect allows a player to "change any targets" of a spell or
     * ability, the process described in rule 114.6a is followed, except that
     * any number of those targets may be changed (rather than all of them or
     * none of them).
     * <p>
     * 114.6d If an effect allows a player to "choose new targets" for a spell
     * or ability, the player may leave any number of the targets unchanged,
     * even if those targets would be illegal. If the player chooses to change
     * some or all of the targets, the new targets must be legal and must not
     * cause any unchanged targets to become illegal.
     * <p>
     * 114.6e When changing targets or choosing new targets for a spell or
     * ability, only the final set of targets is evaluated to determine whether
     * the change is legal.
     * <p>
     * Example: Arc Trail is a sorcery that reads "Arc Trail deals 2 damage to
     * any target and 1 damage to another target creature or player." The
     * current targets of Arc Trail are Runeclaw Bear and Llanowar Elves, in
     * that order. You cast Redirect, an instant that reads "You may choose new
     * targets for target spell," targeting Arc Trail. You can change the first
     * target to Llanowar Elves and change the second target to Runeclaw Bear.
     * <p>
     * 114.7. Modal spells and abilities may have different targeting
     * requirements for each mode. An effect that allows a player to change the
     * target(s) of a modal spell or ability, or to choose new targets for a
     * modal spell or ability, doesn't allow that player to change its mode.
     * (See rule 700.2.)
     * <p>
     * 706.10c Some effects copy a spell or ability and state that its
     * controller may choose new targets for the copy. The player may leave any
     * number of the targets unchanged, even if those targets would be illegal.
     * If the player chooses to change some or all of the targets, the new
     * targets must be legal. Once the player has decided what the copy's
     * targets will be, the copy is put onto the stack with those targets.
     *
     * @param game
     * @param targetControllerId       - player that can/has to change the target of
     *                                 the spell
     * @param forceChange              - does only work for targets with maximum of one
     *                                 targetId
     * @param onlyOneTarget            - 114.6b one target must be changed to another
     *                                 target
     * @param newTargetFilterPredicate restriction for the new target (null - can select same targets)
     * @return
     */
    @Override
    public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, Predicate<MageItem> newTargetFilterPredicate) {
        Player targetController = game.getPlayer(targetControllerId);
        if (targetController != null) {
            StringBuilder oldTargetDescription = new StringBuilder();
            StringBuilder newTargetDescription = new StringBuilder();
            // Fused split spells or spells where "Splice on Arcane" was used can have more than one ability
            Abilities<Ability> objectAbilities = new AbilitiesImpl<>();
            if (this instanceof Spell) {
                objectAbilities.addAll(((Spell) this).getSpellAbilities());
            } else {
                objectAbilities.add(getStackAbility());
            }
            for (Ability ability : objectAbilities) {
                // Some spells can have more than one mode
                for (UUID modeId : ability.getModes().getSelectedModes()) {
                    Mode mode = ability.getModes().get(modeId);
                    ability.getModes().setActiveMode(mode);
                    oldTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game));
                    for (Target target : mode.getTargets()) {
                        Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, newTargetFilterPredicate, game);
                        // clear the old target and copy all targets from new target
                        target.clearChosen();
                        for (UUID targetId : newTarget.getTargets()) {
                            target.addTarget(targetId, newTarget.getTargetAmount(targetId), ability, game, false);
                        }

                    }
                    newTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game));
                }

            }
            if (!newTargetDescription.toString().equals(oldTargetDescription.toString()) && !game.isSimulation()) {
                game.informPlayers(this.getLogName() + " is now " + newTargetDescription);
            }
            return true;
        }
        return false;
    }

    /**
     * Handles the change of one target instance of a mode
     *
     * @param targetController         player that can choose the new target
     * @param ability
     * @param mode
     * @param target                   original target
     * @param forceChange
     * @param newTargetFilterPredicate additional filter for target selection (e.g. force to select 1 specific permanent)
     * @param game
     * @return
     */
    private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, Predicate newTargetFilterPredicate, Game game) {
        Target newTarget = target.copy();
        newTarget.clearChosen();

        // prepare temp target

        // additional filter for possible targets
        if (newTargetFilterPredicate != null) {
            newTarget.getFilter().add(newTargetFilterPredicate);
            // if adding a predicate, there will only be one choice and therefore target can be automatic
            // TODO: it's a card specific code like Radiate and limit usage of newTargetFilterPredicate (only for 1 to 1 target replace),
            //  so must must be refactored to a standard way to check like possibleTargets/currentTargets compare
            newTarget.setRandom(true);
        }
        // TODO: new targets must be selected without TARGETED events - is it ok by rules (triggers like whenever xxx becomes the target)?
        newTarget.setEventReporting(false);
        if (!targetController.getId().equals(getControllerId())) {
            newTarget.setTargetController(targetController.getId()); // target controller for the change is different from spell controller
            newTarget.setAbilityController(getControllerId());
        }

        // try to replace each targeted with a new
        // * target - original target, do not change it
        // * newTarget - final target, must store result from a tempTarget, do not clear it
        // * tempTarget - temporary target for choosing a single replacement
        int targetNumber = 0;
        for (UUID oldTargetId : target.getTargets()) {
            targetNumber++;
            String targetName = getNameOftarget(oldTargetId, game);
            String targetAmount = "";
            if (target.getTargetAmount(oldTargetId) > 0) {
                targetAmount = " (amount: " + target.getTargetAmount(oldTargetId) + ")";
            }

            Outcome outcome = mode.getEffects().getOutcome(ability);
            if (targetName != null
                    && (forceChange || targetController.chooseUse(
                    outcome,
                    String.format("Change this %d of %d target: %s?",
                            targetNumber,
                            target.getTargets().size(),
                            targetName + targetAmount),
                    ability,
                    game))) {

                // try single target replacement by a new
                Set<UUID> possibleTargets = newTarget.possibleTargets(getControllerId(), ability, game);
                if (forceChange && possibleTargets != null && possibleTargets.size() > 1) { // controller of spell must be used (e.g. TargetOpponent)
                    // required replacement of 1 target
                    int iteration = 0;
                    do {
                        if (iteration > 0 && !game.isSimulation()) {
                            game.informPlayer(targetController, "You may only select exactly one target that must be different from the origin target!");
                        }
                        iteration++;

                        // TODO: bugged, must use tempTarget here also must keep amount value?
                        newTarget.clearChosen();
                        newTarget.chooseTarget(outcome, getControllerId(), ability, game);

                        // workaround to stop infinite AI choose (remove after chooseTarget can be called with extra filter to disable some ids)
                        if (iteration > 10) {
                            break;
                        }
                    }
                    while (targetController.canRespond() && (oldTargetId.equals(newTarget.getFirstTarget()) || newTarget.getTargets().size() != 1));
                } else {
                    // optional replacement of 1 target (amount value must be same)

                    // prepare temp target
                    // TODO: must use shared code for target prepare (see comments at the start)
                    Target tempTarget = target.copy();
                    if (newTargetFilterPredicate != null) {
                        tempTarget.getFilter().add(newTargetFilterPredicate);
                        // If adding a predicate, there will only be one choice and therefore target can be automatic
                        tempTarget.setRandom(true);
                    }
                    tempTarget.setEventReporting(false);
                    if (target instanceof TargetAmount) {
                        ((TargetAmount) tempTarget).setAmountDefinition(StaticValue.get(target.getTargetAmount(oldTargetId)));
                    }
                    tempTarget.setMinNumberOfTargets(1);
                    tempTarget.setMaxNumberOfTargets(1);
                    if (!targetController.getId().equals(getControllerId())) {
                        tempTarget.setTargetController(targetController.getId());
                        tempTarget.setAbilityController(getControllerId());
                    }

                    boolean again;
                    do {
                        // TODO: bugged, must have workaround for infinite AI choose (see above)
                        again = false;
                        tempTarget.clearChosen();
                        if (!tempTarget.chooseTarget(outcome, getControllerId(), ability, game)) {
                            if (targetController.chooseUse(Outcome.Benefit, "No target object selected. Reset to original target?", ability, game)) {
                                // use previous target no target was selected
                                newTarget.addTarget(oldTargetId, target.getTargetAmount(oldTargetId), ability, game, true);
                            } else {
                                again = true;
                            }
                        } else {
                            // if possible add the alternate Target - it may not be included in the old definition nor in the already selected targets of the new definition
                            if (newTarget.getTargets().contains(tempTarget.getFirstTarget()) || target.getTargets().contains(tempTarget.getFirstTarget())) {
                                if (targetController.isHuman()) {
                                    if (targetController.chooseUse(Outcome.Benefit, "This target was already selected from origin spell. Reset to original target?", ability, game)) {
                                        // use previous target no target was selected
                                        newTarget.addTarget(oldTargetId, target.getTargetAmount(oldTargetId), ability, game, true);
                                    } else {
                                        again = true;
                                    }
                                } else {
                                    newTarget.addTarget(oldTargetId, target.getTargetAmount(oldTargetId), ability, game, true);
                                }
                            } else if (!target.canTarget(getControllerId(), tempTarget.getFirstTarget(), ability, game)) {
                                // TODO: bugged, must use newTarget.canTarget instead target.canTarget?
                                if (targetController.isHuman()) {
                                    game.informPlayer(targetController, "This target is not valid!");
                                    again = true;
                                } else {
                                    // keep the old
                                    newTarget.addTarget(oldTargetId, target.getTargetAmount(oldTargetId), ability, game, true);
                                }
                            } else {
                                // valid target was selected, add it to the new target definition
                                newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(oldTargetId), ability, game, true);
                            }
                        }
                    } while (again && targetController.canRespond());
                }
            } else {
                // keep old target
                newTarget.addTarget(oldTargetId, target.getTargetAmount(oldTargetId), ability, game, true);
            }
        }
        return newTarget;
    }

    @Override
    public boolean canTarget(Game game, UUID targetId) {
        Abilities<Ability> objectAbilities = new AbilitiesImpl<>();
        if (this instanceof Spell) {
            objectAbilities.addAll(((Spell) this).getSpellAbilities());
        } else {
            objectAbilities.add(getStackAbility());
        }
        for (Ability ability : objectAbilities) {
            if (ability.getModes()
                    .getSelectedModes()
                    .stream()
                    .map(ability.getModes()::get)
                    .filter(Objects::nonNull)
                    .map(Mode::getTargets)
                    .flatMap(Collection::stream)
                    .filter(t -> !t.isNotTarget())
                    .anyMatch(t -> t.canTarget(ability.getControllerId(), targetId, ability, game))) {
                return true;
            }
        }
        return false;
    }

    private String getNameOftarget(UUID targetId, Game game) {
        Player targetPlayer = game.getPlayer(targetId);
        if (targetPlayer != null) {
            return targetPlayer.getLogName();
        }

        MageObject object = game.getObject(targetId);
        if (object != null) {
            return object.getIdName();
        }

        return null;
    }

    @Override
    public void removePTCDA() {
    }

    @Override
    public boolean isTargetChanged() {
        return targetChanged;
    }

    @Override
    public void setTargetChanged(boolean targetChanged) {
        this.targetChanged = targetChanged;
    }
}
