package ru.dbotthepony.kstarbound.client.gl

import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import org.lwjgl.opengl.GL45.*
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributes
import java.util.BitSet

class VertexArrayObject : GLObject() {
	override val client = StarboundClient.current()
	override val pointer = glCreateVertexArrays()

	init {
		checkForGLError("Creating Vertex Array Object")
		client.registerCleanable(this, ::glDeleteVertexArrays, pointer)
	}

	private val enabledAttributes = BitSet()
	private val boundBuffers = Int2ObjectAVLTreeMap<BindConfig>()
	private data class BindConfig(val buffer: BufferObject,  val stride: Int, val offset: Long)

	var elementBuffer: BufferObject.EBO? = null
		set(value) {
			client.ensureSameThread()

			if (field != value) {
				if (value == null)
					glVertexArrayElementBuffer(pointer, 0)
				else
					glVertexArrayElementBuffer(pointer, value.pointer)

				checkForGLError()
				field = value
			}
		}

	override fun bind() {
		client.vao = this
	}

	override fun unbind() {
		if (client.vao == this)
			client.vao = null
	}

	fun bindBufferToIndex(buffer: BufferObject.VBO, index: Int, vertexStride: Int, offsetFromBeginning: Long = 0L): VertexArrayObject {
		client.ensureSameThread()
		require(index >= 0) { "Invalid bind index: $index" }
		require(index < client.maxVertexAttribBindPoints) { "Bind index is too big: $index; Max ${client.maxVertexAttribBindPoints}" }
		val config = BindConfig(buffer, vertexStride, offsetFromBeginning)

		if (boundBuffers[index] != config) {
			glVertexArrayVertexBuffer(pointer, index, buffer.pointer, offsetFromBeginning, vertexStride)
			checkForGLError()
			boundBuffers[index] = config
		}

		return this
	}

	fun unbindBufferFromIndex(index: Int): VertexArrayObject {
		client.ensureSameThread()
		require(index >= 0) { "Invalid attribute index: $index" }
		require(index < client.maxVertexAttribBindPoints) { "Bind index is too big: $index; Max ${client.maxVertexAttribBindPoints}" }

		if (index in boundBuffers) {
			glVertexArrayVertexBuffer(pointer, index, 0, 0L, 0)
			checkForGLError()
			boundBuffers.remove(index)
		}

		return this
	}

	fun bindAttributeToIndex(attrib: Int, index: Int): VertexArrayObject {
		client.ensureSameThread()

		require(attrib >= 0) { "Invalid attribute index: $attrib" }
		require(index >= 0) { "Invalid bind index: $index" }
		require(index < client.maxVertexAttribBindPoints) { "Bind index is too big: $index; Max ${client.maxVertexAttribBindPoints}" }

		glVertexArrayAttribBinding(pointer, attrib, index)
		checkForGLError()

		return this
	}

	fun attributeFormat(attrib: Int, type: GLType, normalized: Boolean, relativeOffset: Int): VertexArrayObject {
		client.ensureSameThread()
		require(attrib >= 0) { "Invalid attribute index: $attrib" }

		glVertexArrayAttribFormat(pointer, attrib, type.elementSize, type.elementType, normalized, relativeOffset)
		checkForGLError()

		return this
	}

	fun bindAttributes(buffer: BufferObject.VBO, attributes: VertexAttributes, bufferOffset: Long = 0L) {
		bindBufferToIndex(buffer, 0, attributes.vertexStride, bufferOffset)

		for (attr in attributes.attributeList) {
			bindAttributeToIndex(attr.index, 0)
			attributeFormat(attr.index, attr.type.type, false, attr.relativeOffset)
			enableAttribute(attr.index)
		}
	}

	fun enableAttribute(position: Int): VertexArrayObject {
		client.ensureSameThread()

		if (!enabledAttributes[position]) {
			glEnableVertexArrayAttrib(pointer, position)
			checkForGLError()
			enabledAttributes[position] = true
		}

		return this
	}

	fun disableAttribute(position: Int): VertexArrayObject {
		client.ensureSameThread()

		if (enabledAttributes[position]) {
			glDisableVertexArrayAttrib(pointer, position)
			checkForGLError()
			enabledAttributes[position] = false
		}

		return this
	}
}
