package ru.dbotthepony.mc.otm.client.render

import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.*
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.client.renderer.RenderStateShard
import net.minecraft.client.renderer.RenderStateShard.LineStateShard
import net.minecraft.client.renderer.RenderType
import net.minecraft.client.renderer.ShaderInstance
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.packs.resources.ResourceProvider
import org.joml.Matrix4f
import org.lwjgl.opengl.GL11.GL_ALWAYS
import org.lwjgl.opengl.GL11.GL_LESS
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import java.util.*
import kotlin.collections.ArrayDeque
import kotlin.math.PI
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.properties.Delegates

var is3DContext = false

/**
 * Prohibits from changing blend function through RenderSystem.enable/disable
 *
 * ALWAYS use try/finally block with this
 */
var lockBlendFunc = false

private val defaultUV = UVCoords(0f, 0f, 1f, 1f)

var customTexColorShader: ShaderInstance by Delegates.notNull()
	private set

fun reloadShaders(provider: ResourceProvider) {
	customTexColorShader = ShaderInstance(provider, ResourceLocation(OverdriveThatMatters.MOD_ID, "position_tex_color"), DefaultVertexFormat.POSITION_TEX_COLOR)
}

fun renderRect(
	matrix: Matrix4f,
	x: Float,
	y: Float,
	width: Float,
	height: Float,
	z: Float = 0f,
	color: RGBAColor = RGBAColor.WHITE
) {
	if (color.isWhite)
		RenderSystem.setShader(GameRenderer::getPositionShader)
	else
		RenderSystem.setShader(GameRenderer::getPositionColorShader)

	RenderSystem.enableBlend()
	RenderSystem.defaultBlendFunc()

	if (!is3DContext)
		RenderSystem.depthFunc(GL_ALWAYS)

	val tess = tesselator
	val builder = tess.builder

	if (color.isWhite) {
		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION)

		builder.vertex(matrix, x, y + height, z).endVertex()
		builder.vertex(matrix, x + width, y + height, z).endVertex()
		builder.vertex(matrix, x + width, y, z).endVertex()
		builder.vertex(matrix, x, y, z).endVertex()
	} else {
		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR)

		builder.vertex(matrix, x, y + height, z).color(color).endVertex()
		builder.vertex(matrix, x + width, y + height, z).color(color).endVertex()
		builder.vertex(matrix, x + width, y, z).color(color).endVertex()
		builder.vertex(matrix, x, y, z).color(color).endVertex()
	}

	tess.end()
}

fun GuiGraphics.renderRect(
	x: Float,
	y: Float,
	width: Float,
	height: Float,
	z: Float = 0f,
	color: RGBAColor = RGBAColor.WHITE
) {
	renderRect(pose().last().pose(), x, y, width, height, z, color)
}

fun renderTexturedRect(
	matrix: Matrix4f,
	x: Float,
	y: Float,
	width: Float,
	height: Float,
	z: Float = 0f,
	uv: IUVCoords = defaultUV,
	uvWinding: UVWindingOrder = UVWindingOrder.NORMAL,
	color: RGBAColor = RGBAColor.WHITE,
	texture: ResourceLocation? = null
) {
	val u0 = uvWinding.u0(uv)
	val v0 = uvWinding.v0(uv)
	val u1 = uvWinding.u1(uv)
	val v1 = uvWinding.v1(uv)

	if (color.isWhite)
		RenderSystem.setShader(GameRenderer::getPositionTexShader)
	else
		RenderSystem.setShader { customTexColorShader }

	if (texture != null)
		RenderSystem.setShaderTexture(0, texture)

	RenderSystem.enableBlend()
	RenderSystem.defaultBlendFunc()

	if (!is3DContext)
		RenderSystem.depthFunc(GL_ALWAYS)

	val builder = tesselator.builder

	if (color.isWhite) {
		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX)

		builder.vertex(matrix, x, y + height, z).uv(u0, v1).endVertex()
		builder.vertex(matrix, x + width, y + height, z).uv(u1, v1).endVertex()
		builder.vertex(matrix, x + width, y, z).uv(u1, v0).endVertex()
		builder.vertex(matrix, x, y, z).uv(u0, v0).endVertex()
	} else {
		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR)

		builder.vertex(matrix, x, y + height, z).uv(u0, v1).color(color).endVertex()
		builder.vertex(matrix, x + width, y + height, z).uv(u1, v1).color(color).endVertex()
		builder.vertex(matrix, x + width, y, z).uv(u1, v0).color(color).endVertex()
		builder.vertex(matrix, x, y, z).uv(u0, v0).color(color).endVertex()
	}

	BufferUploader.drawWithShader(builder.end())
}

fun GuiGraphics.renderTexturedRect(
	x: Float,
	y: Float,
	width: Float,
	height: Float,
	z: Float = 0f,
	uv: IUVCoords = defaultUV,
	uvWinding: UVWindingOrder = UVWindingOrder.NORMAL,
	color: RGBAColor = RGBAColor.WHITE,
	texture: ResourceLocation? = null
) {
	renderTexturedRect(pose().last().pose(), x, y, width, height, z, uv, uvWinding, color, texture)
}

fun renderColoredSphere(pose: PoseStack, radius: Float, color: RGBAColor = RGBAColor.WHITE) {
	val fragments = 32
	RenderSystem.enableBlend()
	RenderSystem.defaultBlendFunc()

	val builder = tesselator.builder
	builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR)

	val turnStep = Math.PI * (1 / fragments.toDouble()) * 2
	val stripeStep = Math.PI * (1 / fragments.toDouble())

	for (turn in 0 until fragments) {
		val turnPre = turnStep * turn
		val turnPost = turnStep * (turn + 1)

		for (sector in 0 until fragments) {
			val tiltPre = Math.PI / 2 - stripeStep * sector
			val tiltPost = Math.PI / 2 - stripeStep * (sector + 1)
			val xPre = sin(turnPre).toFloat() * radius
			val xPost = sin(turnPost).toFloat() * radius
			val zPre = cos(turnPre).toFloat() * radius
			val zPost = cos(turnPost).toFloat() * radius
			val yPre = (sin(tiltPre) + 0.5).toFloat() * radius
			val yPost = (sin(tiltPost) + 0.5).toFloat() * radius

			builder.vertex(pose.last().pose(), xPre * cos(tiltPost).toFloat(), yPost, zPre * cos(tiltPost).toFloat())
				.color(color)
				.endVertex()

			builder.vertex(pose.last().pose(), xPost * cos(tiltPost).toFloat(), yPost, zPost * cos(tiltPost).toFloat())
				.color(color)
				.endVertex()

			builder.vertex(pose.last().pose(), xPost * cos(tiltPre).toFloat(), yPre, zPost * cos(tiltPre).toFloat())
				.color(color)
				.endVertex()

			builder.vertex(pose.last().pose(), xPre * cos(tiltPre).toFloat(), yPre, zPre * cos(tiltPre).toFloat())
				.color(color)
				.endVertex()
		}
	}

	BufferUploader.drawWithShader(builder.end())
}

fun drawLine(
	matrix: Matrix4f,
	startX: Float,
	startY: Float,
	endX: Float,
	endY: Float,
	width: Float,
	z: Float = 0f,
	color: RGBAColor = RGBAColor.WHITE
) {
	RenderSystem.setShader(GameRenderer::getPositionColorShader)
	RenderSystem.enableBlend()
	RenderSystem.defaultBlendFunc()

	if (!is3DContext)
		RenderSystem.depthFunc(GL_ALWAYS)

	val tess = Tesselator.getInstance()
	val builder = tess.builder

	builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR)

	val length = ((startX - endX).pow(2f) + (startY - endY).pow(2f)).pow(0.5f)
	val angle = acos((endX - startX) / length)

	val cos = cos(angle)
	val sin = sin(angle)

	val y0 = -width

	val y1 = width

	val x2 = length
	val y2 = width

	val x3 = length
	val y3 = -width

	builder.vertex(matrix,
		startX - y0 * sin,
		startY + y0 * cos,
		z).color(color).endVertex()

	builder.vertex(matrix,
		startX - y1 * sin,
		startY + y1 * cos,
		z).color(color).endVertex()

	builder.vertex(matrix,
		startX + x2 * cos - y2 * sin,
		startY + x2 * sin + y2 * cos,
		z).color(color).endVertex()

	builder.vertex(matrix,
		startX + x3 * cos - y3 * sin,
		startY + x3 * sin + y3 * cos,
		z).color(color).endVertex()

	tess.end()
}

fun GuiGraphics.drawLine(
	startX: Float,
	startY: Float,
	endX: Float,
	endY: Float,
	width: Float,
	z: Float = 0f,
	color: RGBAColor = RGBAColor.WHITE
) {
	drawLine(pose().last().pose(), startX, startY, endX, endY, width, z, color)
}

data class ScissorRect(val x: Int, val y: Int, val width: Int, val height: Int) {
	fun withinBounds(x: Int, y: Int): Boolean {
		return (x in this.x .. this.x + width) && (y in this.y .. this.y + height)
	}

	fun cross(x: Int, y: Int, width: Int, height: Int): Boolean {
		// corssing any of rect points
		if (withinBounds(x, y) || withinBounds(x + width, y) || withinBounds(x, y + height) || withinBounds(x + width, y + height)) {
			return true
		}

		// crossing at least one line
		if (y in this.y .. this.y + height) {
			if (x < this.x && x + width > this.x + this.width) {
				return true
			}
		} else if (x in this.x .. this.x + width) {
			if (y < this.y && y + height > this.y + this.height) {
				return true
			}
		}

		// final test: check if scissor rect is completely inside provided rect
		return this.x in x .. x + width && this.y in y .. y + height
	}

	fun crossScaled(x: Float, y: Float, width: Float, height: Float): Boolean {
		val scale = minecraft.window.guiScale
		return cross((x * scale).roundToInt(), (y * scale).roundToInt(), (width * scale).roundToInt(), (height * scale).roundToInt())
	}

	fun crossScaled(x: Double, y: Double, width: Double, height: Double): Boolean {
		val scale = minecraft.window.guiScale
		return cross((x * scale).roundToInt(), (y * scale).roundToInt(), (width * scale).roundToInt(), (height * scale).roundToInt())
	}
}

private val scissorStack = ArrayDeque<ScissorRect>()

@Suppress("NAME_SHADOWING")
fun pushScissorRect(x: Int, y: Int, width: Int, height: Int) {
	var x = x
	var y = y
	var width = width
	var height = height

	val peek = scissorStack.lastOrNull()

	if (peek != null) {
		x = x.coerceAtLeast(peek.x)
		y = y.coerceAtLeast(peek.y)
		width = width.coerceAtMost(peek.width)
		height = height.coerceAtMost(peek.height)

		if (peek.x == x && peek.y == y && peek.width == width && peek.height == height) {
			scissorStack.add(peek)
			return
		}
	}

	scissorStack.add(ScissorRect(x, y, width, height))
	y = minecraft.window.height - y - height
	RenderSystem.enableScissor(x, y, width, height)
}

fun popScissorRect() {
	scissorStack.removeLast()

	val peek = scissorStack.lastOrNull()

	if (peek == null) {
		RenderSystem.disableScissor()
		return
	}

	val y = minecraft.window.height - peek.y - peek.height
	RenderSystem.enableScissor(peek.x, y, peek.width, peek.height)
}

val currentScissorRect get() = scissorStack.lastOrNull()

fun TextureAtlasSprite.render(
	stack: PoseStack,
	x: Float = 0f,
	y: Float = 0f,
	width: Float,
	height: Float,
	winding: UVWindingOrder = UVWindingOrder.NORMAL
) {
	RenderSystem.setShaderTexture(0, atlasLocation())

	if (winding.isIdentity) {
		renderTexturedRect(stack.last().pose(), x, y, width, height, uv = UVCoords(u0, v0, u1, v1))
	} else {
		renderTexturedRect(stack.last().pose(), x, y, width, height, uv = winding.translate(u0, v0, u1, v1))
	}
}

fun TextureAtlasSprite.render(
	graphics: GuiGraphics,
	x: Float = 0f,
	y: Float = 0f,
	width: Float,
	height: Float,
	winding: UVWindingOrder = UVWindingOrder.NORMAL
) = render(graphics.pose(), x, y, width, height, winding)

fun determineTooltipPosition(x: Float, y: Float, width: Float, height: Float): Pair<Float, Float> {
	val windowWidth = minecraft.window.guiScaledWidth.toFloat()
	val windowHeight = minecraft.window.guiScaledHeight.toFloat()

	if (x + width <= windowWidth) {
		if (y + height <= windowHeight) {
			return x to y
		}

		return x to (windowHeight - height)
	} else {
		if (y + height <= windowHeight) {
			return (x - width - 4f) to y
		}

		return (x - width - 4f) to (windowHeight - height)
	}
}

/**
 * Draws literally nothing to color buffer, but draws to depth buffer.
 *
 * Allows to fix a fuckton of issues inside GUI.
 *
 * thanks Mojang
 */
fun clearDepth(stack: PoseStack, x: Float, y: Float, width: Float, height: Float, depth: Float = -500f) {
	val oldShader = RenderSystem.getShader()
	RenderSystem.setShader(GameRenderer::getPositionShader)

	RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE, GlStateManager.SourceFactor.ZERO, GlStateManager.DestFactor.ONE)
	RenderSystem.enableBlend()

	RenderSystem.enableDepthTest()
	RenderSystem.depthFunc(GL_ALWAYS)

	val builder = tesselator.builder

	val matrix = stack.last().pose()

	builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION)
	builder.vertex(matrix, x, y + height, depth).endVertex()
	builder.vertex(matrix, x + width, y + height, depth).endVertex()
	builder.vertex(matrix, x + width, y, depth).endVertex()
	builder.vertex(matrix, x, y, depth).endVertex()

	BufferUploader.drawWithShader(builder.end())

	RenderSystem.defaultBlendFunc()
	RenderSystem.depthFunc(GL_LESS)
	RenderSystem.disableDepthTest()

	if (oldShader != null) {
		RenderSystem.setShader { oldShader }
	}
}

@JvmOverloads
fun clearDepth(graphics: GuiGraphics, x: Float, y: Float, width: Float, height: Float, depth: Float = -500f) = ru.dbotthepony.mc.otm.client.render.clearDepth(graphics.pose(), x, y, width, height, depth)

fun drawArc(
	stack: PoseStack,
	x: Float,
	y: Float,
	outerRadius: Float,
	innerRadius: Float = 0f,
	startDegree: Double = 0.0,
	endDegree: Double = PI * 2.0,
	steps: Int = (outerRadius * (endDegree - startDegree) * 4.0).roundToInt().coerceAtLeast(12),
	alignAtCenter: Boolean = true
) = drawArc(stack.last().pose(), x, y, outerRadius, innerRadius, startDegree, endDegree, steps, alignAtCenter)

fun drawArc(
	graphics: GuiGraphics,
	x: Float,
	y: Float,
	outerRadius: Float,
	innerRadius: Float = 0f,
	startDegree: Double = 0.0,
	endDegree: Double = PI * 2.0,
	steps: Int = (outerRadius * (endDegree - startDegree) * 4.0).roundToInt().coerceAtLeast(12),
	alignAtCenter: Boolean = true
) = drawArc(graphics.pose(), x, y, outerRadius, innerRadius, startDegree, endDegree, steps, alignAtCenter)

fun uploadArc(
	matrix: Matrix4f,
	builder: VertexConsumer,
	x: Float,
	y: Float,
	outerRadius: Float,
	innerRadius: Float = 0f,
	startDegree: Double = 0.0,
	endDegree: Double = PI * 2.0,
	steps: Int = (outerRadius * (endDegree - startDegree) * 4.0).roundToInt().coerceAtLeast(12),
	alignAtCenter: Boolean = true,
	z: Float = 0f,
	triangleFan: Boolean
) {
	require(startDegree < endDegree) { "Invalid arc degree range: $startDegree - $endDegree" }
	require(steps >= 0) { "Invalid amount of arc steps: $steps" }

	if (!triangleFan) {
		require(innerRadius >= 0f) { "Invalid inner radius of arc: $innerRadius" }
	}

	@Suppress("name_shadowing")
	var x = x

	@Suppress("name_shadowing")
	var y = y

	@Suppress("name_shadowing")
	val startDegree = startDegree + PI / 2.0

	@Suppress("name_shadowing")
	val endDegree = endDegree + PI / 2.0

	if (!alignAtCenter) {
		x += outerRadius
		y += outerRadius
	}

	if (triangleFan) {
		val singleStep = (endDegree - startDegree) / steps

		builder.vertex(matrix, x, y, z).endVertex()

		for (i in 0 .. steps) {
			val sin = sin(startDegree + i * singleStep).toFloat()
			val cos = cos(startDegree + i * singleStep).toFloat()

			builder.vertex(matrix, x + outerRadius * sin, y + cos * outerRadius, z).endVertex()
		}
	} else {
		val singleStep = (endDegree - startDegree) / (steps + 1)

		for (i in 0 .. steps) {
			val sin = sin(startDegree + i * singleStep).toFloat()
			val cos = cos(startDegree + i * singleStep).toFloat()

			val sin2 = sin(startDegree + (i + 1) * singleStep).toFloat()
			val cos2 = cos(startDegree + (i + 1) * singleStep).toFloat()

			builder.vertex(matrix, x + outerRadius * sin, y + cos * outerRadius, z).endVertex()
			builder.vertex(matrix, x + outerRadius * sin2, y + cos2 * outerRadius, z).endVertex()
			builder.vertex(matrix, x + innerRadius * sin2, y + cos2 * innerRadius, z).endVertex()
			builder.vertex(matrix, x + innerRadius * sin, y + cos * innerRadius, z).endVertex()
		}
	}
}

fun drawArc(
	matrix: Matrix4f,
	x: Float,
	y: Float,
	outerRadius: Float,
	innerRadius: Float = 0f,
	startDegree: Double = 0.0,
	endDegree: Double = PI * 2.0,
	steps: Int = (outerRadius * (endDegree - startDegree) * 4.0).roundToInt().coerceAtLeast(12),
	alignAtCenter: Boolean = true,
	z: Float = 0f
) {
	require(startDegree < endDegree) { "Invalid arc degree range: $startDegree - $endDegree" }
	require(steps >= 0) { "Invalid amount of arc steps: $steps" }
	require(innerRadius >= 0f) { "Invalid inner radius of arc: $innerRadius" }

	RenderSystem.setShader(GameRenderer::getPositionShader)
	RenderSystem.enableBlend()
	RenderSystem.defaultBlendFunc()
	RenderSystem.depthFunc(GL_ALWAYS)

	val builder = tesselator.builder

	if (innerRadius == 0f) {
		if (steps >= 1) {
			builder.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION)
			uploadArc(matrix, builder, x, y, outerRadius, innerRadius, startDegree, endDegree, steps, alignAtCenter, triangleFan = true, z = z)
			BufferUploader.drawWithShader(builder.end())
		}
	} else {
		builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION)
		uploadArc(matrix, builder, x, y, outerRadius, innerRadius, startDegree, endDegree, steps, alignAtCenter, triangleFan = false, z = z)
		BufferUploader.drawWithShader(builder.end())
	}
}

val coloredRectRenderType by lazy {
	val builder = RenderType.CompositeState.builder()

	builder.setShaderState(RenderStateShard.ShaderStateShard(GameRenderer::getPositionColorShader))
	builder.setTransparencyState(RenderStateShard.TransparencyStateShard("normal_blend", {
		RenderSystem.enableBlend()
		RenderSystem.defaultBlendFunc()
	}, {
		RenderSystem.disableBlend()
	}))

	@Suppress("INACCESSIBLE_TYPE")
	RenderType.create("colored_rect",
		DefaultVertexFormat.POSITION_COLOR,
		VertexFormat.Mode.QUADS,
		2048,
		false,
		false,
		builder.createCompositeState(false)) as RenderType
}

val linesIgnoreZRenderType by lazy {
	@Suppress("INACCESSIBLE_TYPE")
	RenderType.create(
		"lines_ignore_z",
		DefaultVertexFormat.POSITION_COLOR_NORMAL,
		VertexFormat.Mode.LINES,
		256,
		false,
		false,
		RenderType.CompositeState.builder()
			.setShaderState(RenderStateShard.RENDERTYPE_LINES_SHADER)
			.setLineState(LineStateShard(OptionalDouble.empty()))
			.setLayeringState(RenderStateShard.VIEW_OFFSET_Z_LAYERING)
			.setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY)
			.setOutputState(RenderStateShard.ITEM_ENTITY_TARGET)
			.setWriteMaskState(RenderStateShard.COLOR_DEPTH_WRITE)
			.setCullState(RenderStateShard.NO_CULL)
			.setDepthTestState(RenderStateShard.NO_DEPTH_TEST)
			.createCompositeState(false)
	) as RenderType
}
