package ru.dbotthepony.kstarbound.client.gl

import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL45.*
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.client.StarboundClient
import java.nio.ByteBuffer
import kotlin.reflect.KProperty

class TextureLoadingException(message: String) : Throwable(message)

data class UVCoord(val u: Float, val v: Float)

private class GLTexturePropertyTracker(private val flag: Int, private var value: Int) {
	operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int {
		return value
	}

	operator fun setValue(thisRef: GLTexture2D, property: KProperty<*>, value: Int) {
		thisRef.client.ensureSameThread()
		if (this.value == value) return
		this.value = value
		glTextureParameteri(thisRef.pointer, flag, value)
		checkForGLError()
	}
}

@Suppress("SameParameterValue")
class GLTexture2D(val width: Int, val height: Int, val format: Int, val levels: Int = 1) : GLObject() {
	init {
		require(width > 0 && height > 0) { "Invalid image size: $width x $height" }
		require(levels > 0) { "Invalid image level count: $levels" }
	}

	override val client = StarboundClient.current()
	override val pointer = glCreateTextures(GL_TEXTURE_2D)

	init {
		checkForGLError("Creating texture")
		client.registerCleanable(this, ::glDeleteTextures, pointer)

		glTextureStorage2D(pointer, levels, format, width, height)
		checkForGLError("Invalid image format: $format")
	}

	val aspectRatioWH: Float get() {
		if (height == 0) {
			return 1f
		}

		return width.toFloat() / height.toFloat()
	}

	val aspectRatioHW: Float get() {
		if (width == 0) {
			return 1f
		}

		return height.toFloat() / width.toFloat()
	}

	var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR)
	var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR)
	var maxLevel by GLTexturePropertyTracker(GL_TEXTURE_MAX_LEVEL, 1000)

	var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT)
	var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT)

	@Deprecated("Has no effect", level = DeprecationLevel.ERROR)
	override fun bind() {
		// do nothing
	}

	@Deprecated("Has no effect", level = DeprecationLevel.ERROR)
	override fun unbind() {
		// do nothing
	}

	fun generateMips(): GLTexture2D {
		client.ensureSameThread()
		glGenerateTextureMipmap(pointer)
		checkForGLError("Generating texture mipmaps")
		return this
	}

	fun pixelToUV(x: Float, y: Float): UVCoord {
		return UVCoord(x / width, y / height)
	}

	fun pixelToUV(x: Int, y: Int): UVCoord {
		return UVCoord(x.toFloat() / width, y.toFloat() / height)
	}

	fun pixelToUV(pos: IStruct2i) = pixelToUV(pos.component1(), pos.component2())

	fun upload(level: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D {
		return upload(level, 0, 0, width, height, bufferFormat, dataFormat, data)
	}

	fun upload(bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D {
		return upload(0, 0, 0, width, height, bufferFormat, dataFormat, data)
	}

	fun upload(level: Int, x: Int, y: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D {
		require(x >= 0 && y >= 0 && width > 0 && height > 0 && x + width <= this.width && y + height <= this.height) {
			"Bad texture rectangle specified: $x x $y -> ${x + width} x ${y + height}, which is out of bounds for texture ${this.width} x ${this.height}"
		}

		require(level in 0 until levels) { "Level out of bounds: $level" }
		glTextureSubImage2D(pointer, level, x, y, width, height, bufferFormat, dataFormat, data)
		checkForGLError("Uploading texture data")
		return this
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
	}
}
