package client.window;

import static org.lwjgl.glfw.Callbacks.glfwFreeCallbacks;
import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_DEBUG;
import static org.lwjgl.glfw.GLFW.GLFW_CURSOR;
import static org.lwjgl.glfw.GLFW.GLFW_CURSOR_DISABLED;
import static org.lwjgl.glfw.GLFW.GLFW_CURSOR_NORMAL;
import static org.lwjgl.glfw.GLFW.GLFW_DONT_CARE;
import static org.lwjgl.glfw.GLFW.GLFW_FALSE;
import static org.lwjgl.glfw.GLFW.GLFW_PRESS;
import static org.lwjgl.glfw.GLFW.GLFW_RELEASE;
import static org.lwjgl.glfw.GLFW.GLFW_REPEAT;
import static org.lwjgl.glfw.GLFW.GLFW_RESIZABLE;
import static org.lwjgl.glfw.GLFW.GLFW_TRUE;
import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE;
import static org.lwjgl.glfw.GLFW.glfwCreateWindow;
import static org.lwjgl.glfw.GLFW.glfwDefaultWindowHints;
import static org.lwjgl.glfw.GLFW.glfwDestroyWindow;
import static org.lwjgl.glfw.GLFW.glfwGetClipboardString;
import static org.lwjgl.glfw.GLFW.glfwGetKey;
import static org.lwjgl.glfw.GLFW.glfwGetMonitorPos;
import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor;
import static org.lwjgl.glfw.GLFW.glfwGetTimerFrequency;
import static org.lwjgl.glfw.GLFW.glfwGetVideoMode;
import static org.lwjgl.glfw.GLFW.glfwGetVideoModes;
import static org.lwjgl.glfw.GLFW.glfwInit;
import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent;
import static org.lwjgl.glfw.GLFW.glfwPollEvents;
import static org.lwjgl.glfw.GLFW.glfwSetCharCallback;
import static org.lwjgl.glfw.GLFW.glfwSetClipboardString;
import static org.lwjgl.glfw.GLFW.glfwSetCursorPosCallback;
import static org.lwjgl.glfw.GLFW.glfwSetErrorCallback;
import static org.lwjgl.glfw.GLFW.glfwSetFramebufferSizeCallback;
import static org.lwjgl.glfw.GLFW.glfwSetInputMode;
import static org.lwjgl.glfw.GLFW.glfwSetKeyCallback;
import static org.lwjgl.glfw.GLFW.glfwSetMouseButtonCallback;
import static org.lwjgl.glfw.GLFW.glfwSetScrollCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowCloseCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowFocusCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowIcon;
import static org.lwjgl.glfw.GLFW.glfwSetWindowMonitor;
import static org.lwjgl.glfw.GLFW.glfwSetWindowPos;
import static org.lwjgl.glfw.GLFW.glfwSetWindowPosCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowRefreshCallback;
import static org.lwjgl.glfw.GLFW.glfwSetWindowSize;
import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeLimits;
import static org.lwjgl.glfw.GLFW.glfwSetWindowTitle;
import static org.lwjgl.glfw.GLFW.glfwShowWindow;
import static org.lwjgl.glfw.GLFW.glfwSwapBuffers;
import static org.lwjgl.glfw.GLFW.glfwSwapInterval;
import static org.lwjgl.glfw.GLFW.glfwTerminate;
import static org.lwjgl.glfw.GLFW.glfwWindowHint;
import static org.lwjgl.system.MemoryUtil.NULL;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.lwjgl.BufferUtils;
import org.lwjgl.Version;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWCharCallback;
import org.lwjgl.glfw.GLFWCursorPosCallback;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWFramebufferSizeCallback;
import org.lwjgl.glfw.GLFWImage;
import org.lwjgl.glfw.GLFWKeyCallback;
import org.lwjgl.glfw.GLFWMouseButtonCallback;
import org.lwjgl.glfw.GLFWScrollCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.glfw.GLFWWindowCloseCallback;
import org.lwjgl.glfw.GLFWWindowFocusCallback;
import org.lwjgl.glfw.GLFWWindowPosCallback;
import org.lwjgl.glfw.GLFWWindowRefreshCallback;
import org.lwjgl.opengl.GL;
import org.lwjgl.system.APIUtil;

import common.log.Log;

public abstract class Window {
	public static void setWindowed(int xpos, int ypos, int xsize, int ysize) {
		if(window != NULL)
			glfwSetWindowMonitor(window, NULL, xpos, ypos, xsize, ysize, GLFW_DONT_CARE);
	}
	public static void setFullscreen(int width, int height, int refresh) {
		if(window != NULL) {
			long monitor = glfwGetPrimaryMonitor();
			if(monitor != NULL)
				glfwSetWindowMonitor(window, monitor, 0, 0, width, height, refresh);
		}
	}
	public static boolean getKey(int code) {
		return window != NULL && glfwGetKey(window, code) == GLFW_PRESS;
	}
	public static void setTitle(String title) {
		if(window != NULL)
			glfwSetWindowTitle(window, title);
	}
	public static void setIcon(byte[] icon, int w, int h) {
		if(window != NULL) {
			GLFWImage img = GLFWImage.create();
			img.width(w);
			img.height(h);
			img.pixels(BufferUtils.createByteBuffer(icon.length).put(icon).rewind());
			GLFWImage.Buffer buf = GLFWImage.create(1).put(img).rewind();
			glfwSetWindowIcon(window, buf);
		}
	}
	public static String getClipboard() {
		return window == NULL ? null : glfwGetClipboardString(window);
	}
	public static void setClipboard(String text) {
		if(window != NULL)
			glfwSetClipboardString(window, text);
	}
	public static void swapBuffers() {
		if(window != NULL)
			glfwSwapBuffers(window);
	}
	public static void grabCursor(boolean grab) {
		if(window != NULL)
			glfwSetInputMode(window, GLFW_CURSOR, grab ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
	}
	public static void setVSync(boolean sync) {
		if(window != NULL)
			glfwSwapInterval(sync ? 1 : 0);
	}
	
	private static final List<WindowEvent> EVENTS = new ArrayList<WindowEvent>();
	
	private static long window;
	
	public static boolean createWindow(String id, boolean gldebug) {
		glfwDefaultWindowHints();
		glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
		glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
		glfwWindowHint(GLFW_CONTEXT_DEBUG, gldebug ? GLFW_TRUE : GLFW_FALSE);
		window = glfwCreateWindow(100, 100, id, NULL, NULL);
		if(window == NULL) {
			Log.SYSTEM.error("Konnte kein Fenster für das Programm erstellen");
		}
		else {
			glfwSetMouseButtonCallback(window, new GLFWMouseButtonCallback() {
				public void invoke(long window, int button, int action, int mods) {
					if(action != GLFW_REPEAT && button >= 0 && button < Button.values().length)
						EVENTS.add(new WindowEvent(WindowAction.BUTTON, button, action == GLFW_PRESS ? 1 : 0));
				}
			});
			glfwSetCharCallback(window, new GLFWCharCallback() {
				public void invoke(long window, int codepoint) {
					EVENTS.add(new WindowEvent(WindowAction.CHARACTER, codepoint, 0));
				}
			});
			glfwSetCursorPosCallback(window, new GLFWCursorPosCallback() {
				public void invoke(long window, double xpos, double ypos) {
					EVENTS.add(new WindowEvent(WindowAction.CURSOR, (int)xpos, (int)ypos));
				}
			});
			glfwSetFramebufferSizeCallback(window, new GLFWFramebufferSizeCallback() {
				public void invoke(long window, int width, int height) {
					EVENTS.add(new WindowEvent(WindowAction.RESIZE, width, height));
				}
			});
			glfwSetKeyCallback(window, new GLFWKeyCallback() {
				public void invoke(long window, int key, int scancode, int action, int mods) {
					Keysym code = Keysym.getForCode(key);
					if(code != null)
						EVENTS.add(new WindowEvent(WindowAction.KEY, code.ordinal(), 
							(action == GLFW_PRESS ? KeyEvent.PRESS : (action == GLFW_RELEASE ? KeyEvent.RELEASE : KeyEvent.REPEAT)).ordinal()));
				}
			});
			glfwSetScrollCallback(window, new GLFWScrollCallback() {
				public void invoke(long window, double xoffset, double yoffset) {
					EVENTS.add(new WindowEvent(WindowAction.SCROLL, (int)xoffset, (int)yoffset));
				}
			});
			glfwSetWindowCloseCallback(window, new GLFWWindowCloseCallback() {
				public void invoke(long window) {
					EVENTS.add(new WindowEvent(WindowAction.CLOSED, 0, 0));
				}
			});
			glfwSetWindowFocusCallback(window, new GLFWWindowFocusCallback() {
				public void invoke(long window, boolean focused) {
					EVENTS.add(new WindowEvent(WindowAction.FOCUS, focused ? 1 : 0, 0));
				}
			});
//			glfwSetWindowSizeCallback(window, new GLFWWindowSizeCallback() {
//				public void invoke(long window, int width, int height) {
//					EVENTS.add(new WindowEvent(WindowAction.RESIZE, width, height));
//				}
//			});
			glfwSetWindowPosCallback(window, new GLFWWindowPosCallback() {
				public void invoke(long window, int xpos, int ypos) {
					EVENTS.add(new WindowEvent(WindowAction.POSITION, xpos, ypos));
				}
			});
			glfwSetWindowRefreshCallback(window, new GLFWWindowRefreshCallback() {
				public void invoke(long window) {
					EVENTS.add(new WindowEvent(WindowAction.REDRAW, 0, 0));
				}
			});
			glfwMakeContextCurrent(window);
			GL.createCapabilities();
		}
		return window != NULL;
	}
	public static void destroyWindow() {
		if(window != NULL) {
			glfwFreeCallbacks(window);
			glfwDestroyWindow(window);
			window = NULL;
		}
	}
	public static void initWindow(int sx, int sy, int wx, int wy, int mx, int my) {
		if(window == NULL)
			return;
		long monitor = glfwGetPrimaryMonitor();
		GLFWVidMode mode = monitor == NULL ? null : glfwGetVideoMode(monitor);
		int[] x = new int[1];
		int[] y = new int[1];
		if(monitor != NULL)
			glfwGetMonitorPos(monitor, x, y);
		int xsize = (mode != null && mode.width() >= mx && wx > mode.width()) ? mode.width() : wx;
		int ysize = (mode != null && mode.height() >= my && wy > mode.height()) ? mode.height() : wy;
		int xpos = x[0] + (mode != null ? mode.width() / 2 - xsize / 2 : 0);
		int ypos = y[0] + (mode != null ? mode.height() / 2 - ysize / 2 : 0);
		xsize = xsize < 1 ? 1 : xsize;
		ysize = ysize < 1 ? 1 : ysize;
		if(sx != 0x80000000 && sy != 0x80000000) {
			xpos = sx;
			ypos = sy;
		}
		if(wx != 0 && wy != 0) {
			xsize = wx;
			ysize = wy;
		}
		glfwSetWindowSize(window, xsize, ysize);
		glfwSetWindowSizeLimits(window, mx, my, GLFW_DONT_CARE, GLFW_DONT_CARE);
		glfwSetWindowPos(window, xpos, ypos);
//		wcf_windowed(win, xpos, ypos, xsize, ysize);
		// wcf_fullscreen does not move while hidden ...
//		wcf_setpos(win, xpos, ypos);
		glfwShowWindow(window);
//		wcf_show(win, 1);
//		wcf_limits(win, 1, 1, -1, -1);
		glfwSetWindowMonitor(window, NULL, xpos, ypos, xsize, ysize, GLFW_DONT_CARE); // set position twice to work around bugs in some tiling window managers
	}
	
	public static WindowEvent[] poll() {
		glfwPollEvents();
		WindowEvent[] events = EVENTS.toArray(new WindowEvent[EVENTS.size()]);
		EVENTS.clear();
		return events;
	}
	public static DisplayMode[] getDisplayModes() {
		long monitor = glfwGetPrimaryMonitor();
		GLFWVidMode.Buffer data = monitor == NULL ? null : glfwGetVideoModes(monitor);
		if(data == null)
			return null;
		DisplayMode[] modes = new DisplayMode[data.limit()];
		for(int z = 0; z < data.limit(); z++) {
			GLFWVidMode mode = data.get(z);
			modes[z] = new DisplayMode(mode.width(), mode.height(), mode.refreshRate());
		}
		return modes;
	}
	public static DisplayMode getDisplayMode() {
		long monitor = glfwGetPrimaryMonitor();
		GLFWVidMode mode = monitor == NULL ? null : glfwGetVideoMode(monitor);
		if(mode == null)
			return null;
		return new DisplayMode(mode.width(), mode.height(), mode.refreshRate());
	}
	public static void init() {
		GLFWErrorCallback.create(new GLFWErrorCallback() {
            private Map<Integer, String> codes = APIUtil.apiClassTokens((field, value) -> 0x10000 < value && value < 0x20000, null, GLFW.class);
            
			public void invoke(int error, long description) {
				Log.SYSTEM.error("LWJGL-Fehler %s: %s", this.codes.get(error), getDescription(description));
			}
		}).set();
		if(!glfwInit())
			throw new IllegalStateException("Kann GLFW nicht initialisieren");
		if(glfwGetTimerFrequency() < 1000000L)
			throw new IllegalStateException("Timer unterstützt keine Mikrosekunden-Präzision");
		Log.SYSTEM.info("LWJGL " + Version.getVersion());
	}
	public static void end() {
		glfwTerminate();
		glfwSetErrorCallback(null).free();
	}
}
