package speiger.src.coreengine.ui.gui.renderer;

import java.util.Comparator;
import java.util.List;

import org.jspecify.annotations.Nullable;

import speiger.src.collections.ints.collections.IntStack;
import speiger.src.collections.ints.lists.IntArrayList;
import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.utils.Stack;
import speiger.src.coreengine.math.bits.BitUtil;
import speiger.src.coreengine.math.vector.ints.Vec4i;
import speiger.src.coreengine.math.vector.matrix.Matrix4fStack;
import speiger.src.coreengine.math.vector.quaternion.Quaternion;
import speiger.src.coreengine.platform.graphics.api.buffer.states.BufferState;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsCommandBuffer;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.mesh.DynamicMesh;
import speiger.src.coreengine.platform.graphics.api.shader.ShaderPipeline;
import speiger.src.coreengine.platform.graphics.api.utils.BufferOwner;
import speiger.src.coreengine.platform.graphics.api.utils.ExecutionType;
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.vertex.builder.IVertexBuffer.BufferResult;
import speiger.src.coreengine.platform.graphics.api.vertex.builder.IVertexBuilder;
import speiger.src.coreengine.platform.graphics.api.vertex.builder.VertexBuilder;
import speiger.src.coreengine.ui.gui.layout.box.IGuiBox;
import speiger.src.coreengine.ui.gui.utils.Align;
import speiger.src.coreengine.ui.gui.utils.UIConstants;

public class UIRenderer implements IUIRenderer {
	GraphicsDevice device;
	Matrix4fStack matrix = new Matrix4fStack();
	VertexBuilder builder = new VertexBuilder(100);
	List<UIDraw> draws = new ObjectArrayList<>();
	Align[] alignment = new Align[] { Align.CENTER, Align.CENTER };
	Stack<WindowLayer> layers = new ObjectArrayList<>();
	int windowLayer = 1;
	int currentComponent = 0;
	ShaderPipeline currentPipeline;
	SampledTexture currentTexture;
	UIDrawer lastDrawer;
	ScissorsManager clippingManager = new ScissorsManager(64, ScissorsManager.NoOp.INSTANCE);
	DynamicMesh mesh;
	
	public UIRenderer(GraphicsDevice device) {
		this.device = device;
		builder.withAutoAlign();
		mesh = device.createDynamicMesh(DynamicMesh.builder().autogenerate().bufferState(0, BufferState.STREAM_DRAW));
	}
	
	public void endFrame() {
		draws.sort(Comparator.comparingInt(UIDraw::currentLayer).thenComparingInt(UIDraw::componentIndex));
		mesh.buffer(0).set(builder.memoryPointer(), builder.totalStoredBytes(), BufferOwner.KEPT);
		GraphicsCommandBuffer buffer = device.createCommandBuffer(ExecutionType.IMMEDIATE);
		buffer.begin();
		buffer.mesh(mesh);
		for(int i = 0,m=draws.size();i<m;i++) {
			UIDraw draw = draws.get(i);
			BufferResult result = draw.result();
			if(draw.drawer != null) {
				draw.drawer.setupExtra(buffer);
				buffer.mesh(mesh);
			}
			if(draw.texture() != null) buffer.texture(0, draw.texture());
			buffer.pipeline(draw.pipeline());
			boolean pop = false;
			Vec4i scissors = draw.scissors();
			if(scissors != Vec4i.MINUS_ONE) {
				pop = true;
				buffer.pushScissors(scissors.x(), scissors.y(), scissors.z() - scissors.x(), scissors.w() - scissors.y());
			}
			buffer.drawArrays(result.startVertex(), result.vertexCount());
			if(pop) buffer.popScissors();
		}
		buffer.end();
	}
	
	public void beginFrame() {
		builder.reset();
		draws.clear();
		layers.clear();
		windowLayer = 1;
		currentComponent = 0;
		lastDrawer = null;
		currentPipeline = null;
		currentTexture = null;
		clippingManager.clear();
		matrix.popRoot();
		matrix.setIdentity();
	}
	
	@Override
	public boolean isClipping(IGuiBox box) {
		return clippingManager.contains(Math.round(box.getMinX()), Math.round(box.getMinY()), Math.round(box.getWidth()), Math.round(box.getHeight()));
	}
	
	@Override
	public void pushClip(IGuiBox box) {
		flushDraw();
		clippingManager.push(Math.round(box.getMinX()), Math.round(box.getMinY()), Math.round(box.getWidth()), Math.round(box.getHeight()));
	}
	
	@Override
	public void popClip() {
		flushDraw();
		clippingManager.pop();
	}
	
	@Override
	public void pushTransform() {
		matrix.push();
	}
	
	@Override
	public void popTransform() {
		matrix.pop();
	}
	
	@Override
	public void translate(float z) {
		matrix.translateZ(z);
	}
	
	@Override
	public void translate(float x, float y) {
		matrix.translate(x, y);
	}
	
	@Override
	public void scale(float scale) {
		matrix.scale(scale);
	}
	
	@Override
	public void scale(float x, float y) {
		matrix.scale(x, y, 1F);
	}
	
	@Override
	public void rotate(Quaternion rotation) {
		matrix.rotate(rotation);
	}
	
	@Override
	public void align(Align vertical, Align horizontal) {
		alignment[0] = vertical;
		alignment[1] = horizontal;
	}
	
	@Override
	public void alignV(Align align) {
		alignment[0] = align;
	}
	
	@Override
	public void alignH(Align align) {
		alignment[1] = align;
	}
	
	@Override
	public void pushLayer(LayerType type) {
		switch(type) {
			case WINDOW:
				layers.push(new WindowLayer(windowLayer++));
				break;
			case COMPONENT_STACK:
				if(layers.isEmpty()) return;
				layers.top().push();
				break;
		}
	}
	
	@Override
	public void popLayer(LayerType type) {
		switch(type) {
			case WINDOW:
				layers.pop();
				break;
			case COMPONENT_STACK:
				if(layers.isEmpty()) return;
				layers.top().pop();
				break;
		}
	}
	
	@Override
	public void drawLine(float startX, float startY, float endX, float endY, float lineWidth, int color) {
		ensureType(UIConstants.GUI, null);
		float dx = endX - startX;
		float dy = endY - startY;
		float xOffset = alignment[0].align(dx);
		float yOffset = alignment[1].align(dy);
		float length = (float)Math.sqrt(dx*dx + dy*dy);
		dx /= length;
		dy /= length;
		
		float px = -dy * (lineWidth * 0.5F);
		float py = dx * (lineWidth * 0.5F);
		
		float x1 = startX + px;
		float y1 = startY + py;

		float x2 = startX - px;
		float y2 = startY - py;

		float x3 = endX + px;
		float y3 = endY + py;

		float x4 = endX - px;
		float y4 = endY - py;
		pos(x3, y3, 0F, xOffset, yOffset).rgba(color);
		pos(x2, y2, 0F, xOffset, yOffset).rgba(color);
		pos(x1, y1, 0F, xOffset, yOffset).rgba(color);
		pos(x4, y4, 0F, xOffset, yOffset).rgba(color);
		pos(x2, y2, 0F, xOffset, yOffset).rgba(color);
		pos(x3, y3, 0F, xOffset, yOffset).rgba(color);
	}
	
	@Override
	public void drawFrame(IGuiBox box, int color) {
		ensureType(UIConstants.GUI, null);
		float xOffset = alignment[0].align(box.getWidth());
		float yOffset = alignment[1].align(box.getHeight());
		float minX = box.getMinX();
		float minY = box.getMinY();
		float maxX = box.getMaxX();
		float maxY = box.getMaxY();
		drawQuad(maxX-1F, minY, maxX, maxY, xOffset, yOffset, color);
		drawQuad(minX, maxY-1F, maxX, maxY, xOffset, yOffset, color);
		drawQuad(minX, minY, minX+1F, maxY, xOffset, yOffset, color);
		drawQuad(minX, minY, maxX, minY+1F, xOffset, yOffset, color);
	}
	
	@Override
	public void drawRect(IGuiBox box, int color) {
		ensureType(UIConstants.GUI, null);
		float xOffset = alignment[0].align(box.getWidth());
		float yOffset = alignment[1].align(box.getHeight());
		drawQuad(box.getMinX(), box.getMinY(), box.getMaxX(), box.getMaxY(), xOffset, yOffset, color);
	}
	
	@Override
	public void drawImage(IGuiBox box, SampledTexture texture, int color) {
		drawImage(box, texture, 0F, 0F, 1F, 1F, color);
	}
	
	@Override
	public void drawImage(IGuiBox box, SampledTexture texture, float minU, float minV, float maxU, float maxV, int color) {
		ensureType(UIConstants.GUI_TEXTURED, texture);
		float xOff = alignment[0].align(box.getWidth());
		float yOff = alignment[1].align(box.getHeight());
		float minX = box.getMinX();
		float minY = box.getMinY();
		float maxX = box.getMaxX();
		float maxY = box.getMaxY();
		pos(minX, maxY, 0F, xOff, yOff).tex(minU, maxV).rgba(color);
		pos(minX, minY, 0F, xOff, yOff).tex(minU, minV).rgba(color);
		pos(maxX, maxY, 0F, xOff, yOff).tex(maxU, maxV).rgba(color);
		pos(maxX, maxY, 0F, xOff, yOff).tex(maxU, maxV).rgba(color);
		pos(minX, minY, 0F, xOff, yOff).tex(minU, minV).rgba(color);
		pos(maxX, minY, 0F, xOff, yOff).tex(maxU, minV).rgba(color);
	}
	
	@Override
	public void drawCustom(UIDrawer drawer) {
		ensureType(drawer.pipeline(), drawer.texture());
		drawer.buildVertecies(builder, matrix);
		if(drawer.requiresSpecialArgs()) lastDrawer = drawer;
	}
	
	protected void drawQuad(float minX, float minY, float maxX, float maxY, float xOff, float yOff, int color) {
		pos(minX, maxY, 0F, xOff, yOff).rgba(color);
		pos(minX, minY, 0F, xOff, yOff).rgba(color);
		pos(maxX, maxY, 0F, xOff, yOff).rgba(color);
		pos(maxX, maxY, 0F, xOff, yOff).rgba(color);
		pos(minX, minY, 0F, xOff, yOff).rgba(color);
		pos(maxX, minY, 0F, xOff, yOff).rgba(color);
	}
	
	protected IVertexBuilder pos(float x, float y, float z, float xOff, float yOff) {
		matrix.transformOffset(x - xOff, y - yOff, z, xOff, yOff, 0F, builder::pos);
		return builder;
	}
	
	protected void ensureType(ShaderPipeline pipeline, @Nullable SampledTexture texture) {
		currentComponent++;
		if(currentPipeline == pipeline && currentTexture == texture && lastDrawer == null) return;
		flushDraw();
		currentPipeline = pipeline;
		currentTexture = texture;
		lastDrawer = null;
		builder.start(pipeline.rasterizer().mode(), pipeline.attributes().get(0).layout());
	}
	
	private void flushDraw() {
		if(builder.hasVertecies()) {
			draws.add(new UIDraw(currentPipeline, currentTexture, builder.finish(), clippingManager.top(), lastDrawer, layers.isEmpty() ? 0 : layers.top().currentLayer(), currentComponent));
		}
	}
	
	protected record UIDraw(ShaderPipeline pipeline, @Nullable SampledTexture texture, BufferResult result, Vec4i scissors, @Nullable UIDrawer drawer, int currentLayer, int componentIndex) {}
	
	class WindowLayer {
		final int layer;
		IntStack stack = new IntArrayList();
		int currentLayer = 1;
		public WindowLayer(int layer) {
			this.layer = layer;
		}
		
		private void push() {
			stack.push(currentLayer++);
		}
		
		private void pop() {
			stack.pop();
		}
		
		public int currentLayer() {
			return BitUtil.toInt(stack.isEmpty() ? 0 : stack.top(), layer);
		}
	}
}
