package tc.oc.pgm.filters;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.logging.Level;
import javax.inject.Inject;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import org.bukkit.event.EventException;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.core.util.MapUtils;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchPlayerDeathEvent;
import tc.oc.pgm.events.MatchScoreChangeEvent;
import tc.oc.pgm.events.MatchStateChangeEvent;
import tc.oc.pgm.events.PlayerChangePartyEvent;
import tc.oc.pgm.victory.RankingsChangeEvent;
import tc.oc.pgm.filters.query.IQuery;
import tc.oc.pgm.flag.event.FlagStateChangeEvent;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
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 tc.oc.pgm.match.Repeatable;
@ListenerScope(MatchScope.LOADED)
public class FilterMatchModule extends MatchModule implements Listener, FilterDispatcher {
private static class ListenerSet {
final Set<FilterListener<?>> rise = new HashSet<>();
final Set<FilterListener<?>> fall = new HashSet<>();
}
private final Table<Filter, Class<? extends Filterable>, ListenerSet> listeners = HashBasedTable.create();
// Most recent responses for each filter with listeners (used to detect changes)
private final Table<Filter, Filterable<?>, Boolean> lastResponses = HashBasedTable.create();
// Filterables that need a check in the next tick (cleared every tick)
private final Set<Filterable<?>> dirtySet = new HashSet<>();
private <F extends Filterable<?>> void register(Class<F> scope, Filter filter, boolean response, FilterListener<? super F> listener) {
if(match.isLoaded()) {
throw new IllegalStateException("Cannot register filter listener after match is loaded");
}
final ListenerSet listenerSet = listeners.row(filter)
.computeIfAbsent(scope, s -> new ListenerSet());
(response ? listenerSet.rise
: listenerSet.fall).add(listener);
match.filterableDescendants(scope)
.forEach(filterable -> {
final boolean last = lastResponse(filter, filterable);
if(last == response) {
dispatch(listener, filter, filterable, last);
}
});
}
@Override
public <F extends Filterable<?>> void onChange(Class<F> scope, Filter filter, FilterListener<? super F> listener) {
logger.fine("onChange scope=" + scope.getSimpleName() + " listener=" + listener + " filter=" + filter);
register(scope, filter, true, listener);
register(scope, filter, false, listener);
}
@Override
public void onChange(Filter filter, FilterListener<? super Filterable<?>> listener) {
onChange((Class) Filterable.class, filter, listener);
}
@Override
public <F extends Filterable<?>> void onRise(Class<F> scope, Filter filter, Consumer<? super F> listener) {
logger.fine("onRise scope=" + scope.getSimpleName() + " listener=" + listener + " filter=" + filter);
register(scope, filter, true, (filterable, response) -> listener.accept(filterable));
}
@Override
public void onRise(Filter filter, Consumer<? super Filterable<?>> listener) {
onRise((Class) Filterable.class, filter, listener);
}
@Override
public <F extends Filterable<?>> void onFall(Class<F> scope, Filter filter, Consumer<? super F> listener) {
logger.fine("onFall scope=" + scope.getSimpleName() + " listener=" + listener + " filter=" + filter);
register(scope, filter, false, (filterable, response) -> listener.accept(filterable));
}
@Override
public void onFall(Filter filter, Consumer<? super Filterable<?>> listener) {
onFall((Class) Filterable.class, filter, listener);
}
private boolean lastResponse(Filter filter, Filterable<?> filterable) {
return MapUtils.computeIfAbsent(lastResponses.row(filter), filterable, filter::response);
}
private <F extends Filterable<?>> void dispatch(FilterListener<? super F> listener, Filter filter, F filterable, boolean response) {
if(logger.isLoggable(Level.FINER)) {
logger.finer("Dispatching response=" + response +
" listener=" + listener +
" filter=" + filter +
" filterable=" + filterable);
}
listener.filterQueryChanged(filterable, response);
}
private <F extends Filterable<?>, Q extends IQuery> void check(F filterable, Q query, List<Runnable> dispatches) {
final Map<Filter, Boolean> beforeCache = new HashMap<>();
final Map<Filter, Boolean> afterCache = lastResponses.column(filterable);
// For each scope that the given filterable applies to
listeners.columnMap().forEach((scope, column) -> {
if(scope.isInstance(filterable)) {
// For each filter in this scope
column.forEach((filter, filterListeners) -> {
final Boolean before;
final boolean after;
if(beforeCache.containsKey(filter)) {
// If the filter has already been checked, we have both responses saved.
before = beforeCache.get(filter);
after = afterCache.get(filter);
} else {
// The first time a particular filter is checked, move the old response to
// a local temporary cache and save the new response to the permanent cache.
before = afterCache.get(filter);
beforeCache.put(filter, before);
after = filter.response(query);
afterCache.put(filter, after);
}
if(before == null || before != after) {
dispatches.add(() -> {
(after ? filterListeners.rise
: filterListeners.fall).forEach(listener -> dispatch((FilterListener<? super F>) listener, filter, filterable, after));
});
}
});
}
});
}
private <F extends Filterable<?>, Q extends IQuery> void check(F filterable, Q query) {
final List<Runnable> dispatches = new ArrayList<>();
check(filterable, query, dispatches);
dispatches.forEach(Runnable::run);
}
@Repeatable(scope = MatchScope.LOADED)
public void tick() {
final Set<Filterable<?>> checked = new HashSet<>();
for(;;) {
// Collect Filterables that are dirty, and have not already been checked in this tick
final Set<Filterable<?>> checking = ImmutableSet.copyOf(Sets.difference(dirtySet, checked));
if(checking.isEmpty()) break;
// Remove what we are about to check from the dirty set, and add them to the checked set
dirtySet.removeAll(checking);
checked.addAll(checking);
// Do all the filter checks and collect the notifications in a list to dispatch afterward.
// This prevents listeners from altering the results of filters for other listeners that
// were invalidated at the same time.
final List<Runnable> dispatches = new ArrayList<>();
checking.forEach(f -> check(f, f, dispatches));
// The Listeners might invalidate more Filterables, which is why we have to loop around
// and empty the dirtySet again after this. We keep looping until there is nothing more
// we can check in this tick. If they invalidate something that has already been checked
// in this tick, it will remain in the dirtySet until the next tick.
dispatches.forEach(Runnable::run);
}
}
public void invalidate(Filterable<?> filterable) {
if(dirtySet.add(filterable)) {
filterable.filterableChildren().forEach(this::invalidate);
}
}
/**
* TODO: optimize using the filter parameter
*/
public void invalidate(Filter filter, Filterable<?> filterable) {
invalidate(filterable);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerMove(CoarsePlayerMoveEvent event) {
// On movement events, check the player immediately instead of invalidating them.
// We can't wait until the end of the tick because the player could move several
// more times by then (i.e. if we received multiple packets from them in the same
// tick) which would make region checks highly unreliable.
match.player(event.getPlayer()).ifPresent(player -> {
invalidate(player);
match.getServer().postToMainThread(match.getPlugin(), true, this::tick);
});
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(MatchPlayerDeathEvent event) {
invalidate(event.getVictim());
event.onlineKiller().ifPresent(this::invalidate);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPartyChange(PlayerChangePartyEvent event) throws EventException {
if(event.newParty().isPresent()) {
invalidate(event.getPlayer());
} else {
// Before a player leaves, force all filters false that are not already false.
// So, all dynamic player filters are effectively wrapped in "___ and online",
// and listeners don't need to do any cleanup as long as they don't hold on to
// players that don't match the filter.
listeners.columnMap().forEach((scope, column) -> {
if(scope.isInstance(event.getPlayer())) {
// For each filter in this scope
column.forEach((filter, filterListeners) -> {
// If player joined very recently, they may not have a cached response yet
final Boolean response = lastResponses.get(filter, event.getPlayer());
if(response != null && response) {
filterListeners.fall.forEach(listener -> dispatch((FilterListener<? super MatchPlayer>) listener, filter, event.getPlayer(), false));
}
});
}
});
event.yield();
// Wait until after the event to remove them, in case they get invalidated during the event.
dirtySet.remove(event.getPlayer());
lastResponses.columnKeySet().remove(event.getPlayer());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onMatchStateChange(MatchStateChangeEvent event) {
invalidate(match);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onGoalComplete(GoalCompleteEvent event) {
invalidate(match);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onFlagChange(FlagStateChangeEvent event) {
invalidate(match);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onScoreChange(MatchScoreChangeEvent event) {
invalidate(match);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onRankingsChange(RankingsChangeEvent event) {
invalidate(match);
}
}