package tc.oc.pgm.restart;

import java.time.Duration;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.eventbus.Subscribe;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.restart.CancelRestartEvent;
import tc.oc.commons.core.restart.RequestRestartEvent;
import tc.oc.commons.core.restart.RestartManager;
import tc.oc.commons.core.util.Comparables;
import tc.oc.pgm.countdowns.MatchCountdown;
import tc.oc.pgm.events.ConfigLoadEvent;
import tc.oc.pgm.events.MatchEndEvent;
import tc.oc.pgm.events.MatchLoadEvent;
import tc.oc.pgm.events.MatchUnloadEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchManager;

/**
 * Listens for {@link RequestRestartEvent} and defers it until after the current match
 * and the restart countdown. Also listens for {@link CancelRestartEvent} and handles
 * it appropriately.
 *
 * Also keeps count of matches and requests a restart from {@link RestartManager} after
 * the configured limit.
 */
@Singleton
public class RestartListener implements PluginFacet, Listener {

    private final Logger logger;
    private final RestartManager restartManager;
    private final MatchManager mm;
    private final Server server;
    private AutoRestartConfiguration config;

    private @Nullable Integer matchLimit;
    private @Nullable RequestRestartEvent.Deferral deferral;
    private boolean startingCountdown;

    @Inject RestartListener(Loggers loggers, RestartManager restartManager, MatchManager mm, Server server, AutoRestartConfiguration config) {
        this.restartManager = restartManager;
        this.logger = loggers.get(getClass());
        this.mm = mm;
        this.server = server;
        this.config = config;
        this.matchLimit = config.matchLimit() > 0 ? config.matchLimit() : null;
    }

    @EventHandler
    public void configure(ConfigLoadEvent event) {
         this.config = new AutoRestartConfiguration(event.getConfig());
    }

    private boolean isCountdownRunning(Match match) {
        return match.countdowns().getCountdown() instanceof RestartCountdown;
    }

    private void startCountdown(Match match, Duration duration) {
        try {
            startingCountdown = true;
            match.ensureNotRunning();
            match.countdowns().start(new RestartCountdown(match), duration);
        } finally {
            startingCountdown = false;
        }
    }

    /**
     * Start a countdown of the given duration after cancelling any existing one
     */
    private void forceCountdown(Match match, Duration duration) {
        match.countdowns().cancelAll();
        logger.info("Starting countdown from " + duration);
        startCountdown(match, duration);
    }

    private void ensureCountdown(Match match) {
        if(!isCountdownRunning(match) && !startingCountdown) {
            logger.info("Starting countdown from " + config.time());
            startCountdown(match, config.time());
        }
    }

    private void cancelCountdown(Match match) {
        if(isCountdownRunning(match)) {
            logger.info("Cancelling countdown");
            match.countdowns().cancelAll();
        }
    }

    public boolean willRestart(Match match) {
        return startingCountdown || canResumeRestart(match);
    }

    private boolean canResumeRestart(Match match) {
        if(deferral == null) return false;
        final int priority = deferral.request().priority();
        if(priority >= ServerDoc.Restart.Priority.HIGH) {
            // Restart immediately
            return true;
        } else if(priority >= ServerDoc.Restart.Priority.NORMAL) {
            // Restart after current match ends
            return match.canAbort();
        } else {
            // Restart when server is empty
            return match.getPlayers().isEmpty();
        }
    }

    private void checkCountdown(Match match) {
        if(deferral != null) {
            if(canResumeRestart(match)) {
                ensureCountdown(match);
            } else {
                cancelCountdown(match);
            }
        }
    }

    /**
     * When a restart is requested, let it restart immediately if the server if empty,
     * otherwise defer the restart and ensure that a countdown is running.
     */
    @Subscribe
    public void onRequestRestart(RequestRestartEvent event) {
        if(!server.isSuspended()) {
            logger.info("Deferring restart");
            deferral = event.defer(getClass().getName());
            checkCountdown(mm.needCurrentMatch());
        }
    }

    /**
     * When restart is cancelled, cancel any countdown and discard our deferral
     */
    @Subscribe
    public void onCancelRestart(CancelRestartEvent event) {
        cancelCountdown(mm.needCurrentMatch());
        deferral = null;
    }

    @EventHandler
    public void onSuspend(ServerSuspendEvent event) {
        if(deferral != null) {
            deferral.resume();
            deferral = null;
        }
    }

    @EventHandler
    public void onMatchLoad(MatchLoadEvent event) {
        checkCountdown(event.getMatch());
    }

    /**
     * When match ends, start a countdown if a restart is already requested,
     * otherwise check for the match limit and request a restart if needed.
     * This listens on LOW priority so that it takes priority over map cycling.
     */
    @EventHandler(priority = EventPriority.LOW)
    public void onMatchEnd(MatchEndEvent event) {
        if(startingCountdown) return;

        if(deferral != null) {
            checkCountdown(event.getMatch());
        } else if(matchLimit != null && event.getMatch().serialNumber() >= matchLimit) {
            restartManager.requestRestart("Reached match limit (" + event.getMatch().serialNumber() + " >= " + matchLimit + ")");
        }
    }

    @EventHandler
    public void onMatchUnload(MatchUnloadEvent event) {
        if(deferral != null && deferral.request().priority() >= ServerDoc.Restart.Priority.NORMAL) {
            logger.info("Resuming restart because the match unloaded");
            deferral.resume();
            deferral = null;
        }
    }

    /**
     * If the match empties out while a restart is queued, end the match
     */
    @EventHandler
    public void onPartyChange(PlayerPartyChangeEvent event) {
        checkCountdown(event.getMatch());
    }

    /**
     * Request a restart from {@link RestartManager} and defer it with
     * a countdown of the given duration.
     */
    public void queueRestart(Match match, Duration duration, String reason) {
        restartManager.requestRestart(reason);
        forceCountdown(match, duration);
    }

    /**
     * Set the match limit restart to the given number of matches from now,
     * or disable the limit if given null;
     * @return The number of matches from now when the server will restart
     */
    public @Nullable Integer restartAfterMatches(Match match, @Nullable Integer matches) {
        matchLimit = matches == null ? null : match.serialNumber() + matches;
        return matchLimit == null ? null : matchLimit - match.serialNumber();
    }

    public class RestartCountdown extends MatchCountdown {
        public RestartCountdown(Match match) {
            super(match);
        }

        @Override
        public BaseComponent barText(Player viewer) {
            if(Comparables.greaterThan(remaining, Duration.ZERO)) {
                return new Component(new TranslatableComponent("broadcast.serverRestart.message",
                                                               secondsRemaining(ChatColor.DARK_RED)),
                                     ChatColor.AQUA);
            } else {
                return new Component(new TranslatableComponent("broadcast.serverRestart.kickMsg"),
                                     ChatColor.RED);
            }
        }

        @Override
        public void onEnd(Duration total) {
            super.onEnd(total);
            if(deferral != null) {
                logger.info("Resuming restart after countdown");
                deferral.resume();
                deferral = null;
            }
        }
    }
}
