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

import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexSorting
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.network.chat.Component
import net.minecraft.util.FormattedCharSequence
import org.joml.Matrix4f
import ru.dbotthepony.mc.otm.core.FloatSupplier
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.core.math.component1
import ru.dbotthepony.mc.otm.core.math.component2
import ru.dbotthepony.mc.otm.core.math.component3

private val buffer = DynamicBufferSource(vertexSorting = VertexSorting.ORTHOGRAPHIC_Z)
private fun buffer() = buffer

private fun Font.drawInBatch(
	text: Any, x: Float, y: Float, color: Int, drawShadow: Boolean,
	matrix4f: Matrix4f, buffer: MultiBufferSource, displayMode: Font.DisplayMode,
	effectColor: Int, packedLightCoords: Int, gravity: RenderGravity,
	rounding: GravityRounding, width: FloatSupplier
): Int {
	if (text is String)
		return drawInBatch(
			text,
			gravity.x(x, width, rounding = rounding),
			gravity.y(y, lineHeight.toFloat(), rounding = rounding),
			color,
			drawShadow,
			matrix4f,
			buffer,
			displayMode,
			effectColor,
			packedLightCoords)
	else if (text is Component)
		return drawInBatch(
			text,
			gravity.x(x, width, rounding = rounding),
			gravity.y(y, lineHeight.toFloat(), rounding = rounding),
			color,
			drawShadow,
			matrix4f,
			buffer,
			displayMode,
			effectColor,
			packedLightCoords)
	else if (text is FormattedCharSequence)
		return drawInBatch(
			text,
			gravity.x(x, width, rounding = rounding),
			gravity.y(y, lineHeight.toFloat(), rounding = rounding),
			color,
			drawShadow,
			matrix4f,
			buffer,
			displayMode,
			effectColor,
			packedLightCoords)
	else
		throw IllegalArgumentException(text::class.qualifiedName)
}

private val outlinePositions = listOf(
	1 to 0,
	-1 to 0,
	0 to 1,
	0 to -1,
)

private class CachedTextWidth0(val font: Font, val text: Component) : FloatSupplier {
	private var computed = false
	private var width = 0f

	override fun getAsFloat(): Float {
		if (!computed) {
			width = font.width(text).toFloat()
			computed = true
		}

		return width
	}
}

private class CachedTextWidth1(val font: Font, val text: String) : FloatSupplier {
	private var computed = false
	private var width = 0f

	override fun getAsFloat(): Float {
		if (!computed) {
			width = font.width(text).toFloat()
			computed = true
		}

		return width
	}
}

private class CachedTextWidth2(val font: Font, val text: FormattedCharSequence) : FloatSupplier {
	private var computed = false
	private var width = 0f

	override fun getAsFloat(): Float {
		if (!computed) {
			width = font.width(text).toFloat()
			computed = true
		}

		return width
	}
}

private fun Font.drawInternal(
	poseStack: PoseStack,
	text: Any,
	x: Float,
	y: Float,
	buffer: MultiBufferSource,
	gravity: RenderGravity,
	scale: Float,
	color: RGBAColor,
	drawShadow: Boolean,
	displayMode: Font.DisplayMode,
	packedLightCoords: Int,
	effectColor: Int,
	shadowColor: RGBAColor,
	shadowX: Float,
	shadowY: Float,
	shadowZ: Float,
	customShadow: Boolean,
	rounding: GravityRounding,
	drawOutline: Boolean,
	outlineColor: RGBAColor,
	outlineZ: Float,
): Float {
	val inv: Float

	if (scale != 1f) {
		poseStack.pushPose()

		val (_x, _y, _z) = poseStack.translation()

		poseStack.translate(-_x, -_y, -_z)
		poseStack.scale(scale, scale, scale)
		inv = 1f / scale
		poseStack.translate(_x * inv, _y * inv, _z * inv)
	} else {
		inv = 1f
	}

	val cache = if (text is Component)
		CachedTextWidth0(this, text)
	else if (text is String)
		CachedTextWidth1(this, text)
	else if (text is FormattedCharSequence)
		CachedTextWidth2(this, text)
	else
		throw IllegalArgumentException(text::class.qualifiedName)

	if (customShadow) {
		if (shadowZ != 0f) {
			poseStack.translate(0f, 0f, shadowZ)
		}

		drawInBatch(
			text,
			(x + shadowX) * inv,
			(y + shadowY) * inv,
			shadowColor.toRGB(),
			false,
			poseStack.last().pose(),
			buffer,
			displayMode,
			effectColor,
			packedLightCoords,
			gravity, rounding, cache)

		if (shadowZ != 0f) {
			poseStack.translate(0f, 0f, -shadowZ)
		}
	}

	if (drawOutline) {
		if (outlineZ != 0f) {
			poseStack.translate(0f, 0f, outlineZ)
		}

		for ((_x, _y) in outlinePositions) {
			drawInBatch(
				text,
				x * inv + _x,
				y * inv + _y,
				outlineColor.toRGB(),
				drawShadow,
				poseStack.last().pose(),
				buffer,
				displayMode,
				effectColor,
				packedLightCoords,
				gravity, rounding, cache)
		}

		if (outlineZ != 0f) {
			poseStack.translate(0f, 0f, -outlineZ)
		}
	}

	val width = drawInBatch(
		text,
		x * inv,
		y * inv,
		color.toRGB(),
		drawShadow,
		poseStack.last().pose(),
		buffer,
		displayMode,
		effectColor,
		packedLightCoords,
		gravity, rounding, cache)

	if (scale != 1f) {
		poseStack.popPose()
	}

	if (buffer === buffer()) {
		buffer.endBatch()
	}

	return width * scale
}

fun Font.draw(
	poseStack: PoseStack,
	text: Component,
	x: Float = 0f,
	y: Float = 0f,
	buffer: MultiBufferSource = buffer(),
	gravity: RenderGravity = RenderGravity.TOP_LEFT,
	scale: Float = 1f,
	color: RGBAColor = RGBAColor.WHITE,
	drawShadow: Boolean = false,
	displayMode: Font.DisplayMode = Font.DisplayMode.NORMAL,
	packedLightCoords: Int = 15728880,
	effectColor: Int = 0,
	shadowColor: RGBAColor = RGBAColor.BLACK,
	shadowX: Float = 1f,
	shadowY: Float = 1f,
	shadowZ: Float = 0.1f,
	customShadow: Boolean = false,
	rounding: GravityRounding = if (scale == 1f) GravityRounding.DEFAULT else GravityRounding.NO,
	drawOutline: Boolean = false,
	outlineColor: RGBAColor = RGBAColor.BLACK,
	outlineZ: Float = 0.1f,
): Float {
	return drawInternal(
		poseStack = poseStack,
		text = text,
		x = x,
		y = y,
		buffer = buffer,
		gravity = gravity,
		scale = scale,
		color = color,
		drawShadow = drawShadow,
		displayMode = displayMode,
		packedLightCoords = packedLightCoords,
		effectColor = effectColor,
		shadowColor = shadowColor,
		shadowX = shadowX,
		shadowY = shadowY,
		shadowZ = shadowZ,
		customShadow = customShadow,
		rounding = rounding,
		drawOutline = drawOutline,
		outlineColor = outlineColor,
		outlineZ = outlineZ,
	)
}

fun Font.draw(
	poseStack: PoseStack,
	text: String,
	x: Float = 0f,
	y: Float = 0f,
	buffer: MultiBufferSource = buffer(),
	gravity: RenderGravity = RenderGravity.TOP_LEFT,
	scale: Float = 1f,
	color: RGBAColor = RGBAColor.WHITE,
	drawShadow: Boolean = false,
	displayMode: Font.DisplayMode = Font.DisplayMode.NORMAL,
	packedLightCoords: Int = 15728880,
	effectColor: Int = 0,
	shadowColor: RGBAColor = RGBAColor.BLACK,
	shadowX: Float = 1f,
	shadowY: Float = 1f,
	shadowZ: Float = 0.1f,
	customShadow: Boolean = false,
	rounding: GravityRounding = if (scale == 1f) GravityRounding.DEFAULT else GravityRounding.NO,
	drawOutline: Boolean = false,
	outlineColor: RGBAColor = RGBAColor.BLACK,
	outlineZ: Float = 0.1f,
): Float {
	return drawInternal(
		poseStack = poseStack,
		text = text,
		x = x,
		y = y,
		buffer = buffer,
		gravity = gravity,
		scale = scale,
		color = color,
		drawShadow = drawShadow,
		displayMode = displayMode,
		packedLightCoords = packedLightCoords,
		effectColor = effectColor,
		shadowColor = shadowColor,
		shadowX = shadowX,
		shadowY = shadowY,
		shadowZ = shadowZ,
		customShadow = customShadow,
		rounding = rounding,
		drawOutline = drawOutline,
		outlineColor = outlineColor,
		outlineZ = outlineZ,
	)
}

fun GuiGraphics.draw(
	font: Font,
	text: Component,
	x: Float = 0f,
	y: Float = 0f,
	gravity: RenderGravity = RenderGravity.TOP_LEFT,
	scale: Float = 1f,
	color: RGBAColor = RGBAColor.WHITE,
	drawShadow: Boolean = false,
	displayMode: Font.DisplayMode = Font.DisplayMode.NORMAL,
	packedLightCoords: Int = 15728880,
	effectColor: Int = 0,
	shadowColor: RGBAColor = RGBAColor.BLACK,
	shadowX: Float = 1f,
	shadowY: Float = 1f,
	shadowZ: Float = -0.1f,
	customShadow: Boolean = false,
	rounding: GravityRounding = if (scale == 1f) GravityRounding.DEFAULT else GravityRounding.NO,
	drawOutline: Boolean = false,
	outlineColor: RGBAColor = RGBAColor.BLACK,
	outlineZ: Float = -0.1f,
): Float {
	val width = font.draw(
		poseStack = pose(),
		text = text,
		x = x,
		y = y,
		buffer = bufferSource(),
		gravity = gravity,
		scale = scale,
		color = color,
		drawShadow = drawShadow,
		displayMode = displayMode,
		packedLightCoords = packedLightCoords,
		effectColor = effectColor,
		shadowColor = shadowColor,
		shadowX = shadowX,
		shadowY = shadowY,
		shadowZ = shadowZ,
		customShadow = customShadow,
		rounding = rounding,
		drawOutline = drawOutline,
		outlineColor = outlineColor,
		outlineZ = outlineZ,
	)

	flush()
	return width
}

fun GuiGraphics.draw(
	font: Font,
	text: String,
	x: Float = 0f,
	y: Float = 0f,
	gravity: RenderGravity = RenderGravity.TOP_LEFT,
	scale: Float = 1f,
	color: RGBAColor = RGBAColor.WHITE,
	drawShadow: Boolean = false,
	displayMode: Font.DisplayMode = Font.DisplayMode.NORMAL,
	packedLightCoords: Int = 15728880,
	effectColor: Int = 0,
	shadowColor: RGBAColor = RGBAColor.BLACK,
	shadowX: Float = 1f,
	shadowY: Float = 1f,
	shadowZ: Float = -0.1f,
	customShadow: Boolean = false,
	rounding: GravityRounding = if (scale == 1f) GravityRounding.DEFAULT else GravityRounding.NO,
	drawOutline: Boolean = false,
	outlineColor: RGBAColor = RGBAColor.BLACK,
	outlineZ: Float = -0.1f,
): Float {
	val width = font.draw(
		poseStack = pose(),
		text = text,
		x = x,
		y = y,
		buffer = bufferSource(),
		gravity = gravity,
		scale = scale,
		color = color,
		drawShadow = drawShadow,
		displayMode = displayMode,
		packedLightCoords = packedLightCoords,
		effectColor = effectColor,
		shadowColor = shadowColor,
		shadowX = shadowX,
		shadowY = shadowY,
		shadowZ = shadowZ,
		customShadow = customShadow,
		rounding = rounding,
		drawOutline = drawOutline,
		outlineColor = outlineColor,
		outlineZ = outlineZ,
	)

	flush()
	return width
}
