package ru.dbotthepony.kstarbound.client.world

import com.google.common.base.Supplier
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArraySet
import it.unimi.dsi.fastutil.objects.ReferenceArraySet
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.Mesh
import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.positiveModulo
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.function.Consumer

class ClientWorld(
	val client: StarboundClient,
	template: WorldTemplate,
) : World<ClientWorld, ClientChunk>(template) {
	private fun determineChunkSize(cells: Int): Int {
		for (i in 64 downTo 1) {
			if (cells % i == 0) {
				return i
			}
		}

		throw RuntimeException("unreachable code")
	}

	override val isClient: Boolean
		get() = true

	val renderRegionWidth = determineChunkSize(geometry.size.x)
	val renderRegionHeight = determineChunkSize(geometry.size.y)
	val renderRegionsX = geometry.size.x / renderRegionWidth
	val renderRegionsY = geometry.size.y / renderRegionHeight

	fun isValidRenderRegionX(value: Int): Boolean {
		return geometry.loopX || value in 0 .. renderRegionsX
	}

	fun isValidRenderRegionY(value: Int): Boolean {
		return geometry.loopY || value in 0 .. renderRegionsY
	}

	override val eventLoop: BlockableEventLoop
		get() = client

	inner class RenderRegion(val x: Int, val y: Int) {
		inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {
			val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
			private var currentBakeTask: Future<LayeredRenderer>? = null
			private var bakeTaskID = 0
			private var isDirty = true

			fun markDirty() {
				isDirty = true
			}

			fun bake() {
				if (!isDirty) return
				isDirty = false

				val bakeTaskID = ++bakeTaskID

				CompletableFuture.supplyAsync(Supplier {
					val meshes = LayeredRenderer(client)

					for (x in 0 until renderRegionWidth) {
						for (y in 0 until renderRegionHeight) {
							if (!geometry.inBoundsCell(x, y)) continue
							if (bakeTaskID != this.bakeTaskID) return@Supplier meshes

							val tile = view.getTile(x, y)
							val material = tile.material

							if (!material.value.isMeta) {
								client.tileRenderers
									.getMaterialRenderer(material.key)
									.tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground)
							}

							val modifier = tile.modifier

							if (modifier != null) {
								client.tileRenderers
									.getModifierRenderer(modifier.key)
									.tesselate(tile, view, meshes, Vector2i(x, y), isBackground = isBackground, isModifier = true)
							}
						}
					}

					meshes
				}, client.executor).thenAcceptAsync(Consumer {
					if (bakeTaskID != this.bakeTaskID) return@Consumer

					bakedMeshes.clear()

					for ((baked, zLevel) in it.bakeIntoMeshes()) {
						bakedMeshes.add(baked to zLevel)
					}

					this.currentBakeTask = null
				}, client.mailbox)
			}
		}

		private var renderCalls = 0
		private val liquidMesh = ArrayList<Pair<Mesh, RGBAColor>>()
		var liquidIsDirty = true

		val view = OffsetCellAccess(this@ClientWorld, x * renderRegionWidth, y * renderRegionHeight)

		val background = Layer(TileView.Background(view), true)
		val foreground = Layer(TileView.Foreground(view), false)

		fun notifyChunkForget() {
			background.markDirty()
			foreground.markDirty()

			val renderCalls = renderCalls

			client.mailbox.schedule(Runnable {
				if (renderCalls == this.renderCalls) {
					background.bakedMeshes.clear()
					foreground.bakedMeshes.clear()
				}
			}, 500L, TimeUnit.MILLISECONDS)
		}

		fun addLayers(layers: LayeredRenderer, renderOrigin: Vector2f) {
			renderCalls++
			background.bake()
			foreground.bake()

			if (liquidIsDirty) {
				liquidIsDirty = false
				liquidMesh.clear()

				val liquidTypes = ReferenceArraySet<Registry.Entry<LiquidDefinition>>()

				for (x in 0 until renderRegionWidth) {
					for (y in 0 until renderRegionHeight) {
						view.getCell(x, y).liquid.state?.let { liquidTypes.add(it) }
					}
				}

				for (type in liquidTypes) {
					val builder = client.programs.liquid.builder.builder

					builder.begin()

					for (x in 0 until renderRegionWidth) {
						for (y in 0 until renderRegionHeight) {
							val state = view.getCell(x, y)

							if (state.liquid.state == type) {
								builder.vertex(x.toFloat(), y.toFloat())
								builder.vertex(x.toFloat() + 1f, y.toFloat())
								builder.vertex(x.toFloat() + 1f, y.toFloat() + 1f)
								builder.vertex(x.toFloat(), y.toFloat() + 1f)
							}
						}
					}

					liquidMesh.add(Mesh(builder) to type.value.color)
				}
			}

			for ((baked, zLevel) in background.bakedMeshes) {
				layers.add(zLevel) {
					client.stack.push().last().translate(renderOrigin.x, renderOrigin.y)
					baked.render()
					client.stack.pop()
				}
			}

			for ((baked, zLevel) in foreground.bakedMeshes) {
				layers.add(zLevel) {
					client.stack.push().last().translate(renderOrigin.x, renderOrigin.y)
					baked.render()
					client.stack.pop()
				}
			}

			/*if (liquidMesh.isNotEmpty()) {
				layers.add(RenderLayer.Liquid.index) {
					client.stack.push().last().translate(renderOrigin.x, renderOrigin.y)

					val program = client.programs.liquid

					program.use()
					program.transform = client.stack.last()

					for ((mesh, color) in liquidMesh) {
						program.baselineColor = color
						mesh.render()
					}

					client.stack.pop()
				}
			}*/
		}
	}

	val renderRegions = Long2ObjectOpenHashMap<RenderRegion>()

	fun renderRegionKey(x: Int, y: Int): Long {
		return positiveModulo(x, renderRegionsX).toLong() shl 32 or positiveModulo(y, renderRegionsY).toLong()
	}

	/**
	 * all intersecting chunks
	 */
	inline fun forEachRenderRegion(pos: ChunkPos, action: (RenderRegion) -> Unit) {
		var (ix, iy) = pos.tile
		ix /= renderRegionWidth
		iy /= renderRegionHeight

		val paddingX = (CHUNK_SIZE / renderRegionWidth).coerceAtLeast(1)
		val paddingY = (CHUNK_SIZE / renderRegionHeight).coerceAtLeast(1)

		for (x in ix - paddingX .. ix + paddingX) {
			for (y in iy - paddingY .. iy + paddingY) {
				renderRegions[renderRegionKey(x, y)]?.let(action)
			}
		}
	}

	/**
	 * cell and cells around
	 */
	inline fun forEachRenderRegion(pos: IStruct2i, action: (RenderRegion) -> Unit) {
		val dx = pos.component1() % renderRegionWidth
		val dy = pos.component2() % renderRegionHeight

		if (dx == 1 || dx == renderRegionWidth - 1 || dy == 1 || dy == renderRegionHeight - 1) {
			val seen = LongArraySet(8)

			for ((x, y) in ring) {
				val ix = (pos.component1() + x) / renderRegionWidth
				val iy = (pos.component2() + y) / renderRegionHeight
				val index = renderRegionKey(ix, iy)

				if (seen.add(index)) {
					renderRegions[index]?.let(action)
				}
			}
		} else {
			val ix = pos.component1() / renderRegionWidth
			val iy = pos.component2() / renderRegionHeight

			renderRegions[renderRegionKey(ix, iy)]?.let(action)
		}
	}

	override fun chunkFactory(pos: ChunkPos): ClientChunk {
		return ClientChunk(this, pos)
	}

	fun addLayers(
		size: AABB,
		layers: LayeredRenderer
	) {
		val rx = roundTowardsNegativeInfinity(size.mins.x) / renderRegionWidth - 1
		val ry = roundTowardsNegativeInfinity(size.mins.y) / renderRegionHeight - 1

		val dx = roundTowardsPositiveInfinity(size.maxs.x - size.mins.x) / renderRegionWidth + 2
		val dy = roundTowardsPositiveInfinity(size.maxs.y - size.mins.y) / renderRegionHeight + 2

		for (x in rx .. rx + dx) {
			for (y in ry .. ry + dy) {
				if (!isValidRenderRegionX(x) || !isValidRenderRegionY(y)) continue
				val renderer = renderRegions.computeIfAbsent(renderRegionKey(x, y), Long2ObjectFunction {
					RenderRegion((it ushr 32).toInt(), it.toInt())
				})

				renderer.addLayers(layers, Vector2f(x * renderRegionWidth.toFloat(), y * renderRegionHeight.toFloat()))
			}
		}

		for (ent in entities.values) {
			ent.render(client, layers)
			ent.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY)
		}
	}

	companion object {
		val ring = listOf(
			Vector2i(0, 0),
			Vector2i(1, 0),
			Vector2i(1, 1),
			Vector2i(1, -1),
			Vector2i(-1, 0),
			Vector2i(-1, 1),
			Vector2i(-1, -1),
		)
	}
}
