package tc.oc.pgm.worldborder;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.bukkit.EntityLocation;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.time.Duration;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.bukkit.util.WorldBorderUtils;
import tc.oc.commons.core.util.DefaultMapAdapter;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.goals.events.GoalEvent;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@ListenerScope(MatchScope.LOADED)
public class WorldBorderMatchModule extends MatchModule implements Listener {
private final List<WorldBorder> borders;
private final Map<WorldBorder, Boolean> results = new DefaultMapAdapter<>(false);
private @Nullable WorldBorder appliedBorder;
private @Nullable Duration appliedAt;
public WorldBorderMatchModule(Match match, List<WorldBorder> borders) {
super(match);
checkNotNull(borders);
checkArgument(!borders.isEmpty());
this.borders = borders;
}
@Override
public void load() {
super.load();
WorldBorder initial = null;
for(WorldBorder border : borders) {
if(!border.isConditional()) initial = border;
}
if(initial != null) {
logger.fine("Initializing with " + initial);
apply(initial);
} else {
reset();
}
}
@Override
public void enable() {
super.enable();
getMatch().getScheduler(MatchScope.RUNNING).createRepeatingTask(Duration.ZERO, Duration.ofSeconds(1), () -> {
if(!update()) {
refresh();
}
});
}
@Override
public void disable() {
freeze();
super.disable();
}
private void apply(WorldBorder border) {
logger.fine("Applying " + border);
border.apply(getMatch().getWorld().getWorldBorder(), appliedBorder != null);
appliedBorder = border;
appliedAt = getMatch().runningTime();
}
private void reset() {
logger.fine("Clearing border");
appliedBorder = null;
appliedAt = null;
getMatch().getWorld().getWorldBorder().reset();
}
/**
* Query the filters of all borders and apply them as needed.
*
* A border is applied when its filter goes from false to true, or when it becomes
* active because of another border further down the list going from true to false.
*
* If multiple borders become active simultaneously, they are applied in order. This
* allows a border to serve as the starting point for another moving border.
*/
private boolean update() {
WorldBorder lastMatched = null;
boolean applied = false;
for(WorldBorder border : borders) {
boolean newResult = border.filter.query(match).isAllowed();
boolean oldResult = results.put(border, newResult);
if(newResult) lastMatched = border;
if(!oldResult && newResult) {
// On the filter's rising edge, apply the border
applied = true;
apply(border);
} else if(oldResult && !newResult) {
if(lastMatched == null) {
// On the filter's falling edge, apply the last border in the list with a passing filter
reset();
} else {
// If no borders have passing filters, clear the border
apply(lastMatched);
}
}
}
return applied;
}
/**
* If the current border is moving, refresh its size/duration on all clients (to keep them in sync)
*/
private void refresh() {
if(appliedBorder != null) {
appliedBorder.refresh(getMatch().getWorld().getWorldBorder(), getMatch().runningTime().minus(appliedAt));
}
}
/**
* If the current border is moving, stop it in-place
*/
private void freeze() {
if(appliedBorder != null && appliedBorder.isMoving()) {
logger.fine("Freezing border " + appliedBorder);
getMatch().getWorld().getWorldBorder().setSize(getMatch().getWorld().getWorldBorder().getSize(), 0);
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onGoalComplete(GoalEvent event) {
update();
}
/**
* Prevent teleporting outside the border
*/
@EventHandler(priority = EventPriority.HIGH)
public void onPlayerTeleport(final PlayerTeleportEvent event) {
if(event.getCause() == PlayerTeleportEvent.TeleportCause.PLUGIN) {
if(WorldBorderUtils.isInsideBorder(event.getFrom()) &&
!WorldBorderUtils.isInsideBorder(event.getTo())) {
event.setCancelled(true);
}
}
}
@EventHandler(priority = EventPriority.HIGH)
public void onPlayerMove(final CoarsePlayerMoveEvent event) {
MatchPlayer player = getMatch().getPlayer(event.getPlayer());
if(player != null && player.isObserving()) {
EntityLocation location = event.getTo();
if(WorldBorderUtils.clampToBorder(location)) {
event.setTo(location);
}
}
}
}