package tc.oc.pgm.flag.state;

import java.util.Collections;
import java.util.Objects;

import net.md_5.bungee.api.ChatColor;
import org.bukkit.Location;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import java.time.Duration;
import java.time.Instant;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.core.scheduler.Task;
import tc.oc.commons.core.util.TimeUtils;
import tc.oc.pgm.events.PlayerLeavePartyEvent;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.ParticipantState;
import tc.oc.pgm.match.Party;
import tc.oc.pgm.spawns.events.ParticipantDespawnEvent;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.goals.events.GoalEvent;
import tc.oc.pgm.flag.Flag;
import tc.oc.pgm.flag.Post;
import tc.oc.pgm.flag.event.FlagCaptureEvent;
import tc.oc.pgm.flag.event.FlagStateChangeEvent;
import tc.oc.pgm.teams.TeamMatchModule;

import javax.annotation.Nullable;

/**
 * Base class for all {@link Flag} states
 */
public abstract class BaseState implements Runnable, State {

    protected final Flag flag;
    protected final Post post;
    protected final Instant enterTime;
    protected @Nullable Long remainingTicks;
    private Task task;

    protected BaseState(Flag flag, Post post) {
        this.flag = flag;
        this.post = post;
        this.enterTime = Instant.now();
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName();
    }

    /**
     * Called just after the flag's state has been changed to this object.
     *
     * This method is NOT allowed to transition this flag into any other state, and doing so
     * will throw an exception. Care must be taken that any events fired cannot cause another
     * transition for this flag. Transitioning other flags is OK.
     *
     * If this state wants to immediately transition, it should return zero from {@link #getDuration},
     * which will cause {@link #finishCountdown} to be called after this method returns.
     */
    public void enterState() {
        this.task = this.flag.getMatch().getScheduler(MatchScope.LOADED).createRepeatingTask(1, 1, this);
    }

    public void leaveState() {
        if(this.task != null) {
            this.task.cancel();
            this.task = null;
        }
    }

    @Override
    public Iterable<Location> getProximityLocations(ParticipantState player) {
        return Collections.emptySet();
    }

    @Override
    public boolean isCurrent() {
        return this.flag.isCurrent(this);
    }

    protected @Nullable Duration getDuration() {
        return null;
    }

    public void startCountdown() {
        Duration duration = getDuration();
        if(duration != null) {
            if(Duration.ZERO.equals(duration)) {
                this.finishCountdown();
            } else if(!TimeUtils.isInfPositive(duration)) {
                this.remainingTicks = duration.toMillis() / 50;
            }
        }
    }

    protected boolean isCountingDown() {
        return this.flag.getMatch().isRunning() && this.remainingTicks != null && this.remainingTicks > 0;
    }

    protected long getRemainingSeconds() {
        return this.remainingTicks == null ? -1 : (this.remainingTicks + 19) / 20;
    }

    @Override
    public void run() {
        this.tickLoaded();
        if(this.flag.getMatch().isRunning()) this.tickRunning();
    }

    public void tickLoaded() { }

    public void tickRunning() {
        if(this.isCountingDown()) {
            long before = this.getRemainingSeconds();
            if(--this.remainingTicks == 0) {
                this.finishCountdown();
            }
            long after = this.getRemainingSeconds();

            if(before != after) {
                this.tickSeconds(after);
            }
        }
    }

    protected void tickSeconds(long seconds) { }

    protected void finishCountdown() { }

    @Override
    public boolean isCarrying(MatchPlayer player) {
        return false;
    }

    @Override
    public boolean isCarrying(ParticipantState player) {
        MatchPlayer matchPlayer = player.getMatchPlayer();
        return matchPlayer != null && isCarrying(matchPlayer);
    }

    @Override
    public boolean isCarrying(Party party) {
        return false;
    }

    @Override
    public Post getPost() {
        return this.post;
    }

    @Override
    public boolean isAtPost(Post post) {
        return Objects.equals(post, this.post);
    }

    @Override
    public @Nullable Team getController() {
        if(this.post.getOwner() != null) {
            return this.flag.getMatch().needMatchModule(TeamMatchModule.class).team(this.post.getOwner());
        } else {
            return null;
        }
    }

    public ChatColor getStatusColor(Party viewer) {
        return this.flag.getChatColor();
    }

    public ChatColor getLabelColor(Party viewer) {
        if(this.flag.hasMultipleControllers()) {
            Team controller = this.getController();
            return controller != null ? controller.getColor() : ChatColor.WHITE;
        } else {
            return ChatColor.WHITE;
        }
    }

    public String getStatusText(Party viewer) {
        if(this.isCountingDown()) {
            return String.valueOf(this.getRemainingSeconds());
        } else {
            return this.getStatusSymbol(viewer);
        }
    }

    public abstract String getStatusSymbol(Party viewer);

    public void onEvent(GoalEvent event) { }
    public void onEvent(FlagStateChangeEvent event) { }
    public void onEvent(FlagCaptureEvent event) { }
    public void onEvent(PlayerMoveEvent event) { }
    public void onEvent(CoarsePlayerMoveEvent event) { }
    public void onEvent(BlockTransformEvent event) { }
    public void onEvent(PlayerDropItemEvent event) { }
    public void onEvent(ParticipantDespawnEvent event) { }
    public void onEvent(PlayerLeavePartyEvent event) { }
    public void onEvent(InventoryClickEvent event) { }
    public void onEvent(EntityDamageEvent event) { }
}
