package ru.dbotthepony.kstarbound.client.gl.vertex

import org.lwjgl.opengl.GL45
import org.lwjgl.opengl.GL45.GL_UNSIGNED_INT
import org.lwjgl.opengl.GL45.GL_UNSIGNED_SHORT
import org.lwjgl.opengl.GL45.GL_UNSIGNED_BYTE
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f
import ru.dbotthepony.kommons.util.IStruct4f
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kstarbound.client.gl.BufferObject
import java.nio.ByteBuffer
import java.nio.ByteOrder

private fun interface IndexWriter {
	fun write(buffer: ByteBuffer, value: Int)
}

private fun writer(type: Int): IndexWriter {
	return when (type) {
		GL_UNSIGNED_SHORT -> IndexWriter { it, value -> it.putShort(value.toShort()) }
		GL_UNSIGNED_BYTE -> IndexWriter { it, value -> it.put(value.toByte()) }
		else -> IndexWriter { it, value -> it.putInt(value) }
	}
}

private fun chooseIndexType(capacity: Int): Int {
	return when (capacity) {
		// api performance issue 102: glDrawElements uses element index type 'GL_UNSIGNED_BYTE' that is not optimal for the current hardware configuration; consider using 'GL_UNSIGNED_SHORT' instead
		// in 0 .. 255 -> GL_UNSIGNED_BYTE
		in 0 .. 65535 -> GL_UNSIGNED_SHORT
		else -> GL_UNSIGNED_INT
	}
}

private fun indexSize(type: Int): Int {
	return when (type) {
		GL_UNSIGNED_BYTE -> 1
		GL_UNSIGNED_SHORT -> 2
		GL_UNSIGNED_INT -> 4
		else -> throw IllegalStateException()
	}
}

/**
 * Создаёт буферы вне кучи для загрузки геометрии в них, с набором аттрибутов [attributes], типом [mode] и указанным начальным размером.
 *
 * По мере необходимости, буферы могут быть увеличены в размерах.
 *
 * Загрузка в память видеокарты происходит напрямую из буферов, через метод [upload]
 */
class VertexBuilder(
	val attributes: VertexAttributes,
	val defaultMode: GeometryType? = null,
	initialCapacity: Int = 32
) {
	constructor(attributes: VertexAttributes, initialCapacity: Int) : this(attributes, null, initialCapacity)

	init {
		require(initialCapacity > 0) { "Invalid capacity: $initialCapacity" }
	}

	private var mode: GeometryType? = defaultMode

	private var vertexCapacity = if (defaultMode == null) initialCapacity else defaultMode.elements * initialCapacity
	private var indexCapacity = if (defaultMode == null) initialCapacity else defaultMode.indices.size * initialCapacity

	/**
	 * [GL_UNSIGNED_BYTE], [GL_UNSIGNED_SHORT] или [GL_UNSIGNED_INT]
	 */
	var indexType: Int = chooseIndexType(indexCapacity)
		private set
	var indexSize: Int = indexSize(indexType)
		private set

	private var vertexMemory: ByteBuffer = ByteBuffer.allocateDirect(vertexCapacity * attributes.vertexStride).also { it.order(ByteOrder.LITTLE_ENDIAN) }
	private var indexMemory: ByteBuffer = ByteBuffer.allocateDirect(indexCapacity * indexSize).also { it.order(ByteOrder.LITTLE_ENDIAN) }
	private var indexWriter = writer(indexType)

	private var inVertex = false
	private var vertexAttributes = 0L
	private var elementIndexOffset = 0
	private var elementVertices = 0

	var vertexCount = 0 // distinct vertices
		private set
	var indexCount = 0 // distinct indices
		private set
	var elementCount = 0 // GeometryType specific (quads, triangles, lines), less than or equal (points) to vertexCount
		private set

	fun isEmpty(): Boolean {
		return !inVertex && vertexCount == 0
	}

	fun isNotEmpty(): Boolean {
		return inVertex || vertexCount != 0
	}

	private fun ensureCapacity() {
		val mode = mode ?: return

		if (indexCapacity < indexCount + mode.indices.size) {
			indexCapacity = (indexCapacity * 2).coerceAtLeast(indexCount + mode.indices.size)

			// probable double recreation if we also resize vertex array AND it ends up
			val indexPos = indexMemory.position()
			val indexMemory = ByteBuffer.allocateDirect(indexCapacity * indexSize).also { it.order(ByteOrder.LITTLE_ENDIAN) }
			this.indexMemory.position(0)
			indexMemory.put(this.indexMemory)
			indexMemory.position(indexPos)

			this.indexMemory = indexMemory
		}

		if (vertexCapacity < vertexCount + mode.elements) {
			vertexCapacity = (vertexCapacity * 2).coerceAtLeast(vertexCount + mode.elements)

			val indexType = chooseIndexType(vertexCapacity)
			val indexSize = indexSize(indexType)

			val vertexPos = vertexMemory.position()
			val vertexMemory = ByteBuffer.allocateDirect(vertexCapacity * attributes.vertexStride).also { it.order(ByteOrder.LITTLE_ENDIAN) }

			this.vertexMemory.position(0)
			vertexMemory.put(this.vertexMemory)
			vertexMemory.position(vertexPos)

			if (indexType != this.indexType) {
				val indexMemory = ByteBuffer.allocateDirect(vertexCapacity * indexSize).also { it.order(ByteOrder.LITTLE_ENDIAN) }

				when (this.indexType) {
					GL_UNSIGNED_BYTE -> {
						when (indexType) {
							GL_UNSIGNED_BYTE -> throw IllegalArgumentException()
							GL_UNSIGNED_SHORT -> {
								this.indexMemory.position(0)

								for (i in 0 until indexCount) {
									indexMemory.putShort(this.indexMemory.get().toShort())
								}
							}
							GL_UNSIGNED_INT -> {
								this.indexMemory.position(0)

								for (i in 0 until indexCount) {
									indexMemory.putInt(this.indexMemory.get().toInt())
								}
							}
						}
					}

					GL_UNSIGNED_SHORT -> {
						when (indexType) {
							GL_UNSIGNED_BYTE -> throw IllegalArgumentException()
							GL_UNSIGNED_SHORT -> throw IllegalArgumentException()
							GL_UNSIGNED_INT -> {
								this.indexMemory.position(0)

								for (i in 0 until indexCount) {
									indexMemory.putInt(this.indexMemory.getShort().toInt())
								}
							}
						}
					}

					else -> throw IllegalArgumentException()
				}

				this.indexMemory = indexMemory
				this.indexType = indexType
				this.indexSize = indexSize
			}

			this.vertexMemory = vertexMemory
			this.indexWriter = writer(indexType)
		}
	}

	fun mode(mode: GeometryType): VertexBuilder {
		if (inVertex) {
			if (vertexAttributes != attributes.allFlags)
				throw IllegalStateException("Can't change buffer geometry type during vertex construction")
			else
				end()
		}

		check(elementVertices == 0) { "Can't change buffer geometry type while not having fully built current element" }

		if (this.mode !== mode) {
			this.mode = mode
			ensureCapacity()
		}

		return this
	}

	fun mode() = mode

	fun begin(mode: GeometryType? = defaultMode): VertexBuilder {
		this.mode = mode
		inVertex = false
		elementIndexOffset = 0
		vertexAttributes = 0L
		vertexCount = 0
		indexCount = 0
		elementCount = 0
		elementVertices = 0
		vertexMemory.position(0)
		indexMemory.position(0)
		ensureCapacity()
		return this
	}

	fun upload(vertices: BufferObject.VBO, elements: BufferObject.EBO, drawType: Int = GL45.GL_DYNAMIC_DRAW) {
		end()

		check(elementVertices == 0) { "Not fully built vertex element ($mode requires ${mode?.elements} vertex points to be present, yet last strip has only $elementVertices elements)" }

		val vertexPos = vertexMemory.position()
		val elementPos = indexMemory.position()

		vertexMemory.position(0)
		indexMemory.position(0)

		vertices.bufferData(vertexMemory, drawType, length = vertexPos.toLong())
		elements.bufferData(indexMemory, drawType, length = elementPos.toLong())

		vertexMemory.position(vertexPos)
		indexMemory.position(elementPos)
	}

	fun end() {
		if (!inVertex) return

		val mode = mode!!
		inVertex = false

		if (vertexAttributes != attributes.allFlags) {
			val missing = ArrayList<VertexAttributeType>()
			attributes.attributeList.forEach { if (it.flag and vertexAttributes == 0L) missing.add(it.type) }

			throw IllegalStateException("Unfinished vertex, missing ${missing.joinToString()} attributes")
		}

		vertexCount++
		elementVertices++

		if (elementVertices == mode.elements) {
			elementCount++
			elementVertices = 0

			val indexWriter = indexWriter
			val indexMemory = indexMemory
			val elementIndexOffset = elementIndexOffset

			for (index in mode.indices.indices) {
				indexWriter.write(indexMemory, mode.indices.getInt(index) + elementIndexOffset)
			}

			this.elementIndexOffset += mode.elements
			indexCount += mode.indices.size

			ensureCapacity()
		}
	}

	fun vertex(): VertexBuilder {
		end()
		checkNotNull(mode) { "No builder mode is set" }

		inVertex = true
		vertexAttributes = 0L
		return this
	}

	fun vertex(x: Float, y: Float): VertexBuilder {
		vertex()
		position(x, y)
		return this
	}

	inline fun quad(x0: Float, y0: Float, x1: Float, y1: Float, configurator: VertexBuilder.() -> Unit = {}): VertexBuilder {
		vertex(x0, y0).also(configurator)
		vertex(x1, y0).also(configurator)
		vertex(x1, y1).also(configurator)
		vertex(x0, y1).also(configurator)
		return this
	}

	inline fun centeredQuad(x: Float, y: Float, width: Float, height: Float, configurator: VertexBuilder.() -> Unit = {}): VertexBuilder {
		vertex(x - width * 0.5f, y - height * 0.5f).also(configurator)
		vertex(x + width * 0.5f, y - height * 0.5f).also(configurator)
		vertex(x + width * 0.5f, y + height * 0.5f).also(configurator)
		vertex(x - width * 0.5f, y + height * 0.5f).also(configurator)
		return this
	}

	fun vertex(value: IStruct2f): VertexBuilder {
		vertex()
		position(value.component1(), value.component2())
		return this
	}

	fun vertex(value: IStruct2d): VertexBuilder {
		vertex()
		position(value.component1().toFloat(), value.component2().toFloat())
		return this
	}

	fun vertex(transform: Matrix3f, x: Float, y: Float): VertexBuilder {
		vertex()
		position(transform, x, y)
		return this
	}

	fun vertex(transform: Matrix3f, value: IStruct2f): VertexBuilder {
		vertex()
		position(transform, value)
		return this
	}

	fun vertex(transform: Matrix3f, value: IStruct2d): VertexBuilder {
		vertex()
		position(transform, value)
		return this
	}

	private fun pushFloat(attr: VertexAttributes.Attribute, x: Float): VertexBuilder {
		vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset)
		vertexMemory.putFloat(x)
		vertexAttributes = vertexAttributes or attr.flag

		return this
	}

	private fun pushInt(attr: VertexAttributes.Attribute, x: Int): VertexBuilder {
		vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset)
		vertexMemory.putInt(x)
		vertexAttributes = vertexAttributes or attr.flag

		return this
	}

	private fun pushFloat2(attr: VertexAttributes.Attribute, x: Float, y: Float): VertexBuilder {
		vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset)
		vertexMemory.putFloat(x)
		vertexMemory.putFloat(y)
		vertexAttributes = vertexAttributes or attr.flag

		return this
	}

	private fun pushFloat3(attr: VertexAttributes.Attribute, x: Float, y: Float, z: Float): VertexBuilder {
		vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset)
		vertexMemory.putFloat(x)
		vertexMemory.putFloat(y)
		vertexMemory.putFloat(z)
		vertexAttributes = vertexAttributes or attr.flag

		return this
	}

	private fun pushFloat4(attr: VertexAttributes.Attribute, x: Float, y: Float, z: Float, w: Float): VertexBuilder {
		vertexMemory.position(vertexCount * attributes.vertexStride + attr.relativeOffset)
		vertexMemory.putFloat(x)
		vertexMemory.putFloat(y)
		vertexMemory.putFloat(z)
		vertexMemory.putFloat(w)
		vertexAttributes = vertexAttributes or attr.flag

		return this
	}

	private val position = attributes[VertexAttributeType.POSITION]
	private val uv = attributes[VertexAttributeType.UV]
	private val color = attributes[VertexAttributeType.COLOR]
	private val hueShift = attributes[VertexAttributeType.HUE_SHIFT]
	private val textureIndex = attributes[VertexAttributeType.TEXTURE_INDEX]

	fun position(x: Float, y: Float): VertexBuilder {
		return pushFloat3(requireNotNull(position) { "Vertex format does not have position attribute" }, x, y, 1f)
	}

	fun position(value: IStruct2f) = position(value.component1(), value.component2())

	fun position(transform: Matrix3f, x: Float, y: Float): VertexBuilder {
		return pushFloat3(
			requireNotNull(position) { "Vertex format does not have position attribute" },
			x * transform.c00 + y * transform.c10 + transform.c20,
			x * transform.c01 + y * transform.c11 + transform.c21,
			x * transform.c02 + y * transform.c12 + transform.c22)
	}

	fun position(transform: Matrix3f, value: IStruct2f) = position(transform, value.component1(), value.component2())
	fun position(transform: Matrix3f, value: IStruct2d) = position(transform, value.component1().toFloat(), value.component2().toFloat())

	fun uv(x: Float, y: Float): VertexBuilder {
		return pushFloat2(requireNotNull(uv) { "Vertex format does not have texture UV attribute" }, x, y)
	}

	fun uv(value: IStruct2f) = position(value.component1(), value.component2())

	fun color(x: Float, y: Float, z: Float, w: Float): VertexBuilder {
		return pushFloat4(requireNotNull(color) { "Vertex format does not have color attribute" }, x, y, z, w)
	}

	fun color(value: IStruct4f) = color(value.component1(), value.component2(), value.component3(), value.component4())

	fun hueShift(x: Float): VertexBuilder {
		return pushFloat(requireNotNull(hueShift) { "Vertex format does not have texture UV attribute" }, x)
	}

	fun textureIndex(x: Int): VertexBuilder {
		return pushFloat(requireNotNull(textureIndex) { "Vertex format does not have texture index attribute" }, x.toFloat())
	}
}
