package ru.dbotthepony.kstarbound.client.render

import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap
import org.lwjgl.opengl.GL15.GL_STREAM_DRAW
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import java.util.concurrent.Callable
import java.util.function.Function
import java.util.stream.Stream

interface IGeometryLayer {
	fun add(renderer: () -> Unit)
	fun getBuilder(config: RenderConfig<*>): VertexBuilder
	fun render()
	fun clear()
	fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, RenderLayer.Point>>
}

object OneShotGeometryLayer : IGeometryLayer {
	override fun add(renderer: () -> Unit) {
		TODO("Not yet implemented")
	}

	override fun getBuilder(config: RenderConfig<*>): VertexBuilder {
		TODO("Not yet implemented")
	}

	override fun render() {
		TODO("Not yet implemented")
	}

	override fun clear() {
		TODO("Not yet implemented")
	}

	override fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, RenderLayer.Point>> {
		TODO("Not yet implemented")
	}
}

class LayeredRenderer(val client: StarboundClient) {
	private data class Tracker(val builder: StreamVertexBuilder, var emptyFrames: Int = 0)

	inner class Layer(val layer: RenderLayer.Point) : IGeometryLayer {
		private val meshes = ArrayList<ConfiguredMesh<*>>()
		private val callbacks = ArrayList<() -> Unit>()
		private val builders = Reference2ObjectLinkedOpenHashMap<RenderConfig<*>, Tracker>()

		override fun add(renderer: () -> Unit) {
			callbacks.add(renderer)
		}

		override fun getBuilder(config: RenderConfig<*>): VertexBuilder {
			return builders.computeIfAbsent(config, Function {
				Tracker(StreamVertexBuilder(config.program.attributes, initialCapacity = config.initialBuilderCapacity, client = client))
			}).builder.builder
		}

		override fun render() {
			val iterator = builders.entries.iterator()

			for ((k, v) in iterator) {
				if (v.builder.builder.isNotEmpty()) {
					v.emptyFrames = 0
					k.setup()
					v.builder.upload(GL_STREAM_DRAW)
					v.builder.draw()
					k.uninstall()
				} else {
					v.emptyFrames++

					if (v.emptyFrames >= 600) {
						// ~10 seconds of inactivity, free resources
						iterator.remove()
					}
				}
			}

			meshes.forEach {
				it.render()
			}

			callbacks.forEach {
				it.invoke()
			}
		}

		override fun clear() {
			builders.values.forEach { it.builder.builder.begin() }
			meshes.clear()
			callbacks.clear()
		}

		override fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, RenderLayer.Point>> {
			if (meshes.isNotEmpty() || callbacks.isNotEmpty())
				throw IllegalStateException("This layer (index $layer) contains preconfigured meshes and/or callbacks")

			return builders.entries.stream()
				.filter { it.value.builder.builder.isNotEmpty() }
				.map { ConfiguredMesh(it.key, Mesh(it.value.builder.builder)) to layer }
		}
	}

	private val layers = Object2ObjectAVLTreeMap<RenderLayer.Point, Layer>()

	fun getBuilder(layer: RenderLayer.Point, config: RenderConfig<*>): VertexBuilder {
		return getLayer(layer).getBuilder(config)
	}

	fun bakeIntoMeshes(): Stream<Pair<ConfiguredMesh<*>, RenderLayer.Point>> {
		return layers.values.stream().flatMap { it.bakeIntoMeshes() }
	}

	fun add(layer: RenderLayer.Point, renderer: () -> Unit) {
		getLayer(layer).add(renderer)
	}

	fun getLayer(layer: RenderLayer.Point): Layer {
		return layers.computeIfAbsent(layer, Function { Layer(it) })
	}

	fun render() {
		layers.values.forEach { it.render() }
	}

	fun clear() {
		layers.values.forEach { it.clear() }
	}
}
