package speiger.src.coreengine.platform.input.window;

import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.system.Callback;
import org.lwjgl.system.CallbackI;
import org.lwjgl.system.Platform;

import speiger.src.collections.longs.maps.impl.concurrent.Long2ObjectConcurrentOpenHashMap;
import speiger.src.collections.longs.maps.interfaces.Long2ObjectMap;
import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.coreengine.platform.graphics.api.core.Graphics;
import speiger.src.coreengine.platform.input.device.InputDevice;
import speiger.src.coreengine.platform.input.window.WindowCallback.SimpleReloadFunction;

public class WindowManager {
	public static final ScopedValue<WindowManager> MANAGER = ScopedValue.newInstance();
	BlockingQueue<Runnable> tasks = new LinkedBlockingQueue<>();
	Graphics graphcis;
	
	Long2ObjectMap<Window> windows = new Long2ObjectConcurrentOpenHashMap<>();
	Long2ObjectMap<Monitor> monitors;
	Window activeWindow;
	Window primaryWindow;

	
	List<WindowCallback> callbacks = new ObjectArrayList<>();
	Callback monitorTracker;
	List<InputDevice> devices = new ObjectArrayList<>();

	boolean closing = false;
	Thread manager;
		
	public void setup(Runnable runnable, Graphics graphics) {
		if(this.graphcis != null) throw new IllegalStateException("We already init the Graphis Pipeline");
		this.graphcis = graphics;
		int[] major = new int[1];
		int[] minor = new int[1];
		int[] revision = new int[1];
		GLFW.glfwGetVersion(major, minor, revision);
		System.out.println("GLFW Version ["+major[0]+"."+minor[0]+"."+revision[0]+"] found");
		GLFW.glfwSetErrorCallback((E, D) -> System.out.println(GLFWUtil.glfwError(E, D)));
		
		manager = Thread.currentThread();
		manager.setName("Window Manager");
		
		if(Platform.get() == Platform.LINUX) {
			GLFW.glfwInitHint(GLFW.GLFW_PLATFORM, GLFW.glfwPlatformSupported(GLFW.GLFW_PLATFORM_WAYLAND) ? GLFW.GLFW_PLATFORM_WAYLAND : GLFW.GLFW_PLATFORM_X11);
		}
		
		if(!GLFW.glfwInit()) {
			throw new IllegalStateException("GLFW Couldn't init. Reason: "+GLFWUtil.glfwError());
		}
		
		monitors = Monitor.createMonitors();
		addCallback(this::onMonitorChanged, GLFW::glfwSetMonitorCallback);
		
		Thread.ofPlatform().start(() -> ScopedValue.where(MANAGER, this).run(runnable));
		while(!closing) {
			Runnable task = null;
			try { task = tasks.take(); }
			catch(Exception e) { e.printStackTrace(); }
			if(task == null) continue;
			try { task.run(); }
			catch(Exception e) { e.printStackTrace(); }
		}
		while(!tasks.isEmpty()) {
			try { tasks.poll().run(); }
			catch(Exception e) { e.printStackTrace(); }
		}
		
		callbacks.forEach(WindowCallback::destroy);
		callbacks.clear();
		
		System.out.println("GLFW Shutting down");
		GLFW.glfwTerminate();
		GLFW.glfwSetErrorCallback(null).free();
		
		System.exit(0);
	}
	
	@Nullable
	public static WindowManager get() {
		return MANAGER.orElse(null);
	}
	
	public boolean isManagerThread() {
		return manager.threadId() == Thread.currentThread().threadId();
	}
	
	public void joinOnWindowThread(Runnable run) {
		CompletableFuture.runAsync(run, tasks::add).join();
	}
	
	public <T> T joinOnWindowThread(Supplier<T> provider) {
		return CompletableFuture.supplyAsync(provider, tasks::add).join();
	}
	
	public CompletableFuture<Void> runOnWindowThread(Runnable run) {
		return CompletableFuture.runAsync(run, tasks::add);
	}
	
	public <T> CompletableFuture<T> runOnWindowThread(Supplier<T> provider) {
		return CompletableFuture.supplyAsync(provider, tasks::add); 
	}
	
	public void shutdown() {
		closing = true;
		tasks.add(() -> {});
	}
	
	private void onMonitorChanged(long monitor, int event) {
		switch(event) {
			case GLFW.GLFW_CONNECTED -> monitors.put(monitor, new Monitor(monitor));
			case GLFW.GLFW_DISCONNECTED -> monitors.remove(monitor);
		}
	}
	
	public WindowBuilder builder() { return new WindowBuilder(this); }
	Window create(WindowBuilder builder) { return new Window(builder, graphcis); }
	
	public void addDevices(InputDevice...devices) {
		for(InputDevice device : devices) addDevice(device);
	}
	public void addDevice(InputDevice device) {
		devices.add(device);
		windows.values().forEach(device::register);
	}
	public void removeDevice(InputDevice device) { devices.remove(device); }
	public void processDevices(long windowId) {
		for(int i = 0,m=devices.size();i<m;i++) {
			devices.get(i).processInput(windowId);
		}
	}
	
	@SuppressWarnings("unchecked")
	public <T extends CallbackI> WindowCallback addCallback(T listener, SimpleReloadFunction<T> function) {
		WindowCallback callback = new WindowCallback(listener, (SimpleReloadFunction<CallbackI>)function);
		callbacks.add(callback);
		callback.load(0L);
		return callback;
	}
	
	public void removeCallback(WindowCallback callback) {
		if(callbacks.remove(callback)) {
			callback.destroy();
		}
	}
	
	void addWindow(Window window) {
		windows.put(window.id(), window);
		if(primaryWindow == null) primaryWindow = window;
		for(int i = 0,m=devices.size();i<m;i++) {
			devices.get(i).register(window);
		}
	}
	
	void updateWindow(long oldId) {
		Window prev = windows.remove(oldId);
		if(prev == null) return;
		windows.put(prev.id(), prev);
	}
	
	void removeWindow(long id) {
		Window window = windows.remove(id);
		if(window == null) return;
		if(window == activeWindow) activeWindow = null;
		if(window == primaryWindow) {
			Iterator<Window> iter = windows.values().iterator();
			primaryWindow = iter.hasNext() ? iter.next() : null;
		}
	}
	
	void updateFocus(Window window, boolean focus) {
		if(focus) this.activeWindow = window;
		else if(activeWindow == window) activeWindow = null;
	}
	
	void updateInputs(long windowId) {
		for(int i = 0,m=devices.size();i<m;i++) {
			InputDevice device = devices.get(i);
			device.reset(windowId);
			device.processInput(windowId);
		}
	}
	
	public Graphics graphics() {
		return graphcis;
	}
	
	public long getActiveWindow() {
		return activeWindow == null ? 0L : activeWindow.id();
	}
	
	public long getPrimaryWindow() {
		return primaryWindow == null ? 0 : primaryWindow.id();
	}
	
	public Window getWindow(long window) {
		return windows.get(window);
	}
	
	public Monitor getMonitor(long id) {
		return monitors.get(id);
	}
	
	public Monitor getPriamryMonitor() {
		return getMonitor(GLFW.glfwGetPrimaryMonitor());
	}
	
	public Monitor getMonitorForWindow(Window window) {
		long current = GLFW.glfwGetWindowMonitor(window.id());
		if(current != 0L) return getMonitor(current);
		int minX = window.x();
		int minY = window.y();
		int maxX = minX + window.screenWidth();
		int maxY = minY + window.screenHeight();
		int largest = 0;
		Monitor mon = null;
		for(Monitor monitor : monitors.values()) {
			int next = monitor.getOverlap(minX, minY, maxX, maxY);
			if(next > largest) {
				largest = next;
				mon = monitor;
			}
		}
		return mon;
	}
}
