package mage.abilities.costs;

import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.mana.ManaCost;
import mage.cards.Card;
import mage.constants.AbilityType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;

import java.util.Iterator;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * @author LevelX2
 */
public class AlternativeCostSourceAbility extends StaticAbility implements AlternativeSourceCosts {

    private static final String ALTERNATIVE_COST_ACTIVATION_KEY = "AlternativeCostActivated";
    private static final String ALTERNATIVE_DYNAMIC_COST_ACTIVATED_KEY = "AlternativeDynamicCostActivated";

    private Costs<AlternativeCost> alternateCosts = new CostsImpl<>();
    protected Condition condition;
    protected String rule;
    protected FilterCard filter;
    protected boolean onlyMana;
    protected DynamicCost dynamicCost;

    public AlternativeCostSourceAbility(Cost cost) {
        this(cost, null);
    }

    public AlternativeCostSourceAbility(Condition conditon) {
        this(null, conditon, null);
    }

    public AlternativeCostSourceAbility(Cost cost, Condition conditon) {
        this(cost, conditon, null);
    }

    public AlternativeCostSourceAbility(Cost cost, Condition condition, String rule) {
        this(cost, condition, rule, null, true);
    }

    /**
     * @param cost      alternate cost to pay
     * @param condition only if the condition is true it's possible to use the
     *                  alternate costs
     * @param rule      if != null used as rule text
     * @param filter    filters the cards this alternate cost can be applied to
     * @param onlyMana  if true only the mana costs are replaced by this costs,
     *                  other costs stay untouched
     */
    public AlternativeCostSourceAbility(Cost cost, Condition condition, String rule, FilterCard filter, boolean onlyMana) {
        super(Zone.ALL, null);
        this.addCost(cost);
        this.setRuleAtTheTop(true);
        this.condition = condition;
        this.rule = rule;
        this.filter = filter;
        this.onlyMana = onlyMana;
    }

    public AlternativeCostSourceAbility(Condition condition, String rule, FilterCard filter, boolean onlyMana, DynamicCost dynamicCost) {
        super(Zone.ALL, null);
        this.setRuleAtTheTop(true);
        this.condition = condition;
        this.rule = rule;
        this.filter = filter;
        this.onlyMana = onlyMana;
        this.dynamicCost = dynamicCost;
    }

    protected AlternativeCostSourceAbility(final AlternativeCostSourceAbility ability) {
        super(ability);
        this.alternateCosts = ability.alternateCosts;
        this.condition = ability.condition;
        this.rule = ability.rule;
        this.filter = ability.filter;
        this.onlyMana = ability.onlyMana;
        this.dynamicCost = ability.dynamicCost;
    }

    @Override
    public void addCost(Cost cost) {
        AlternativeCost alternativeCost = convertToAlternativeCost(cost);
        if (alternativeCost != null) {
            this.alternateCosts.add(alternativeCost);
        }
    }

    private AlternativeCost convertToAlternativeCost(Cost cost) {
        //return cost != null ? new AlternativeCost2Impl(null, cost.getText(), cost) : null;
        return cost != null ? new AlternativeCostImpl(null, "", cost) : null;
    }

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

    @Override
    public boolean isAvailable(Ability source, Game game) {
        boolean conditionApplies = condition == null || condition.apply(game, source);
        boolean filterApplies = filter == null || filter.match(game.getCard(source.getSourceId()), game);

        return conditionApplies && filterApplies;
    }

    @Override
    public boolean canActivateAlternativeCostsNow(Ability ability, Game game) {
        if (ability == null || !AbilityType.SPELL.equals(ability.getAbilityType())) {
            return isActivated(ability, game);
        }
        if (filter != null) {
            Card card = game.getCard(ability.getSourceId());
            if (!filter.match(card, ability.getControllerId(), ability, game)) {
                return false;
            }
        }
        Player player = game.getPlayer(ability.getControllerId());
        if (player == null) {
            return false;
        }
        Costs<AlternativeCost> alternativeCostsToCheck;
        if (dynamicCost != null) {
            alternativeCostsToCheck = new CostsImpl<>();
            alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
        } else {
            alternativeCostsToCheck = this.alternateCosts;
        }

        return alternativeCostsToCheck.canPay(ability, ability, ability.getControllerId(), game);
    }

    @Override
    public String getAlternativeCostText(Ability ability, Game game) {
        if (dynamicCost != null) {
            return "Cast with alternative cost: " + dynamicCost.getText(ability, game) + CardUtil.getSourceLogName(game, this);
        } else {
            Costs<AlternativeCost> alternativeCostsToCheck;
            if (dynamicCost != null) {
                alternativeCostsToCheck = new CostsImpl<>();
                alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
            } else {
                alternativeCostsToCheck = this.alternateCosts;
            }
            return alternativeCostsToCheck.isEmpty()
                    ? "Cast without paying its mana cost" + CardUtil.getSourceLogName(game, this)
                    : "Cast with alternative cost: " + alternativeCostsToCheck.getText() + CardUtil.getSourceLogName(game, this);
        }
    }

    @Override
    public boolean activateAlternativeCosts(Ability ability, Game game) {
        Costs<AlternativeCost> alternativeCostsToCheck;
        if (dynamicCost != null) {
            alternativeCostsToCheck = new CostsImpl<>();
            alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game)));
        } else {
            alternativeCostsToCheck = this.alternateCosts;
        }
        if (ability instanceof SpellAbility) {
            ability.getManaCostsToPay().removeIf(VariableCost.class::isInstance);
            CardUtil.reduceCost((SpellAbility) ability, ability.getManaCosts());

        } else {
            ability.clearManaCostsToPay();
        }
        if (!onlyMana) {
            ability.clearCosts();
        }
        for (AlternativeCost alternateCost : alternativeCostsToCheck) {
            alternateCost.activate();
            for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext(); ) {
                Cost costDetailed = (Cost) it.next();
                if (costDetailed instanceof ManaCost) {
                    ability.addManaCostsToPay((ManaCost) costDetailed.copy());
                } else if (costDetailed != null) {
                    ability.addCost(costDetailed.copy());
                }
            }
        }

        // Those cost have been paid, we want to store them.
        if (dynamicCost != null) {
            rememberDynamicCost(game, ability, alternativeCostsToCheck);
        }

        // save activated status
        doActivate(game, ability);
        return true;
    }

    protected void doActivate(Game game, Ability ability) {
        game.getState().setValue(getActivatedKey(ability), Boolean.TRUE);
    }

    private String getActivatedKey(Ability source) {
        return getActivatedKey(this.getOriginalId(), source.getSourceId(), source.getSourceObjectZoneChangeCounter());
    }

    private static String getActivatedKey(UUID alternativeCostOriginalId, UUID sourceId, int sourceZCC) {
        // can't use sourceId cause copied cards are different...
        // TODO: enable sourceId after copy card fix (it must copy cards with all related game state values)
        return ALTERNATIVE_COST_ACTIVATION_KEY + "_" + alternativeCostOriginalId + "_" /*+ sourceId + "_"*/ + sourceZCC;
    }

    private void rememberDynamicCost(Game game, Ability ability, Costs<AlternativeCost> costs) {
        game.getState().setValue(getDynamicCostActivatedKey(ability), costs);
    }

    private String getDynamicCostActivatedKey(Ability source) {
        return getDynamicCostActivatedKey(this.getOriginalId(), source.getSourceId(), source.getSourceObjectZoneChangeCounter());
    }

    private static String getDynamicCostActivatedKey(UUID alternativeCostOriginalId, UUID sourceId, int sourceZCC) {
        // can't use sourceId cause copied cards are different...
        // TODO: enable sourceId after copy card fix (it must copy cards with all related game state values)
        return ALTERNATIVE_DYNAMIC_COST_ACTIVATED_KEY + "_" + alternativeCostOriginalId + "_" /*+ sourceId + "_"*/ + sourceZCC;
    }

    /**
     * Search activated status of alternative cost.
     * <p>
     * If you need it on resolve then use current ZCC (on stack) If you need it
     * on battlefield then use previous ZCC (-1)
     *
     * @param game
     * @param source
     * @param alternativeCostOriginalId you must save originalId on card's
     *                                  creation
     * @param searchPrevZCC             true on battlefield, false on stack
     * @return
     */
    public static boolean getActivatedStatus(Game game, Ability source, UUID alternativeCostOriginalId, boolean searchPrevZCC) {
        String key = getActivatedKey(
                alternativeCostOriginalId,
                source.getSourceId(),
                source.getSourceObjectZoneChangeCounter() + (searchPrevZCC ? -1 : 0)
        );
        Boolean status = (Boolean) game.getState().getValue(key);
        return status != null && status;
    }

    @Override
    public boolean isActivated(Ability source, Game game) {
        Costs<AlternativeCost> alternativeCostsToCheck = dynamicCost != null
                ? (Costs<AlternativeCost>) game.getState().getValue(getDynamicCostActivatedKey(source))
                : this.alternateCosts;

        if (alternativeCostsToCheck == null) {
            return false;
        }

        for (AlternativeCost cost : alternativeCostsToCheck) {
            if (cost.isActivated(game)) {
                return true;
            }
        }
        return onlyMana && alternativeCostsToCheck.isEmpty();
    }

    @Override
    public String getCastMessageSuffix(Game game) {
        return alternateCosts.isEmpty() ? " without paying its mana costs" : " using alternative casting costs";
    }

    @Override
    public void resetCost() {

    }

    @Override
    public String getRule() {
        if (rule != null) {
            return rule;
        }
        StringBuilder sb = new StringBuilder();
        if (condition != null) {
            sb.append(condition.toString());
            if (alternateCosts.size() > 1) {
                sb.append(", rather than pay this spell's mana cost, ");
            } else {
                sb.append(", you may ");
            }
        } else {
            sb.append("You may ");
        }
        int numberCosts = 0;
        String remarkText = "";
        sb.append(CardUtil.concatWithAnd(alternateCosts
                .stream()
                .map(cost -> cost.getCost() instanceof ManaCost
                        ? "pay " + cost.getText(true)
                        : cost.getText(true))
                .map(CardUtil::getTextWithFirstCharLowerCase)
                .collect(Collectors.toList())));
        if (condition == null || alternateCosts.size() == 1) {
            sb.append(" rather than pay this spell's mana cost");
        } else if (alternateCosts.isEmpty()) {
            sb.append("cast this spell without paying its mana cost");
        }
        sb.append('.');
        if (numberCosts == 1 && remarkText != null) {
            sb.append(' ').append(remarkText);
        }
        return sb.toString();
    }

    @Override
    public Costs<Cost> getCosts() {
        Costs<Cost> alterCosts = new CostsImpl<>();
        alterCosts.addAll(alternateCosts);
        return alterCosts;
    }

    public DynamicCost getDynamicCost() {
        return dynamicCost;
    }

}
