package tc.oc.pgm.victory;

import java.util.Comparator;
import java.util.Set;
import javax.inject.Inject;

import com.google.common.collect.ImmutableList;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.commons.core.util.RankedSet;
import tc.oc.pgm.events.CompetitorAddEvent;
import tc.oc.pgm.events.CompetitorRemoveEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchEndEvent;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchScope;

/**
 * Decides when the match should end and who should win
 *
 * Regression tests:
 *
 *     - In a goals + blitz match, a team that completes all their goals should win,
 *       even if they have fewer players than other teams.
 *     - In a goals + blitz match, a team that runs out of players should lose,
 *       even if they have more goals completed.
 *     - When a time limit elapses with a default result, it should end the match,
 *       but not affect the final ranking of competitors.
 *     - When a time limit elapses with a tie result, no team should win, even if
 *       some teams are leading according to other conditions.
 */
@ListenerScope(MatchScope.LOADED)
public class VictoryMatchModule extends MatchModule implements Listener {

    @Inject private EventBus eventBus;

    private final VictoryCalculator calculator = new VictoryCalculator();

    @Override
    public void load() {
        super.load();
        calculator().addCompetitors(match.getCompetitors());
        invalidateCompetitorRanking();
    }

    @Override
    public void enable() {
        super.enable();
        match.getScheduler(MatchScope.RUNNING)
             .createTask(this::invalidateAndCheckEnd);
    }

    public VictoryCalculator calculator() {
        return calculator;
    }

    public Comparator<Competitor> victoryOrder() {
        return calculator().victoryOrder();
    }

    public void setImmediateWinner(Competitor competitor) {
        calculator().setImmediateWinner(competitor);
    }

    public void setVictoryCondition(VictoryCondition condition) {
        calculator().setVictoryCondition(condition);
    }

    /**
     * Re-sort all {@link Competitor}s from scratch, according to the current {@link VictoryCondition}.
     * This should be called when the state of the match changes in a way that affects the
     * rank of any competitors.
     */
    public void invalidateCompetitorRanking() {
        final ImmutableList<Competitor> before = ImmutableList.copyOf(calculator().rankedCompetitors());
        calculator().invalidateRanking();
        final ImmutableList<Competitor> after = ImmutableList.copyOf(calculator().rankedCompetitors());

        if(!before.equals(after)) {
            eventBus.callEvent(new RankingsChangeEvent(match, before, after));
        }
    }

    /**
     * Return all currently active competitors, ordered by closeness to winning the match.
     * Competitors that are tied are returned in arbitrary, and inconsistent order.
     */
    public RankedSet<Competitor> rankedCompetitors() {
        return calculator().rankedCompetitors();
    }

    /**
     * Return all {@link Competitor}s that are as close as, or closer to winning
     * the match than any other competitor. Unless the match is empty, the returned
     * set will always contain at least one competitor. If all competitors are tied
     * for the lead, the returned set will contain all of them.
     */
    public Set<Competitor> leaders() {
        return calculator().leaders();
    }

    /**
     * If some competitors finished ahead of others, return the winners,
     * otherwise return an empty set.
     */
    public Set<Competitor> winners() {
        return calculator().winners();
    }

    public boolean shouldMatchEnd() {
        return match.isFinished() || (match.hasStarted() &&
                                      calculator().shouldMatchEnd());
    }

    public boolean checkMatchEnd() {
        if(match.hasStarted() && !match.isFinished() && shouldMatchEnd()) {
            match.end();
            return true;
        }
        return false;
    }

    public boolean invalidateAndCheckEnd() {
        invalidateCompetitorRanking();
        return checkMatchEnd();
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onCompetitorAddEarly(CompetitorAddEvent event) {
        calculator().addCompetitor(event.getCompetitor());
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onCompetitorAddLate(CompetitorAddEvent event) {
        invalidateCompetitorRanking();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onCompetitorRemove(CompetitorRemoveEvent event) {
        calculator().removeCompetitor(event.getCompetitor());
        invalidateCompetitorRanking();
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onMatchEnd(MatchEndEvent event) {
        invalidateCompetitorRanking();
    }
}
