package tc.oc.commons.bukkit.event.targeted;

import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.cache.LoadingCache;
import com.google.common.collect.SetMultimap;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import tc.oc.commons.bukkit.event.EventHandlerInfo;
import tc.oc.commons.bukkit.event.EventKey;
import tc.oc.commons.core.exception.ExceptionHandler;
import tc.oc.commons.core.exception.InvalidMemberException;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.CacheUtils;
import tc.oc.commons.core.util.TypeMap;
import tc.oc.minecraft.api.event.Listener;

/**
 * Listens for targeted events and dispatches them to registered {@link TargetedEventHandler}s
 */
@Singleton
public class TargetedEventBusImpl implements PluginFacet, TargetedEventBus {

    private final ExceptionHandler exceptionHandler;
    private final EventRegistry eventRegistry;
    private final Set<EventKey> events = new HashSet<>();
    private final TypeMap<Object, TargetedEventRouter<?>> routers;
    private final LoadingCache<Class<? extends Listener>, SetMultimap<EventKey<? extends Event>, EventHandlerInfo<? extends Event>>> listenerCache;

    @Inject TargetedEventBusImpl(ExceptionHandler exceptionHandler, EventRegistry eventRegistry, TargetedEventHandlerScanner eventHandlerScanner, TypeMap<Object, TargetedEventRouter<?>> routers) {
        this.exceptionHandler = exceptionHandler;
        this.eventRegistry = eventRegistry;
        this.routers = routers;

        // Cache of handler methods per listener type
        this.listenerCache = CacheUtils.newCache(listener -> {
            final SetMultimap<EventKey<? extends Event>, EventHandlerInfo<? extends Event>> handlers = eventHandlerScanner.findEventHandlers(listener);
            for(EventHandlerInfo<? extends Event> info : handlers.values()) {
                if(this.routers.allAssignableFrom(info.event()).isEmpty()) {
                    throw new InvalidMemberException(
                        info.method(),
                        "No router registered for targeted event type " + info.event().getName()
                    );
                }
            }
            return handlers;
        });
    }

    @Override
    public void registerListener(Listener listener) {
        registerListenerType(listener.getClass());
    }

    @Override
    public void unregisterListener(Listener listener) {
    }

    /**
     * Generate MethodHandles for all {@link TargetedEventHandler}s in the given listener class,
     * and ensure we are listening for their event types. If the given class has already been
     * registered, do nothing.
     */
    private void registerListenerType(Class<? extends Listener> listenerType) {
        listenerCache.getUnchecked(listenerType).keySet().forEach(this::registerEvent);
    }

    /**
     * Start listening for the given event/priority, if we aren't already.
     */
    private void registerEvent(EventKey<? extends Event> key) {
        // The first time we see a particular event at a particular priority level, start listening for it
        if(events.add(key)) {
            Event.register(eventRegistry.bindHandler(new EventHandlerMeta<>(key.event(), key.priority(), false), this, new Executor(key)));
        }
    }

    private class Executor implements EventExecutor {

        final EventKey<? extends Event> key;
        Executor(EventKey<? extends Event> key) { this.key = key; }

        @Override
        public void execute(Listener dispatcher, Event event) throws EventException {
            final Set<Listener> listeners = new HashSet<>();

            for(TargetedEventRouter router : routers.allAssignableFrom(event.getClass())) {
                router.listeners(event).forEach(listener -> listeners.add((Listener) listener));
            }

            for(Listener listener : listeners) {
                final SetMultimap<EventKey<? extends Event>, EventHandlerInfo<? extends Event>> handlerMap = listenerCache.getIfPresent(listener.getClass());

                if(handlerMap == null) {
                    exceptionHandler.handleException(new Exception("Targeted event listener is not registered: " + listener));
                    continue;
                }

                for(EventHandlerInfo<? extends Event> handlerInfo : handlerMap.get(key)) {
                    if(handlerInfo.method().getParameterTypes()[0].isInstance(event) &&
                       !(handlerInfo.ignoreCancelled() && event.isCancelled())) {
                        try {
                            handlerInfo.methodHandle().invokeWithArguments(listener, event);
                        } catch(Throwable throwable) {
                            exceptionHandler.handleException(throwable, null, null);
                        }
                    }
                }
            }
        }
    }
}

