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

import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.ByteBufferBuilder
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexConsumer
import com.mojang.blaze3d.vertex.VertexFormat
import com.mojang.blaze3d.vertex.VertexSorting
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.client.renderer.MultiBufferSource
import net.minecraft.client.renderer.RenderStateShard
import net.minecraft.client.renderer.RenderType
import net.minecraft.network.chat.Component
import net.minecraft.world.inventory.tooltip.TooltipComponent
import net.neoforged.neoforge.client.event.RegisterClientTooltipComponentFactoriesEvent
import org.lwjgl.opengl.GL11.GL_LESS
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.mc.otm.client.minecraft
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sign
import kotlin.math.sin

private const val LINE_WIDTH = 1f
private const val HIGHLIGHT_WIDTH = 3f

private val TEXT_BACKGROUND = RGBAColor(0f, 0f, 0f, 0.5f)
private val LINE_COLOR = RGBAColor(1f, 0f, 1f, 0.25f)
private val LINE_TEXT_COLOR = RGBAColor(1f, 0f, 1f, 0.5f)

data class ChartMouseLabels(
	val mouseX: Float,
	val mouseY: Float,
	val labels: (Int) -> List<Component>,
	val font: Font,
	val color: RGBAColor = RGBAColor.WHITE,
	val pillarColor: RGBAColor = RGBAColor.WHITE,
	val textColor: RGBAColor = RGBAColor.WHITE,
	val textBackgroundColor: RGBAColor = TEXT_BACKGROUND
)

data class ChartLevelLabels(
	val labels: Map<Float, Component>,
	val font: Font? = null,
	val lineColor: RGBAColor = RGBAColor.DARK_GREEN,
	val textColor: RGBAColor = RGBAColor.DARK_GREEN,
	val textGravity: RenderGravity = RenderGravity.TOP_LEFT,
	val textOutline: RGBAColor = RGBAColor.BLACK
)

private val BYTE_BUFFER_BUILDER = ByteBufferBuilder(786_432)

class ChartTooltipElement(
	private val normalized: List<Pair<FloatArray, RGBAColor>>,
	private val width: Float,
	private val height: Float,
	private val levelLabels: ChartLevelLabels? = null,
) : ClientTooltipComponent, TooltipComponent {
	override fun getHeight(): Int {
		return this.height.toInt()
	}

	override fun getWidth(font: Font): Int {
		return this.width.toInt()
	}

	override fun renderImage(font: Font, x: Int, y: Int, graphics: GuiGraphics) {
		graphics.pose().pushPose()
		graphics.pose().translate(0f, 0f, 0.1f)
		renderChart(graphics.pose(), normalized, this.width, this.height - 10f, levelLabels = this.levelLabels, x = x.toFloat(), y = y.toFloat() + 10f, buffer = graphics.bufferSource())
		graphics.pose().popPose()
	}

	companion object {
		fun register(event: RegisterClientTooltipComponentFactoriesEvent) {
			event.register(ChartTooltipElement::class.java) { it }
		}
	}
}

val CHART_RENDER_TYPE = run {
	val builder = RenderType.CompositeState.builder()

	builder.setShaderState(RenderStateShard.ShaderStateShard(GameRenderer::getPositionColorShader))
	builder.setDepthTestState(RenderStateShard.DepthTestStateShard("always", GL_LESS))

	builder.setTransparencyState(RenderStateShard.TransparencyStateShard("normal_blend", {
		RenderSystem.enableBlend()
		RenderSystem.defaultBlendFunc()
	}, { RenderSystem.disableBlend() }))

	@Suppress("INACCESSIBLE_TYPE")
	RenderType.create(
		"otm_chart",
		DefaultVertexFormat.POSITION_COLOR,
		VertexFormat.Mode.QUADS,
		16_000,
		false,
		true,
		builder.createCompositeState(false)
	) as RenderType
}

val CHART_RENDER_TYPE2 = run {
	val builder = RenderType.CompositeState.builder()

	builder.setShaderState(RenderStateShard.ShaderStateShard(GameRenderer::getPositionColorShader))
	builder.setDepthTestState(RenderStateShard.DepthTestStateShard("always", GL_LESS))

	builder.setTransparencyState(RenderStateShard.TransparencyStateShard("normal_blend", {
		RenderSystem.enableBlend()
		RenderSystem.defaultBlendFunc()
	}, { RenderSystem.disableBlend() }))

	@Suppress("INACCESSIBLE_TYPE")
	RenderType.create(
		"otm_chart2",
		DefaultVertexFormat.POSITION_COLOR,
		VertexFormat.Mode.TRIANGLES,
		16_000,
		false,
		true,
		builder.createCompositeState(false)
	) as RenderType
}

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

private val sinValues = FloatArray(16) { sin(it / 16f * PI.toFloat() * 2f) }
private val cosValues = FloatArray(16) { cos(it / 16f * PI.toFloat() * 2f) }

fun renderChart(
	poseStack: PoseStack,
	charts: List<Pair<FloatArray, RGBAColor>>,
	width: Float,
	height: Float,
	labels: ChartMouseLabels? = null,
	levelLabels: ChartLevelLabels? = null,
	buffer: MultiBufferSource = buffer(),
	x: Float = 0f,
	y: Float = 0f,
	textScale: Float = 1f,
	lineWidth: Float = LINE_WIDTH
) {
	require(charts.all { it.first.size == charts[0].first.size }) { "Provided chart data is not of same width" }
	val builder = buffer.getBuffer(CHART_RENDER_TYPE)
	val step = width / charts[0].first.size
	// so we can work with crumbling buffer sources
	val bendFuncs = ArrayList<VertexConsumer.() -> Unit>()

	val checkLabels = labels != null && labels.mouseY in 0f .. height && labels.mouseX in 0f .. width

	poseStack.pushPose()

	var drawLabel = -1
	var drawPointX = 0f
	var drawPointY = 0f

	val halfWidth = lineWidth / 2f

	if (levelLabels != null) {
		for ((level, label) in levelLabels.labels) {
			val y0 = y + (1f - level) * height

			builder.vertex(poseStack, x + width, halfWidth + y0, 0f).color(levelLabels.lineColor)
			builder.vertex(poseStack, x + width, -halfWidth + y0, 0f).color(levelLabels.lineColor)
			builder.vertex(poseStack, x, -halfWidth + y0, 0f).color(levelLabels.lineColor)
			builder.vertex(poseStack, x, halfWidth + y0, 0f).color(levelLabels.lineColor)
		}
	}

	poseStack.translate(0f, 0f, 0.2f)

	for ((normalized, color) in charts) {
		var previousAngle = 0f

		for (i in 0 until normalized.size - 1) {
			val y0 = y + (1f - normalized[i]) * height
			val y1 = y + (1f - normalized[i + 1]) * height

			val x0 = x + width - (i + 0.5f) * step
			val x1 = x + width - (i + 1.5f) * step

			val xDiff = x1 - x0
			val yDiff = y1 - y0

			val currentAngle: Float

			if (yDiff.sign == 0f) {
				builder.vertex(poseStack, x0, y0 + halfWidth, 0f).color(color)
				builder.vertex(poseStack, x0, y0 - halfWidth, 0f).color(color)
				builder.vertex(poseStack, x1, y1 - halfWidth, 0f).color(color)
				builder.vertex(poseStack, x1, y1 + halfWidth, 0f).color(color)

				currentAngle = PI.toFloat() / 2f
			} else {
				val length = (xDiff.pow(2f) + yDiff.pow(2f)).pow(0.5f)
				val angle = acos(xDiff / length) * yDiff.sign - PI.toFloat() / 2f // rotate 90 deg
				currentAngle = angle

				val xDisp = cos(angle) * halfWidth
				val yDisp = sin(angle) * halfWidth

				builder.vertex(poseStack, x0 + xDisp, y0 + yDisp, 0f).color(color)
				builder.vertex(poseStack, x0 - xDisp, y0 - yDisp, 0f).color(color)
				builder.vertex(poseStack, x1 - xDisp, y1 - yDisp, 0f).color(color)
				builder.vertex(poseStack, x1 + xDisp, y1 + yDisp, 0f).color(color)
			}

			if (currentAngle != previousAngle) {
				for (i2 in sinValues.indices) {
					val i3 = (i2 + 1) % cosValues.size
					val cos0 = cosValues[i2] * halfWidth
					val cos1 = cosValues[i3] * halfWidth
					val sin0 = sinValues[i2] * halfWidth
					val sin1 = sinValues[i3] * halfWidth

					bendFuncs.add {
						vertex(poseStack, x0 + cos0, y0 + sin0, 0f).color(color)
						vertex(poseStack, x0, y0, 0f).color(color)
						vertex(poseStack, x0 + cos1, y0 + sin1, 0f).color(color)
					}
				}
			}

			// TODO: actually make this work, because this should be more efficient than drawing full circle
			/*
			if (previousAngle < currentAngle) {
				// peak bend
				var bendAngle = previousAngle - 0.05f

				while (bendAngle < currentAngle + 0.05f) {
					val sin0 = sin(bendAngle)
					val cos0 = cos(bendAngle)
					val sin1 = sin(bendAngle + 0.05f)
					val cos1 = cos(bendAngle + 0.05f)

					builder2.vertex(poseStack, x0 + cos0, y0 + sin0, 0f).color(color)
					builder2.vertex(poseStack, x0, y0, 0f).color(color)
					builder2.vertex(poseStack, x0 + cos1, y0 + sin1, 0f).color(color)

					bendAngle += 0.05f
				}
			} else if (previousAngle > currentAngle) {
				// chasm bend
				var bendAngle = currentAngle - 0.05f

				while (bendAngle < previousAngle + 0.05f) {
					val sin0 = sin(bendAngle)
					val cos0 = cos(bendAngle)
					val sin1 = sin(bendAngle + 0.05f)
					val cos1 = cos(bendAngle + 0.05f)

					builder2.vertex(poseStack, x0 + cos0, y0 + sin0, 0f).color(color)
					builder2.vertex(poseStack, x0, y0, 0f).color(color)
					builder2.vertex(poseStack, x0 + cos1, y0 + sin1, 0f).color(color)

					bendAngle += 0.05f
				}
			}
			*/

			previousAngle = currentAngle

			if (checkLabels && drawLabel == -1 && labels!!.mouseX in x1 .. x0) {
				drawLabel = i
				drawPointX = x0 // linearInterpolation(0.5f, x0, x1)
				drawPointY = y0 // linearInterpolation(0.5f, y0, y1)
			}
		}
	}

	if (bendFuncs.isNotEmpty()) {
		val builder2 = buffer.getBuffer(CHART_RENDER_TYPE2)
		bendFuncs.forEach { it.invoke(builder2) }
	}

	if (levelLabels != null && levelLabels.labels.isNotEmpty()) {
		poseStack.translate(0f, 0f, 0.2f)
		val font = levelLabels.font ?: minecraft.font

		for ((level, label) in levelLabels.labels) {
			var conflictingTop = false
			var conflictingBottom = false

			levelLabels.labels.keys.forEach {
				if (it != level) {
					conflictingTop = conflictingTop || it > level && (it - level).absoluteValue * height < font.lineHeight.toFloat()
					conflictingBottom = conflictingBottom || it < level && (it - level).absoluteValue * height < font.lineHeight.toFloat()
				}
			}

			val y0 = y + (1f - level) * height

			val tX = x + (levelLabels.textGravity.repositionX(width - 1f, font.width(label).toFloat() * textScale - 1f))
			var tY = y0 + levelLabels.textGravity.repositionY(font.lineHeight * 2f * textScale + lineWidth + 2f, font.lineHeight.toFloat() * textScale)

			if (conflictingTop && conflictingBottom) {
				// do nothing, draw at center
				tY -= 2f + lineWidth
			} else if (conflictingTop) {
				// draw at bottom
				tY += lineWidth + 2f
			} else {
				// draw at top
				tY -= font.lineHeight * textScale + lineWidth
			}

			font.draw(
				poseStack,
				label,
				buffer = buffer,
				x = tX,
				y = tY,
				color = levelLabels.textColor,
				drawOutline = true,
				outlineZ = -0.1f,
				outlineColor = levelLabels.textOutline,
				scale = textScale
			)
		}
	}

	if (drawLabel != -1) {
		poseStack.translate(0f, 0f, 0.2f)

		uploadRect(poseStack, drawPointX - lineWidth / 2f, y, lineWidth, height, buffer = buffer, color = labels!!.pillarColor)
		uploadRect(poseStack, drawPointX - HIGHLIGHT_WIDTH / 2f, drawPointY - HIGHLIGHT_WIDTH / 2f, HIGHLIGHT_WIDTH, HIGHLIGHT_WIDTH, buffer = buffer, color = labels.color)

		val label = labels.labels(drawLabel)

		if (label.isNotEmpty()) {
			val fWidth = label.maxOf { (labels.font.width(it).toFloat() + 2f) * textScale }
			val fHeight = (labels.font.lineHeight.toFloat() * label.size + 2f) * textScale

			val anchorX = labels.mouseX // drawPointX
			var anchorY = labels.mouseY - fHeight - 2f

			uploadRect(poseStack, anchorX - fWidth / 2f, anchorY - 2f, fWidth, fHeight + 4f, buffer = buffer, color = labels.textBackgroundColor)

			label.forEach {
				labels.font.draw(poseStack, it, anchorX, anchorY, buffer = buffer, gravity = RenderGravity.TOP_CENTER, color = labels.textColor, scale = textScale)
				anchorY += labels.font.lineHeight
			}
		}
	}

	poseStack.popPose()

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