package com.danitheskunk.skunkworks.backends.gl;

import com.danitheskunk.skunkworks.*;
import com.danitheskunk.skunkworks.gfx.IRenderContext;
import com.danitheskunk.skunkworks.gfx.ITexture;
import com.danitheskunk.skunkworks.gfx.Image;
import com.danitheskunk.skunkworks.gfx.threedee.IRenderContext3D;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL46.*;
import static org.lwjgl.system.MemoryUtil.NULL;

public class Window extends BaseWindow {
	private static final String fragmentSource = """
				#version 450
				layout(location = 1) in vec2 texCoord;
				layout(binding = 0) uniform sampler2D tex;
				layout(location = 3) uniform ivec2 texOffset;
				layout(location = 4) uniform ivec2 texSize;
				layout(location = 5) uniform vec4 tint;
				out vec4 color;
				void main() {
					//color = vec4(vec2(texCoord).x/1000.f, 0.2f, 0.7f, 1.0f);
					//color = texture(tex, texCoord);
					vec4 col = texelFetch(tex, ivec2(mod(texCoord, texSize)) + texOffset, 0);
					
					if(tint.a > 0.0) {
						color = col * tint;
					}
				}
		""";
	private static final String fragmentSource3D = """
				#version 450
				layout(location = 1) in vec2 texCoord;
				layout(binding = 0) uniform sampler2D tex;
				out vec4 color;
				void main() {
					//color = vec4(vec2(texCoord).x/1000.f, 0.2f, 0.7f, 1.0f);
					color = texture(tex, texCoord);
					//color = vec4(0.0, texCoord.xy, 1.0);
					//color = vec4(1.0, texCoord.x / 2, 1.0, 1.0);
				}
		""";
	private static final String fragmentSourceScaler = """
				#version 450
				layout(location = 1) in vec2 texCoord;
				layout(binding = 0) uniform sampler2D tex;
				out vec4 color;
				void main() {
					//color = vec4(vec2(texCoord).x/1000.f, 0.2f, 0.7f, 1.0f);
					vec4 col = texture(tex, texCoord);
					if(col.a > 0.0) {
						color = col;
					}
					//color = texelFetch(tex, ivec2(texCoord), 0);
					//color = vec4(1.0, 1.0, 1.0, 1.0);
					//color = vec4(texCoord.xy, 0.0, 1.0);
				}
		""";
	private static final String vertexSource = """
		#version 450
		layout(location = 0) in vec2 pos;
		layout(location = 1) in ivec2 texCoord;
		layout(location = 2) uniform vec2 windowSize;
		layout(location = 1) out vec2 out_texCoord;
		void main() {
			gl_Position = vec4(pos / windowSize * vec2(2.0f, -2.0f) + vec2(-1.0f, 1.0f), 0.0f, 1.0f);
			out_texCoord = texCoord;
		}
		""";
	private static final String vertexSource3D = """
		#version 450
		layout(location = 0) in vec3 pos;
		layout(location = 1) in vec2 texCoord;
		layout(location = 2) uniform mat4 projection;
		layout(location = 1) out vec2 out_texCoord;
		void main() {
			gl_Position = projection * vec4(pos, 1.0f);
			out_texCoord = texCoord;
		}
		""";
	private static final String vertexSourceScaler = """
		#version 450
		layout(location = 0) in vec2 pos;
		layout(location = 1) in vec2 texCoord;
		layout(location = 2) uniform vec2 windowSize;
		layout(location = 1) out vec2 out_texCoord;
		void main() {
			gl_Position = vec4(pos / windowSize * vec2(2.0f, 2.0f) + vec2(-1.0f, -1.0f), 0.0f, 1.0f);
			out_texCoord = texCoord;
		}
		""";
	private final int framebuffer;
	private final int framebufferTex;
	private final Program program;
	private final Program program3D;
	private final Program programScaler;
	private final Mat4f projection;
	private final RenderContext renderContext;
	private final RenderContext3D renderContext3D;
	private final Vec2i size;
	private final boolean[] stateMouseClicked;
	private final boolean[] stateMouseDown;
	private final TextureAtlas textureAtlas;
	private final long window;
	private boolean debug;
	private boolean shouldClose;
	private Vec2i windowSize;

	public Window(Vec2i size, String title, Engine engine) {
		super(engine);
		GLFWErrorCallback.createPrint(System.err).set();
		if(!glfwInit()) throw new IllegalStateException(
			"Unable to initialize GLFW");

		glfwDefaultWindowHints();
		debug = false;
		this.size = size;
		windowSize = size;

		stateMouseClicked = new boolean[8];
		stateMouseDown = new boolean[8];

		window = glfwCreateWindow(size.getX(), size.getY(), title, NULL, NULL);
		if(window == NULL) throw new RuntimeException(
			"Failed to create GLFW window");


		glfwShowWindow(window);
		glfwMakeContextCurrent(window);
		glfwSwapInterval(1); //vsync
		GL.createCapabilities();
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		//glOrtho(0.0f, size.getX(), size.getY(), 0.0f, 0.0f, 1.0f);
		glEnable(GL_COLOR);
		glEnable(GL_TEXTURE_2D);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glEnable(GL_BLEND);
		glEnable(GL_DEPTH_TEST);

		glfwSetWindowSizeCallback(window, this::windowSizeCallback);
		glfwSetMouseButtonCallback(window, this::mouseButtonCallback);


		framebuffer = glGenFramebuffers();
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
		framebufferTex = glGenTextures();
		glBindTexture(GL_TEXTURE_2D, framebufferTex);
		glTexImage2D(GL_TEXTURE_2D,
			0,
			GL_RGBA,
			size.getX(),
			size.getY(),
			0,
			GL_RGBA,
			GL_UNSIGNED_BYTE,
			0
		);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glFramebufferTexture(GL_FRAMEBUFFER,
			GL_COLOR_ATTACHMENT0,
			framebufferTex,
			0
		);

		int framebufferDepthTex = glGenTextures();
		glBindTexture(GL_TEXTURE_2D, framebufferDepthTex);
		glTexImage2D(GL_TEXTURE_2D,
			0,
			GL_DEPTH_COMPONENT,
			size.getX(),
			size.getY(),
			0,
			GL_DEPTH_COMPONENT,
			GL_UNSIGNED_BYTE,
			0
		);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glFramebufferTexture(GL_FRAMEBUFFER,
			GL_DEPTH_ATTACHMENT,
			framebufferDepthTex,
			0
		);

		glDrawBuffers(GL_COLOR_ATTACHMENT0);

		textureAtlas = new TextureAtlas();
		program = new Program(vertexSource, fragmentSource);
		program3D = new Program(vertexSource3D, fragmentSource3D);
		programScaler = new Program(vertexSourceScaler, fragmentSourceScaler);

		projection = Mat4f.perspective(Math.PI / 2, 16.0 / 9.0, 0.1, 100.0);

		renderContext = new RenderContext(size,
			program.getAttribLocation("texCoord"),
			program.getUniformLocation("texOffset"),
			program.getUniformLocation("texSize"),
			program.getUniformLocation("tint")
		);

		renderContext3D = new RenderContext3D(program.getAttribLocation(
			"texCoord"));
		renderContext3D.setTextureAtlas(textureAtlas);

		glProgramUniform2f(
			program.getProgram(),
			program.getUniformLocation("windowSize"),
			size.getX(),
			size.getY()
		);

		shouldClose = false;

		System.out.println("Skunkworks window initialised");
		System.out.print("Maximum Texture Size: ");
		System.out.println(GL11.glGetInteger(GL_MAX_TEXTURE_SIZE));
	}

	@Override
	public Vec2i getMousePos() {
		//todo: scale mouse to stretchMode
		double[] x = {0.0};
		double[] y = {0.0};
		glfwGetCursorPos(window, x, y);
		var mousePos = new Vec2i((int) x[0], (int) y[0]);

		var scaledMousePos = switch(stretchMode) {
			case STRETCH -> {
				var stretch = Vec2f.div(size, windowSize);
				yield Vec2i.mul(stretch, mousePos);
			}
			case ASPECT -> {
				var scale = Vec2f.div(size, windowSize);
				var scalef = Math.max(scale.getX(), scale.getY());

				var off = Vec2f.div(Vec2i.sub(windowSize,
					Vec2f.mul(size.toVec2f(), 1.0 / scalef).toVec2i()
				).toVec2f(), 2).toVec2i();

				yield Vec2i.mul(Vec2i.sub(mousePos, off), 1.0 / scalef);
			}
			case INTEGER -> {
				var scale = Vec2i.div(windowSize, size);
				var scalei = Math.max(1, Math.min(scale.getX(), scale.getY()));

				var off = Vec2i.div(Vec2i.sub(windowSize,
					Vec2i.mul(size, scalei)
				), 2);

				yield Vec2i.div(Vec2i.sub(mousePos, off), scalei);
			}
		};
		return Vec2i.max(Vec2i.ZERO,
			Vec2i.min(Vec2i.sub(size, new Vec2i(1, 1)), scaledMousePos)
		);
	}

	@Override
	public boolean isMouseClicked(int button) {
		return stateMouseClicked[button];
	}

	@Override
	public boolean isMouseDown(int button) {
		return stateMouseDown[button];
	}

	@Override
	public ITexture loadTexture(Image image) {
		return textureAtlas.addTexture(image);
	}

	@Override
	public ITexture loadTexture(String path) {
		var img = engine.loadImage(path);
		return textureAtlas.addTexture(img);
	}

	@Override
	public List<ITexture> loadTextureArray(Image image, Vec2i tileSize) {
		var tileCount = Vec2i.div(image.getSize(), tileSize);
		var tiles = new ArrayList<ITexture>();

		for(int y = 0; y < tileCount.getY(); ++y) {
			for(int x = 0; x < tileCount.getX(); ++x) {
				var rect = new Recti(Vec2i.mul(new Vec2i(x, y), tileSize),
					tileSize
				);
				var img = image.getSubImage(rect);
				tiles.add(loadTexture(img));
			}
		}

		return tiles;
	}

	@Override
	public List<ITexture> loadTextureArray(String path, Vec2i tileSize) {
		var img = engine.loadImage(path);
		return loadTextureArray(img, tileSize);
	}


	private void mouseButtonCallback(
		@SuppressWarnings("unused") long window, int button, int action,
		@SuppressWarnings(
		"unused") int mods
	) {
		switch(action) {
			case GLFW_PRESS -> {
				stateMouseClicked[button] = true;
				stateMouseDown[button] = true;
			}
			case GLFW_RELEASE -> stateMouseDown[button] = false;
		}
	}

	@Override
	public void renderFinish(IRenderContext context) {
		if(debug) {
			context.drawTextureRectangle(new Recti(Vec2i.ZERO, size),
				textureAtlas.getAtlasTexture(),
				false
			);
		}
	}

	public void renderFinish3D(IRenderContext3D context) {

	}

	@Override
	public IRenderContext renderStart() {
		program.use();
		textureAtlas.bind();
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
		glViewport(0, 0, size.getX(), size.getY());
		textureAtlas.update();
		glClearColor(0.f, 0.f, 0.f, 0.0f);
		glClear(GL_DEPTH_BUFFER_BIT);
		glDepthFunc(GL_ALWAYS);
		//glBegin(GL_TRIANGLES);
		return renderContext;
	}

	@Override
	public IRenderContext3D renderStart3D() {
		program3D.use();
		textureAtlas.bind();
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
		glViewport(0, 0, size.getX(), size.getY());
		textureAtlas.update();
		glClearColor(0.f, 0.f, 0.f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glDepthFunc(GL_LESS);
		//glBegin(GL_TRIANGLES);
		glUniformMatrix4fv(program3D.getUniformLocation("projection"),
			true,
			projection.asFloatArray()
		);

		return renderContext3D;
	}

	@Override
	public void runScaler() {
		//glEnd();


		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		glViewport(0, 0, windowSize.getX(), windowSize.getY());

		programScaler.use();
		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		glBindTexture(GL_TEXTURE_2D, framebufferTex);
		glProgramUniform2f(
			programScaler.getProgram(),
			programScaler.getUniformLocation("windowSize"),
			windowSize.getX(),
			windowSize.getY()
		);

		int tlx1 = 0;
		int tly1 = 0;
		int tlx2 = 0;
		int tly2 = 0;

		switch(stretchMode) {
			case STRETCH -> {
				tlx2 = windowSize.getX();
				tly2 = windowSize.getY();
			}
			case ASPECT -> {
				float scalex = (float) windowSize.getX() / (float) size.getX();
				float scaley = (float) windowSize.getY() / (float) size.getY();
				float scale = Math.min(scalex, scaley);

				int xoff = (int) (
					(windowSize.getX() - size.getX() * scale) / 2
				);
				int yoff = (int) (
					(windowSize.getY() - size.getY() * scale) / 2
				);

				tlx1 = xoff;
				tly1 = yoff;
				tlx2 = (int) (xoff + size.getX() * scale);
				tly2 = (int) (yoff + size.getY() * scale);
			}
			case INTEGER -> {
				int scalex = windowSize.getX() / size.getX();
				int scaley = windowSize.getY() / size.getY();
				int scale = Math.max(1, Math.min(scalex, scaley));

				int xoff = (windowSize.getX() - size.getX() * scale) / 2;
				int yoff = (windowSize.getY() - size.getY() * scale) / 2;

				tlx1 = xoff;
				tly1 = yoff;
				tlx2 = xoff + size.getX() * scale;
				tly2 = yoff + size.getY() * scale;
			}
		}


		var texCoordIndex = programScaler.getAttribLocation("texCoord");
		glBegin(GL_TRIANGLES);
		//counterclockwise triangles
		glVertexAttrib2f(texCoordIndex, 0, 1);
		glVertex2i(tlx1, tly2);
		glVertexAttrib2f(texCoordIndex, 1, 0);
		glVertex2i(tlx2, tly1);
		glVertexAttrib2f(texCoordIndex, 0, 0);
		glVertex2i(tlx1, tly1);

		glVertexAttrib2f(texCoordIndex, 1, 0);
		glVertex2i(tlx2, tly1);
		glVertexAttrib2f(texCoordIndex, 0, 1);
		glVertex2i(tlx1, tly2);
		glVertexAttrib2f(texCoordIndex, 1, 1);
		glVertex2i(tlx2, tly2);

		glEnd();


		glfwSwapBuffers(window);
	}

	@Override
	public void setDebug(boolean on) {
		debug = on;
	}

	@Override
	public boolean shouldClose() {
		return shouldClose;
	}

	@Override
	public void tick() { //one tick per update call?
		glfwMakeContextCurrent(window);
		if(glfwWindowShouldClose(window)) {
			shouldClose = true;
			return;
		}

		Arrays.fill(stateMouseClicked, false);
		glfwPollEvents();
	}

	private void windowSizeCallback(
		@SuppressWarnings("unused") long window, int width, int height
	) {
		System.out.printf("new window size %d x %d\n", width, height);
		windowSize = new Vec2i(width, height);
		glViewport(0, 0, width, height);
	}
}
