package speiger.src.coreengine.platform.graphics.opengl.core;

import java.util.List;
import java.util.Objects;

import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.GL45;

import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.coreengine.math.vector.ints.Vec4i;
import speiger.src.coreengine.platform.graphics.api.buffer.VertexBuffer;
import speiger.src.coreengine.platform.graphics.api.buffer.states.BufferType;
import speiger.src.coreengine.platform.graphics.api.buffer.states.IndeciesType;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsCommandBuffer;
import speiger.src.coreengine.platform.graphics.api.mesh.DynamicMesh;
import speiger.src.coreengine.platform.graphics.api.mesh.Mesh;
import speiger.src.coreengine.platform.graphics.api.shader.ColorTarget;
import speiger.src.coreengine.platform.graphics.api.shader.DepthTarget;
import speiger.src.coreengine.platform.graphics.api.shader.RasterizerState;
import speiger.src.coreengine.platform.graphics.api.shader.ShaderPipeline;
import speiger.src.coreengine.platform.graphics.api.shader.states.DrawMode;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.utils.AccessType;
import speiger.src.coreengine.platform.graphics.api.utils.SampledTexture;
import speiger.src.coreengine.platform.graphics.api.utils.ScissorsManager;
import speiger.src.coreengine.platform.graphics.api.utils.ScissorsManager.NoOp;
import speiger.src.coreengine.platform.graphics.opengl.buffer.GLVertexBuffer;
import speiger.src.coreengine.platform.graphics.opengl.mesh.GLDynamicMesh;
import speiger.src.coreengine.platform.graphics.opengl.mesh.GLMesh;
import speiger.src.coreengine.platform.graphics.opengl.mesh.VertexArrayCache;
import speiger.src.coreengine.platform.graphics.opengl.sampler.GLSampler;
import speiger.src.coreengine.platform.graphics.opengl.shader.ShaderInstance;
import speiger.src.coreengine.platform.graphics.opengl.texture.GLTexture;
import speiger.src.coreengine.platform.graphics.opengl.utils.GLStates;
import speiger.src.coreengine.platform.graphics.opengl.utils.GLUtils;

public class GLRecordingCommandBuffer implements GraphicsCommandBuffer {
	List<Runnable> tasks = new ObjectArrayList<>();
	boolean recording = false;
	boolean removed = false;
	GLGraphicsDevice device;
	
	ScissorsManager scissors = new ScissorsManager(8, NoOp.INSTANCE);
	ShaderPipeline pipeline;
	ShaderInstance shader;
	GLMesh mesh;
	GLDynamicMesh dynamicMesh;
	
	public GLRecordingCommandBuffer(GLGraphicsDevice device) {
		this.device = device;
	}
	
	protected void ensureDrawing() {
		if(removed) throw new IllegalStateException("CommandBuffer is removed");		
		if(!recording) throw new IllegalStateException("CommandBuffer isn't recording");
	}
	
	@Override
	public boolean isRemoved() {
		return removed;
	}
	
	@Override
	public void remove() {
		removed = true;
		clear();
		recording = false;
		tasks.clear();
	}
	
	@Override
	public GraphicsCommandBuffer begin() {
		if(removed) throw new IllegalStateException("CommandBuffer is removed");
		if(recording) throw new IllegalStateException("CommandBuffer already recording");
		recording = true;
		tasks.clear();
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer setScissors(int x, int y, int width, int height) {
		ensureDrawing();
		scissors.setRoot(x, y, width, height);
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer pipeline(ShaderPipeline pipeline) {
		Objects.requireNonNull(pipeline);
		ensureDrawing();
		if(this.pipeline == pipeline) return this;
		this.pipeline = pipeline;
		shader = device.getShader(pipeline);
		if(shader == null) {
			//TODO implement logging
			return this;
		}
		tasks.add(new SetPipeline(pipeline, shader, dynamicMesh, device.states, device.vertexCache));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer mesh(Mesh mesh) {
		Objects.requireNonNull(mesh);
		ensureDrawing();
		if(this.mesh == mesh) return this;
		this.mesh = (GLMesh)mesh;
		this.tasks.add(new SetMesh(this.mesh));
		this.dynamicMesh = null;
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer mesh(DynamicMesh mesh) {
		Objects.requireNonNull(mesh);
		ensureDrawing();
		if(this.dynamicMesh == mesh) return this;
		this.dynamicMesh = (GLDynamicMesh)mesh;
		this.tasks.add(new SetDynamicMesh(dynamicMesh, pipeline, device.vertexCache));
		this.mesh = null;
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer texture(int binding, SampledTexture texture) {
		Objects.requireNonNull(texture);
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		Objects.requireNonNull(shader.samplers().byIndex(binding), "Texture["+binding+"] binding not found");
		tasks.add(new SetTexture(binding, (GLTexture)texture.texture(), (GLSampler)texture.sampler(), device.states));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer texture(int binding, Texture texture, AccessType access) {
		Objects.requireNonNull(texture);
		Objects.requireNonNull(access);
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		Objects.requireNonNull(shader.buffers().byIndex(binding), "Texture["+binding+"] binding not found");
		tasks.add(new SetImageStorage(binding, (GLTexture)texture, access));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer uniform(int binding, VertexBuffer buffer) {
		Objects.requireNonNull(buffer);
		if(buffer.type() != BufferType.UNIFORM_BUFFER) throw new IllegalStateException("Buffer isn't a Uniform Buffer");
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		Objects.requireNonNull(shader.uniforms().byIndex(binding), "Uniform for binding ["+binding+"] doesn't exist");
		tasks.add(new SetUniform(binding, (GLVertexBuffer)buffer, -1, -1, false));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer uniform(int binding, VertexBuffer buffer, long offset, long size) {
		Objects.requireNonNull(buffer);
		if(buffer.type() != BufferType.UNIFORM_BUFFER) throw new IllegalStateException("Buffer isn't a Uniform Buffer");
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		Objects.requireNonNull(shader.uniforms().byIndex(binding), "Uniform for binding ["+binding+"] doesn't exist");
		tasks.add(new SetUniform(binding, (GLVertexBuffer)buffer, offset, size, false));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer storage(int binding, VertexBuffer buffer) {
		Objects.requireNonNull(buffer);
		if(buffer.type() != BufferType.SHADER_STORAGE_BUFFER) throw new IllegalStateException("Buffer isn't a Shader Storage Buffer");
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		Objects.requireNonNull(shader.storages().byIndex(binding), "Shader Storage for binding ["+binding+"] doesn't exist");
		tasks.add(new SetUniform(binding, (GLVertexBuffer)buffer, -1, -1, true));
		return this;
	}

	@Override
	public GraphicsCommandBuffer storage(int binding, VertexBuffer buffer, long offset, long size) {
		Objects.requireNonNull(buffer);
		if(buffer.type() != BufferType.SHADER_STORAGE_BUFFER) throw new IllegalStateException("Buffer isn't a Shader Storage Buffer");
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		Objects.requireNonNull(shader.storages().byIndex(binding), "Shader Storage for binding ["+binding+"] doesn't exist");
		tasks.add(new SetUniform(binding, (GLVertexBuffer)buffer, offset, size, true));
		return this;
	}

	@Override
	public GraphicsCommandBuffer drawArrays(int offset, int count) {
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		DrawMode mode = pipeline.rasterizer().mode();
		tasks.add(() -> GL11.glDrawArrays(GLUtils.toGL(mode), offset, count));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer drawElements(int offset, int count) {
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		DrawMode mode = pipeline.rasterizer().mode();
		IndeciesType type = mesh.indeciesType();
		tasks.add(() -> GL11.nglDrawElements(GLUtils.toGL(mode), count, GLUtils.toGL(type), offset));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer pushScissors(int x, int y, int width, int height) {
		ensureDrawing();
		scissors.push(x, y, width, height);
		tasks.add(new SetScissors(scissors.top(), device.states));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer popScissors() {
		ensureDrawing();
		scissors.pop();
		tasks.add(new SetScissors(scissors.top(), device.states));
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer end() {
		if(!recording) throw new IllegalStateException("Already Stopped Recording");
		clear();
		return this;
	}
	
	@Override
	public int commandCount() {
		return tasks.size();
	}
	
	public void execute() {
		tasks.forEach(Runnable::run);
	}
	
	private void clear() {
		recording = false;
		pipeline = null;
		shader = null;
		mesh = null;
		scissors.clear();
	}
	
	public record SetScissors(Vec4i area, GLStates states) implements Runnable {
		@Override
		public void run() { states.scissors.set(area); }
	}
	
	public record SetPipeline(ShaderPipeline pipeline, ShaderInstance instance, GLDynamicMesh mesh, GLStates states, VertexArrayCache cache) implements Runnable {
		@Override
		public void run() {
			states.shaders.bind(instance);
			RasterizerState rasterization = pipeline.rasterizer();
			states.polygon.set(rasterization.fillMode());
			states.backCulling.set(rasterization.cullBack());
			DepthTarget depth = pipeline.depthTarget();
			if(depth.enabled()) {
				states.depth.enable();
				states.setDepthMask(depth.write());
				states.setDepthFunction(depth.function());
			}
			else states.depth.disable();
			ColorTarget color = pipeline.colorTarget();
			color.blend().ifPresentOrElse(states.blend::enableSet, states.blend::disable);
			states.setColorMask(color.writeMask());
			if(mesh != null) cache.apply(pipeline, mesh);
		}	
	}
	
	public record SetMesh(GLMesh mesh) implements Runnable {
		@Override
		public void run() { GL30.glBindVertexArray(mesh.id()); }
	}
	
	public record SetDynamicMesh(GLDynamicMesh mesh, ShaderPipeline pipeline, VertexArrayCache cache) implements Runnable {
		@Override
		public void run() { cache.apply(pipeline, mesh); }
	}
	
	public record SetTexture(int binding, GLTexture texture, GLSampler sampler, GLStates states) implements Runnable {
		@Override
		public void run() {
			states.textures.bind(binding, texture.id());
			states.samplers.bind(binding, sampler.id());
		}
	}
	
	public record SetImageStorage(int binding, GLTexture texture, AccessType type) implements Runnable {
		@Override
		public void run() {
			GL45.glBindImageTexture(0, binding, texture.id(), false, 0, GLUtils.toGL(type), GLUtils.toGLInternal(texture.settings().internal()));
		}
	}
	
	public record SetUniform(int binding, GLVertexBuffer buffer, long offset, long size, boolean shader) implements Runnable {
		@Override
		public void run() {
			if(size < 0 || offset < 0) GL30.glBindBufferBase(shader ? GL43.GL_SHADER_STORAGE_BUFFER : GL31.GL_UNIFORM_BUFFER, binding, buffer.id());
			else GL30.glBindBufferRange(shader ? GL43.GL_SHADER_STORAGE_BUFFER : GL31.GL_UNIFORM_BUFFER, binding, buffer.id(), offset, size);
		}
	}
}
