package ru.dbotthepony.kstarbound.client.gl.shader

import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap
import it.unimi.dsi.fastutil.objects.Object2BooleanFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import org.lwjgl.opengl.GL45.*
import ru.dbotthepony.kommons.util.IStruct2f
import ru.dbotthepony.kommons.util.IStruct3f
import ru.dbotthepony.kommons.util.IStruct4f
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.matrix.Matrix4f
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector3f
import ru.dbotthepony.kommons.vector.Vector4f
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.GLObject
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexAttributes
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import kotlin.NoSuchElementException
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class GLShaderProgram(
	shaders: Iterable<GLShader>,
	val attributes: VertexAttributes
) : GLObject() {
	final override val client = StarboundClient.current()
	final override val pointer = glCreateProgram()

	init {
		checkForGLError("Creating shader program")
		client.registerCleanable(this, ::glDeleteProgram, pointer)

		for (shader in shaders) {
			glAttachShader(pointer, shader.pointer)
			checkForGLError("Attaching shader")
		}

		glLinkProgram(pointer)

		if (glGetProgrami(pointer, GL_LINK_STATUS) == 0)
			throw ShaderLinkException(glGetProgramInfoLog(pointer))

		glGetError()

		client.addShaderProgram(this)
	}

	fun use(): GLShaderProgram {
		client.program = this
		return this
	}

	final override fun bind() {
		client.program = this
	}

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

	private val locations = Object2ObjectArrayMap<String, Uniform<*>>()
	private val uniformsExist = Object2BooleanArrayMap<String>()

	fun isUniformPresent(name: String): Boolean {
		client.ensureSameThread()
		return uniformsExist.computeIfAbsent(name, Object2BooleanFunction { glGetUniformLocation(pointer, name) != -1 })
	}

	fun getUniform(name: String, orElse: ((String) -> Uniform<*>)? = null): Uniform<*>? {
		return locations[name] ?: orElse?.invoke(name)
	}

	fun int(name: String): IUniform {
		return locations.computeIfAbsent(name, Object2ObjectFunction { IUniform(name) }) as? IUniform ?: throw IllegalStateException("Uniform $name has type of ${locations[name]!!::class.simpleName}")
	}

	abstract inner class Uniform<V : Any>(val name: String, allowMissing: Boolean = false) : ReadWriteProperty<Any?, V> {
		init {
			client.ensureSameThread()
			require(!locations.containsKey(name)) { "Already has uniform $name for ${this@GLShaderProgram} (${locations[name]})" }
		}

		val location = glGetUniformLocation(pointer, name)
		val isMissing = location == -1

		init {
			if (isMissing && !allowMissing)
				throw NoSuchElementException("Program ${this@GLShaderProgram} does not have uniform with name $name")

			locations[name] = this
		}

		abstract var value: V

		override fun toString(): String {
			return "Uniform[$name, $value]"
		}

		// они переопределены в дочерних классах для бетонной реализации примитивов
		override fun getValue(thisRef: Any?, property: KProperty<*>): V {
			return value
		}

		override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
			this.value = value
		}
	}

	inner class FUniform(name: String, allowMissing: Boolean = false) : Uniform<Float>(name, allowMissing) {
		override var value: Float = 0f
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				if (field != value) {
					glProgramUniform1f(pointer, location, value)
					checkForGLError()
				}

				field = value
			}

		override fun getValue(thisRef: Any?, property: KProperty<*>): Float {
			return value
		}

		override fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) {
			this.value = value
		}
	}

	inner class F2Uniform(name: String, allowMissing: Boolean = false) : Uniform<IStruct2f>(name, allowMissing) {
		private var v0 = 0f
		private var v1 = 0f

		override var value: IStruct2f = Vector2f.ZERO
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				val (v0, v1) = value

				if (this.v0 != v0 || this.v1 != v1) {
					glProgramUniform2f(pointer, location, v0, v1)
					checkForGLError()

					field = if (value is Vector2f) value else Vector2f(value)
					this.v0 = v0
					this.v1 = v1
				}
			}
	}

	inner class F3Uniform(name: String, allowMissing: Boolean = false) : Uniform<IStruct3f>(name, allowMissing) {
		private var v0 = 0f
		private var v1 = 0f
		private var v2 = 0f

		override var value: IStruct3f = Vector3f.ZERO
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				val (v0, v1, v2) = value

				if (this.v0 != v0 || this.v1 != v1 || this.v2 != v2) {
					glProgramUniform3f(pointer, location, v0, v1, v2)
					checkForGLError()

					field = if (value is Vector3f) value else Vector3f(value)
					this.v0 = v0
					this.v1 = v1
					this.v2 = v2
				}
			}
	}

	private val buff3x3: FloatBuffer = ByteBuffer.allocateDirect(4 * 3 * 3).also { it.order(ByteOrder.nativeOrder()) }.asFloatBuffer()
	private val buff4x4: FloatBuffer = ByteBuffer.allocateDirect(4 * 4 * 4).also { it.order(ByteOrder.nativeOrder()) }.asFloatBuffer()

	inner class F3x3Uniform(name: String, allowMissing: Boolean = false) : Uniform<Matrix3f>(name, allowMissing) {
		override var value: Matrix3f = Matrix3f.zero()
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				if (field != value) {
					buff3x3.position(0)
					value.storeColumnRow(buff3x3)
					buff3x3.position(0)
					glProgramUniformMatrix3fv(pointer, location, false, buff3x3)
					checkForGLError()

					field = value.copy()
				}
			}
	}

	inner class F4x4Uniform(name: String, allowMissing: Boolean = false) : Uniform<Matrix4f>(name, allowMissing) {
		private val _value = ByteBuffer.allocate(4 * 4 * 4).also { it.order(ByteOrder.nativeOrder()) }.asFloatBuffer()

		override var value: Matrix4f = Matrix4f.zero()
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				buff4x4.position(0)
				value.storeColumnRow(buff4x4)
				buff4x4.position(0)
				_value.position(0)

				if (buff4x4 != _value) {
					glProgramUniformMatrix4fv(pointer, location, false, buff4x4)
					checkForGLError()
					buff4x4.position(0)
					_value.put(buff4x4)
					field = Matrix4f.unmodifiable(value)
				}
			}
	}

	inner class F4Uniform(name: String, allowMissing: Boolean = false) : Uniform<IStruct4f>(name, allowMissing) {
		private var v0 = 0f
		private var v1 = 0f
		private var v2 = 0f
		private var v3 = 0f

		override var value: IStruct4f = Vector4f.ZERO
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				val (v0, v1, v2, v3) = value

				if (this.v0 != v0 || this.v1 != v1 || this.v2 != v2 || this.v3 != v3) {
					glProgramUniform4f(pointer, location, v0, v1, v2, v3)
					checkForGLError()

					field = if (value is Vector4f) value else Vector4f(value)
					this.v0 = v0
					this.v1 = v1
					this.v2 = v2
					this.v3 = v3
				}
			}
	}

	inner class IUniform(name: String, allowMissing: Boolean = false) : Uniform<Int>(name, allowMissing) {
		override var value: Int = 0
			set(value) {
				client.ensureSameThread()
				if (isMissing) return

				if (field != value) {
					glProgramUniform1i(pointer, location, value)
					checkForGLError()
				}

				field = value
			}

		override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
			return value
		}

		override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
			this.value = value
		}
	}
}
