package net.minestom.server.event;
import net.minestom.server.event.trait.CancellableEvent;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* Represents an event listener (handler) in an event graph.
*
* A listener is responsible for executing some action based on an event triggering.
*
* @param The event type being handled.
*/
public interface EventListener {
@NotNull Class eventType();
@NotNull Result run(@NotNull T event);
@Contract(pure = true)
static EventListener.@NotNull Builder builder(@NotNull Class eventType) {
return new EventListener.Builder<>(eventType);
}
/**
* Create an event listener without any special options. The given listener will be executed
* if the event passes all parent filtering.
*
* @param eventType The event type to handle
* @param listener The handler function
* @param The event type to handle
* @return An event listener with the given properties
*/
@Contract(pure = true)
static @NotNull EventListener of(@NotNull Class eventType, @NotNull Consumer<@NotNull T> listener) {
return builder(eventType).handler(listener).build();
}
class Builder {
private final Class eventType;
private final List> filters = new ArrayList<>();
private boolean ignoreCancelled = true;
private int expireCount;
private Predicate expireWhen;
private Consumer handler;
protected Builder(Class eventType) {
this.eventType = eventType;
}
/**
* Adds a filter to the executor of this listener. The executor will only
* be called if this condition passes on the given event.
*/
@Contract(value = "_ -> this")
public @NotNull EventListener.Builder filter(Predicate filter) {
this.filters.add(filter);
return this;
}
/**
* Specifies if the handler should still be called if {@link CancellableEvent#isCancelled()} returns {@code true}.
*
* Default is set to {@code true}.
*
* @param ignoreCancelled True to stop processing the event when cancelled
*/
@Contract(value = "_ -> this")
public @NotNull EventListener.Builder ignoreCancelled(boolean ignoreCancelled) {
this.ignoreCancelled = ignoreCancelled;
return this;
}
/**
* Removes this listener after it has been executed the given number of times.
*
* @param expireCount The number of times to execute
*/
@Contract(value = "_ -> this")
public @NotNull EventListener.Builder expireCount(int expireCount) {
this.expireCount = expireCount;
return this;
}
/**
* Expires this listener when it passes the given condition. The expiration will
* happen before the event is executed.
*
* @param expireWhen The condition to test
*/
@Contract(value = "_ -> this")
public @NotNull EventListener.Builder expireWhen(Predicate expireWhen) {
this.expireWhen = expireWhen;
return this;
}
/**
* Sets the handler for this event listener. This will be executed if the listener passes
* all conditions.
*/
@Contract(value = "_ -> this")
public @NotNull EventListener.Builder handler(Consumer handler) {
this.handler = handler;
return this;
}
@Contract(value = "-> new", pure = true)
public @NotNull EventListener build() {
final boolean ignoreCancelled = this.ignoreCancelled;
AtomicInteger expirationCount = new AtomicInteger(this.expireCount);
final boolean hasExpirationCount = expirationCount.get() > 0;
final Predicate expireWhen = this.expireWhen;
final var filters = new ArrayList<>(this.filters);
final var handler = this.handler;
return new EventListener<>() {
@Override
public @NotNull Class eventType() {
return eventType;
}
@Override
public @NotNull Result run(@NotNull T event) {
// Event cancellation
if (ignoreCancelled && event instanceof CancellableEvent &&
((CancellableEvent) event).isCancelled()) {
return Result.INVALID;
}
// Expiration predicate
if (expireWhen != null && expireWhen.test(event)) {
return Result.EXPIRED;
}
// Filtering
if (!filters.isEmpty()) {
for (var filter : filters) {
if (!filter.test(event)) {
// Cancelled
return Result.INVALID;
}
}
}
// Handler
if (handler != null) {
handler.accept(event);
}
// Expiration count
if (hasExpirationCount && expirationCount.decrementAndGet() == 0) {
return Result.EXPIRED;
}
return Result.SUCCESS;
}
};
}
}
enum Result {
SUCCESS,
INVALID,
EXPIRED,
EXCEPTION
}
}