package speiger.src.coreengine.rendering.inputOld.window;

import java.util.List;
import java.util.function.LongFunction;

import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.system.Callback;
import org.lwjgl.system.MemoryUtil;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.coreengine.math.MathUtils;
import speiger.src.coreengine.math.bits.FlagHolder;
import speiger.src.coreengine.math.vector.matrix.Matrix4f;
import speiger.src.coreengine.rendering.inputOld.window.WindowProvider.WindowStats;
import speiger.src.coreengine.rendering.utils.GLStateTracker;

public class Window
{
	static final int FLAG_VISIBLE = 1;
	static final int FLAG_VSYNC = 2;
	static final int FLAG_FOCUS = 4;
	static final int FLAG_CPU_FPS_CAP = 8;
	static final int FLAG_FULL_SCREEN = 16;
	static final int FLAG_MAXIMIZED = 32;
	static final int FLAG_BORDERLESS = 64;
	static final int FLAG_CLOSE = 128;
	static final int FLAG_WINDOW_CHANGE = 256;
	static final int FLAG_FLOATING = 512;
	
	WindowProvider owner;
	long windowId;
	VideoMode videoMode;
	FlagHolder flags = new FlagHolder();	
	String title = "";
	int xPos;
	int yPos;
	int tempWidth;
	int tempHeight;
	int width;
	int height;
	int aaLevel;
	int uiScale;
	ScaledResolution ui_frame = new ScaledResolution(this);
	
	float fieldOfView = 70F;
	final float near_plane = 0.1F;
	final float far_plane = 2000F;
	Matrix4f projection = new Matrix4f();
	Matrix4f invertedProjection = new Matrix4f();
	
	List<WindowCallback> callbacks = new ObjectArrayList<>();
	List<IWindowListener> listeners = new ObjectArrayList<>();
	GLCapabilities capabilities;
	
	public Window() {
		GLFW.glfwDefaultWindowHints();
		GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, 0); // 0 = False, 1 = true
		GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, 1);
		GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4);
		GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 0);
		GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);//GLFW.GLFW_OPENGL_COMPAT_PROFILE);
		GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, 1);
	}
	
	protected void initWindowListeners() {
		addCallback(T -> GLFW.glfwSetFramebufferSizeCallback(T, this::onResize));
		addCallback(T -> GLFW.glfwSetWindowMaximizeCallback(T, (_, _) -> flags.setFlag(FLAG_WINDOW_CHANGE)));
		addCallback(T -> GLFW.glfwSetWindowPosCallback(T, (_, X, Y) -> setPosition(X, Y)));
		addCallback(T -> GLFW.glfwSetWindowFocusCallback(T, (_, V) -> flags.setFlag(FLAG_FOCUS, V)));
		addCallback(_ -> GLFW.glfwSetErrorCallback(this::error));
	}
	
	private void error(int error, long text) {
		System.out.println("Error: "+error+", "+MemoryUtil.memUTF8(text));
	}
	
	Window createWindow(WindowStats stat) {
		owner = stat.provider;
		title = stat.name;
		width = stat.width;
		height = stat.height;
		aaLevel = stat.antiAlis;
		videoMode = stat.videoMode;
		flags.setFlag(FLAG_BORDERLESS, stat.borderless);
		flags.setFlag(FLAG_FULL_SCREEN, stat.fullScreen);
		flags.setFlag(FLAG_FLOATING, stat.floating);
		flags.setFlag(FLAG_VSYNC, stat.vsync);
		flags.setFlag(FLAG_CPU_FPS_CAP, stat.fpsCap);
		GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, aaLevel);
		GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, flags.isFlagNotSet(FLAG_FULL_SCREEN) && flags.isFlagSet(FLAG_BORDERLESS) ? 0 : 1);
		GLFW.glfwWindowHint(GLFW.GLFW_FLOATING, flags.isFlagNotSet(FLAG_FULL_SCREEN) && flags.isFlagSet(FLAG_FLOATING) ? 1 : 0);
		if(videoMode == null || videoMode.monitor() == null) throw new IllegalStateException("Monitor or Video Mode is missing: "+videoMode);
		Monitor monitor = videoMode.monitor();
		windowId = GLFW.glfwCreateWindow(flags.isFlagSet(FLAG_FULL_SCREEN) ? videoMode.width() : width, flags.isFlagSet(FLAG_FULL_SCREEN) ? videoMode.height() : height, title, flags.isFlagSet(FLAG_FULL_SCREEN) ? monitor.getMonitorId() : 0L, stat.parent == null ? 0 : stat.parent.windowId);
		if(windowId == 0) throw new IllegalStateException("Window Couldn't be Created");
		initWindowListeners();
		GLFW.glfwMakeContextCurrent(windowId);
		if(capabilities == null) capabilities = GL.createCapabilities();
		if(flags.isFlagNotSet(FLAG_FULL_SCREEN)) {
			xPos = stat.center ? (monitor.getXOffset() + (videoMode.width() / 2) - (width / 2)) : monitor.getXOffset() + xPos;
			yPos = stat.center ? (monitor.getYOffset() + (videoMode.height() / 2) - (width / 2)) : monitor.getYOffset() + yPos;
			GLFW.glfwSetWindowPos(windowId, xPos, yPos);
		}
		else {
			xPos = monitor.getXOffset() + (videoMode.width() / 2) - (width / 2);
			yPos = monitor.getYOffset() + (videoMode.height() / 2) - (width / 2);
			tempWidth = width;
			tempHeight = height;
		}
		GLFW.glfwSwapInterval(flags.isFlagSet(FLAG_VSYNC) ? 1 : 0);
		fetchWindowBounds();
		updateViewPort();
		return this;
	}
	
	protected void onResize(long window, int newWidth, int newHeight) {
		if(flags.isFlagNotSet(FLAG_FULL_SCREEN | FLAG_MAXIMIZED) && window == windowId && (width != newWidth || height != newHeight)) {
			width = newWidth;
			height = newHeight;
			flags.setFlag(FLAG_WINDOW_CHANGE);
		}
	}
	
	protected void updateCallbacks() {
		for(int i = 0,m=callbacks.size();i<m;i++) {
			callbacks.get(i).reload(windowId);
		}
		GLStateTracker.instance().reapplyState();
	}
	
	protected void updateViewPort() {
		flags.clearFlag(FLAG_WINDOW_CHANGE);
		GLStateTracker.instance().viewPort.setDefault(0, 0, width, height);
		createProjectionMatrix();
		ui_frame.updateScale();
		for(int i = 0,m=listeners.size();i<m;i++) {
			listeners.get(i).onWindowChanged(this);
		}
	}
	
	public void setVideoMode(VideoMode mode) {
		if(videoMode != mode && mode.monitor() == getMonitor()) {
			videoMode = mode;
			if(flags.isAnyFlagSet(FLAG_FULL_SCREEN | FLAG_MAXIMIZED)) {
				if(flags.isFlagSet(FLAG_FULL_SCREEN)) {
					GLFW.glfwSetWindowMonitor(windowId, videoMode.monitor().getMonitorId(), 0, 0, videoMode.width(), videoMode.height(), videoMode.refreshrate());
				}
				else if(flags.isFlagSet(FLAG_MAXIMIZED)) {
					GLFW.glfwRestoreWindow(windowId);
					GLFW.glfwMaximizeWindow(windowId);
				}
				fetchWindowBounds();
				updateViewPort();
			}
		}
	}
	
	public boolean setTitle(String title) {
		if(title != null && !this.title.equals(title)) {
			this.title = title;
			GLFW.glfwSetWindowTitle(windowId, title);
			return true;
		}
		return false;
	}
	
	public boolean setVsync(boolean value) {
		if(flags.setFlag(FLAG_VSYNC, value)) {
			GLFW.glfwSwapInterval(value ? 1 : 0);
			return true;
		}
		return false;
	}
	
	public boolean setFPSLimit(boolean value) {
		return flags.setFlag(FLAG_CPU_FPS_CAP, value);
	}
	
	public boolean setFullscreen(boolean value) {
		if(flags.setFlag(FLAG_FULL_SCREEN, value)) {
			if(value) {
				if(flags.isFlagNotSet(FLAG_MAXIMIZED)) {
					tempWidth = width;
					tempHeight = height;
				}
				Monitor monitor = getMonitor();
				if(!monitor.contains(videoMode)) videoMode = monitor.getDefaultMode();
				GLFW.glfwSetWindowMonitor(windowId, monitor.getMonitorId(), 0, 0, videoMode.width(), videoMode.height(), videoMode.refreshrate());
				fetchWindowBounds();
			}
			else {
				if(flags.isFlagNotSet(FLAG_MAXIMIZED)) {
					width = tempWidth;
					height = tempHeight;
					GLFW.glfwSetWindowMonitor(windowId, 0, xPos, yPos, width, height, videoMode.refreshrate());
				}
				else {
					GLFW.glfwSetWindowMonitor(windowId, 0, 0, 0, width, height, videoMode.refreshrate());
					GLFW.glfwMaximizeWindow(windowId);
					fetchWindowBounds();
				}
				if(flags.isFlagSet(FLAG_BORDERLESS)) {
					GLFW.glfwSetWindowAttrib(windowId, GLFW.GLFW_DECORATED, 0);
					GLFW.glfwRestoreWindow(windowId);
					GLFW.glfwMaximizeWindow(windowId);
				}
				else {
					GLFW.glfwSetWindowAttrib(windowId, GLFW.GLFW_DECORATED, 1);					
				}
			}
			updateViewPort();
			GLFW.glfwSwapInterval(flags.isFlagSet(FLAG_VSYNC) ? 1 : 0);
			return true;
		}
		return false;
	}
	
	public boolean setBorderless(boolean value) {
		if(flags.isFlagNotSet(FLAG_FULL_SCREEN) && flags.setFlag(FLAG_BORDERLESS, value)) {
			GLFW.glfwSetWindowAttrib(windowId, GLFW.GLFW_DECORATED, value ? 0 : 1);
			if(flags.isFlagSet(FLAG_MAXIMIZED)) {
				GLFW.glfwRestoreWindow(windowId);
				GLFW.glfwMaximizeWindow(windowId);
				fetchWindowBounds();
			}
			return true;
		}
		return false;
	}
	
	public boolean setFloating(boolean value) {
		if(flags.isFlagNotSet(FLAG_FULL_SCREEN) && flags.setFlag(FLAG_FLOATING, value)) {
			GLFW.glfwSetWindowAttrib(windowId, GLFW.GLFW_FLOATING, value ? 1 : 0);
			return true;
		}
		return false;
	}
	
	public boolean setMaximized(boolean value) {
		if(flags.isFlagNotSet(FLAG_FULL_SCREEN) && flags.setFlag(FLAG_MAXIMIZED, value)) {
			if(value) {
				tempWidth = width;
				tempHeight = height;
				GLFW.glfwMaximizeWindow(windowId);
				fetchWindowBounds();
			}
			else {
				width = tempWidth;
				height = tempHeight;
				GLFW.glfwRestoreWindow(windowId);
			}
			return true;
		}
		return false;
	}
	
	public boolean setAntiAliasing(int level) {
		if(aaLevel != level) {
			aaLevel = level;
//			GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, aaLevel);
			long last = windowId;
			GLFW.glfwDefaultWindowHints();
			GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, 1); // 0 = False, 1 = true
			GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, 1);
			GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, aaLevel);
			GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, flags.isFlagNotSet(FLAG_FULL_SCREEN) && flags.isFlagSet(FLAG_BORDERLESS) ? 0 : 1);
			Monitor monitor = getMonitor();
			if(!monitor.contains(videoMode)) videoMode = monitor.getDefaultMode();
			windowId = GLFW.glfwCreateWindow(flags.isFlagSet(FLAG_FULL_SCREEN) ? videoMode.width() : width, flags.isFlagSet(FLAG_FULL_SCREEN) ? videoMode.height() : height, title, flags.isFlagSet(FLAG_FULL_SCREEN) ? monitor.getMonitorId() : 0L, windowId);
			if(windowId == 0) throw new IllegalStateException("Window Couldn't be Created");
			GLFW.glfwMakeContextCurrent(windowId);
			GLFW.glfwDestroyWindow(last);
			GLFW.glfwSwapInterval(flags.isFlagSet(FLAG_VSYNC) ? 1 : 0);
			GLFW.glfwSetWindowPos(windowId, xPos, yPos);
			updateCallbacks();
			updateViewPort();
			return true;
		}
		return false;
	}
	
	public boolean setWindowSize(int newWidth, int newHeight) {
		if(flags.isFlagNotSet(FLAG_MAXIMIZED | FLAG_FULL_SCREEN) && (width != newWidth || height != newWidth)) {
			width = newWidth;
			height = newHeight;
			flags.setFlag(FLAG_WINDOW_CHANGE);
			return true;
		}
		return false;
	}
	
	public boolean setPosition(int xNewPos, int yNewPos) {
		if(flags.isFlagNotSet(FLAG_MAXIMIZED | FLAG_FULL_SCREEN) && (xPos != xNewPos || yPos != yNewPos)) {
			xPos = xNewPos;
			yPos = yNewPos;
			return true;
		}
		return false;
	}
	
	public boolean setVisible(boolean value) {
		if(flags.setFlag(FLAG_VISIBLE, value)) {
			if(value) GLFW.glfwShowWindow(windowId);
			else GLFW.glfwHideWindow(windowId);
			return true;
		}
		return false;
	}
	
	public void setUIScale(int newScale) {
		int lastScale = uiScale;
		uiScale = MathUtils.clamp(0, 100, newScale);
		if(lastScale != uiScale) flags.setFlag(FLAG_WINDOW_CHANGE);
	}
	
	public void editScale(int change) {
		if(change != 0) {
			uiScale = MathUtils.clamp(0, 100, uiScale + change);
			flags.setFlag(FLAG_WINDOW_CHANGE);			
		}
	}
	
	public boolean isChanged() { return flags.isFlagSet(FLAG_WINDOW_CHANGE); }
	public boolean isFullscreen() { return flags.isFlagSet(FLAG_FULL_SCREEN); }
	public boolean isMaximized() { return flags.isFlagSet(FLAG_MAXIMIZED); }
	public boolean isBorderless() { return flags.isFlagSet(FLAG_BORDERLESS); }
	public boolean isCPULimited() { return flags.isFlagSet(FLAG_CPU_FPS_CAP); }
	public int getAntiAliasingLevel() { return aaLevel; }
	public long getId() { return windowId; }
	public int getWidth() { return width; }
	public int getHeight() { return height; }
	int getActualWidth() { return flags.isAnyFlagSet(FLAG_FULL_SCREEN | FLAG_MAXIMIZED) ? tempWidth : width; }
	int getActualHeight() { return flags.isAnyFlagSet(FLAG_FULL_SCREEN | FLAG_MAXIMIZED) ? tempHeight : height; }
	int getXPos() { return xPos; }
	int getYPos() { return yPos; }
	
	public Monitor getMonitor() { return owner.getMonitorFromWindow(this); }
	public String getClipboardString() { return GLFW.glfwGetClipboardString(windowId); }
	public void setClipboardString(String text) { GLFW.glfwSetClipboardString(windowId, text); }

	public void requestClose() { flags.setFlag(FLAG_CLOSE); }
	public boolean shouldRun() { return flags.isFlagNotSet(FLAG_CLOSE) && !GLFW.glfwWindowShouldClose(windowId); }
	
	public void finishWindow() {
		setVisible(true);
		GLFW.glfwShowWindow(windowId);
		updateViewPort();
	}
	
	public void update() {
		if(flags.isFlagSet(FLAG_WINDOW_CHANGE)) updateViewPort();
	}
	
	public void finishFrame() {
		GLFW.glfwSwapBuffers(windowId);
	}
	
	public void gatherInputs() {
		GLFW.glfwPollEvents();
	}
	
	public void destroy() {
		GLFW.glfwDestroyWindow(windowId);
		callbacks.forEach(WindowCallback::destroy);
		callbacks.clear();
	}
	
	public Matrix4f getProjectionMatrix() {
		return projection;
	}
	
	public Matrix4f getInvertedProjectionMatrix() {
		return invertedProjection;
	}
	
	public ScaledResolution getUIFrame() {
		return ui_frame;
	}
	
	public void addListener(IWindowListener listener, boolean notify) {
		if(listener != null) {
			listeners.add(listener);
			if(notify) listener.onWindowChanged(this);
		}
	}
	
	public void removeListener(IWindowListener listener) {
		listeners.remove(listener);
	}
	
	public WindowCallback addCallback(LongFunction<Callback> provider) {
		if(provider == null) return null;
		WindowCallback callback = new WindowCallback(provider);
		callbacks.add(callback);
		if(windowId != 0) {
			callback.load(windowId);
		}
		return callback;
	}
	
	public void removeCallback(WindowCallback call) {
		if(callbacks.remove(call)) {
			call.destroy();
		}
	}
	
	private void createProjectionMatrix() {
		projection.setIdentity().setPerspective((float)Math.toRadians(fieldOfView), (float)width / (float)height, near_plane, far_plane);
		invertedProjection.load(projection).invert();
	}
	
	protected void fetchWindowBounds() {
		int[] width = new int[1];
		int[] height = new int[1];
		GLFW.glfwGetWindowSize(windowId, width, height);
		this.width = width[0];
		this.height = height[0];
	}
}
