package ru.dbotthepony.kstarbound.client.render

import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.client.gl.shader.GLTileProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
import ru.dbotthepony.kstarbound.client.gl.vertex.*
import ru.dbotthepony.kstarbound.defs.tile.*
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2i
import java.util.stream.Stream
import kotlin.collections.HashMap

data class TileLayer(
	val program: ConfiguredShaderProgram<GLTileProgram>,
	val vertices: VertexBuilder,
	val zPos: Int
)

class TileLayerList {
	private val layers = HashMap<ConfiguredShaderProgram<GLTileProgram>, Int2ObjectOpenHashMap<TileLayer>>()

	/**
	 * Получает геометрию слоя ([DynamicVertexBuilder]), который имеет программу для отрисовки [program] и располагается на [zLevel].
	 *
	 * Если такого слоя нет, вызывается [compute] и создаётся новый [TileLayer], затем возвращается результат [compute].
	 */
	fun computeIfAbsent(program: ConfiguredShaderProgram<GLTileProgram>, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder {
		return layers.computeIfAbsent(program) { Int2ObjectOpenHashMap() }.computeIfAbsent(zLevel, Int2ObjectFunction {
			return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel)
		}).vertices
	}

	fun layers(): Stream<TileLayer> {
		return layers.values.stream().flatMap { it.values.stream() }
	}

	fun clear() = layers.clear()

	val isEmpty get() = layers.isEmpty()
	val isNotEmpty get() = layers.isNotEmpty()
}

/**
 * Хранит в себе программы для отрисовки определённых [TileDefinition]
 *
 * Создаётся единожды как потомок [GLStateTracker]
 */
class TileRenderers(val client: StarboundClient) {
	val state get() = client.gl
	private val foregroundTilePrograms = HashMap<GLTexture2D, ForegroundTileProgram>()
	private val backgroundTilePrograms = HashMap<GLTexture2D, BackgroundTileProgram>()
	private val tileRenderersCache = HashMap<String, TileRenderer>()
	private val modifierRenderersCache = HashMap<String, TileRenderer>()

	fun getTileRenderer(defName: String): TileRenderer {
		return tileRenderersCache.computeIfAbsent(defName) {
			val def = client.starbound.tiles[defName] // TODO: Пустой рендерер
			return@computeIfAbsent TileRenderer(this, def!!.value)
		}
	}

	fun getModifierRenderer(defName: String): TileRenderer {
		return modifierRenderersCache.computeIfAbsent(defName) {
			val def = client.starbound.tileModifiers[defName] // TODO: Пустой рендерер
			return@computeIfAbsent TileRenderer(this, def!!.value)
		}
	}

	private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram<GLTileProgram>(state.programs.tile) {
		override fun setup() {
			super.setup()
			state.activeTexture = 0
			state.depthTest = false
			program.texture = 0
			texture.bind()
			texture.textureMagFilter = GL_NEAREST
			texture.textureMinFilter = GL_NEAREST

			program.color = FOREGROUND_COLOR
		}

		override fun equals(other: Any?): Boolean {
			if (this === other) {
				return true
			}

			if (other is ForegroundTileProgram) {
				return texture == other.texture
			}

			return super.equals(other)
		}

		override fun hashCode(): Int {
			return texture.hashCode()
		}
	}

	private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram<GLTileProgram>(state.programs.tile) {
		override fun setup() {
			super.setup()
			state.activeTexture = 0
			state.depthTest = false
			program.texture = 0
			texture.bind()
			texture.textureMagFilter = GL_NEAREST
			texture.textureMinFilter = GL_NEAREST

			program.color = BACKGROUND_COLOR
		}

		override fun equals(other: Any?): Boolean {
			if (this === other) {
				return true
			}

			if (other is BackgroundTileProgram) {
				return texture == other.texture
			}

			return super.equals(other)
		}

		override fun hashCode(): Int {
			return texture.hashCode()
		}
	}

	/**
	 * Возвращает запечённое состояние шейдера shaderVertexTexture с данной текстурой
	 */
	fun foreground(texture: GLTexture2D): ConfiguredShaderProgram<GLTileProgram> {
		return foregroundTilePrograms.computeIfAbsent(texture, ::ForegroundTileProgram)
	}

	/**
	 * Возвращает запечённое состояние шейдера shaderVertexTextureRGBAColor с данной текстурой
	 */
	fun background(texture: GLTexture2D): ConfiguredShaderProgram<GLTileProgram> {
		return backgroundTilePrograms.computeIfAbsent(texture, ::BackgroundTileProgram)
	}

	companion object {
		val BACKGROUND_COLOR = RGBAColor(0.4f, 0.4f, 0.4f)
		val FOREGROUND_COLOR = RGBAColor(1f, 1f, 1f)
	}
}

private fun vertexTextureBuilder() = VertexBuilder(GLAttributeList.TILE, GeometryType.QUADS)

private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester {
	override fun test(thisTile: ITileState?, otherTile: ITileState?): Boolean {
		return otherTile?.material == definition && thisTile?.hueShift == otherTile.hueShift
	}
}

private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester {
	override fun test(thisTile: ITileState?, otherTile: ITileState?): Boolean {
		return otherTile?.modifier == definition
	}
}

class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
	private enum class TestResult {
		NO_MATCH,
		CONTINUE,
		HALT
	}

	val state get() = renderers.state
	val texture = state.loadTexture(def.renderParameters.texture.imagePath.value!!).also {
		it.textureMagFilter = GL_NEAREST
	}

	val equalityTester: EqualityRuleTester = when (def) {
		is TileDefinition -> TileEqualityTester(def)
		is MaterialModifier -> ModifierEqualityTester(def)
		else -> throw IllegalStateException()
	}

	val bakedProgramState = renderers.foreground(texture)
	val bakedBackgroundProgramState = renderers.background(texture)
	// private var notifiedDepth = false

	private fun tesselateAt(self: ITileState, piece: RenderPiece, getter: ITileAccess, builder: VertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO, isModifier: Boolean) {
		val fx = pos.x.toFloat()
		val fy = pos.y.toFloat()

		var a = fx
		var b = fy

		var c = fx + piece.textureSize.x / PIXELS_IN_STARBOUND_UNITf
		var d = fy + piece.textureSize.y / PIXELS_IN_STARBOUND_UNITf

		if (offset != Vector2i.ZERO) {
			a += offset.x / PIXELS_IN_STARBOUND_UNITf

			// в json файлах Y указан как положительный вверх,
			// что соответствует нашему миру
			b += offset.y / PIXELS_IN_STARBOUND_UNITf

			c += offset.x / PIXELS_IN_STARBOUND_UNITf
			d += offset.y / PIXELS_IN_STARBOUND_UNITf
		}

		var mins = piece.texturePosition
		var maxs = piece.texturePosition + piece.textureSize

		if (def.renderParameters.variants != 0 && piece.variantStride != null && piece.texture == null) {
			val variant = (getter.randomDoubleFor(pos) * def.renderParameters.variants).toInt()
			mins += piece.variantStride * variant
			maxs += piece.variantStride * variant
		}

		if (def.renderParameters.multiColored && piece.colorStride != null && self.color != TileColor.DEFAULT) {
			mins += piece.colorStride * self.color.ordinal
			maxs += piece.colorStride * self.color.ordinal
		}

		val (u0, v0) = texture.pixelToUV(mins)
		val (u1, v1) = texture.pixelToUV(maxs)

		builder.quadZ(a, b, c, d, Z_LEVEL, QuadTransformers.uv(u0, v1, u1, v0).after { it, _ -> it.push(if (isModifier) self.modifierHueShift else self.hueShift) })
	}

	private fun tesselatePiece(
		self: ITileState,
		matchPiece: RenderMatch,
		getter: ITileAccess,
		layers: TileLayerList,
		pos: Vector2i,
		thisBuilder: VertexBuilder,
		background: Boolean,
		isModifier: Boolean,
	): TestResult {
		if (matchPiece.test(getter, equalityTester, pos)) {
			for (renderPiece in matchPiece.pieces) {
				if (renderPiece.piece.texture != null) {
					val program = if (background) {
						renderers.background(state.loadTexture(renderPiece.piece.texture!!))
					} else {
						renderers.foreground(state.loadTexture(renderPiece.piece.texture!!))
					}

					tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier)
				} else {
					tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier)
				}
			}

			for (subPiece in matchPiece.subMatches) {
				val matched = tesselatePiece(self, subPiece, getter, layers, pos, thisBuilder, background, isModifier)

				if (matched == TestResult.HALT || matched == TestResult.CONTINUE && matchPiece.haltOnSubMatch) {
					return TestResult.HALT
				}
			}

			if (matchPiece.haltOnMatch) {
				return TestResult.HALT
			}

			return TestResult.CONTINUE
		}

		return TestResult.NO_MATCH
	}

	/**
	 * Тесселирует тайлы в заданной позиции в нужном билдере
	 *
	 * [getter] Нужен для получения информации о ближайших блоках
	 *
	 * [layers] содержит текущие программы и их билдеры и их zPos
	 *
	 * Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf]
	 */
	fun tesselate(self: ITileState, getter: ITileAccess, layers: TileLayerList, pos: Vector2i, background: Boolean = false, isModifier: Boolean = false) {
		// если у нас нет renderTemplate
		// то мы просто не можем его отрисовать
		val template = def.renderTemplate.value ?: return

		val vertexBuilder = layers.computeIfAbsent(if (background) bakedBackgroundProgramState else bakedProgramState, def.renderParameters.zLevel, ::vertexTextureBuilder)

		for ((_, matcher) in template.matches) {
			for (matchPiece in matcher) {
				val matched = tesselatePiece(self, matchPiece, getter, layers, pos, vertexBuilder, background, isModifier)

				if (matched == TestResult.HALT) {
					break
				}
			}
		}
	}

	companion object {
		const val Z_LEVEL = 10f
		private val LOGGER = LogManager.getLogger()
	}
}