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

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL45;

import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.math.vector.floats.Vec4f;
import speiger.src.coreengine.math.vector.ints.Vec4i;
import speiger.src.coreengine.platform.graphics.api.core.CommandBuffer;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsCommandQueue;
import speiger.src.coreengine.platform.graphics.api.shader.states.GraphicsDataType;
import speiger.src.coreengine.platform.graphics.api.target.RenderTarget;
import speiger.src.coreengine.platform.graphics.api.target.ScreenTarget;
import speiger.src.coreengine.platform.graphics.api.target.TextureTarget;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.states.TextureFormat;
import speiger.src.coreengine.platform.graphics.api.utils.PushableResource;
import speiger.src.coreengine.platform.graphics.opengl.texture.FrameBufferCache;
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;
import speiger.src.coreengine.platform.graphics.opengl.utils.ViewPortStack;
import speiger.src.coreengine.platform.graphics.opengl.utils.states.ScissorState;
import speiger.src.coreengine.platform.input.window.Window;

public class GLCommandQueue implements GraphicsCommandQueue {
	GLGraphicsDevice device;
	FrameBufferCache fboCache = new FrameBufferCache();
	RenderTarget renderTarget = ScreenTarget.INSTANCE;
	ScreenBuffer targetFBO;
	int tempWriteFBO;
	Deque<Frame> frames = new ArrayDeque<>(4);
	long currentFrame = 0L;
	Vec4f clearColor = Vec4f.mutable(0F, 0F, 0F, 1F);
	double clearDepth = 0D;
	
	public GLCommandQueue(GLGraphicsDevice device) {
		this.device = device;
		this.targetFBO = new ScreenBuffer(0, device.window().width(), device.window().height());
		tempWriteFBO = GL45.glCreateFramebuffers();
		frames.add(new Frame(-2));
		frames.add(new Frame(-1));
	}
	
	@Override
	public PushableResource putClearColor(Vec4f color) {
		return new PushableObject<>(clearColor.copyAsImmutable(), setClearColor(color), () -> clearColor, this::setClearColor);
	}
	
	@Override
	public PushableResource putClearDepth(double value) {
		return new PushableObject<>(clearDepth, setClearDepth(value), () -> clearDepth, this::setClearDepth);
	}
	
	@Override
	public PushableResource putRenderTarget(RenderTarget target) {
		return new PushableObject<>(renderTarget, setRenderTarget(target), () -> renderTarget, this::setRenderTarget);
	}
	
	@Override
	public PushableResource pushViewPort(int x, int y, int width, int height) {
		ViewPortStack stack = device.states.viewPort;
		Vec4i top = stack.top();
		Vec4i bounds = Vec4i.of(x, y, height, width);
		stack.push(x, y, width, height);
		return new PushableObject<>(top, bounds, stack::top, _ -> stack.pop());
	}
	
	@Override
	public void submitCommands(CommandBuffer buffer) {
		if(buffer instanceof GLRecordingCommandBuffer recorder) {
			recorder.execute();
		}
		else if(buffer instanceof GLRecordingComputeCommandBuffer recorder) {
			recorder.execute();
		}
	}
	
	@Override
	public long currentFrame() {
		return currentFrame;
	}
	
	@Override
	public void submitFrame() {
		frames.getFirst().fenceIn();
		currentFrame++;
		frames.add(new Frame(currentFrame));
		if(!frames.pop().isCompleted(Long.MAX_VALUE)) {
			throw new IllegalStateException("Couldn't complete frames");
		}
	}
	
	@Override
	public GLFence createFence() {
		return new GLFence(frames.getFirst());
	}
	
	private Vec4f setClearColor(Vec4f value) {
		if(clearColor.equals(value)) return value;
		clearColor.set(value);
		GL11.glClearColor(value.x(), value.y(), value.y(), value.w());
		return value;
	}
	
	private double setClearDepth(double value) {
		if(Double.compare(clearDepth, value) == 0) return value;
		clearDepth = value;
		GL11.glClearDepth(value);
		return value;
	}
	
	private RenderTarget setRenderTarget(RenderTarget target) {
		if(this.renderTarget.equals(target)) return target;
		this.renderTarget = target;
		targetFBO = switch(target) {
			case ScreenTarget _ -> {
				Window window = device.window();
				yield new ScreenBuffer(0, window.width(), window.height());
			}
			case TextureTarget(ID id, Texture color, Texture depth) -> {
				int fbo = fboCache.getOrCreateFBO(id);
				if(color != null) {
					GL45.glNamedFramebufferTexture(fbo, GL30.GL_COLOR_ATTACHMENT0, ((GLTexture)color).id(), 0);
					yield new ScreenBuffer(fbo, color.width(), color.height());
				}
				if(depth != null) {
					GL45.glNamedFramebufferTexture(fbo, GL30.GL_DEPTH_ATTACHMENT, ((GLTexture)depth).id(), 0);
					yield new ScreenBuffer(fbo, depth.width(), depth.height());
				}
				throw new IllegalArgumentException("No Color or Depth Provided");
			}
		};
		GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, targetFBO.fbo());
		GL11.glViewport(0, 0, targetFBO.width(), targetFBO.height());
		return target;
	}
	
	@Override
	public void clearTexture(int clearParam) {
		GL11.glClear(((clearParam & CLEAR_COLOR) != 0 ? GL11.GL_COLOR_BUFFER_BIT : 0) | ((clearParam & CLEAR_DEPTH) != 0 ? GL11.GL_DEPTH_BUFFER_BIT : 0));
	}
	
	@Override
	public void clearTexture(int x, int y, int width, int height, int clearParam) {
		ScissorState state = device.states.scissors;
		state.backup();
		try(PushableResource _ = pushViewPort(0, 0, targetFBO.width(), targetFBO.height())) {
			state.set(Vec4i.of(x, y, x + width, y + height));
			clearTexture(clearParam);
		}
		state.restore();
	}
	
	@Override
	public void clearTexture(Texture texture, int clearParam) {
		if((clearParam & CLEAR_BOTH) == CLEAR_BOTH) {
			//TODO implement Logging
			return;
		}
		GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, tempWriteFBO);
		GL45.glNamedFramebufferTexture(tempWriteFBO, GL30.GL_COLOR_ATTACHMENT0, ((GLTexture)texture).id(), 0);
		clearTexture(clearParam);
		GL45.glNamedFramebufferTexture(tempWriteFBO, GL30.GL_COLOR_ATTACHMENT0, 0, 0);
		GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, targetFBO.fbo());
	}
	
	@Override
	public void clearTexture(Texture texture, int x, int y, int width, int height, int clearParam) {
		ScissorState state = device.states.scissors;
		state.backup();
		try(PushableResource _ = pushViewPort(x, y, texture.width(), texture.height())) {
			state.set(Vec4i.of(x, y, x + width, y + height));
			clearTexture(texture, clearParam);
		}
		state.restore();
	}
	
	@Override
	public void writeToTexture(Texture target, TextureFormat format, GraphicsDataType dataType, long source) {
		GL45.glTextureSubImage2D(((GLTexture)target).id(), 0, 0, 0, target.width(), target.height(), GLUtils.toGLExternal(format), GLUtils.toGL(dataType), source);
	}
	
	@Override
	public void writeToTexture(Texture target, int level, int sourceX, int sourceY, int targetX, int targetY, int width, int height, TextureFormat format, GraphicsDataType dataType, long source) {
		GLStates states = device.states;
		states.unpack_alignment.set(target.settings().internal().components());
		states.unpack_row_length.set(target.width());
		states.unpack_skip_pixel.set(sourceX);
		states.unpack_skip_rows.set(sourceY);
		GL45.glTextureSubImage2D(((GLTexture)target).id(), level, targetX, targetY, width, height, GLUtils.toGLExternal(format), GLUtils.toGL(dataType), source);
		states.unpack_alignment.setDefault();
		states.unpack_row_length.setDefault();
		states.unpack_skip_pixel.setDefault();
		states.unpack_skip_rows.setDefault();
	}
	
	private record PushableObject<T>(T original, T applying, Supplier<T> current, Consumer<T> apply) implements PushableResource {
		@Override
		public void close() {
			if(current.get().equals(applying)) {
				apply.accept(original);
			}
		}
	}
	
	private record ScreenBuffer(int fbo, int width, int height) {}
	public class Frame {
		final long frame;
		boolean hasBegun;
		long fenceId;
		
		public Frame(long frame) {
			this.frame = frame;
			hasBegun = frame < 0;
		}
		
		public void fenceIn() {
			hasBegun = true;
			fenceId = GL32.glFenceSync(GL32.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
		}
		
		public boolean isCompleted(long timeout) {
			if(!hasBegun) {
				if(timeout == 0) return false;
				throw new IllegalStateException("Can't wait on something you are supposed to submit to");
			}
			if(fenceId == 0) return true;
			int result = GL32.glClientWaitSync(fenceId, GL32.GL_SYNC_FLUSH_COMMANDS_BIT, timeout);
			if(result == GL32.GL_TIMEOUT_EXPIRED) return false;
			if(result == GL32.GL_WAIT_FAILED) throw new IllegalStateException("Waiting has failed?");
			GL32.glDeleteSync(fenceId);
			fenceId = 0L;
			return true;
		}
	}
}
