package mage.abilities.keyword;

import mage.MageIdentifier;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.CardUtil;

import java.util.List;
import java.util.Set;
import java.util.UUID;

/**
 * @author Susucr
 */
public class PlotAbility extends SpecialAction {

    private final String rule;

    public PlotAbility(String plotCost) {
        super(Zone.ALL); // Usually, plot only works from hand. However [[Fblthp, Lost on the Range]] allows plotting from library
        this.addCost(new ManaCostsImpl<>(plotCost));
        this.addEffect(new PlotSourceExileEffect());
        this.setTiming(TimingRule.SORCERY);
        this.usesStack = false;
        this.rule = "Plot " + plotCost;
    }

    private PlotAbility(final PlotAbility ability) {
        super(ability);
        this.rule = ability.rule;
    }

    @Override
    public PlotAbility copy() {
        return new PlotAbility(this);
    }

    @Override
    public String getRule() {
        return rule;
    }

    @Override
    public ActivationStatus canActivate(UUID playerId, Game game) {
        // Plot ability uses card's timing restriction
        Card card = game.getCard(getSourceId());
        if (card == null) {
            return ActivationStatus.getFalse();
        }
        // plot can only be activated from hand or from top of library if allowed to.
        Zone zone = game.getState().getZone(getSourceId());
        if (zone == Zone.HAND) {
            // Allowed from hand
        } else if (zone == Zone.LIBRARY) {
            // Allowed only if permitted for top card, and only if the card is on top and is nonland
            // Note: if another effect changes zones where permitted, or if different card categories are permitted,
            //       it would be better to refactor this as an unique AsThoughEffect.
            //       As of now, only Fblthp, Lost on the Range changes permission of plot.
            Player player = game.getPlayer(getControllerId());
            if (player == null || !player.canPlotFromTopOfLibrary()) {
                return ActivationStatus.getFalse();
            }
            Card topCardLibrary = player.getLibrary().getFromTop(game);
            if (topCardLibrary == null || !topCardLibrary.getId().equals(card.getId()) || card.isLand()) {
                return ActivationStatus.getFalse();
            }
        } else {
            // Not Allowed from other zones
            return ActivationStatus.getFalse();
        }
        Set<MageIdentifier> allowedToBeCastNow = card.getSpellAbility().spellCanBeActivatedNow(playerId, game);
        if (!allowedToBeCastNow.contains(MageIdentifier.Default) && !allowedToBeCastNow.contains(card.getSpellAbility().getIdentifier())) {
            return ActivationStatus.getFalse();
        }
        return super.canActivate(playerId, game);
    }

    static UUID getPlotExileId(UUID playerId, Game game) {
        UUID exileId = (UUID) game.getState().getValue("PlotExileId" + playerId.toString());
        if (exileId == null) {
            exileId = UUID.randomUUID();
            game.getState().setValue("PlotExileId" + playerId, exileId);
        }
        return exileId;
    }

    static String getPlotTurnKeyForCard(UUID cardId) {
        return cardId.toString() + "|" + "Plotted Turn";
    }

    /**
     * To be used in an OneShotEffect's apply.
     * 'Plot' the provided card. The card is exiled in it's owner plot zone,
     * and may be cast by that player without paying its mana cost at sorcery
     * speed on a future turn.
     */
    public static boolean doExileAndPlotCard(Card card, Game game, Ability source) {
        if (card == null) {
            return false;
        }
        Player owner = game.getPlayer(card.getOwnerId());
        if (owner == null) {
            return false;
        }
        UUID exileId = PlotAbility.getPlotExileId(owner.getId(), game);
        String exileZoneName = "Plots of " + owner.getName();
        Card mainCard = card.getMainCard();
        Zone zone = game.getState().getZone(mainCard.getId());
        if (mainCard.moveToExile(exileId, exileZoneName, source, game)) {
            // Remember on which turn the card was last plotted.
            game.getState().setValue(PlotAbility.getPlotTurnKeyForCard(mainCard.getId()), game.getTurnNum());
            game.addEffect(new PlotAddSpellAbilityEffect(new MageObjectReference(mainCard, game)), source);
            game.informPlayers(
                    owner.getLogName()
                            + " plots " + mainCard.getLogName()
                            + " from " + zone.toString().toLowerCase()
                            + CardUtil.getSourceLogName(game, source, card.getId())
            );
            game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOME_PLOTTED, mainCard.getId(), source, owner.getId()));
        }
        return true;
    }
}

/**
 * Exile the source card in the plot exile zone of its owner
 * and allow its owner to cast it at sorcery speed starting
 * next turn.
 */
class PlotSourceExileEffect extends OneShotEffect {

    PlotSourceExileEffect() {
        super(Outcome.Benefit);
    }

    private PlotSourceExileEffect(final PlotSourceExileEffect effect) {
        super(effect);
    }

    @Override
    public PlotSourceExileEffect copy() {
        return new PlotSourceExileEffect(this);
    }

    @Override
    public boolean apply(Game game, Ability source) {
        return PlotAbility.doExileAndPlotCard(game.getCard(source.getSourceId()), game, source);
    }
}

class PlotAddSpellAbilityEffect extends ContinuousEffectImpl {

    private final MageObjectReference mor;

    PlotAddSpellAbilityEffect(MageObjectReference mor) {
        super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
        this.mor = mor;
        staticText = "Plot card";
    }

    private PlotAddSpellAbilityEffect(final PlotAddSpellAbilityEffect effect) {
        super(effect);
        this.mor = effect.mor;
    }

    @Override
    public PlotAddSpellAbilityEffect copy() {
        return new PlotAddSpellAbilityEffect(this);
    }

    @Override
    public boolean apply(Game game, Ability source) {
        Card card = mor.getCard(game);
        if (card == null) {
            discard();
            return true;
        }

        Card mainCard = card.getMainCard();
        UUID mainCardId = mainCard.getId();
        Player player = game.getPlayer(card.getOwnerId());
        if (game.getState().getZone(mainCardId) != Zone.EXILED || player == null) {
            discard();
            return true;
        }

        List<Card> faces = CardUtil.getCastableComponents(mainCard, null, source, player, game, null, false);
        for (Card face : faces) {
            // Add the spell ability to each castable face to have the proper name/paramaters.
            PlotSpellAbility ability = new PlotSpellAbility(face.getName());
            ability.setSourceId(face.getId());
            ability.setControllerId(player.getId());
            ability.setSpellAbilityType(face.getSpellAbility().getSpellAbilityType());
            game.getState().addOtherAbility(face, ability);
        }
        return true;
    }
}

/**
 * This is inspired (after a little cleanup) by how {@link ForetellAbility} does it.
 */
class PlotSpellAbility extends SpellAbility {

    private String faceCardName; // Same as with Foretell, we identify the proper face with its spell name.
    private SpellAbility spellAbilityToResolve;

    PlotSpellAbility(String faceCardName) {
        super(null, faceCardName, Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.PLOT);
        this.setRuleVisible(false);
        this.setAdditionalCostsRuleVisible(false);
        this.faceCardName = faceCardName;
        this.addCost(new ManaCostsImpl<>("{0}"));
    }

    private PlotSpellAbility(final PlotSpellAbility ability) {
        super(ability);
        this.faceCardName = ability.faceCardName;
        this.spellAbilityToResolve = ability.spellAbilityToResolve;
    }

    @Override
    public PlotSpellAbility copy() {
        return new PlotSpellAbility(this);
    }

    @Override
    public ActivationStatus canActivate(UUID playerId, Game game) {
        if (super.canActivate(playerId, game).canActivate()) {
            Card card = game.getCard(getSourceId());
            if (card != null) {
                Card mainCard = card.getMainCard();
                UUID mainCardId = mainCard.getId();
                // Card must be in the exile zone
                if (game.getState().getZone(mainCardId) != Zone.EXILED) {
                    return ActivationStatus.getFalse();
                }
                Integer plottedTurn = (Integer) game.getState().getValue(PlotAbility.getPlotTurnKeyForCard(mainCardId));
                // Card must have been plotted
                if (plottedTurn == null) {
                    return ActivationStatus.getFalse();
                }
                // Can't be cast if the turn it was last Plotted is the same
                if (plottedTurn == game.getTurnNum()) {
                    return ActivationStatus.getFalse();
                }
                // Only allow the cast at sorcery speed
                if (!game.canPlaySorcery(playerId)) {
                    return ActivationStatus.getFalse();
                }
                // Check that the proper face can be cast.
                // TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling canActivate?
                if (mainCard instanceof CardWithHalves) {
                    if (((CardWithHalves) mainCard).getLeftHalfCard().getName().equals(faceCardName)) {
                        return ((CardWithHalves) mainCard).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
                    } else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
                        return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
                    }
                } else if (card instanceof AdventureCard) {
                    if (card.getMainCard().getName().equals(faceCardName)) {
                        return card.getMainCard().getSpellAbility().canActivate(playerId, game);
                    } else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
                        return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
                    }
                }
                return card.getSpellAbility().canActivate(playerId, game);
            }
        }
        return ActivationStatus.getFalse();
    }

    @Override
    public SpellAbility getSpellAbilityToResolve(Game game) {
        Card card = game.getCard(getSourceId());
        if (card != null) {
            if (spellAbilityToResolve == null) {
                SpellAbility spellAbilityCopy = null;
                // TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling getSpellAbilityToResolve?
                if (card instanceof CardWithHalves) {
                    if (((CardWithHalves) card).getLeftHalfCard().getName().equals(faceCardName)) {
                        spellAbilityCopy = ((CardWithHalves) card).getLeftHalfCard().getSpellAbility().copy();
                    } else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
                        spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
                    }
                } else if (card instanceof AdventureCard) {
                    if (card.getMainCard().getName().equals(faceCardName)) {
                        spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
                    } else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
                        spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
                    }
                } else {
                    spellAbilityCopy = card.getSpellAbility().copy();
                }
                if (spellAbilityCopy == null) {
                    return null;
                }
                spellAbilityCopy.setId(this.getId());
                spellAbilityCopy.clearManaCosts();
                spellAbilityCopy.clearManaCostsToPay();
                spellAbilityCopy.addCost(this.getCosts().copy());
                spellAbilityCopy.addCost(this.getManaCosts().copy());
                spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
                spellAbilityToResolve = spellAbilityCopy;
            }
        }
        return spellAbilityToResolve;
    }

    @Override
    public Costs<Cost> getCosts() {
        if (spellAbilityToResolve == null) {
            return super.getCosts();
        }
        return spellAbilityToResolve.getCosts();
    }
}