package mage.game;

import mage.abilities.Ability;
import mage.abilities.keyword.TransformAbility;
import mage.cards.*;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.command.Commander;
import mage.game.events.ZoneChangeEvent;
import mage.game.events.ZoneChangeGroupEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentMeld;
import mage.game.permanent.PermanentToken;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.TargetCard;

import java.util.*;

/**
 * Created by samuelsandeen on 9/6/16.
 */
public final class ZonesHandler {

    public static boolean cast(ZoneChangeInfo info, Ability source, Game game) {
        if (maybeRemoveFromSourceZone(info, game, source)) {
            placeInDestinationZone(info, 0, source, game);
            // create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx))
            Set<Card> cards = new HashSet<>();
            Set<PermanentToken> tokens = new HashSet<>();
            Card targetCard = getTargetCard(game, info.event.getTargetId());
            if (targetCard instanceof PermanentToken) {
                tokens.add((PermanentToken) targetCard);
            } else {
                cards.add(targetCard);
            }
            game.fireEvent(new ZoneChangeGroupEvent(
                    cards,
                    tokens,
                    info.event.getSourceId(),
                    info.event.getSource(),
                    info.event.getPlayerId(),
                    info.event.getFromZone(),
                    info.event.getToZone()));
            // normal movement
            game.fireEvent(info.event);
            return true;
        }
        return false;
    }

    public static boolean moveCard(ZoneChangeInfo info, Game game, Ability source) {
        List<ZoneChangeInfo> list = new ArrayList<>();
        list.add(info);
        return !moveCards(list, source, game).isEmpty();
    }

    public static List<ZoneChangeInfo> moveCards(List<ZoneChangeInfo> zoneChangeInfos, Ability source, Game game) {
        // handle unmelded meld cards (if something moved a melded card to non-battlefield then parts must be moved too)
        for (ListIterator<ZoneChangeInfo> itr = zoneChangeInfos.listIterator(); itr.hasNext(); ) {
            ZoneChangeInfo info = itr.next();
            if (info.event.getToZone().equals(Zone.BATTLEFIELD)) {
                continue;
            }
            MeldCard card = game.getMeldCard(info.event.getTargetId());
            // Copies should be handled as normal cards.
            if (card != null && !card.isMelded(game) && !card.isCopy()) {
                // TODO: WTF, never worked code here. Need research possible typo: !card.isMelded(game) -> card.isMelded(game)
                ZoneChangeInfo.Unmelded unmelded = new ZoneChangeInfo.Unmelded(info, game);
                if (unmelded.additionalMoves.isEmpty()) {
                    // already moved halves somehow
                    itr.remove();
                } else {
                    itr.set(unmelded);
                }
            }
        }

        // process Modal Double Faces cards (e.g. put card from hand)
        // rules:
        // If an effect puts a double-faced card onto the battlefield, it enters with its front face up.
        // If that front face can’t be put onto the battlefield, it doesn’t enter the battlefield.
        // For example, if an effect exiles Sejiri Glacier and returns it to the battlefield,
        // it remains in exile because an instant can’t be put onto the battlefield.
        for (ListIterator<ZoneChangeInfo> itr = zoneChangeInfos.listIterator(); itr.hasNext(); ) {
            ZoneChangeInfo info = itr.next();
            if (info.event.getToZone().equals(Zone.BATTLEFIELD)) {
                Card card = game.getCard(info.event.getTargetId());
                if (card instanceof ModalDoubleFacedCard || card instanceof ModalDoubleFacedCardHalf) {
                    boolean forceToMainSide = false;

                    // if effect put half mdf card to battlefield then it must be the main side only (example: return targeted half card to battle)
                    if (card instanceof ModalDoubleFacedCardHalf && !source.getAbilityType().isPlayCardAbility()) {
                        forceToMainSide = true;
                    }

                    // if effect put mdf card to battlefield then it must be main side only
                    if (card instanceof ModalDoubleFacedCard) {
                        forceToMainSide = true;
                    }

                    if (forceToMainSide) {
                        info.event.setTargetId(((ModalDoubleFacedCard) card.getMainCard()).getLeftHalfCard().getId());
                    }
                }
            }
        }

        zoneChangeInfos.removeIf(zoneChangeInfo -> !maybeRemoveFromSourceZone(zoneChangeInfo, game, source));
        int createOrder = 0;
        for (ZoneChangeInfo zoneChangeInfo : zoneChangeInfos) {
            if (createOrder == 0 && Zone.BATTLEFIELD.equals(zoneChangeInfo.event.getToZone())) {
                // All permanents go to battlefield at the same time (=create order)
                createOrder = game.getState().getNextPermanentOrderNumber();
            }
            placeInDestinationZone(zoneChangeInfo, createOrder, source, game);
            if (game.getPhase() != null) { // moving cards to zones before game started does not need events
                game.addSimultaneousEvent(zoneChangeInfo.event);
            }
        }
        return zoneChangeInfos;
    }

    private static void placeInDestinationZone(ZoneChangeInfo info, int createOrder, Ability source, Game game) {
        // Handle unmelded cards
        if (info instanceof ZoneChangeInfo.Unmelded) {
            ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info;
            Zone toZone = null;
            for (ZoneChangeInfo additionalMove : unmelded.additionalMoves) {
                toZone = additionalMove.event.getToZone();
                placeInDestinationZone(additionalMove, createOrder, source, game);
            }
            // We arbitrarily prefer the bottom half card. This should never be relevant.
            if (toZone != null) {
                game.setZone(unmelded.event.getTargetId(), toZone);
            }
            return;
        }
        // Handle normal cases
        ZoneChangeEvent event = info.event;
        Zone toZone = event.getToZone();
        Card targetCard = getTargetCard(game, event.getTargetId());

        Cards cardsToMove = null; // moving real cards
        Map<Zone, Cards> cardsToUpdate = new LinkedHashMap<>(); // updating all card's parts (must be ordered LinkedHashMap)
        cardsToUpdate.put(toZone, new CardsImpl());
        cardsToUpdate.put(Zone.OUTSIDE, new CardsImpl());
        // if we're moving a token it shouldn't be put into any zone as an object.
        if (!(targetCard instanceof Permanent) && targetCard != null) {
            if (targetCard instanceof MeldCard) {
                // meld/group cards must be independent (use can choose order)
                cardsToMove = ((MeldCard) targetCard).getHalves();
                cardsToUpdate.get(toZone).addAll(cardsToMove);
            } else if (targetCard instanceof ModalDoubleFacedCard
                    || targetCard instanceof ModalDoubleFacedCardHalf) {
                // mdf cards must be moved as single object, but each half must be updated separately
                ModalDoubleFacedCard mdfCard = (ModalDoubleFacedCard) targetCard.getMainCard();
                cardsToMove = new CardsImpl(mdfCard);
                cardsToUpdate.get(toZone).add(mdfCard);
                // example: cast left side
                // result:
                // * main to battlefield
                // * left to battlefield
                // * right to outside (it helps to ignore all triggers and other effects from that card)
                switch (toZone) {
                    case STACK:
                    case BATTLEFIELD:
                        if (targetCard.getId().equals(mdfCard.getLeftHalfCard().getId())) {
                            // play left
                            cardsToUpdate.get(toZone).add(mdfCard.getLeftHalfCard());
                            cardsToUpdate.get(Zone.OUTSIDE).add(mdfCard.getRightHalfCard());
                        } else if (targetCard.getId().equals(mdfCard.getRightHalfCard().getId())) {
                            // play right
                            cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard());
                            cardsToUpdate.get(Zone.OUTSIDE).add(mdfCard.getLeftHalfCard());
                        } else {
                            // cast mdf (only on stack)
                            if (!toZone.equals(Zone.STACK)) {
                                throw new IllegalStateException("Wrong mdf card move to " + toZone + " in placeInDestinationZone");
                            }
                            cardsToUpdate.get(toZone).add(mdfCard.getLeftHalfCard());
                            cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard());
                        }
                        break;
                    default:
                        // move all parts
                        cardsToUpdate.get(toZone).add(mdfCard.getLeftHalfCard());
                        cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard());
                        break;
                }
            } else {
                cardsToMove = new CardsImpl(targetCard);
                cardsToUpdate.get(toZone).addAll(cardsToMove);
            }

            Player owner = game.getPlayer(targetCard.getOwnerId());
            switch (toZone) {
                case HAND:
                    for (Card card : cardsToMove.getCards(game)) {
                        game.getPlayer(card.getOwnerId()).getHand().add(card);
                    }
                    break;
                case GRAVEYARD:
                    for (Card card : chooseOrder(
                            "order to put in graveyard (last chosen will be on top)",
                            cardsToMove, owner, source, game)) {
                        game.getPlayer(card.getOwnerId()).getGraveyard().add(card);
                    }
                    break;
                case LIBRARY:
                    if (info instanceof ZoneChangeInfo.Library && ((ZoneChangeInfo.Library) info).top) {
                        // on top
                        for (Card card : chooseOrder(
                                "order to put on top of library (last chosen will be topmost)",
                                cardsToMove, owner, source, game)) {
                            game.getPlayer(card.getOwnerId()).getLibrary().putOnTop(card, game);
                        }
                    } else {
                        // on bottom
                        for (Card card : chooseOrder(
                                "order to put on bottom of library (last chosen will be bottommost)",
                                cardsToMove, owner, source, game)) {
                            game.getPlayer(card.getOwnerId()).getLibrary().putOnBottom(card, game);
                        }
                    }
                    break;
                case EXILED:
                    for (Card card : cardsToMove.getCards(game)) {
                        if (info instanceof ZoneChangeInfo.Exile && ((ZoneChangeInfo.Exile) info).id != null) {
                            ZoneChangeInfo.Exile exileInfo = (ZoneChangeInfo.Exile) info;
                            game.getExile().createZone(exileInfo.id, exileInfo.name).add(card);
                        } else {
                            game.getExile().getPermanentExile().add(card);
                        }
                    }
                    break;
                case COMMAND:
                    // There should never be more than one card here.
                    for (Card card : cardsToMove.getCards(game)) {
                        game.addCommander(new Commander(card));
                    }
                    break;
                case STACK:
                    // There should never be more than one card here.
                    for (Card card : cardsToMove.getCards(game)) {
                        Spell spell;
                        if (info instanceof ZoneChangeInfo.Stack && ((ZoneChangeInfo.Stack) info).spell != null) {
                            spell = ((ZoneChangeInfo.Stack) info).spell;
                        } else {
                            spell = new Spell(card, card.getSpellAbility().copy(), card.getOwnerId(), event.getFromZone(), game);
                        }
                        spell.syncZoneChangeCounterOnStack(card, game);
                        game.getStack().push(spell);
                        game.getState().setZone(spell.getId(), Zone.STACK);
                        game.getState().setZone(card.getId(), Zone.STACK);
                    }
                    break;
                case BATTLEFIELD:
                    Permanent permanent = event.getTarget();
                    game.addPermanent(permanent, createOrder);
                    game.getPermanentsEntering().remove(permanent.getId());
                    break;
                default:
                    throw new UnsupportedOperationException("to Zone " + toZone.toString() + " not supported yet");
            }
        }

        // update zone in main
        game.setZone(event.getTargetId(), event.getToZone());

        // update zone in other parts (meld cards, mdf half cards)
        cardsToUpdate.entrySet().forEach(entry -> {
            for (Card card : entry.getValue().getCards(game)) {
                if (!card.getId().equals(event.getTargetId())) {
                    game.setZone(card.getId(), entry.getKey());
                }
            }
        });

        // reset meld status
        if (targetCard instanceof MeldCard) {
            if (event.getToZone() != Zone.BATTLEFIELD) {
                ((MeldCard) targetCard).setMelded(false, game);
            }
        }
    }

    public static Card getTargetCard(Game game, UUID targetId) {
        Card card = game.getCard(targetId);
        if (card != null) {
            return card;
        }

        card = game.getMeldCard(targetId);
        if (card != null) {
            return card;
        }

        return game.getPermanent(targetId);
    }

    private static boolean maybeRemoveFromSourceZone(ZoneChangeInfo info, Game game, Ability source) {
        ZoneChangeEvent event = info.event;

        // Handle Unmelded Cards
        if (info instanceof ZoneChangeInfo.Unmelded) {
            ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info;
            MeldCard meld = game.getMeldCard(event.getTargetId());
            for (Iterator<ZoneChangeInfo> itr = unmelded.additionalMoves.iterator(); itr.hasNext(); ) {
                ZoneChangeInfo additionalMove = itr.next();
                if (!maybeRemoveFromSourceZone(additionalMove, game, source)) {
                    itr.remove();
                } else if (Objects.equals(additionalMove.event.getTargetId(), meld.getTopHalfCard().getId())) {
                    meld.setTopLastZoneChangeCounter(meld.getTopHalfCard().getZoneChangeCounter(game));
                } else if (Objects.equals(additionalMove.event.getTargetId(), meld.getBottomHalfCard().getId())) {
                    meld.setBottomLastZoneChangeCounter(meld.getBottomHalfCard().getZoneChangeCounter(game));
                }
            }
            if (unmelded.additionalMoves.isEmpty()) {
                return false;
            }
            // We arbitrarily prefer the bottom half card. This should never be relevant.
            meld.updateZoneChangeCounter(game, unmelded.additionalMoves.get(unmelded.additionalMoves.size() - 1).event);
            return true;
        }

        // Handle all normal cases
        Card card = getTargetCard(game, event.getTargetId());
        if (card == null) {
            // if we can't find the card we can't remove it.
            return false;
        }

        boolean isGoodToMove = false;
        if (info.faceDown) {
            // any card can be moved as face down (doubled faced cards also support face down)
            isGoodToMove = true;
        } else if (event.getToZone().equals(Zone.BATTLEFIELD)) {
            // non-permanents can't move to battlefield
            // TODO: possible bug with Nightbound, search all usage of getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED and insert additional check Ability.checkCard
            /*
             * 712.14a. If a spell or ability puts a transforming double-faced card onto the battlefield "transformed"
             * or "converted," it enters the battlefield with its back face up. If a player is instructed to put a card
             * that isn't a transforming double-faced card onto the battlefield transformed or converted, that card stays in
             * its current zone.
             */
            boolean wantToTransform = Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
            if (wantToTransform) {
                isGoodToMove = card.isTransformable() && card.getSecondCardFace().isPermanent(game);
            } else {
                isGoodToMove = card.isPermanent(game);
            }
        } else {
            // other zones allows to move
            isGoodToMove = true;
        }
        if (!isGoodToMove) {
            return false;
        }

        // TODO: is it buggy? Card characteristics are global - if you change face down then it will be
        //  changed in original card too, not in blueprint only
        card.setFaceDown(info.faceDown, game);

        boolean success = false;
        if (!game.replaceEvent(event)) {
            Zone fromZone = event.getFromZone();
            if (event.getToZone() == Zone.BATTLEFIELD) {
                // PUT TO BATTLEFIELD AS PERMANENT
                // prepare card and permanent (card must contain full data, even for face down)
                // if needed to take attributes from the spell (e.g. color of spell was changed)
                card = prepareBlueprintCardFromSpell(card, event, game);

                // controlling player can be replaced so use event player now
                Permanent permanent;
                if (card instanceof MeldCard) {
                    permanent = new PermanentMeld(card, event.getPlayerId(), game);
                } else if (card instanceof ModalDoubleFacedCard) {
                    // main mdf card must be processed before that call (e.g. only halves can be moved to battlefield)
                    throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half");
                } else if (card instanceof Permanent) {
                    throw new IllegalStateException("Unexpected trying of move permanent to battlefield instead card");
                } else {
                    permanent = new PermanentCard(card, event.getPlayerId(), game);
                }

                // put onto battlefield with possible counters
                game.getPermanentsEntering().put(permanent.getId(), permanent);
                card.applyEnterWithCounters(permanent, source, game);

                permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield
                        && ((ZoneChangeInfo.Battlefield) info).tapped);

                // if need prototyped version
                if (Zone.STACK == event.getFromZone()) {
                    Spell spell = game.getStack().getSpell(event.getTargetId());
                    if (spell != null) {
                        permanent.setPrototyped(spell.isPrototyped());
                    }
                }

                permanent.setFaceDown(info.faceDown, game);
                if (info.faceDown) {
                    // TODO: need research cards with "setFaceDown(false"
                    // TODO: delete after new release and new face down bugs (old code remove face down status from a card for unknown reason), 2024-02-20
                    //card.setFaceDown(false, game);
                }

                // make sure the controller of all continuous effects of this card are switched to the current controller
                game.setScopeRelevant(true);
                try {
                    game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId());
                    if (permanent.entersBattlefield(source, game, fromZone, true)
                            && card.removeFromZone(game, fromZone, source)) {
                        success = true;
                        event.setTarget(permanent);
                    } else {
                        // revert controller to owner if permanent does not enter
                        game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());
                        game.getPermanentsEntering().remove(permanent.getId());
                    }
                } finally {
                    game.setScopeRelevant(false);
                }
            } else if (event.getTarget() != null) {
                // PUT PERMANENT TO OTHER ZONE (e.g. remove only)
                Permanent target = event.getTarget();
                success = target.removeFromZone(game, fromZone, source)
                        && game.getPlayer(target.getControllerId()).removeFromBattlefield(target, source, game);
            } else {
                // PUT CARD TO OTHER ZONE
                success = card.removeFromZone(game, fromZone, source);
            }
        }
        if (success) {
            // change ZCC on real enter
            // warning, tokens creation code uses same zcc logic as cards (+1 zcc on enter to battlefield)
            // so if you want to change zcc logic here (but I know you don't) then change token code
            // too in TokenImpl.putOntoBattlefieldHelper
            // KickerTest do many tests for token's zcc
            if (event.getToZone() == Zone.BATTLEFIELD && event.getTarget() != null) {
                event.getTarget().updateZoneChangeCounter(game, event);
            } else if (!(card instanceof Permanent)) {
                card.updateZoneChangeCounter(game, event);
            }
        }
        return success;
    }

    public static List<Card> chooseOrder(String message, Cards cards, Player player, Ability source, Game game) {
        List<Card> order = new ArrayList<>();
        if (cards.isEmpty()) {
            return order;
        }
        TargetCard target = new TargetCard(Zone.ALL, new FilterCard(message));
        target.setRequired(true);
        while (player.canRespond() && cards.size() > 1) {
            player.choose(Outcome.Neutral, cards, target, source, game);
            UUID targetObjectId = target.getFirstTarget();
            order.add(cards.get(targetObjectId, game));
            cards.remove(targetObjectId);
            target.clearChosen();
        }
        order.addAll(cards.getCards(game));
        return order;
    }

    private static Card prepareBlueprintCardFromSpell(Card card, ZoneChangeEvent event, Game game) {
        card = card.copy();
        if (Zone.STACK == event.getFromZone()) {
            // TODO: wtf, why only colors!? Must research and remove colors workaround or add all other data like types too
            Spell spell = game.getStack().getSpell(event.getTargetId());

            // old version
            if (false && spell != null && !spell.isFaceDown(game)) {
                if (!card.getColor(game).equals(spell.getColor(game))) {
                    // the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card
                    card.getColor(game).setColor(spell.getColor(game));
                }
            }

            // new version
            if (true && spell != null && spell.getSpellAbility() != null) {
                Card characteristics = spell.getSpellAbility().getCharacteristics(game);
                if (!characteristics.isFaceDown(game)) {
                    if (!card.getColor(game).equals(characteristics.getColor(game))) {
                        // TODO: don't work with prototyped spells (setColor can't set colorless color)
                        card.getColor(game).setColor(characteristics.getColor(game));
                    }
                }
            }
        }
        return card;
    }

}
