package speiger.src.coreengine.utils.eventbus;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.maps.impl.hash.Object2ObjectOpenHashMap;
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
import speiger.src.collections.objects.utils.ObjectLists;
import speiger.src.collections.objects.utils.maps.Object2ObjectMaps;

public class EventBus
{
	private static final Lookup LOOKUP = MethodHandles.lookup();
	Object2ObjectMap<Class<? extends Event>, Listeners> listeners = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>());
	Map<Object, List<EventListener>> instances = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>());
	
	public <T extends Event> void register(Class<T> event, Consumer<T> listener)
	{
		register(event, EventPriority.MEDIUM, listener);
	}
	
	public <T extends Event> void register(Class<T> event, EventPriority priority, Consumer<T> listener)
	{
		if(instances.containsKey(listener)) return;
		getListeners(event).addListener(priority, (Consumer<Event>)listener);
		instances.put(listener, ObjectLists.singleton(new EventListener(event, (Consumer<Event>)listener)));
	}
	
	public void register(Object obj)
	{
		register(obj, false);
	}
	
	public void register(Object obj, boolean superClasses)
	{
		if(instances.containsKey(obj)) return;
		final List<EventListener> list = new ObjectArrayList<>();
		try
		{
			register(obj.getClass().getDeclaredAnnotation(SubscribeEvent.class), obj, list);
			findMethods(obj.getClass(), superClasses, new Consumer<Method>() {
				@Override
				public void accept(Method 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));
						getListeners(data.value()).addListener(data.priority(), listener);
						list.add(new EventListener(data.value(), listener));
					}
					catch(Exception e) { e.printStackTrace(); }
				}
			});
			findFields(obj.getClass(), superClasses, new Consumer<Field>() {
				@Override
				public void accept(Field t) {
					try
					{
						t.setAccessible(true);
						register(t.getAnnotation(SubscribeEvent.class), t.get(obj), list);						
					}
					catch(Exception e) { e.printStackTrace(); }
				}
			});
		}
		catch(Exception e) { e.printStackTrace(); }
		if(list.isEmpty()) return;
		instances.put(obj, list);
	}
	
	private void register(SubscribeEvent data, Object obj, List<EventListener> listeners)
	{
		if(data == null || !(obj instanceof Consumer))
		{
			return;
		}
		getListeners(data.value()).addListener(data.priority(), (Consumer<Event>)obj);
		listeners.add(new EventListener(data.value(), (Consumer<Event>)obj));
	}
	
	private void findFields(Class<?> clz, boolean superClasses, Consumer<Field> fields)
	{
		do
		{
			for(Field field : clz.getDeclaredFields())
			{
				fields.accept(field);
			}
			clz = clz.getSuperclass();
		}
		while(clz != Object.class && superClasses);
	}
	
	private void findMethods(Class<?> clz, boolean superClasses, Consumer<Method> methods)
	{
		do
		{
			for(Method field : clz.getDeclaredMethods())
			{
				methods.accept(field);
			}
			clz = clz.getSuperclass();
		}
		while(clz != Object.class && superClasses);
	}
	
	public void unregister(Object obj)
	{
		List<EventListener> instance = instances.remove(obj);
		if(instance == null)
		{
			return;
		}
		for(EventListener entry : instance)
		{
			getListeners(entry.getEvent()).removeListeners(entry.getListener());
		}
	}
	
	public void post(Event event)
	{
		Consumer<Event>[] listeners = getListeners(event.getClass()).getListeners();
		if(listeners.length <= 0)
		{
			return;
		}
		int index = 0;
		try
		{
			for(;index<listeners.length && !event.isCanceled();index++)
			{
				listeners[index].accept(event);
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}
	
	protected Listeners getListeners(Class<? extends Event> event)
	{
		Listeners entry = listeners.get(event);
		if(entry == null)
		{
			if(event == Event.class) entry = new Listeners();
			else entry = new Listeners(getListeners((Class<? extends Event>)event.getSuperclass()));
			listeners.put(event, entry);
		}
		return entry;
	}
	
	
	public static class EventListener
	{
		Class<? extends Event> event;
		Consumer<Event> listener;
		
		public EventListener(Class<? extends Event> event, Consumer<Event> listener)
		{
			this.event = event;
			this.listener = listener;
		}
		
		public Class<? extends Event> getEvent()
		{
			return event;
		}
		
		public Consumer<Event> getListener()
		{
			return listener;
		}
	}
	
	public static class MethodListener implements Consumer<Event>
	{
		MethodHandle handle;
		
		public MethodListener(MethodHandle handle)
		{
			this.handle = handle;
		}
		
		@Override
		public void accept(Event t)
		{
			try
			{
				handle.invoke(t);
			}
			catch(Throwable e)
			{
				e.printStackTrace();
			}
		}
		
	}
}
