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

import java.util.List;

import org.lwjgl.glfw.GLFW;
import org.lwjgl.system.CallbackI;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.coreengine.math.bits.BitUtil;
import speiger.src.coreengine.math.bits.FlagHolder;
import speiger.src.coreengine.math.vector.ints.Vec2i;
import speiger.src.coreengine.math.vector.ints.Vec4i;
import speiger.src.coreengine.platform.graphics.api.core.Graphics;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsSurface;
import speiger.src.coreengine.platform.input.window.IWindowListener.Reason;
import speiger.src.coreengine.platform.input.window.WindowCallback.ReloadFunction;

public class Window {
	public static final ScopedValue<Window> SCOPE = ScopedValue.newInstance();
	static final int VISIBLE = 1;
	static final int VSYNC = 2;
	static final int FOCUS = 4;
	static final int CPU_FPS_CAP = 8;
	static final int FULL_SCREEN = 16;
	static final int MAXIMIZED = 32;
	static final int BORDERLESS = 64;
	static final int RESIZABLE = 128;
	static final int FLOATING = 256;
	static final int CLOSE = 512;
	static final int WINDOW_CHANGE = 1024;
	WindowManager manager;
	GraphicsDevice device;
	GraphicsSurface surface;
	FlagHolder flags = new FlagHolder();
	long id;
	VideoMode fullScreenMode;
	String title = "";
	int x;
	int y;
	int width;
	int height;
	
	int frameWidth;
	int frameHeight;
	
	Vec4i[] backup = new Vec4i[] {Vec4i.mutable(), Vec4i.mutable()};
	int backupIndex = 0;
	
	final int antialiasing;
	List<WindowCallback> callbacks = new ObjectArrayList<>();
	List<IWindowListener> listeners = new ObjectArrayList<>();
	
	protected Window(WindowBuilder builder, Graphics graphics) {
		manager = builder.manager;
		title = builder.title;
		frameWidth = width = builder.width;
		frameHeight = height = builder.height;
		antialiasing = builder.antiAlis;
		fullScreenMode = builder.fullScreenTarget;
		flags.setFlag(BORDERLESS, builder.borderless);
		flags.setFlag(FULL_SCREEN, builder.fullScreen);
		flags.setFlag(FLOATING, builder.floating);
		flags.setFlag(RESIZABLE, builder.resizable);
		flags.setFlag(VSYNC, builder.vsync);
		flags.setFlag(CPU_FPS_CAP, builder.fpsCap);
		Monitor monitor = manager.getMonitor(builder.monitor);
		if(monitor == null) throw new IllegalStateException("Monitor is missing: "+monitor);
		boolean fullscreen = builder.fullScreen;
		VideoMode mode = monitor.defaultMode();
		id = manager.joinOnWindowThread(() -> {
			graphics.setupWindowArguments();
			for(int i = 0,m=builder.windowHints.size();i<m;i++) {
				long value = builder.windowHints.getLong(i);
				GLFW.glfwWindowHint(BitUtil.intKey(value), BitUtil.intValue(value));
			}
			GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, antialiasing);
			GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, flags.isFlagSet(RESIZABLE) ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE);
			GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, flags.isFlagNotSet(FULL_SCREEN) && flags.isFlagSet(BORDERLESS) ? GLFW.GLFW_FALSE : GLFW.GLFW_TRUE);
			GLFW.glfwWindowHint(GLFW.GLFW_FLOATING, flags.isFlagNotSet(FULL_SCREEN) && flags.isFlagSet(FLOATING) ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE);
			GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, flags.isFlagNotSet(FULL_SCREEN) && flags.isFlagSet(MAXIMIZED) ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE);
			return GLFW.glfwCreateWindow(fullscreen ? mode.width() : width, fullscreen ? mode.height() : height, title, builder.fullScreen ? monitor.id() : 0, manager.getPrimaryWindow());
		});
		if(id == 0) throw new IllegalStateException("Window Couldn't be Created");
		manager.addWindow(this);
		manager.joinOnWindowThread(this::createWindowListeners);
		GLFW.glfwMakeContextCurrent(id);
		device = graphics.createDevice(this);
		surface = device.createSurface();
		x = monitor.xOffset() + (builder.center ? (mode.width() / 2) - (width / 2) : 0);
		y = monitor.yOffset() + (builder.center ? (mode.height() / 2) - (height / 2) : 0);
		if(!fullscreen) manager.joinOnWindowThread(() -> GLFW.glfwSetWindowPos(id, x, y));
		GLFW.glfwSwapInterval(flags.isFlagSet(VSYNC) ? 1 : 0);
		fetchWindowBounds();
		updateViewport();
	}
	
	protected void createWindowListeners() {
		addCallback(this::framebuffer, GLFW::glfwSetFramebufferSizeCallback);
		addCallback(this::focused, GLFW::glfwSetWindowFocusCallback);
		addCallback(this::position, GLFW::glfwSetWindowPosCallback);
		addCallback(this::bounds, GLFW::glfwSetWindowSizeCallback);
	}
	
	private void framebuffer(long window, int width, int height) {
		frameWidth = width;
		frameHeight = height;
		flags.setFlag(WINDOW_CHANGE);
	}
	
	private void focused(long window, boolean focused) {
		manager.updateFocus(this, focused);
	}
	
	private void position(long window, int x, int y) {
		this.x = x;
		this.y = y;
		flags.setFlag(WINDOW_CHANGE);
	}
	
	private void bounds(long window, int width, int height) {
		this.width = width;
		this.height = height;
		flags.setFlag(WINDOW_CHANGE);
	}
	
	protected void fetchWindowBounds() {
		Vec2i result = manager.joinOnWindowThread(() -> {
			int[] width = new int[1];
			int[] height = new int[1];
			GLFW.glfwGetWindowSize(id, width, height);
			return Vec2i.of(width[0], height[0]);
		});
		this.frameWidth = result.x();
		this.frameHeight = result.y();
	}
	
	public void updateViewport() {
		flags.clearFlag(WINDOW_CHANGE);
//		GLStateTracker.instance().viewPort.setDefault(0, 0, frameWidth, frameHeight);
		for(int i = 0,m=listeners.size();i<m;i++) {
			listeners.get(i).onChanged(this, Reason.CHANGE);
		}
	}
	
	public void setupContext(Runnable run) {
		ScopedValue.where(GraphicsDevice.SCOPE, device).where(SCOPE, this).run(run);
	}
	
	public void beginFrame() {
		GLFW.glfwMakeContextCurrent(id);
		if(flags.isFlagSet(WINDOW_CHANGE)) updateViewport();
	}
	
	public void handleInput() { manager.processDevices(id); }
	public boolean shouldClose() { return flags.isFlagSet(CLOSE) || GLFW.glfwWindowShouldClose(id); }
	public void finishFrame() { GLFW.glfwSwapBuffers(id); }
	
	public void destroy() {
		manager.removeWindow(id);
		for(int i = 0,m=listeners.size();i<m;i++) {
			listeners.get(i).onChanged(this, Reason.CLOSING);
		}
		manager.joinOnWindowThread(() -> {
			GLFW.glfwDestroyWindow(id);
			callbacks.forEach(WindowCallback::destroy);
			callbacks.clear();
		});
	}
	
	@SuppressWarnings("unchecked")
	public <T extends CallbackI> WindowCallback addCallback(T listener, ReloadFunction<T> function) {
		if(!manager.isManagerThread()) return manager.joinOnWindowThread(() -> addCallback(listener, function));
		WindowCallback callback = new WindowCallback(listener, (ReloadFunction<CallbackI>)function);
		callbacks.add(callback);
		callback.load(id);
		return callback;
	}
	
	public void removeCallback(WindowCallback callback) {
		if(callbacks.remove(callback)) {
			manager.joinOnWindowThread(callback::destroy);
		}
	}
	
	public void addListener(IWindowListener listener) {
		listeners.add(listener);
	}
	
	public void removeListener(IWindowListener listener) {
		listeners.remove(listener);
	}
	
	public void title(String name) {
		if(name == null || title.equals(name)) return;
		title = name;
		manager.joinOnWindowThread(() -> GLFW.glfwSetWindowTitle(id, title));
	}
	
	public void vsync(boolean vsync) {
		if(!flags.setFlag(VSYNC, vsync)) return;
		GLFW.glfwSwapInterval(vsync ? 1 : 0);
	}
	
	public void fpsCap(boolean fpsCap) {
		flags.setFlag(CPU_FPS_CAP, fpsCap);
	}
	
	public void visible(boolean visible) {
		if(!flags.setFlag(VISIBLE, visible)) return;
		if(visible) manager.joinOnWindowThread(() -> GLFW.glfwShowWindow(id));
		else manager.joinOnWindowThread(() -> GLFW.glfwHideWindow(id));
	}
	
	public void floating(boolean floating) {
		if(flags.isFlagNotSet(FULL_SCREEN) && flags.setFlag(FLOATING, floating)) {
			manager.joinOnWindowThread(() -> GLFW.glfwSetWindowAttrib(id, GLFW.GLFW_FLOATING, floating ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE));
		}
	}
	
	public void maximized(boolean maximized) {
		if(flags.isFlagNotSet(FULL_SCREEN) && flags.setFlag(MAXIMIZED, maximized)) {
			if(maximized) {
				backupSize();
				manager.joinOnWindowThread(() -> GLFW.glfwMaximizeWindow(id));
			}
			else {
				manager.joinOnWindowThread(() -> GLFW.glfwRestoreWindow(id));
				restoreSize();
			}
			fetchWindowBounds();
		}
	}
	
	public void resizeable(boolean resizeable) {
		if(flags.isFlagNotSet(FULL_SCREEN) && flags.setFlag(RESIZABLE, resizeable)) {
			manager.joinOnWindowThread(() -> GLFW.glfwSetWindowAttrib(id, GLFW.GLFW_RESIZABLE, resizeable ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE));
		}
	}
	
	public void borderless(boolean borderless) {
		if(flags.isFlagNotSet(FULL_SCREEN) && flags.setFlag(BORDERLESS, borderless)) {
			manager.joinOnWindowThread(() -> GLFW.glfwSetWindowAttrib(id, GLFW.GLFW_DECORATED, borderless ? GLFW.GLFW_FALSE : GLFW.GLFW_TRUE));
			if(flags.isFlagSet(MAXIMIZED)) {
				manager.joinOnWindowThread(() -> {
					GLFW.glfwRestoreWindow(id);
					GLFW.glfwMaximizeWindow(id);
				});
				fetchWindowBounds();
			}
		}
	}
	
	public void fullscreen(boolean fullscreen) {
		if(flags.setFlag(FULL_SCREEN, fullscreen)) {
			if(fullscreen) {
				Monitor monitor = manager.getMonitorForWindow(this);
				if(monitor == null) {
					flags.clearFlag(FULL_SCREEN);
					return;
				}
				backupSize();
				VideoMode mode = fullScreenMode == null || !monitor.has(fullScreenMode) ? monitor.defaultMode() : fullScreenMode;
				x = 0;
				y = 0;
				width = mode.width();
				height = mode.height();
				manager.joinOnWindowThread(() -> GLFW.glfwSetWindowMonitor(id, monitor.id(), 0, 0, mode.width(), mode.height(), mode.refreshrate()));
			}
			else
			{
				restoreSize();
				manager.joinOnWindowThread(() -> {
					GLFW.glfwSetWindowMonitor(id, 0, x, y, width, height, -1);
					if(flags.isFlagSet(BORDERLESS)) {
						GLFW.glfwSetWindowAttrib(id, GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE);
					}
					if(flags.isFlagSet(MAXIMIZED)) {
						GLFW.glfwMaximizeWindow(id);
					}
				});
			}
			fetchWindowBounds();
			updateViewport();
			GLFW.glfwSwapInterval(flags.isFlagSet(VSYNC) ? 1 : 0);
		}
	}
	
	protected void backupSize() {
		if(backupIndex >= backup.length) return;
		backup[backupIndex++].set(x, y, width, height);
	}
	
	protected void restoreSize() {
		if(backupIndex == 0) return;
		Vec4i prev = backup[--backupIndex];
		x = prev.x();
		y = prev.y();
		width = prev.z();
		height = prev.w();		
	}
	
	public boolean position(int x, int y) {
		if(flags.isAnyFlagSet(MAXIMIZED | FULL_SCREEN) || (this.x == x && this.y == y)) return false;
		this.x = x;
		this.y = y;
		manager.joinOnWindowThread(() -> GLFW.glfwSetWindowPos(id, x, y));
		flags.setFlag(WINDOW_CHANGE);
		return true;
	}
	
	public boolean size(int width, int height) {
		if(flags.isAnyFlagSet(MAXIMIZED | FULL_SCREEN) || (this.width == width && this.height == height)) return false;
		this.width = width;
		this.height = height;
		manager.joinOnWindowThread(() -> GLFW.glfwSetWindowSize(id, width, height));
		flags.setFlag(WINDOW_CHANGE);
		return true;
	}
	
	public boolean width(int width) { return size(width, height); }
	public boolean height(int height) { return size(width, height); }
	
	public long id() { return id; }
	public boolean isPrimaryWindow() { return manager.primaryWindow == this; } 
	public VideoMode desiredFullScreen() { return fullScreenMode; }
	
	public WindowManager manager() { return manager; }
	public GraphicsDevice device() { return device; }
	public GraphicsSurface surface() { return surface; }
	
	public int x() { return x; }
	public int y() { return y; }
	public int width() { return frameWidth; }
	public int height() { return frameHeight; }
	public int screenWidth() { return width; }
	public int screenHeight() { return height; }
	public boolean changed() { return flags.isFlagSet(WINDOW_CHANGE); }
	
	public String title() { return title; }
	public boolean isVsync() { return flags.isFlagSet(VSYNC); }
	public boolean shouldFPSCap() { return flags.isFlagSet(CPU_FPS_CAP); }
	public boolean isVisible() { return flags.isFlagSet(VISIBLE); }
	public boolean isFloating() { return flags.isFlagSet(FLOATING); }
	public boolean isMaximized() { return flags.isFlagSet(MAXIMIZED); }
	public boolean isResizeable() { return flags.isFlagSet(RESIZABLE); }
	public boolean isBorderless() { return flags.isFlagSet(BORDERLESS); }
	public boolean isFullscreen() { return flags.isFlagSet(FULL_SCREEN); }
	public int antialiasing() { return antialiasing; }
}
