package speiger.src.coreengine.core.events.bus;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.maps.impl.concurrent.Object2ObjectConcurrentOpenHashMap;
import speiger.src.coreengine.core.events.api.Event;
import speiger.src.coreengine.core.events.api.EventPriority;
import speiger.src.coreengine.core.events.api.SubscribeEvent;
import speiger.src.coreengine.core.events.utility.IEventDispatcher;
import speiger.src.coreengine.core.events.utility.IEventExceptionHandler;
import speiger.src.coreengine.core.events.utility.Listeners;
import speiger.src.coreengine.core.events.utility.Subscription;

public class EventBus implements IEventExceptionHandler {
	private static final Lookup LOOKUP = MethodHandles.lookup();
	IEventDispatcher dispatcher;
	IEventExceptionHandler exceptions;
	ClassFilter typeCheck;
	boolean shutdown = false;
	
	Map<Class<? extends Event>, Listeners> listeners = new Object2ObjectConcurrentOpenHashMap<>();
	
	EventBus(BusBuilder builder) {
		dispatcher = builder.dispatcher;
		exceptions = builder.exceptions.orElse(this);
		typeCheck = builder.typeCheck;
		shutdown = !builder.running;
	}
	
	public static BusBuilder builder() {
		return new BusBuilder();
	}
	
	public Subscriptions create() {
		return new Subscriptions(this);
	}
	
	public void start() {
		shutdown = false;
	}
	
	public void shutdown() {
		shutdown = true;
	}
	
	public <T extends Event> T post(T event) {
		if(shutdown) return event;
		validateType(event.getClass());
		Listeners listener = listeners.get(event.getClass());
		if(listener == null) return event;
		dispatcher.post(listener.getListeners(), event, exceptions);
		return event;
	}
	
	public <T extends Event> CompletableFuture<T> postAsync(T event) {
		if(shutdown) return CompletableFuture.completedFuture(event);
		return CompletableFuture.supplyAsync(() -> post(event)); 
	}
	
	public <T extends Event> CompletableFuture<T> postAsync(T event, Executor executor) {
		if(shutdown) return CompletableFuture.completedFuture(event);
		return CompletableFuture.supplyAsync(() -> post(event), executor);
	}
	
	@Override
	public void handleException(Event event, Consumer<Event>[] listeners, int index, Throwable crash) {
		StringBuilder builder = new StringBuilder();
		builder.append("Event ["+event.getClass()+"] caused an Exception: ").append(crash.getMessage()).append('\n');
		builder.append('\t').append("At Index=").append(index).append('\n');
		builder.append("Listeners: ");
		for (int x = 0; x < listeners.length; x++) {
			builder.append("\t\t").append(x).append(": ").append(listeners[x]).append('\n');
		}
		StringWriter exception = new StringWriter();
		crash.printStackTrace(new PrintWriter(exception));
		builder.append(exception.getBuffer());
		//TODO implement logging
		System.err.println(builder.toString());
	}
	
	<T extends Event> Subscription register(Class<T> clz, EventPriority prio, Consumer<T> listener) {
		validateType(clz);
		registerListener(clz).addListener(prio, cast(listener));
		return new EventListener(castClass(clz), cast(listener));
	}
	
	<T extends Event> List<Subscription> register(Object obj, boolean superClasses) {
		final List<Subscription> list = new ObjectArrayList<>();
		try {
			register(obj.getClass().getDeclaredAnnotation(SubscribeEvent.class), obj, list);
			find(obj.getClass(), superClasses, Class::getDeclaredMethods, t -> {
				try {
					t.setAccessible(true);
					SubscribeEvent data = t.getAnnotation(SubscribeEvent.class);
					if(data == null) return;
					Consumer<Event> listener = new MethodListener(LOOKUP.unreflect(t).bindTo(obj));
					Class<Event> clz = castClass(t.getParameterTypes()[0]); 
					validateType(clz);
					registerListener(clz).addListener(data.priority(), listener);
					list.add(new EventListener(clz, listener));
				}
				catch(Exception e) { e.printStackTrace(); }
			});
			find(obj.getClass(), superClasses, Class::getDeclaredFields, t -> {
				try {
					t.setAccessible(true);
					register(t.getAnnotation(SubscribeEvent.class), t.get(obj), list);						
				}
				catch(Exception e) { e.printStackTrace(); }
			});
		}
		catch(Exception e) { e.printStackTrace(); }
		return list;
	}
	
	private synchronized Listeners registerListener(Class<? extends Event> event) {
		Listeners result = listeners.get(event);
		if(result == null) {
			result = event == Event.class ? new Listeners() : new Listeners(registerListener(castClass(event.getSuperclass())));
			listeners.put(event, result);
		}
		return result;
	}
		
	public class EventListener implements Subscription {
		Class<Event> event;
		Consumer<Event> listener;
		
		public EventListener(Class<Event> event, Consumer<Event> listener) {
			this.event = event;
			this.listener = listener;
		}
		
		@Override
		public void unsubscribe() { registerListener(event).removeListeners(listener); }
		@Override
		public Class<Event> type() { return event; }
	}
	
	public static record MethodListener(MethodHandle handle) implements Consumer<Event> {
		@Override
		public void accept(Event t) {
			try { handle.invoke(t); }
			catch(Throwable e) { e.printStackTrace(); }
		}
		
		@Override
		public final String toString() {
			MethodHandleInfo type = LOOKUP.revealDirect(handle);
			return String.format("%s(%s)", type.getDeclaringClass().getCanonicalName(), String.join(", ", type.getMethodType().parameterList().stream().map(Class::getSimpleName).toList()));
		}
	}
	
	public static record ClassFilter(boolean enabled, Predicate<Class<? extends Event>> event, String message) {
		public boolean isInvalid(Class<? extends Event> filter) {
			return enabled() && !event().test(filter);
		}
	}
	
	private void register(SubscribeEvent data, Object obj, List<Subscription> listeners) {
		if(data == null || !(obj instanceof Consumer)) return;
		validateType(data.value());
		registerListener(data.value()).addListener(data.priority(), cast(obj));
		listeners.add(new EventListener(castClass(data.value()), cast(obj)));
	}
	
	private <T> void find(Class<?> clz, boolean superClasses, Function<Class<?>, T[]> mapper, Consumer<T> result) {
		do {
			for(T value : mapper.apply(clz)) result.accept(value);
			clz = clz.getSuperclass();
		}
		while(clz != Object.class && superClasses);
	}
	
	private void validateType(Class<? extends Event> type) {
		if(typeCheck.isInvalid(type)) throw new IllegalStateException(String.format(typeCheck.message(), type.getSimpleName()));
	}
	
	@SuppressWarnings("unchecked")
	private <T> Consumer<T> cast(Object obj) {
		return (Consumer<T>) obj;
	}
	
	@SuppressWarnings("unchecked")
	private <T> Class<T> castClass(Class<?> clz) {
		return (Class<T>) clz;
	}

}
