package tc.oc.commons.bukkit.util;

import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableSet;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;

import java.util.HashMap;
import java.util.Map;

/**
 * {@link java.util.Map} adapter that uses {@link K} keys and guarantees that the map only ever contains valid entries
 * defined by the {@link #isValid(K)} method, subclasses override that method to define what keys are valid.
 * Subclass are also responsible for defining the events and their actions when the adapter is enabled.
 * MapAdapter is a {@link org.bukkit.event.Listener} and registers itself to receive events on behalf of the plugin
 * passed to the constructor when {@link #enable()} is called. This must be called before using the map. The map
 * can be unregistered by calling {@link #disable()}.
 */
public abstract class ListeningMapAdapter<K, V> extends ForwardingMap<K, V> implements Listener {

    protected final Plugin plugin;
    protected final Map<K, V> map;
    protected boolean enabled;
    protected boolean lazyEnable = true;

    public ListeningMapAdapter(Plugin plugin) {
        this(new HashMap<>(), plugin);
    }

    public ListeningMapAdapter(Map<K, V> map, Plugin plugin) {
        this.plugin = plugin;
        this.map = map;
    }

    protected void assertEnabled() {
        if(!this.enabled) {
            if(this.lazyEnable) {
                this.enable();
            } else {
                throw new IllegalStateException("This " + this.getClass().getSimpleName() + " is not enabled");
            }
        }
    }

    /**
     * If the given entry is valid, add it to the map and return any previous value for
     * that entry, or null if they were previously not in the map. If the entry is not valid,
     * no change is made to the map and null is returned.
     */
    @Override
    public V put(K key, V value) {
        this.assertEnabled();
        if(isValid(key)) {
            return this.map.put(key, value);
        } else {
            return null;
        }
    }

    /**
     * If the entry is a valid new entry
     */
    public abstract boolean isValid(K key);

    /**
     * Add an entry to the map for the given key and return any previous value for
     * that entity, or null if they were previously not in the map. The entry
     * is added ignoring the checks.
     */
    public V force(K key, V value) {
        this.assertEnabled();
        return this.map.put(key, value);
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> otherMap) {
        for(Entry<? extends K, ? extends V> entry : otherMap.entrySet()) {
            this.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Special case of putAll that does not check if each entry is valid
     */
    public void putAll(ListeningMapAdapter<? extends K, ? extends V> otherMap) {
        this.assertEnabled();
        this.map.putAll(otherMap);
    }

    /**
     * Register to receive events. This must be called before adding any entries to the map.
     */
    public void enable() {
        if(!this.enabled) {
            this.plugin.getServer().getPluginManager().registerEvents(this, plugin);
            this.enabled = true;
        }
    }

    /**
     * Clear the list and stop listening for events. This map should not be used after this method is called.
     */
    public void disable() {
        if(this.enabled) {
            this.clear();
            HandlerList.unregisterAll(this);
        }
    }

    /**
     * Return an immutable copy of this container's {@link #entrySet} that is
     * safe to iterate over while the container is modified, which tends to
     * happen unexpectedly from events the container is listening to.
     */
    public ImmutableSet<Entry<K, V>> entrySetCopy() {
        return ImmutableSet.copyOf(this.entrySet());
    }

    @Override
    protected Map<K, V> delegate() {
        return this.map;
    }
}
