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

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.coreengine.platform.graphics.api.buffer.VertexBuffer;
import speiger.src.coreengine.platform.graphics.api.buffer.states.BufferType;
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.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.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.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.GLScissorsHandler;
import speiger.src.coreengine.platform.graphics.opengl.utils.GLStates;
import speiger.src.coreengine.platform.graphics.opengl.utils.GLUtils;

public class GLImmidateCommandBuffer implements GraphicsCommandBuffer {
	GLGraphicsDevice device;
	boolean removed = false;
	boolean recording = false;
	int recorded = 0;
	
	ScissorsManager scissors;
	ShaderPipeline pipeline;
	ShaderInstance shader;
	GLMesh mesh;
	GLDynamicMesh dynamicMesh;
	
	public GLImmidateCommandBuffer(GLGraphicsDevice device) {
		this.device = device;
		this.scissors = new ScissorsManager(8, new GLScissorsHandler(device.states));
	}

	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() {
		clear();
		device = null;
		removed = true;
		recorded = 0;
	}
	
	@Override
	public GraphicsCommandBuffer begin() {
		if(removed) throw new IllegalStateException("CommandBuffer is removed");
		if(recording) throw new IllegalStateException("CommandBuffer already recording");
		recording = true;
		recorded = 0;
		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;
		}
		GLStates states = device.states;
		states.shaders.bind(shader);
		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(dynamicMesh != null) bindDynamicMesh();
		recorded++;
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer mesh(Mesh mesh) {
		Objects.requireNonNull(mesh);
		ensureDrawing();
		if(this.mesh == mesh) return this;
		this.mesh = (GLMesh)mesh;
		GL30.glBindVertexArray(this.mesh.id());
		this.dynamicMesh = null;
		recorded++;
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer mesh(DynamicMesh mesh) {
		Objects.requireNonNull(mesh);
		ensureDrawing();
		if(this.dynamicMesh == mesh) return this;
		this.dynamicMesh = (GLDynamicMesh)mesh;
		bindDynamicMesh();
		this.mesh = null;
		return this;
	}
	
	protected void bindDynamicMesh() {
		if(pipeline == null || dynamicMesh == null) return;
		device.vertexCache.apply(pipeline, dynamicMesh);
	}
	
	@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");
		GLStates states = device.states;
		states.textures.bind(binding, ((GLTexture)texture.texture()).id());
		states.samplers.bind(binding, ((GLSampler)texture.sampler()).id());
		recorded++;
		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");
		GL45.glBindImageTexture(0, binding, ((GLTexture)texture).id(), false, 0, GLUtils.toGL(access), GLUtils.toGLInternal(texture.settings().internal()));
		recorded++;
		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");
		GL30.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, binding, ((GLVertexBuffer)buffer).id());
		recorded++;
		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");
		GL30.glBindBufferRange(GL31.GL_UNIFORM_BUFFER, binding, ((GLVertexBuffer)buffer).id(), offset, size);
		recorded++;
		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");
		GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, binding, ((GLVertexBuffer)buffer).id());
		recorded++;
		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");
		GL30.glBindBufferRange(GL43.GL_SHADER_STORAGE_BUFFER, binding, ((GLVertexBuffer)buffer).id(), offset, size);
		recorded++;
		return this;
	}

	@Override
	public GraphicsCommandBuffer drawArrays(int offset, int count) {
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		GL11.glDrawArrays(GLUtils.toGL(pipeline.rasterizer().mode()), offset, count);
		recorded++;
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer drawElements(int offset, int count) {
		ensureDrawing();
		Objects.requireNonNull(shader, "No Pipeline found");
		GL11.nglDrawElements(GLUtils.toGL(pipeline.rasterizer().mode()), count, GLUtils.toGL(mesh.indeciesType()), offset);
		recorded++;
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer pushScissors(int x, int y, int width, int height) {
		ensureDrawing();
		scissors.push(x, y, width, height);
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer popScissors() {
		ensureDrawing();
		scissors.pop();
		return this;
	}
	
	@Override
	public GraphicsCommandBuffer end() {
		if(!recording) throw new IllegalStateException("Already Stopped Recording");
		clear();
		return this;
	}
	
	private void clear() {
		recording = false;
		pipeline = null;
		shader = null;
		mesh = null;
		scissors.clear();
	}
	
	@Override
	public int commandCount() { return recorded; }
}
