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.plot.flag;
020
021import com.google.common.base.Preconditions;
022import com.google.common.collect.ImmutableMap;
023import com.intellectualsites.annotations.NotPublic;
024import com.plotsquared.core.configuration.caption.CaptionUtility;
025import org.apache.logging.log4j.LogManager;
026import org.apache.logging.log4j.Logger;
027import org.checkerframework.checker.nullness.qual.NonNull;
028import org.checkerframework.checker.nullness.qual.Nullable;
029
030import java.util.Collection;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Locale;
034import java.util.Map;
035
036/**
037 * Container type for {@link PlotFlag plot flags}.
038 */
039public class FlagContainer {
040
041    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + FlagContainer.class.getSimpleName());
042
043    private final Map<String, String> unknownFlags = new HashMap<>();
044    private final Map<Class<?>, PlotFlag<?, ?>> flagMap = new HashMap<>();
045    private final PlotFlagUpdateHandler plotFlagUpdateHandler;
046    private final Collection<PlotFlagUpdateHandler> updateSubscribers = new HashSet<>();
047    private final PlotFlagUpdateHandler unknownsRef;
048    private FlagContainer parentContainer;
049
050    /**
051     * Construct a new flag container with an optional parent container and update handler.
052     * Default values are inherited from the parent container. At the top
053     * of the parent-child hierarchy must be the {@link GlobalFlagContainer}
054     * (or an equivalent top level flag container).
055     *
056     * @param parentContainer       Parent container. The top level flag container should not have a parent,
057     *                              and can set this parameter to null. If this is not a top level
058     *                              flag container, the parent should not be null.
059     * @param plotFlagUpdateHandler Event handler that will be called whenever a plot flag is
060     *                              added, removed or updated in this flag container.
061     */
062    public FlagContainer(
063            final @Nullable FlagContainer parentContainer,
064            @Nullable PlotFlagUpdateHandler plotFlagUpdateHandler
065    ) {
066        this.parentContainer = parentContainer;
067        this.plotFlagUpdateHandler = plotFlagUpdateHandler;
068        if (!(this instanceof GlobalFlagContainer)) {
069            this.unknownsRef = this::handleUnknowns;
070            GlobalFlagContainer.getInstance().subscribe(this.unknownsRef);
071        } else {
072            this.unknownsRef = null;
073        }
074    }
075
076    /**
077     * Construct a new flag container with an optional parent container.
078     * Default values are inherited from the parent container. At the top
079     * of the parent-child hierarchy must be the {@link GlobalFlagContainer}
080     * (or an equivalent top level flag container).
081     *
082     * @param parentContainer Parent container. The top level flag container should not have a parent,
083     *                        and can set this parameter to null. If this is not a top level
084     *                        flag container, the parent should not be null.
085     */
086    public FlagContainer(final @Nullable FlagContainer parentContainer) {
087        this(parentContainer, null);
088    }
089
090    /**
091     * Cast a plot flag with wildcard parameters into a parametrized
092     * PlotFlag. This is an unsafe operation, and should only be performed
093     * if the generic parameters are known beforehand.
094     *
095     * @param flag Flag instance
096     * @param <V>  Flag value type
097     * @param <T>  Flag type
098     * @return Casted flag
099     */
100    @SuppressWarnings("unchecked")
101    public static <V, T extends PlotFlag<V, ?>> T castUnsafe(
102            final PlotFlag<?, ?> flag
103    ) {
104        return (T) flag;
105    }
106
107    /**
108     * Return the parent container (if the container has a parent)
109     *
110     * @return Parent container, if it exists
111     */
112    public @Nullable FlagContainer getParentContainer() {
113        return this.parentContainer;
114    }
115
116    public void setParentContainer(FlagContainer parentContainer) {
117        this.parentContainer = parentContainer;
118    }
119
120    @SuppressWarnings("unused")
121    protected Map<Class<?>, PlotFlag<?, ?>> getInternalPlotFlagMap() {
122        return this.flagMap;
123    }
124
125    /**
126     * Get an immutable view of the underlying flag map
127     *
128     * @return Immutable flag map
129     */
130    public Map<Class<?>, PlotFlag<?, ?>> getFlagMap() {
131        return ImmutableMap.<Class<?>, PlotFlag<?, ?>>builder().putAll(this.flagMap).build();
132    }
133
134    /**
135     * Add a flag to the container
136     *
137     * <p>
138     * Use {@link #addAll(Collection)} to add multiple flags.
139     * </p>
140     *
141     * @param flag Flag to add
142     * @param <T>  flag type
143     * @param <V>  flag value type
144     */
145    public <V, T extends PlotFlag<V, ?>> void addFlag(final T flag) {
146        try {
147            Preconditions.checkState(
148                    flag.getName().length() <= 64,
149                    "flag name may not be more than 64 characters. Check: " + flag.getName()
150            );
151            final PlotFlag<?, ?> oldInstance = this.flagMap.put(flag.getClass(), flag);
152            final PlotFlagUpdateType plotFlagUpdateType;
153            if (oldInstance != null) {
154                plotFlagUpdateType = PlotFlagUpdateType.FLAG_UPDATED;
155            } else {
156                plotFlagUpdateType = PlotFlagUpdateType.FLAG_ADDED;
157            }
158            if (this.plotFlagUpdateHandler != null) {
159                this.plotFlagUpdateHandler.handle(flag, plotFlagUpdateType);
160            }
161            this.updateSubscribers
162                    .forEach(subscriber -> subscriber.handle(flag, plotFlagUpdateType));
163        } catch (IllegalStateException e) {
164            LOGGER.info("Flag {} (class '{}') could not be added to the container because the "
165                    + "flag name exceeded the allowed limit of 64 characters. Please tell the developer "
166                    + "of the flag to fix this.", flag.getName(), flag.getClass().getName());
167            e.printStackTrace();
168        }
169    }
170
171    /**
172     * Remove a flag from the container
173     *
174     * @param flag Flag to remove
175     * @param <T>  flag type
176     * @param <V>  flag value type
177     * @return value of flag removed
178     */
179    @SuppressWarnings("unchecked")
180    public <V, T extends PlotFlag<V, ?>> V removeFlag(final T flag) {
181        final Object value = this.flagMap.remove(flag.getClass());
182        if (this.plotFlagUpdateHandler != null) {
183            this.plotFlagUpdateHandler.handle(flag, PlotFlagUpdateType.FLAG_REMOVED);
184        }
185        this.updateSubscribers
186                .forEach(subscriber -> subscriber.handle(flag, PlotFlagUpdateType.FLAG_REMOVED));
187        if (value == null) {
188            return null;
189        } else {
190            return (V) value;
191        }
192    }
193
194    /**
195     * Add all flags to the container
196     *
197     * <p>
198     * Use {@link #addFlag(PlotFlag)} to add a single flag.
199     * </p>
200     *
201     * @param flags Flags to add
202     */
203    public void addAll(final Collection<PlotFlag<?, ?>> flags) {
204        for (final PlotFlag<?, ?> flag : flags) {
205            this.addFlag(flag);
206        }
207    }
208
209    public void addAll(final FlagContainer container) {
210        this.addAll(container.flagMap.values());
211    }
212
213    /**
214     * Clears the local flag map
215     */
216    public void clearLocal() {
217        this.flagMap.clear();
218    }
219
220    /**
221     * Get a collection of all recognized plot flags. Will by
222     * default use the values contained in {@link GlobalFlagContainer}.
223     *
224     * @return All recognized flag types
225     */
226    public Collection<PlotFlag<?, ?>> getRecognizedPlotFlags() {
227        return this.getHighestClassContainer().getFlagMap().values();
228    }
229
230    /**
231     * Recursively seek for the highest order flag container.
232     * This will by default return {@link GlobalFlagContainer}.
233     *
234     * @return Highest order class container.
235     */
236    public final FlagContainer getHighestClassContainer() {
237        if (this.getParentContainer() != null) {
238            return this.getParentContainer();
239        }
240        return this;
241    }
242
243    /**
244     * Has the same functionality as {@link #getFlag(Class)}, but
245     * with wildcard generic types.
246     *
247     * @param flagClass The {@link PlotFlag} class.
248     * @return the plot flag
249     */
250    public PlotFlag<?, ?> getFlagErased(Class<?> flagClass) {
251        final PlotFlag<?, ?> flag = this.flagMap.get(flagClass);
252        if (flag != null) {
253            return flag;
254        } else {
255            if (getParentContainer() != null) {
256                return getParentContainer().getFlagErased(flagClass);
257            }
258        }
259        return null;
260    }
261
262    /**
263     * Query all levels of flag containers for a flag. This guarantees that a flag
264     * instance is returned, as long as it is registered in the
265     * {@link GlobalFlagContainer global flag container}.
266     *
267     * @param flagClass Flag class to query for
268     * @param <V>       Flag value type
269     * @param <T>       Flag type
270     * @return Flag instance
271     */
272    public <V, T extends PlotFlag<V, ?>> T getFlag(final Class<? extends T> flagClass) {
273        final PlotFlag<?, ?> flag = this.flagMap.get(flagClass);
274        if (flag != null) {
275            return castUnsafe(flag);
276        } else {
277            if (getParentContainer() != null) {
278                return getParentContainer().getFlag(flagClass);
279            }
280        }
281        return null;
282    }
283
284    /**
285     * Check for flag existence in this flag container instance.
286     *
287     * @param flagClass Flag class to query for
288     * @param <V>       Flag value type
289     * @param <T>       Flag type
290     * @return The flag instance, if it exists in this container, else null.
291     */
292    public @Nullable <V, T extends PlotFlag<V, ?>> T queryLocal(final Class<?> flagClass) {
293        final PlotFlag<?, ?> localFlag = this.flagMap.get(flagClass);
294        if (localFlag == null) {
295            return null;
296        } else {
297            return castUnsafe(localFlag);
298        }
299    }
300
301    /**
302     * Subscribe to flag updates in this particular flag container instance.
303     * Updates are: a flag being removed, a flag being added or a flag
304     * being updated.
305     *
306     * <p>
307     * Use {@link PlotFlagUpdateType} to see the update types available.
308     * </p>
309     *
310     * @param plotFlagUpdateHandler The update handler which will react to changes.
311     */
312    public void subscribe(final @NonNull PlotFlagUpdateHandler plotFlagUpdateHandler) {
313        this.updateSubscribers.add(plotFlagUpdateHandler);
314    }
315
316    private void handleUnknowns(
317            final PlotFlag<?, ?> flag,
318            final PlotFlagUpdateType plotFlagUpdateType
319    ) {
320        if (plotFlagUpdateType != PlotFlagUpdateType.FLAG_REMOVED && this.unknownFlags
321                .containsKey(flag.getName())) {
322            String value = this.unknownFlags.remove(flag.getName());
323            if (value != null) {
324                value = CaptionUtility.stripClickEvents(flag, value);
325                try {
326                    this.addFlag(flag.parse(value));
327                } catch (final Exception ignored) {
328                }
329            }
330        }
331    }
332
333    /**
334     * Register a flag key-value pair which cannot yet be associated with
335     * an existing flag instance (such as when third party flag values are
336     * loaded before the flag type has been registered).
337     * <p>
338     * These values will be registered in the flag container if the associated
339     * flag type is registered in the top level flag container.
340     *
341     * @param flagName Flag name
342     * @param value    Flag value
343     */
344    public void addUnknownFlag(final String flagName, final String value) {
345        this.unknownFlags.put(flagName.toLowerCase(Locale.ENGLISH), value);
346    }
347
348    /**
349     * Creates a cleanup hook that is meant to run once this FlagContainer isn't needed anymore.
350     * This is to prevent memory leaks. This method is not part of the API.
351     *
352     * @return a new Runnable that cleans up once the FlagContainer isn't needed anymore.
353     * @since 6.0.10
354     */
355    @NotPublic
356    public Runnable createCleanupHook() {
357        return () -> GlobalFlagContainer.getInstance().unsubscribe(unknownsRef);
358    }
359
360    void unsubscribe(final @Nullable PlotFlagUpdateHandler updateHandler) {
361        if (updateHandler != null) {
362            this.updateSubscribers.remove(updateHandler);
363        }
364    }
365
366    @Override
367    public boolean equals(final Object o) {
368        if (this == o) {
369            return true;
370        }
371        if (o == null || getClass() != o.getClass()) {
372            return false;
373        }
374        final FlagContainer that = (FlagContainer) o;
375        return flagMap.equals(that.flagMap);
376    }
377
378    @Override
379    public int hashCode() {
380        return flagMap.hashCode();
381    }
382
383    /**
384     * @deprecated This method is not meant to be invoked or overridden, with no replacement.
385     */
386    @Deprecated(forRemoval = true, since = "6.6.0")
387    protected boolean canEqual(final Object other) {
388        return other instanceof FlagContainer;
389    }
390
391    /**
392     * Update event types used in {@link PlotFlagUpdateHandler}.
393     */
394    public enum PlotFlagUpdateType {
395        /**
396         * A flag was added to a plot container
397         */
398        FLAG_ADDED,
399        /**
400         * A flag was removed from a plot container
401         */
402        FLAG_REMOVED,
403        /**
404         * A flag was already stored in this container,
405         * but a new instance has bow replaced it
406         */
407        FLAG_UPDATED
408    }
409
410
411    /**
412     * Handler for update events in {@link FlagContainer flag containers}.
413     */
414    @FunctionalInterface
415    public interface PlotFlagUpdateHandler {
416
417        /**
418         * Act on the flag update event
419         *
420         * @param plotFlag Plot flag
421         * @param type     Update type
422         */
423        void handle(PlotFlag<?, ?> plotFlag, PlotFlagUpdateType type);
424
425    }
426
427}