package de.accso.flexinale.common.infrastructure.eventbus;

import de.accso.flexinale.common.api.eventbus.EventSubscriber;
import de.accso.flexinale.common.api.event.Event;
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

final class EventSubscriberReceiveDispatcher<E extends Event> {

    void callSubscriberReceiveMethod(final Class<E> eventType, final E event, final EventSubscriber<E> subscriber) {
        boolean called = false;

        if (subscriber instanceof EventSubscriber.EventSubscriber9<?, ?, ?, ?, ?, ?, ?, ?, ?>) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE9);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber8<?, ?, ?, ?, ?, ?, ?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE8);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber7<?, ?, ?, ?, ?, ?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE7);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber6<?, ?, ?, ?, ?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE6);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber5<?, ?, ?, ?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE5);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber4<?, ?, ?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE4);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber3<?, ?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE3);
        }
        if (!called && (subscriber instanceof EventSubscriber.EventSubscriber2<?, ?>)) {
            called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE2);
        }
        if (!called) {
            subscriber.receive(event);
        }
    }

    private record ReflectionMethodCacheKey<E extends Event>(
            EventSubscriber<E> subscriber, String methodName, Class<E> eventType) {}

    private final Map<ReflectionMethodCacheKey<E>, Method> reflectionMethodCache = new HashMap<>();

    private boolean tryToGetAndInvokeReceiveMethod(
            final Class<E> eventType, final E event, final EventSubscriber<E> subscriber, final String methodName)
    {
        if (subscriber == null) {
            throw new DeveloperMistakeException("could not call a method for a null subscriber  for event type %s, event=%s"
                    .formatted(eventType, event.toString()));
        }

        ReflectionMethodCacheKey<E> key = new ReflectionMethodCacheKey<>(subscriber, methodName, eventType);
        Method receiveMethod = reflectionMethodCache.get(key);
        try {
            // we could extend this that also receive methods are found with have an event type as parameter as a
            //   superclass of 'eventtype', cf test case
            //   InMemorySyncEventBusAndEventBusSpyTest.testGenericEventSubscriberWithExtraSubscriptionCannotBeUsedDueToDispatchingError()
            if (receiveMethod == null) {
                receiveMethod = subscriber.getClass().getMethod(methodName, eventType);
                receiveMethod.setAccessible(true);
                reflectionMethodCache.put(key, receiveMethod);
            }
            receiveMethod.invoke(subscriber, event);
            return true;
        }
        catch (NoSuchMethodException nsmex) {
            return false;
        }
        // any exception which is thrown because of "wrong" reflection due to a developer mistake (there is no test for that, could not create a test setup for this)
        catch (IllegalAccessException | IllegalArgumentException | NullPointerException | ExceptionInInitializerError ex) {
            throw new DeveloperMistakeException(("DeveloperMistakeException when calling the receive method %s for the subscriber %s " +
                    "for event type %s, event=%s").formatted(methodName, subscriber, eventType, event.toString()), ex);
        }
        // Any other exception which is thrown at runtime - which is ok and hence not a developer mistake
        // Such exceptions are always wrapped in a InvocationTargetException
        catch (InvocationTargetException ex) {
            throw new RuntimeException("Exception when calling the receive method '%s' for the subscriber %s for event type %s, event=%s"
                    .formatted(methodName, subscriber, eventType, event.toString()), ex);
        }
    }
}