001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.util.query;
020
021import com.google.common.base.Preconditions;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.player.PlotPlayer;
024import com.plotsquared.core.plot.Plot;
025import com.plotsquared.core.plot.PlotArea;
026import com.plotsquared.core.plot.Rating;
027import com.plotsquared.core.plot.flag.implementations.DoneFlag;
028import com.plotsquared.core.plot.world.PlotAreaManager;
029import com.plotsquared.core.util.MathMan;
030import org.checkerframework.checker.nullness.qual.NonNull;
031
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.Comparator;
036import java.util.HashSet;
037import java.util.Iterator;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042import java.util.UUID;
043import java.util.function.Predicate;
044import java.util.stream.Stream;
045
046/**
047 * This represents a plot query, and can be used to
048 * search for plots matching certain criteria.
049 * <p>
050 * The queries can be reused as no results are stored
051 * in the query itself
052 */
053public final class PlotQuery implements Iterable<Plot> {
054
055    private final Collection<PlotFilter> filters = new LinkedList<>();
056    private final PlotAreaManager plotAreaManager;
057    private PlotProvider plotProvider;
058    private SortingStrategy sortingStrategy = SortingStrategy.NO_SORTING;
059    private PlotArea priorityArea;
060    private Comparator<Plot> plotComparator;
061
062    private PlotQuery(final @NonNull PlotAreaManager plotAreaManager) {
063        this.plotAreaManager = plotAreaManager;
064        this.plotProvider = new GlobalPlotProvider(plotAreaManager);
065    }
066
067    /**
068     * Create a new plot query instance
069     *
070     * @return New query
071     */
072    public static PlotQuery newQuery() {
073        return new PlotQuery(PlotSquared.get().getPlotAreaManager());
074    }
075
076    /**
077     * Query for plots in a single area
078     *
079     * @param area Area
080     * @return The query instance
081     */
082    public @NonNull PlotQuery inArea(final @NonNull PlotArea area) {
083        Preconditions.checkNotNull(area, "Area may not be null");
084        this.plotProvider = new AreaLimitedPlotProvider(Collections.singletonList(area));
085        return this;
086    }
087
088    /**
089     * Query for plots in all areas in a world
090     *
091     * @param world World name
092     * @return The query instance
093     */
094    public @NonNull PlotQuery inWorld(final @NonNull String world) {
095        Preconditions.checkNotNull(world, "World may not be null");
096        this.plotProvider = new AreaLimitedPlotProvider(this.plotAreaManager.getPlotAreasSet(world));
097        return this;
098    }
099
100    /**
101     * Query for plots in specific areas
102     *
103     * @param areas Plot areas
104     * @return The query instance
105     */
106    public @NonNull PlotQuery inAreas(final @NonNull Collection<PlotArea> areas) {
107        Preconditions.checkNotNull(areas, "Areas may not be null");
108        Preconditions.checkState(!areas.isEmpty(), "At least one area must be provided");
109        this.plotProvider = new AreaLimitedPlotProvider(Collections.unmodifiableCollection(areas));
110        return this;
111    }
112
113    /**
114     * Query for expired plots
115     *
116     * @return The query instance
117     */
118    public @NonNull PlotQuery expiredPlots() {
119        this.plotProvider = new ExpiredPlotProvider();
120        return this;
121    }
122
123    /**
124     * Query for all plots
125     *
126     * @return The query instance
127     */
128    public @NonNull PlotQuery allPlots() {
129        this.plotProvider = new GlobalPlotProvider(this.plotAreaManager);
130        return this;
131    }
132
133    /**
134     * Don't query at all
135     *
136     * @return The query instance
137     */
138    public @NonNull PlotQuery noPlots() {
139        this.plotProvider = new NullProvider();
140        return this;
141    }
142
143    /**
144     * Query for plots based on a search term
145     *
146     * @param searchTerm search term to use (uuid, plotID, username)
147     * @return The query instance
148     */
149    public @NonNull PlotQuery plotsBySearch(final @NonNull String searchTerm) {
150        Preconditions.checkNotNull(searchTerm, "Search term may not be null");
151        this.plotProvider = new SearchPlotProvider(searchTerm);
152        return this;
153    }
154
155    /**
156     * Query with a pre-defined result
157     *
158     * @param plot to return when Query is searched
159     * @return The query instance
160     */
161    public @NonNull PlotQuery withPlot(final @NonNull Plot plot) {
162        Preconditions.checkNotNull(plot, "Plot may not be null");
163        this.plotProvider = new FixedPlotProvider(plot);
164        return this;
165    }
166
167    /**
168     * Query for base plots only
169     *
170     * @return The query instance
171     */
172    public @NonNull PlotQuery whereBasePlot() {
173        return this.addFilter(new PredicateFilter(Plot::isBasePlot));
174    }
175
176    /**
177     * Query for plots owned by a specific player
178     *
179     * @param owner Owner UUID
180     * @return The query instance
181     */
182    public @NonNull PlotQuery ownedBy(final @NonNull UUID owner) {
183        Preconditions.checkNotNull(owner, "Owner may not be null");
184        return this.addFilter(new OwnerFilter(owner));
185    }
186
187    /**
188     * Query for plots owned by a specific player
189     *
190     * @param owner Owner
191     * @return The query instance
192     */
193    public @NonNull PlotQuery ownedBy(final @NonNull PlotPlayer<?> owner) {
194        Preconditions.checkNotNull(owner, "Owner may not be null");
195        return this.addFilter(new OwnerFilter(owner.getUUID()));
196    }
197
198    /**
199     * Query for base plots where one of the merged plots is owned by a specific player
200     *
201     * @param owner Owner UUID
202     * @return The query instance
203     * @since 6.1.0
204     */
205    public @NonNull PlotQuery ownersInclude(final @NonNull UUID owner) {
206        Preconditions.checkNotNull(owner, "Owner may not be null");
207        return this.addFilter(new OwnersIncludeFilter(owner));
208    }
209
210    /**
211     * Query for base plots where one of the merged plots is owned by a specific player
212     *
213     * @param owner Owner
214     * @return The query instance
215     * @since 6.1.0
216     */
217    public @NonNull PlotQuery ownersInclude(final @NonNull PlotPlayer<?> owner) {
218        Preconditions.checkNotNull(owner, "Owner may not be null");
219        return this.addFilter(new OwnersIncludeFilter(owner.getUUID()));
220    }
221
222    /**
223     * Query for plots with a specific alias
224     *
225     * @param alias Plot alias
226     * @return The query instance
227     */
228    public @NonNull PlotQuery withAlias(final @NonNull String alias) {
229        Preconditions.checkNotNull(alias, "Alias may not be null");
230        return this.addFilter(new AliasFilter(alias));
231    }
232
233    /**
234     * Query for plots with a specific member (added/trusted/owner)
235     *
236     * @param member Member UUID
237     * @return The query instance
238     */
239    public @NonNull PlotQuery withMember(final @NonNull UUID member) {
240        Preconditions.checkNotNull(member, "Member may not be null");
241        return this.addFilter(new MemberFilter(member));
242    }
243
244    /**
245     * Query for plots that passes a given predicate
246     *
247     * @param predicate Predicate
248     * @return The query instance
249     */
250    public @NonNull PlotQuery thatPasses(final @NonNull Predicate<Plot> predicate) {
251        Preconditions.checkNotNull(predicate, "Predicate may not be null");
252        return this.addFilter(new PredicateFilter(predicate));
253    }
254
255    /**
256     * Specify the sorting strategy that will decide how to
257     * sort the results. This only matters if you use {@link #asList()}
258     *
259     * @param strategy Strategy
260     * @return The query instance
261     */
262    public @NonNull PlotQuery withSortingStrategy(final @NonNull SortingStrategy strategy) {
263        Preconditions.checkNotNull(strategy, "Strategy may not be null");
264        this.sortingStrategy = strategy;
265        return this;
266    }
267
268    /**
269     * Use a custom comparator to sort the results
270     *
271     * @param comparator Comparator
272     * @return The query instance
273     */
274    public @NonNull PlotQuery sorted(final @NonNull Comparator<Plot> comparator) {
275        Preconditions.checkNotNull(comparator, "Comparator may not be null");
276        this.sortingStrategy = SortingStrategy.COMPARATOR;
277        this.plotComparator = comparator;
278        return this;
279    }
280
281    /**
282     * Defines the area around which plots may be sorted, depending on the
283     * sorting strategy
284     *
285     * @param plotArea Plot area
286     * @return The query instance
287     */
288    public @NonNull PlotQuery relativeToArea(final @NonNull PlotArea plotArea) {
289        Preconditions.checkNotNull(plotArea, "Area may not be null");
290        this.priorityArea = plotArea;
291        return this;
292    }
293
294    /**
295     * Get all plots that match the given criteria
296     *
297     * @return Matching plots
298     */
299    public @NonNull Stream<Plot> asStream() {
300        return this.asList().stream();
301    }
302
303    /**
304     * Get all plots that match the given criteria
305     *
306     * @return Matching plots as a mutable
307     */
308    public @NonNull List<Plot> asList() {
309        final List<Plot> result;
310        if (this.filters.isEmpty()) {
311            result = new ArrayList<>(this.plotProvider.getPlots());
312        } else {
313            final Collection<Plot> plots = this.plotProvider.getPlots();
314            result = new ArrayList<>(plots.size());
315            outer:
316            for (final Plot plot : plots) {
317                for (final PlotFilter filter : this.filters) {
318                    if (!filter.accepts(plot)) {
319                        continue outer;
320                    }
321                }
322                result.add(plot);
323            }
324        }
325        if (this.sortingStrategy == SortingStrategy.NO_SORTING) {
326            return result;
327        } else if (this.sortingStrategy == SortingStrategy.SORT_BY_TEMP) {
328            return PlotSquared.get().sortPlotsByTemp(result);
329        } else if (this.sortingStrategy == SortingStrategy.SORT_BY_DONE) {
330            result.sort((a, b) -> {
331                String va = a.getFlag(DoneFlag.class);
332                String vb = b.getFlag(DoneFlag.class);
333                if (MathMan.isInteger(va)) {
334                    if (MathMan.isInteger(vb)) {
335                        return Integer.parseInt(vb) - Integer.parseInt(va);
336                    }
337                    return -1;
338                }
339                return 1;
340            });
341        } else if (this.sortingStrategy == SortingStrategy.SORT_BY_RATING) {
342            result.sort((p1, p2) -> {
343                double v1 = 0;
344                int p1s = p1.getSettings().getRatings().size();
345                int p2s = p2.getRatings().size();
346                if (!p1.getSettings().getRatings().isEmpty()) {
347                    v1 = p1.getRatings().values().stream().mapToDouble(Rating::getAverageRating)
348                            .map(av -> av * av).sum();
349                    v1 /= p1s;
350                    v1 += p1s;
351                }
352                double v2 = 0;
353                if (!p2.getSettings().getRatings().isEmpty()) {
354                    for (Map.Entry<UUID, Rating> entry : p2.getRatings().entrySet()) {
355                        double av = entry.getValue().getAverageRating();
356                        v2 += av * av;
357                    }
358                    v2 /= p2s;
359                    v2 += p2s;
360                }
361                if (v2 == v1 && v2 != 0) {
362                    return p2s - p1s;
363                }
364                return (int) Math.signum(v2 - v1);
365            });
366        } else if (this.sortingStrategy == SortingStrategy.SORT_BY_CREATION) {
367            return PlotSquared.get().sortPlots(result, PlotSquared.SortType.CREATION_DATE, this.priorityArea);
368        } else if (this.sortingStrategy == SortingStrategy.COMPARATOR) {
369            result.sort(this.plotComparator);
370        }
371        return result;
372    }
373
374    /**
375     * Get all plots that match the given criteria
376     *
377     * @return Matching plots as a mutable set
378     */
379    public @NonNull Set<Plot> asSet() {
380        return new HashSet<>(this.asList());
381    }
382
383    /**
384     * Get all plots that match the given criteria
385     * in the form of a {@link PaginatedPlotResult}
386     *
387     * @param pageSize The size of the pages. Must be positive.
388     * @return Paginated plot result
389     */
390    public @NonNull PaginatedPlotResult getPaginated(final int pageSize) {
391        Preconditions.checkState(pageSize > 0, "Page size must be greater than 0");
392        return new PaginatedPlotResult(this.asList(), pageSize);
393    }
394
395    /**
396     * Get all plots that match the given criteria
397     *
398     * @return Matching plots as an immutable collection
399     */
400    public @NonNull Collection<Plot> asCollection() {
401        return this.asList();
402    }
403
404    /**
405     * Get the amount of plots contained in the query result
406     *
407     * @return Result count
408     */
409    public int count() {
410        return this.asList().size();
411    }
412
413    /**
414     * Get whether any provided plot matches the given filters.
415     * If no plot was provided, false will be returned.
416     *
417     * @return {@code true} if any provided plot matches the filters.
418     */
419    public boolean anyMatch() {
420        if (this.filters.isEmpty()) {
421            return !this.plotProvider.getPlots().isEmpty();
422        } else {
423            final Collection<Plot> plots = this.plotProvider.getPlots();
424            outer:
425            for (final Plot plot : plots) {
426                // a plot must pass all filters to match the criteria
427                for (final PlotFilter filter : this.filters) {
428                    if (!filter.accepts(plot)) {
429                        continue outer;
430                    }
431                }
432                return true; // a plot passed all filters, so we have a match
433            }
434            return false;
435        }
436    }
437
438    @NonNull
439    private PlotQuery addFilter(final @NonNull PlotFilter filter) {
440        this.filters.add(filter);
441        return this;
442    }
443
444    @NonNull
445    @Override
446    public Iterator<Plot> iterator() {
447        return this.asCollection().iterator();
448    }
449
450}