package tc.oc.pgm.filters; import java.util.function.Predicate; import java.util.stream.Stream; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import tc.oc.pgm.features.FeatureDefinition; import tc.oc.pgm.features.FeatureInfo; import tc.oc.pgm.filters.operator.AllFilter; import tc.oc.pgm.filters.operator.AnyFilter; import tc.oc.pgm.filters.operator.InverseFilter; import tc.oc.pgm.filters.query.BlockQuery; import tc.oc.pgm.filters.query.IQuery; @FeatureInfo(name = "filter") public interface Filter extends FeatureDefinition { /** * ALLOW or DENY the given {@link IQuery}, or ABSTAIN from responding. */ QueryResponse query(IQuery query); /** * Return true if this filter ALLOWs the given {@link IQuery}, false if this filter DENYes it, * or throw a {@link UnsupportedOperationException} if this filter cannot respond to the given query. * * {@link #assertRespondsTo(Class)} can be used to ensure that this method will not throw. */ default boolean response(IQuery query) { switch(query(query)) { case ALLOW: return true; case DENY: return false; default: throw new UnsupportedOperationException("Filter did not respond to the query"); } } /** * Return true only if ALL of the following conditions are true: * * 1. This filter always responds to queries of the given type (i.e. never ABSTAINS) * 2. This filter's response is derived only from properties of the given query type, * and not on any properties in subtypes of that query. * 3. All dependencies of this filter also meet the above conditions. */ boolean respondsTo(Class<? extends IQuery> queryType); /** * Throw a {@link FilterTypeException} unless this filter, and ALL dependency filters, * return true from {@link #respondsTo(Class)} for the given query type. * * If this filter has dependencies, it should call this method on them directly, * so that the exception contains the specific filter that failed the test. */ default void assertRespondsTo(Class<? extends IQuery> queryType) throws FilterTypeException { if(!respondsTo(queryType)) { throw new FilterTypeException(this, queryType); } } /** * Does this filter support dynamic notifications? * * If this returns true, then any change in the response of this filter to * a query that passes {@link #assertRespondsTo(Class)} must notify {@link FilterListener}s * registered through {@link FilterMatchModule}. * * This method should NOT account for the behavior of any {@link #dependencies()}, * as that is done automatically by the calling code. This method can return true * as long as it does NOT change its response to any query, without firing a notification, * at a time when none of its dynamic dependencies change their response. */ default boolean isDynamic() { return false; } default Predicate<IQuery> respondsWith(QueryResponse response) { return q -> query(q) == response; } default Predicate<IQuery> isAllowed() { return respondsWith(QueryResponse.ALLOW); } default Predicate<IQuery> isDenied() { return respondsWith(QueryResponse.DENY); } /** * @see #and */ default Filter not() { return new InverseFilter(this); } /** * Be careful not to call this on any undefined proxies while parsing. * * If one of the filters is static, you can call it on that one e.g. * * MatchStateFilter.running().and(filter) * * instead of * * filter.and(MatchStateFilter.running()) */ default Filter and(Filter that) { return this == that ? this : AllFilter.of(this, that); } /** * @see #and */ default Filter or(Filter that) { return this == that ? this : AnyFilter.of(this, that); } enum QueryResponse { ALLOW, DENY, ABSTAIN; // TODO: Rename this to isNotDenied public boolean isAllowed() { return this == ALLOW || this == ABSTAIN; } public boolean isDenied() { return this == DENY; } public boolean isPresent() { return this != ABSTAIN; } public boolean toBoolean(boolean def) { switch(this) { case ALLOW: return true; case DENY: return false; default: return def; } } public QueryResponse orElse(QueryResponse def) { return isPresent() ? this : def; } public static QueryResponse any(QueryResponse... responses) { return any(Stream.of(responses)); } public static QueryResponse any(Stream<QueryResponse> responses) { return responses.filter(QueryResponse::isPresent) .reduce((a, b) -> QueryResponse.fromBoolean(a.isAllowed() || b.isAllowed())) .orElse(ABSTAIN); } public static QueryResponse all(QueryResponse... responses) { return all(Stream.of(responses)); } public static QueryResponse all(Stream<QueryResponse> responses) { return responses.filter(QueryResponse::isPresent) .reduce((a, b) -> QueryResponse.fromBoolean(a.isAllowed() && b.isAllowed())) .orElse(ABSTAIN); } public static QueryResponse first(QueryResponse... responses) { return first(Stream.of(responses)); } public static QueryResponse first(Stream<QueryResponse> responses) { return responses.filter(QueryResponse::isPresent) .findFirst() .orElse(ABSTAIN); } public static QueryResponse fromBoolean(boolean allow) { return allow ? ALLOW : DENY; } } default QueryResponse query(Block block) { return query(new BlockQuery(block)); } default QueryResponse query(BlockState block) { return query(new BlockQuery(block)); } default boolean allows(IQuery query) { return query(query) == QueryResponse.ALLOW; } default boolean allows(Block block) { return query(block) == QueryResponse.ALLOW; } default boolean allows(BlockState block) { return query(block) == QueryResponse.ALLOW; } default boolean denies(IQuery query) { return query(query) == QueryResponse.DENY; } default boolean denies(Block block) { return query(block) == QueryResponse.DENY; } default boolean denies(BlockState block) { return query(block) == QueryResponse.DENY; } abstract class Impl extends FeatureDefinition.Impl implements Filter {} }