package projectzombie.display;

import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL33;

import gl_engine.graphics.GraphicsHelpers;
import gl_engine.graphics.GraphicsShader;
import mainloop.task.IMainloopTask;
import projectzombie.Main;
import projectzombie.init.Resources;
import projectzombie.input.CursorEnterCallback;
import projectzombie.input.CursorPosCallback;
import projectzombie.input.JoystickCallback;
import projectzombie.input.KeyCallback;
import projectzombie.input.KeyCharCallback;
import projectzombie.input.MouseButtonCallback;
import projectzombie.input.ScrollWheelCallback;
import projectzombie.mainloop.MainloopEventHandler;
import projectzombie.menu.MenuSettings;
import projectzombie.settings.Settings;

public class DisplayWindow implements IMainloopTask
{
	public static boolean fullscreen;
	public static boolean vsync;
	
	private long window;
	private long monitor;
	private int width;
	private int height;
	private boolean mouseVisibility_last = false;
	public int texture_max_size;
	
	public static int fps = 0;
	
	public GraphicsShader environmentRenderer;
	public GraphicsShader effectRenderer;
	
	public int effect_vao;
	
	public int glsl_mist;
	public int glsl_color;
	public int glsl_contrast;
	public int glsl_discard_coords;
	public int glsl_do_discard_coords;
	public int glsl_do_lighting;
	public int glsl_model;
	public int glsl_camera;
	public int glsl_projection;
	public int glsl_projection_sun;
	public int glsl_rotated;
	public int glsl_billboard;
	public int glsl_mode;
	public int glsl_time;
	
	public int glsl_day_low;
	public int glsl_day_high;
	
	public int glsl_lightmap_offset;
	public int glsl_lightmap_size;
	
	public int glsl_effect_time;
	public int glsl_effect_red;
	public int glsl_effect_vortex;
	public int glsl_effect_vsnow;
	public int glsl_effect_red_freq;
	public int glsl_effect_chill;
	
	public static void setVSync(boolean status)
	{
		vsync = status;
		GLFW.glfwSwapInterval(vsync ? 1 : 0);
	}
	
	public int getWidth() {
		return this.width;
	}
	
	public int getHeight() {
		return this.height;
	}
	
	void setWidth(int w) {
		width = w;
	}
	
	void setHeight(int h) {
		height = h;
	}
	
	public void init()
	{
		// Initialize GLFW
		if(!GLFW.glfwInit()) {
			throw new RuntimeException("Failed to initialize GLFW");
		}
		
		// Get the monitor size
		IntBuffer w = BufferUtils.createIntBuffer(1);
		IntBuffer h = BufferUtils.createIntBuffer(1);
		monitor = GLFW.glfwGetPrimaryMonitor();
		GLFW.glfwGetMonitorPhysicalSize(monitor, w, h);
		width = w.get()*4;
		height = h.get()*4;
		
		//GLFW.glfwWindowHint(GLFW.GLFW_DOUBLEBUFFER, GLFW.GLFW_FALSE);
		
		// Create the window
		window = GraphicsHelpers.initWindow("Project Zombie", width, height, fullscreen ? monitor : 0);
		
		// Make the context current
		GLFW.glfwMakeContextCurrent(this.window);
		
		// Set the key handlers
		GLFW.glfwSetCursorPosCallback(this.window, new CursorPosCallback());
		GLFW.glfwSetCharCallback(this.window, new KeyCharCallback());
		GLFW.glfwSetCursorEnterCallback(this.window, new CursorEnterCallback());
		GLFW.glfwSetMouseButtonCallback(this.window, new MouseButtonCallback());
		GLFW.glfwSetKeyCallback(this.window, new KeyCallback());
		GLFW.glfwSetJoystickCallback(JoystickCallback.JOYSTICK_CALLBACK);
		GLFW.glfwSetScrollCallback(this.window, new ScrollWheelCallback());
		
		// Make the cursor invisible
		GLFW.glfwSetInputMode(this.window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_HIDDEN);
		
		setVSync(vsync);
		
		// Show the window
		//GLFW.glfwShowWindow(this.window);
		
		environmentRenderer = new GraphicsShader("/resources/shader/environmentRenderer");
		environmentRenderer.use();
		
		glsl_mist = GL33.glGetUniformLocation(environmentRenderer.program, "mist");
		glsl_model = GL33.glGetUniformLocation(environmentRenderer.program, "model");
		glsl_camera = GL33.glGetUniformLocation(environmentRenderer.program, "camera");
		glsl_rotated = GL33.glGetUniformLocation(environmentRenderer.program, "rotated");
		glsl_projection = GL33.glGetUniformLocation(environmentRenderer.program, "projection");
		glsl_projection_sun = GL33.glGetUniformLocation(environmentRenderer.program, "projection_sun");
		glsl_time = GL33.glGetUniformLocation(environmentRenderer.program, "time");
		glsl_discard_coords = GL33.glGetUniformLocation(environmentRenderer.program, "discard_coords");
		glsl_do_discard_coords = GL33.glGetUniformLocation(environmentRenderer.program, "do_discard_coords");
		glsl_do_lighting = GL33.glGetUniformLocation(environmentRenderer.program, "do_lighting");
		glsl_color = GL33.glGetUniformLocation(environmentRenderer.program, "color");
		glsl_contrast = GL33.glGetUniformLocation(environmentRenderer.program, "contrast");
		glsl_billboard = GL33.glGetUniformLocation(environmentRenderer.program, "billboard");
		glsl_mode = GL33.glGetUniformLocation(environmentRenderer.program, "mode");
		
		glsl_day_low = GL33.glGetUniformLocation(environmentRenderer.program, "lighting_day_low");
		glsl_day_high = GL33.glGetUniformLocation(environmentRenderer.program, "lighting_day_high");
		
		glsl_lightmap_offset = GL33.glGetUniformLocation(environmentRenderer.program, "lightmap_offset");
		glsl_lightmap_size = GL33.glGetUniformLocation(environmentRenderer.program, "lightmap_size");
		
		int glsl_atlas = GL33.glGetUniformLocation(environmentRenderer.program, "atlas");
		int glsl_lightmap = GL33.glGetUniformLocation(environmentRenderer.program, "lightmap");
		int glsl_depthmap = GL33.glGetUniformLocation(environmentRenderer.program, "depthmap");
		
		GL33.glUniform1i(glsl_atlas, 0);
		GL33.glUniform1i(glsl_lightmap, 1);
		GL33.glUniform1i(glsl_depthmap, 2);
		
		effectRenderer = new GraphicsShader("/resources/shader/effectRenderer");
		effectRenderer.use();
		
		glsl_effect_time = GL33.glGetUniformLocation(effectRenderer.program, "time");
		glsl_effect_red = GL33.glGetUniformLocation(effectRenderer.program, "red");
		glsl_effect_vsnow = GL33.glGetUniformLocation(effectRenderer.program, "vsnow");
		glsl_effect_vortex = GL33.glGetUniformLocation(effectRenderer.program, "vortex");
		glsl_effect_red_freq = GL33.glGetUniformLocation(effectRenderer.program, "red_freq");
		glsl_effect_chill = GL33.glGetUniformLocation(effectRenderer.program, "chill");
		
		//GL33.glUniform1i(GL33.glGetUniformLocation(effectRenderer.program, "shadowMap"), 2);
		
		effect_vao = GL33.glGenVertexArrays();
		GL33.glBindVertexArray(effect_vao);
		
		int effect_vbo = GL33.glGenBuffers();
		GL33.glBindBuffer(GL33.GL_ARRAY_BUFFER, effect_vbo);
		GL33.glBufferData(GL33.GL_ARRAY_BUFFER, new float[] {
				-1, -1,
				-1,  1,
				 1,  1,
				 
				-1, -1,
				 1, -1,
				 1,  1,
		}, GL33.GL_STATIC_DRAW);
		
		GL33.glVertexAttribPointer(0, 2, GL33.GL_FLOAT, false, Float.BYTES * 2, 0);
		GL33.glEnableVertexAttribArray(0);
		
		int[] ptr = new int[1];
		GL33.glGetIntegerv(GL33.GL_MAX_TEXTURE_SIZE, ptr);
		
		texture_max_size = ptr[0];
		System.out.println("Max texture size: " + texture_max_size);
		
		DisplayRender.init();
	}

	public void render()
	{
		// Set the framebuffer size
		int w[] = {0};
		int h[] = {0};
		GLFW.glfwGetFramebufferSize(this.window, w, h);
		width = w[0];
		height = h[0];
		
		environmentRenderer.use();
		DisplayLighting.updateLighting();
		
		GL33.glUniform1f(glsl_mist, 0);
		
		if(Main.player.getHydration() < 0.2) {
			GL33.glUniform1f(glsl_contrast, (float)(0.2 - Main.player.getHydration()) * 1.6f);
		} else {
			GL33.glUniform1f(glsl_contrast, 0);
		}
		
		// Bind the texture atlas
		GL33.glActiveTexture(GL33.GL_TEXTURE0);
		Resources.ATLAS.bind();
		
		// Bind the lightmap
		GL33.glActiveTexture(GL33.GL_TEXTURE1);
		GL33.glBindTexture(GL33.GL_TEXTURE_2D, DisplayLighting.lightmap);
		
		// Bind the shadow depth map
		GL33.glActiveTexture(GL33.GL_TEXTURE2);
		GL33.glBindTexture(GL33.GL_TEXTURE_2D, DisplayRender.shadow_depth);
		
		// Render everything
		DisplayRender.render(w[0], h[0]);
		
		// Use the effect shader
		effectRenderer.use();
		
		// Bind the shadow depth map
		GL33.glActiveTexture(GL33.GL_TEXTURE0);
		GL33.glBindTexture(GL33.GL_TEXTURE_2D, DisplayRender.shadow_depth);
		
		// Send extra data to the shader
		if(!Main.player.dead && Main.menu.doGameloop) {
			GL33.glUniform1i(glsl_effect_time, (int)System.currentTimeMillis());
		}
		
		if(Main.player.getHydration() < 0.2) {
			GL33.glUniform1f(glsl_effect_vsnow, (float)(0.2 - Main.player.getHydration()));
			GL33.glUniform1f(glsl_effect_vortex, (float)(0.2 - Main.player.getHydration()) / 10);
		} else {
			GL33.glUniform1f(glsl_effect_vsnow, 0);
			GL33.glUniform1f(glsl_effect_vortex, 0);
		}
		
		double player_health = Main.player.getHealth() / Main.player.maxHealth();
		
		if(player_health < 0.25) {
			GL33.glUniform1f(glsl_effect_red, (float)Math.min(2 * (0.25 - player_health), 0.5));
			GL33.glUniform1i(glsl_effect_red_freq, 20);
		} else {
			GL33.glUniform1f(glsl_effect_red, 0);
		}
		
		if(Main.player.getTemperature() < 0.3) {
			GL33.glUniform1f(glsl_effect_chill, (float)(0.3 - Main.player.getTemperature()) / 3 * 10);
		} else {
			GL33.glUniform1f(glsl_effect_chill, 0);
		}
		
		// Draw a quad to the whole screen for the effects
		GL33.glBindVertexArray(effect_vao);
		GL33.glDrawArrays(GL33.GL_TRIANGLES, 0, 6);
		
		// Swap the framebuffers and poll events
		GLFW.glfwSwapBuffers(window);
		GLFW.glfwPollEvents();
		
		fps += 1;
	}
	
	public void toggleFullscreen()
	{
		// Exit fullscreen if the window is in fullscreen
		if(fullscreen)
		{
			fullscreen = false;
			GLFW.glfwSetWindowMonitor(window, 0, 1, 1, width, height, GLFW.GLFW_DONT_CARE);
		}
		
		// Enter fullscreen if the window is windowed
		else
		{
			// Get the monitor size
			IntBuffer w = BufferUtils.createIntBuffer(1);
			IntBuffer h = BufferUtils.createIntBuffer(1);
			GLFW.glfwGetMonitorPhysicalSize(monitor, w, h);
			width = w.get()*4;
			height = h.get()*4;
			
			// Enter fullscreen mode
			fullscreen = true;
			GLFW.glfwSetWindowMonitor(window, monitor, 0, 0, width, height, GLFW.GLFW_DONT_CARE);
		}
		
		// Update the settings file
		Settings.update();
		
		if(Main.menu instanceof MenuSettings) {
			((MenuSettings) Main.menu).buttonFullscreen.setText(
					"Fullscreen: " + (fullscreen ? "On" : "Off"));
		}
	}
	
	public void setMouseVisibility(boolean status) {
		if(status != mouseVisibility_last) {
			mouseVisibility_last = status;
			if(status) {
				GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
			} else {
				GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_HIDDEN);
			}
		}
	}
	
	public boolean shouldClose() {
		return GLFW.glfwWindowShouldClose(window);
	}
	
	public void makeContextCurrent() {
		GLFW.glfwMakeContextCurrent(window);
	}

	@Override
	public boolean MainLoopDelay(long millis) {
		return millis > MainloopEventHandler.MAINLOOP_EVENT_HANDLER.mspf;
	}

	@Override
	public boolean MainLoopRepeat() {
		return true;
	}

	@Override
	public void MainLoopUpdate() {
		this.render();
	}

	public void setTitle(String string) {
		GLFW.glfwSetWindowTitle(window, string);
	}
}
