package ru.dbotthepony.kstarbound.client.render

import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.lwjgl.opengl.GL45.*
import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector4f
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.freetype.LoadFlag
import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.client.freetype.struct.FT_Pixel_Mode
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributes
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributeType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.defs.image.IUVCoordinates
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity

private fun breakLines(text: String): List<String> {
	var nextLineBreak = text.indexOf('\n', 0)

	if (nextLineBreak == -1) {
		return listOf(text)
	}

	val list = ArrayList<String>()
	var previousLineBreak = 0

	while (nextLineBreak != -1) {
		if (nextLineBreak == previousLineBreak) {
			list.add("")
		} else {
			list.add(text.substring(previousLineBreak, nextLineBreak))
		}

		if (text.length - 1 <= nextLineBreak) {
			break
		}

		previousLineBreak = nextLineBreak + 1
		nextLineBreak = text.indexOf('\n', previousLineBreak)
	}

	if (previousLineBreak <= text.length - 1) {
		list.add(text.substring(previousLineBreak))
	}

	return list
}

data class TextSize(val width: Float, val height: Float)

enum class TextAlignX {
	LEFT, CENTER, RIGHT
}

enum class TextAlignY {
	TOP, CENTER, BOTTOM
}

val FONT_VERTEX_FORMAT = VertexAttributes.of(VertexAttributeType.POSITION, VertexAttributeType.UV)

class Font(
	val font: String = "hobo.ttf",
	val size: Int = 48
) {
	val client = StarboundClient.current()
	val face = client.freeType.Face(font, 0L)

	init {
		face.setSize(0, size)
	}

	private val charMap = Char2ObjectArrayMap<Glyph>()

	private fun getGlyph(char: Char): Glyph {
		return charMap.computeIfAbsent(char, Object2ObjectFunction {
			return@Object2ObjectFunction Glyph(char)
		})
	}

	private val ascender = face.nativeMemory.ascender.toFloat() / ((size / 48f) * 32f)
	private val descender = face.nativeMemory.descender.toFloat() / ((size / 48f) * 32f)
	val lineHeight = ascender - descender

	val bbox = AABBi(
		Vector2i(roundTowardsNegativeInfinity(face.nativeMemory.bbox.xMin.toInt() / (12.0 * 48.0 / size)), roundTowardsNegativeInfinity(face.nativeMemory.bbox.yMin.toInt() / (12.0 * 48.0 / size))),
		Vector2i(roundTowardsPositiveInfinity(face.nativeMemory.bbox.xMax.toInt() / (12.0 * 48.0 / size)), roundTowardsPositiveInfinity(face.nativeMemory.bbox.yMax.toInt() / (12.0 * 48.0 / size))),
	)

	private val atlasWidth: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_WIDTH).coerceAtMost(4096)
	private val atlasHeight: Int = glGetInternalformati(GL_TEXTURE_2D, GL_RED, GL_MAX_HEIGHT).coerceAtMost(4096)
	private var nextAtlasX = 0
	private var nextAtlasY = 0
	private val atlas = GLTexture2D(atlasWidth, atlasHeight, GL_R8)

	init {
		atlas.textureMinFilter = GL_LINEAR
		atlas.textureMagFilter = GL_LINEAR
		atlas.textureWrapS = GL_CLAMP_TO_EDGE
		atlas.textureWrapT = GL_CLAMP_TO_EDGE
	}

	private val builder = VertexBuilder(FONT_VERTEX_FORMAT, GeometryType.QUADS)
	private val vao = VertexArrayObject()
	private val vbo = BufferObject.VBO()
	private val ebo = BufferObject.EBO()

	init {
		vao.elementBuffer = ebo
		vao.bindAttributes(vbo, FONT_VERTEX_FORMAT)
	}

	fun render(
		text: List<String>,

		x: Float = 0f,
		y: Float = 0f,

		alignX: TextAlignX = TextAlignX.LEFT,
		alignY: TextAlignY = TextAlignY.TOP,

		color: RGBAColor = RGBAColor.WHITE,

		scale: Float = 1f,
	): TextSize {
		if (text.isEmpty())
			return TextSize(0f, 0f)
		else if (text.size == 1 && text[0] == "")
			return TextSize(0f, lineHeight * scale)

		val totalSize = size(text)
		val totalX = totalSize.width
		val totalY = totalSize.height

		val model = Matrix3f.identity()

		when (alignY) {
			TextAlignY.TOP -> model.translate(x = x, y = lineHeight * scale + y)
			TextAlignY.CENTER -> model.translate(x = x, y = lineHeight * scale - totalY * scale / 2f + y)
			TextAlignY.BOTTOM -> model.translate(x = x, y = lineHeight * scale - totalY * scale + y)
		}

		if (scale != 1f)
			model.scale(x = scale, y = scale)

		builder.begin()

		val space = getGlyph(' ')

		var advancedX = 0f

		for (line in text) {
			if (line == "") {
				model.translate(y = lineHeight)
				continue
			}

			var movedX = 0f

			when (alignX) {
				TextAlignX.LEFT -> {}

				TextAlignX.CENTER -> {
					movedX = totalX / 2f - lineWidth(line, space) / 2f
					model.translate(x = movedX)
				}

				TextAlignX.RIGHT -> {
					movedX = -lineWidth(line, space)
					model.translate(x = movedX)
				}
			}

			var lineGlyphs = 0
			var lineWidth = 0f

			for (chr in line) {
				if (chr == '\t') {
					if (lineGlyphs % 4 == 0) {
						advancedX += space.advanceX * 4
						model.translate(x = space.advanceX * 4)
					} else {
						advancedX += space.advanceX * (lineGlyphs % 4)
						model.translate(x = space.advanceX * (lineGlyphs % 4))
					}

					lineGlyphs += lineGlyphs % 4
					continue
				}

				val glyph = getGlyph(chr)
				glyph.render(model)
				lineWidth += glyph.advanceX
				lineGlyphs++
			}

			advancedX = advancedX.coerceAtLeast(lineWidth)
			model.translate(x = -lineWidth - movedX, y = lineHeight)
		}

		builder.end()

		if (builder.elementCount != 0) {
			client.programs.font.use()
			client.programs.font.colorMultiplier = color
			client.programs.font.modelMatrix = client.stack.last()

			builder.upload(vbo, ebo, GL_STREAM_DRAW)

			client.textures2D[0] = atlas

			client.vao = vao
			glDrawElements(GL_TRIANGLES, builder.indexCount, builder.indexType, 0L)
			checkForGLError()
			client.vao = null
		}

		return TextSize(totalX * scale, totalY * scale)
	}

	fun render(
		text: String,

		x: Float = 0f,
		y: Float = 0f,

		alignX: TextAlignX = TextAlignX.LEFT,
		alignY: TextAlignY = TextAlignY.TOP,

		color: RGBAColor = RGBAColor.WHITE,

		scale: Float = 1f,
	): TextSize {
		return render(
			breakLines(text),
			x = x,
			y = y,
			alignX = alignX,
			alignY = alignY,
			scale = scale,
			color = color,
		)
	}

	private fun lineWidth(line: String, space: Glyph): Float {
		var lineWidth = 0f
		var lineGlyphs = 0

		for (chr in line) {
			if (chr == '\t') {
				if (lineGlyphs % 4 == 0) {
					lineWidth += space.advanceX * 4
				} else {
					lineWidth += space.advanceX * (lineGlyphs % 4)
				}

				lineGlyphs += lineGlyphs % 4
				continue
			}

			lineWidth += getGlyph(chr).advanceX
			lineGlyphs++
		}

		return lineWidth
	}

	fun size(lines: List<String>): TextSize {
		var advancedX = 0f
		var advancedY = 0f

		val space = getGlyph(' ')

		for (line in lines) {
			advancedX = advancedX.coerceAtLeast(lineWidth(line, space))
			advancedY += lineHeight
		}

		return TextSize(advancedX, advancedY)
	}

	fun size(text: String): TextSize {
		return size(breakLines(text))
	}

	private inner class Glyph(val char: Char) {
		val isEmpty: Boolean

		val width: Float
		val height: Float

		val bearingX: Float
		val bearingY: Float
		val advanceX: Float
		val advanceY: Float

		val uv: Vector4f

		init {
			if (nextAtlasY + bbox.height > atlasHeight) {
				throw IllegalStateException("Font atlas is full")
			}

			face.loadChar(char, LoadFlag.RENDER)
			val glyph = face.nativeMemory.glyph ?: throw IllegalStateException("Unable to load glyph data for $char (code ${char.code})")
			val bitmap = glyph.bitmap

			check(bitmap.pixelMode() == FT_Pixel_Mode.FT_PIXEL_MODE_GRAY) { "Unexpected pixel mode of loaded glyph: ${face.nativeMemory.glyph?.bitmap?.pixelMode()} for $char (code ${char.code})" }

			width = bitmap.width.toFloat()
			height = bitmap.rows.toFloat()

			bearingX = glyph.bitmap_left.toFloat()
			bearingY = glyph.bitmap_top.toFloat()
			advanceX = glyph.advance.x.toFloat() / 64f
			advanceY = glyph.advance.y.toFloat() / 64f

			if (bitmap.buffer != null) {
				isEmpty = false

				// интересно, а что будет когда буфер будет "вычищен" из памяти? Чо, будет double free?
				val buff = bitmap.byteBuffer!!

				glPixelStorei(GL_UNPACK_ALIGNMENT, 1)

				try {
					uv = Vector4f(nextAtlasX / atlasWidth.toFloat(), nextAtlasY / atlasHeight.toFloat(), (nextAtlasX + bitmap.width) / atlasWidth.toFloat(), (nextAtlasY + bitmap.rows) / atlasHeight.toFloat())

					atlas.upload(0, nextAtlasX, nextAtlasY, bitmap.width, bitmap.rows, GL_RED, GL_UNSIGNED_BYTE, buff)

					nextAtlasX += bbox.width

					if (nextAtlasX + bbox.width > atlasWidth) {
						nextAtlasX = 0
						nextAtlasY += bbox.height
					}
				} finally {
					glPixelStorei(GL_UNPACK_ALIGNMENT, 4)
				}
			} else {
				isEmpty = true
				uv = Vector4f.ZERO
			}
		}

		fun render(model: Matrix3f) {
			if (isEmpty) {
				model.translate(advanceX)
				return
			}

			model.translate(bearingX, -bearingY)

			builder.vertex(model, 0f, 0f).uv(uv.x, uv.y)
			builder.vertex(model, width, 0f).uv(uv.z, uv.y)
			builder.vertex(model, width, height).uv(uv.z, uv.w)
			builder.vertex(model, 0f, height).uv(uv.x, uv.w)

			model.translate(advanceX - bearingX, bearingY)
		}
	}
}
