package ru.dbotthepony.kstarbound.server.world

import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isMetaTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
import ru.dbotthepony.kstarbound.defs.world.Biome
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.function.Supplier
import kotlin.concurrent.withLock
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
	override var state: ChunkState = ChunkState.FRESH
		private set

	private var isBusy = false
	private var idleTicks = 0
	private var ticks = 0
	private val targetState = Channel<ChunkState>(Int.MAX_VALUE)
	private val permanent = CopyOnWriteArrayList<Ticket>()
	private val temporary = ObjectAVLTreeSet<TimedTicket>()
	private val temporaryList = CopyOnWriteArrayList<TimedTicket>()
	private var nextTicketID = 0
	// ticket lock because tickets *could* be canceled (or created) concurrently
	// BUT, front-end ticket creation in ServerWorld is expected to be called only on ServerWorld's thread
	// because ChunkMap is not thread-safe
	private val ticketsLock = ReentrantLock()
	private val loadJob = world.eventLoop.scope.launch { loadChunk() }

	var isUnloaded = false
		private set

	private suspend fun chunkGeneratorLoop() {
		while (true) {
			if (state == ChunkState.FULL)
				break

			val targetState = targetState.receive()

			while (state < targetState) {
				isBusy = true

				val nextState = ChunkState.entries[state.ordinal + 1]

				try {
					if (nextState >= ChunkState.MICRO_DUNGEONS) {
						val neighbours = ArrayList<ITicket>()

						try {
							// wait for neighbouring chunks to be as generated as our current state
							// before we advance to next stage
							// Yes, this will create quite big loading rectangle around
							// players and other watching entities, but this is required
							// to avoid generation artifacts, where this chunk tries to read
							// from not sufficiently generated neighbours

							for (neighbour in pos.neighbours()) {
								val ticket = world.permanentChunkTicket(neighbour, state).await() ?: continue
								neighbours.add(ticket)
							}

							for (neighbour in neighbours) {
								if (neighbour.chunk.isDone)
									continue

								suspendCancellableCoroutine<Unit> { block ->
									val future = world.eventLoop.schedule(Runnable {
										if (!neighbour.chunk.isDone) {
											LOGGER.error("Giving up waiting on ${neighbour.pos} while advancing generation stage of $pos to $nextState (neighbour chunk was in state ${world.chunkMap[neighbour.pos]?.state}, expected $state)")
											block.resume(Unit)
										}
									}, 30L, TimeUnit.SECONDS)

									neighbour.chunk.thenAccept {
										future.cancel(false)
										block.resume(Unit)
									}.exceptionally {
										future.cancel(false)
										block.resumeWithException(it); null
									}

									block.invokeOnCancellation {
										future.cancel(false)
										neighbour.cancel()
									}
								}
							}
						} catch (err: Throwable) {
							LOGGER.error("Error while waiting on neighbouring chunks while generating this $this", err)
							throw err
						} finally {
							neighbours.forEach { it.cancel() }
						}
					}

					when (nextState) {
						ChunkState.TERRAIN -> {
							if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) {
								// skip since no cells will be generated anyway

								val cells = cells.value

								for (x in 0 until width) {
									for (y in 0 until height) {
										cells[x, y] = AbstractCell.EMPTY
									}
								}

								signalChunkContentsUpdated()
							} else {
								// tiles can be generated concurrently without any consequences
								CompletableFuture.runAsync(Runnable { prepareCells() }, Starbound.EXECUTOR).await()
								signalChunkContentsUpdated()
							}
						}

						ChunkState.MICRO_DUNGEONS -> {
							// skip if we have no layout
							if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
								placeMicroDungeons()
							}
						}

						ChunkState.CAVE_LIQUID -> {
							// skip if we have no layout
							if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
								generateLiquid()
							}
						}

						ChunkState.FULL -> {
							CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await()

							// skip if we have no layout
							if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
								placeGrass()
							}

							signalChunkContentsUpdated()
						}

						ChunkState.FRESH -> throw RuntimeException()
						ChunkState.EMPTY -> {} // do nothing
					}

					bumpState(nextState)
				} catch (err: Throwable) {
					LOGGER.error("Exception while propagating $this to next generation state $nextState", err)
					break
				}
			}

			isBusy = false
		}

		isBusy = false
	}

	private suspend fun loadChunk() {
		try {
			val cells = world.storage.loadCells(pos).await()

			// very good.
			if (cells.isPresent) {
				loadCells(cells.value)
				// bumping state while loading chunk might have
				// undesired consequences, such as if chunk requester
				// is pessimistic and want "fully loaded chunk or chunk generated to at least X stage"
				// bumpState(State.CAVE_LIQUID)

				world.storage.loadEntities(pos).await().ifPresent {
					for (obj in it) {
						obj.joinWorld(world)
					}
				}

				bumpState(ChunkState.FULL)
				isBusy = false
				return
			} else {
				bumpState(ChunkState.EMPTY)
				// generate.
				chunkGeneratorLoop()
			}
		} catch (err: Throwable) {
			if (err is CancellationException) {
				// harmless
			} else {
				LOGGER.error("Exception while loading chunk $this", err)
				permanent.forEach { it.chunk.completeExceptionally(err) }
				temporaryList.forEach { it.chunk.completeExceptionally(err) }
			}
		} finally {
			isBusy = false
		}
	}

	fun permanentTicket(target: ChunkState = ChunkState.FULL): ITicket {
		ticketsLock.withLock {
			return Ticket(target)
		}
	}

	fun temporaryTicket(time: Int, target: ChunkState = ChunkState.FULL): ITimedTicket {
		require(time >= 0) { "Invalid ticket time: $time" }

		ticketsLock.withLock {
			return TimedTicket(time, target)
		}
	}

	override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
		if (chunkState > state) return false
		return setCell(x, y, cell)
	}

	interface ITicket {
		fun cancel()
		val isCanceled: Boolean
		val pos: ChunkPos
		val id: Int
		val chunk: CompletableFuture<ServerChunk>
		var listener: IChunkListener?
	}

	interface ITimedTicket : ITicket, Comparable<ITimedTicket> {
		val timeRemaining: Int
		fun prolong(ticks: Int)

		override fun compareTo(other: ITimedTicket): Int {
			val cmp = timeRemaining.compareTo(other.timeRemaining)
			if (cmp != 0) return cmp
			return id.compareTo(other.id)
		}
	}

	override fun cellChanges(x: Int, y: Int, cell: ImmutableCell) {
		super.cellChanges(x, y, cell)

		permanent.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
		temporaryList.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
	}

	private fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
		permanent.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
		temporaryList.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
	}

	private abstract inner class AbstractTicket(val targetState: ChunkState) : ITicket {
		final override val id: Int = nextTicketID++
		final override val pos: ChunkPos
			get() = this@ServerChunk.pos

		final override var isCanceled: Boolean = false
		final override val chunk = CompletableFuture<ServerChunk>()

		init {
			if (this@ServerChunk.state >= targetState) {
				chunk.complete(this@ServerChunk)
			}
		}

		final override fun cancel() {
			if (isCanceled) return

			ticketsLock.withLock {
				if (isCanceled) return
				isCanceled = true
				chunk.cancel(false)
				listener = null
				cancel0()
			}
		}

		protected abstract fun cancel0()
		final override var listener: IChunkListener? = null
	}

	private inner class Ticket(state: ChunkState) : AbstractTicket(state) {
		init {
			permanent.add(this)

			this@ServerChunk.targetState.trySend(targetState)
		}

		override fun cancel0() {
			permanent.remove(this)
		}
	}

	private inner class TimedTicket(expiresAt: Int, state: ChunkState) : AbstractTicket(state), ITimedTicket {
		var expiresAt = expiresAt + ticks

		override val timeRemaining: Int
			get() = (expiresAt - ticks).coerceAtLeast(0)

		init {
			temporary.add(this)
			temporaryList.add(this)
			this@ServerChunk.targetState.trySend(targetState)
		}

		override fun cancel0() {
			temporary.remove(this)
			temporaryList.remove(this)
		}

		override fun prolong(ticks: Int) {
			if (ticks == 0 || isCanceled) return

			ticketsLock.withLock {
				if (isCanceled) return

				temporary.remove(this)
				expiresAt += ticks

				if (timeRemaining > 0) {
					temporary.add(this)
				} else {
					temporaryList.remove(this)
				}
			}
		}
	}

	private fun bumpState(newState: ChunkState) {
		if (newState == state) return
		require(newState >= state) { "Tried to downgrade $this state from $state to $newState" }
		this.state = newState

		permanent.forEach { if (it.targetState <= state) it.chunk.complete(this) }
		temporaryList.forEach { if (it.targetState <= state) it.chunk.complete(this) }
	}

	data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null)

	fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult {
		val cell = cells.value[pos.x, pos.y]

		if (cell.tile(isBackground).material.value.isMeta) {
			return DamageResult(TileDamageResult.NONE)
		}

		var damage = damage
		var result = TileDamageResult.NORMAL

		if (cell.dungeonId in world.protectedDungeonIDs) {
			damage = damage.copy(type = TileDamageType.PROTECTED)
			result = TileDamageResult.PROTECTED
		}

		val health = if (isBackground) backgroundHealth.computeIfAbsent(pos) { TileHealth.Tile() } else foregroundHealth.computeIfAbsent(pos) { TileHealth.Tile() }
		val tile = cell.tile(isBackground)

		val params = if (!damage.type.isPenetrating && tile.modifier.value.breaksWithTile) {
			tile.material.value.actualDamageTable + tile.modifier.value.actualDamageTable
		} else {
			tile.material.value.actualDamageTable
		}

		health.damage(params, sourcePosition, damage)

		if (health.isDead) {
			val drops = ArrayList<ItemDescriptor>()

			val copyHealth = health.copy()
			val mCell = cell.mutable()
			val mTile = mCell.tile(isBackground)

			if (health.isHarvested && mTile.material.value.itemDrop != null) {
				drops.add(ItemDescriptor(mTile.material.value.itemDrop!!, 1L))
			}

			mTile.material = BuiltinMetaMaterials.EMPTY
			mTile.color = TileColor.DEFAULT
			mTile.hueShift = 0f

			if (tile.modifier.value.breaksWithTile) {
				if (health.isHarvested && mTile.modifier.value.itemDrop != null) {
					drops.add(ItemDescriptor(mTile.modifier.value.itemDrop!!, 1L))
				}

				mTile.modifier = BuiltinMetaMaterials.EMPTY_MOD
			}

			for (item in drops) {
				val entity = ItemDropEntity(item)
				entity.position = (pos + this.pos.tile).toDoubleVector() + Vector2d(0.5, 0.5)
				entity.joinWorld(world)
			}

			if (isBackground && cell.foreground.material.isEmptyTile) {
				val info = world.template.cellInfo(pos + this.pos.tile)

				if (info.oceanLiquid.isNotEmptyLiquid && !info.encloseLiquids && pos.y < info.oceanLiquidLevel) {
					mCell.liquid.setInfinite(info.oceanLiquid.entry!!, (info.oceanLiquidLevel - pos.y).toFloat())
				}
			}

			setCell(pos.x, pos.y, mCell.immutable())
			health.reset()
			onTileHealthUpdate(pos.x, pos.y, isBackground, health)
			return DamageResult(result, copyHealth, cell)
		} else {
			onTileHealthUpdate(pos.x, pos.y, isBackground, health)
			return DamageResult(result, health, cell)
		}
	}

	fun tileDamagePackets(): List<TileDamageUpdatePacket> {
		val result = ArrayList<TileDamageUpdatePacket>()

		for ((pos, health) in backgroundHealth) {
			if (!health.isHealthy) {
				result.add(TileDamageUpdatePacket(this.pos.tileX + pos.x, this.pos.tileY + pos.y, true, health))
			}
		}

		for ((pos, health) in foregroundHealth) {
			if (!health.isHealthy) {
				result.add(TileDamageUpdatePacket(this.pos.tileX + pos.x, this.pos.tileY + pos.y, false, health))
			}
		}

		return result
	}

	override fun tick(delta: Double) {
		ticks++

		ticketsLock.withLock {
			while (temporary.isNotEmpty() && temporary.first().timeRemaining <= 0) {
				val ticket = temporary.first()
				ticket.isCanceled = true
				temporary.remove(ticket)
				temporaryList.remove(ticket)
			}
		}

		var shouldUnload = !isBusy && temporary.isEmpty() && permanent.isEmpty() && world.template.worldParameters !is FloatingDungeonWorldParameters

		if (shouldUnload) {
			idleTicks++
			// don't load-save-load-save too frequently
			// also make partially-generated chunks stay in memory for way longer, because re-generating
			// them is very costly operation
			shouldUnload = if (state == ChunkState.FULL) idleTicks > 600 else idleTicks > 32000
		} else {
			idleTicks = 0
		}

		if (shouldUnload) {
			// unload()
			return
		}

		if (state != ChunkState.FULL)
			return

		super.tick(delta)

		foregroundHealth.entries.removeIf { (pos, health) ->
			val (x, y) = pos
			val remove = !health.tick(cells.value[x, y].foreground.material.value.actualDamageTable, delta)
			onTileHealthUpdate(x, y, false, health)
			remove
		}

		backgroundHealth.entries.removeIf { (pos, health) ->
			val (x, y) = pos
			val remove = !health.tick(cells.value[x, y].background.material.value.actualDamageTable, delta)
			onTileHealthUpdate(x, y, true, health)
			remove
		}
	}

	fun unload() {
		if (isUnloaded)
			return

		isUnloaded = true
		loadJob.cancel()
		targetState.close()

		if (state == ChunkState.FULL) {
			val unloadable = world.entityIndex
				.query(
					aabbd,
					filter = Predicate { it.isPersistent && !it.isRemote && aabbd.isInside(it.position) },
					withEdges = false)

			world.storage.saveCells(pos, copyCells())
			world.storage.saveEntities(pos, unloadable)

			unloadable.forEach {
				it.remove(AbstractEntity.RemovalReason.UNLOADED)
			}
		}

		world.chunkMap.remove(pos)
	}

	fun cancelLoadJob() {
		loadJob.cancel()
	}

	fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
		if (cells.isInitialized()) {
			val cells = cells.value
			return Object2DArray(width, height) { a, b -> cells[a, b].toLegacyNet() }
		} else {
			return Object2DArray(width, height, AbstractCell.NULL.toLegacyNet())
		}
	}

	private fun prepareCells() {
		val cells = cells.value

		for (x in 0 until width) {
			for (y in 0 until height) {
				val info = world.template.cellInfo(pos.tileX + x, pos.tileY + y)

				val state = cells[x, y].mutable()

				state.blockBiome = info.blockBiomeIndex
				state.envBiome = info.environmentBiomeIndex

				if (state.foreground.material.isNullTile) {
					state.foreground.material = info.foreground.entry ?: BuiltinMetaMaterials.EMPTY
					state.foreground.color = TileColor.DEFAULT
					state.foreground.hueShift = info.blockBiome?.hueShift(state.foreground.material) ?: 0f

					if (state.foreground.material.supportsModifier(info.foregroundMod)) {
						state.foreground.modifier = info.foregroundMod.entry ?: BuiltinMetaMaterials.EMPTY_MOD
						state.foreground.modifierHueShift = info.blockBiome?.hueShift(info.foregroundMod) ?: 0f
					}
				}

				if (state.background.material.isNullTile) {
					state.background.material = info.background.entry ?: BuiltinMetaMaterials.EMPTY
					state.background.color = TileColor.DEFAULT
					state.background.hueShift = info.blockBiome?.hueShift(state.background.material) ?: 0f

					if (state.background.material.supportsModifier(info.backgroundMod)) {
						state.background.modifier = info.backgroundMod.entry ?: BuiltinMetaMaterials.EMPTY_MOD
						state.background.modifierHueShift = info.blockBiome?.hueShift(info.backgroundMod) ?: 0f
					}
				}

				if (!state.foreground.material.isEmptyTile) {
					state.liquid.reset()
				} else if (y < info.oceanLiquidLevel && info.oceanLiquid.isPresent && !info.oceanLiquid.value!!.isMeta) {
					val pressure = (info.oceanLiquidLevel - y).toFloat()

					if (state.background.material.isEmptyTile) {
						state.liquid.pressure = pressure
						state.liquid.isInfinite = true
						state.liquid.state = info.oceanLiquid.entry!!
					} else if (info.encloseLiquids) {
						state.liquid.pressure = pressure
						state.liquid.isInfinite = false
						state.liquid.level = 1f
						state.liquid.state = info.oceanLiquid.entry!!
					}
				}

				cells[x, y] = state.immutable()
			}
		}
	}

	private fun finalizeCells() {
		val cells = cells.value

		for (x in 0 until width) {
			for (y in 0 until height) {
				val cell = cells[x, y].mutable()
				val info by lazy { world.template.cellInfo(pos.tileX + x, pos.tileY + y) }

				if (cell.liquid.isInfinite) {
					// make sure that ocean liquid never exists on tiles without empty
					// background (except in real dungeons)
					if (cell.dungeonId < FIRST_RESERVED_DUNGEON_ID && cell.background.material.isNotEmptyTile) {
						cell.liquid.isInfinite = false
					}

					// pressurize liquid under the ocean
					if (info.oceanLiquid.isNotEmptyLiquid && pos.tileY + y < info.oceanLiquidLevel) {
						cell.liquid.pressure = cell.liquid.pressure.coerceAtLeast((info.oceanLiquidLevel - (pos.tileY + y)).toFloat())
					}
				}

				if (cell.foreground.material.isMetaTile) {
					cell.foreground.color = TileColor.DEFAULT
				}

				if (cell.background.material.isMetaTile) {
					cell.background.color = TileColor.DEFAULT
				}

				replaceBiomeBlocks(cell, info)
				cells[x, y] = cell.immutable()
			}
		}
	}

	private fun doReplaceBiomeTile(tile: MutableTileState, biome: Biome?) {
		// TODO: Maybe somehow expand this biome meta material list?
		// that's only 6 meta blocks in total!

		val indexOf = BuiltinMetaMaterials.BIOME_META_MATERIALS.indexOf(tile.material)

		if (indexOf != -1) {
			if (indexOf == 0) {
				tile.material = biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
			} else {
				tile.material = biome?.subBlocks?.getOrNull(indexOf - 1)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
			}

			tile.hueShift = biome?.hueShift(tile.material) ?: tile.hueShift
		}

		if (biome == null && tile.modifier == BuiltinMetaMaterials.BIOME_MOD) {
			tile.modifier = BuiltinMetaMaterials.EMPTY_MOD
			tile.modifierHueShift = 0f
		}
	}

	fun replaceBiomeBlocks(cell: MutableCell, info: WorldTemplate.CellInfo) {
		doReplaceBiomeTile(cell.foreground, info.blockBiome)
		doReplaceBiomeTile(cell.background, info.blockBiome)
	}

	private fun replaceBiomeBlocks() {
		val cells = cells.value

		for (x in 0 until width) {
			for (y in 0 until height) {
				val cell = cells[x, y].mutable()
				replaceBiomeBlocks(cell, world.template.cellInfo(pos.tileX + x, pos.tileY + y))
				cells[x, y] = cell.immutable()
			}
		}
	}

	private val missingDungeonNames = ObjectAVLTreeSet<String>()

	private suspend fun placeMicroDungeons() {
		val placements = CompletableFuture.supplyAsync(Supplier {
			val placements = ArrayList<BiomePlaceables.Placement>()

			for (x in 0 until width) {
				for (y in 0 until height) {
					placements.addAll(world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y))
				}
			}

			placements.sortByDescending { it.priority }
			placements
		}, Starbound.EXECUTOR).await()

		val bounds = AABBi(
			pos.tile - Vector2i(CHUNK_SIZE_FF, CHUNK_SIZE_FF),
			pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF)
		)

		for (placement in placements) {
			if (placement.item is BiomePlaceables.MicroDungeon) {
				if (placement.item.microdungeons.isEmpty())
					continue // ???

				val seed = world.template.seedFor(placement.position.x, placement.position.y)
				val random = random(seed)
				val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size))

				if (dungeon.isEmpty) {
					if (missingDungeonNames.add(dungeon.key.left()))
						LOGGER.error("Tried to place dungeon ${dungeon.key.left()}, but there is no such dungeon.")
				} else {
					val anchors = dungeon.value!!.validAnchors(world)

					if (anchors.isEmpty())
						continue

					val anchor = anchors.random(random)

					for (dy in MICRODUNGEON_PLACEMENT_SHIFTS) {
						val pos = placement.position - anchor.anchor + Vector2i(y = dy)

						if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY))
							continue

						val collision = anchor.reader.walkTiles<Boolean> { x, y, tile ->
							if (tile.usesPlaces && world.getCell(pos.x + x, pos.y + y).dungeonId != NO_DUNGEON_ID) {
								return@walkTiles KOptional(true)
							}

							return@walkTiles KOptional()
						}.orElse(false)

						if (!collision && anchor.canPlace(pos.x, pos.y, world)) {
							try {
								dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await()
							} catch (err: Throwable) {
								LOGGER.error("Error while generating microdungeon ${dungeon.key.left()} at $pos", err)
							}

							break
						}
					}
				}
			}
		}
	}

	private fun placeGrass() {
		val cells = cells.value

		for (x in 0 until width) {
			for (y in 0 until height) {
				val biome = world.template.cellInfo(pos.tileX + x, pos.tileY + y).blockBiome ?: continue
				val cell = cells[x, y]

				// determine layer for grass mod calculation
				val isBackground = cell.foreground.material.isEmptyTile

				val tile = cell.tile(isBackground)
				val tileInv = cell.tile(!isBackground)

				// don't place mods in dungeons unless explicitly specified, also don't
				// touch non-grass mods
				if (
					tile.modifier == BuiltinMetaMaterials.BIOME_MOD ||
					tile.modifier == BuiltinMetaMaterials.UNDERGROUND_BIOME_MOD ||
					(cell.dungeonId == NO_DUNGEON_ID && tile.modifier == BuiltinMetaMaterials.EMPTY_MOD)
				) {
					// check whether we're floor or ceiling

					// NOTE: we are querying other chunks while generating,
					// and we might read stale data if we are reading neighbouring chunks
					// since they might be in process of generation, too
					// (only if they are in process of generating someing significant, which modify terrain)
					// shouldn't be an issue though
					val cellAbove = world.getCell(pos.tileX + x, pos.tileY + y + 1)
					val cellBelow = world.getCell(pos.tileX + x, pos.tileY + y - 1)

					val isFloor = (!cell.foreground.material.isEmptyTile && cellAbove.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellAbove.background.material.isEmptyTile)
					val isCeiling = !isFloor && ((!cell.foreground.material.isEmptyTile && cellBelow.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellBelow.background.material.isEmptyTile))

					// get the appropriate placeables for above/below ground
					val placeables = if (isFloor && !cellAbove.background.material.isEmptyTile || isCeiling && !cellBelow.background.material.isEmptyTile) {
						biome.undergroundPlaceables
					} else {
						biome.surfacePlaceables
					}

					// determine the proper grass mod or lack thereof
					var grassMod = BuiltinMetaMaterials.EMPTY_MOD

					if (isFloor) {
						if (staticRandomDouble(world.template.seed, pos.tileX + x, pos.tileY + y, "grass") <= placeables.grassModDensity) {
							grassMod = placeables.grassMod.native.entry ?: BuiltinMetaMaterials.EMPTY_MOD
						}
					} else if (isCeiling) {
						if (staticRandomDouble(world.template.seed, pos.tileX + x, pos.tileY + y, "grass") <= placeables.ceilingGrassModDensity) {
							grassMod = placeables.ceilingGrassMod.native.entry ?: BuiltinMetaMaterials.EMPTY_MOD
						}
					}

					val modify = cell.mutable()

					if (isBackground) {
						modify.background.modifier = grassMod
						modify.foreground.modifier = BuiltinMetaMaterials.EMPTY_MOD
					} else {
						modify.foreground.modifier = grassMod
						modify.background.modifier = BuiltinMetaMaterials.EMPTY_MOD
					}

					modify.background.modifierHueShift = biome.hueShift(modify.background.modifier)
					modify.foreground.modifierHueShift = biome.hueShift(modify.foreground.modifier)

					cells[x, y] = modify.immutable()
				}
			}
		}
	}

	// this doesn't exactly place liquids inside this chunk, it places
	// them in this or any neighbouring chunk
	// quite lame, since this fact makes it not thread-safe
	// unless we make chunk map thread-safe
	// TODO: make necessary changes for it to be thread safe?
	// idea: fetch neighbour chunks and pass localized view
	// of these chunks
	// One big BUT: This function takes minuscule time to execute,
	// is it necessary to be parallelized?
	private fun generateLiquid() {
		val samplePointX = pos.tileX + width / 2
		val samplePointY = pos.tileY + height / 2

		val blockInfo = world.template.cellInfo(samplePointX, samplePointY)

		val fillLiquid = blockInfo.caveLiquid
		val fillMicrodungeons = blockInfo.fillMicrodungeons
		val encloseLiquids = blockInfo.encloseLiquids

		// nothing to fill
		if (fillLiquid.isEmptyLiquid)
			return

		val seedDensity = blockInfo.caveLiquidSeedDensity

		if (seedDensity < 0.0)
			return

		// hashset instead of objectopenhashset because
		// we don't care about memory footprint, only performance
		var openNodes = HashSet<Vector2i>()

		run {
			val frequency = (100 / seedDensity).toInt()

			// TODO: I don't know what to do with this code yet,
			// but original sources have weird for() loop here
			// (trying to do float division using integers?)

			val yMin = frequency * (pos.tileY / frequency)
			val xMin = frequency * (pos.tileX / frequency)

			for (x in (xMin until (pos.tileX + width)).step(frequency)) {
				for (y in (yMin until (pos.tileY + height)).step(frequency)) {
					if (
						x in pos.tileX until pos.tileX + width &&
						y in pos.tileY until pos.tileY + height
					) {
						openNodes.add(Vector2i(x, y))
					}
				}
			}
		}

		if (openNodes.isEmpty())
			return

		// Here be dragons, because I have no idea what original code
		// is trying to achieve, mostly because the style it is written in
		// suggests it was written WAY early in game development

		var badNodes = HashSet<Vector2i>()
		val candidateNodes = HashSet<Vector2i>()

		// this gives a rectangle of this chunk plus all neighbours BUT the very last border cell
		val bounds = AABBi(pos.tile - Vector2i(CHUNK_SIZE_FF, CHUNK_SIZE_FF), pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF))

		// prohibits from opening 2nd border cell
		for (x in bounds.mins.x .. bounds.maxs.x) {
			badNodes.add(Vector2i(x, bounds.mins.y))
			badNodes.add(Vector2i(x, bounds.maxs.y))
		}

		for (y in bounds.mins.y .. bounds.maxs.y) {
			badNodes.add(Vector2i(bounds.mins.x, y))
			badNodes.add(Vector2i(bounds.maxs.x, y))
		}

		fun propose(position: Vector2i) {
			if (!bounds.isInside(position) || position in badNodes || position in candidateNodes) {
				return
			}

			val cell = world.chunkMap.getCell(position)

			if (cell.foreground.material.isNotEmptyTile) {
				// Not sure why this doesn't poison solid materials, but it does (occasionally) encounter that case
				// TODO: check if that is still the case
				if (!cell.foreground.material.value.collisionKind.isSolidCollision) {
					badNodes.add(position)
				}

				return
			}

			if (
				(cell.dungeonId != NO_DUNGEON_ID && (!fillMicrodungeons || cell.dungeonId != MICRO_DUNGEON_ID)) ||
				(!encloseLiquids && cell.background.material.isEmptyTile) ||
				(cell.liquid.state != fillLiquid && cell.liquid.state != BuiltinMetaMaterials.NO_LIQUID)
			) {
				badNodes.add(position)
				return
			}

			candidateNodes.add(position)
			openNodes.add(position)
		}

		while (openNodes.isNotEmpty()) {
			val oldNodes = openNodes
			openNodes = HashSet()

			// TODO: thats a weird choice of candidates
			// maybe this can be improved?
			for (node in oldNodes) {
				propose(node + Vector2i(-1, 0))
				propose(node + Vector2i(1, 0))
				propose(node + Vector2i(0, -1))
			}
		}

		val visitedNodes = HashSet<Vector2i>()

		fun poison(position: Vector2i) {
			if (!bounds.isInside(position) || !visitedNodes.add(position)) {
				return
			}

			val cell = world.chunkMap.getCell(position)

			if (cell.foreground.material.isEmptyTile) {
				badNodes.add(position)
			}
		}

		while (badNodes.isNotEmpty()) {
			val oldNodes = badNodes
			badNodes = HashSet()

			// TODO: same as above
			for (node in oldNodes) {
				candidateNodes.remove(node)
				poison(node + Vector2i(-1, 0))
				poison(node + Vector2i(1, 0))
				poison(node + Vector2i(0, 1)) // upwards, not downwards
			}
		}

		val solidSurroundings = HashSet(candidateNodes)

		fun solids(position: Vector2i) {
			val cell = world.chunkMap.getCell(position)

			if (cell.foreground.material.isNotEmptyTile) {
				solidSurroundings.add(position)
			}
		}

		for (position in candidateNodes) {
			solids(position + Vector2i(1, 0))
			solids(position + Vector2i(-1, 0))
			solids(position + Vector2i(0, 1))
			solids(position + Vector2i(0, -1))
		}

		val biomeBlock = blockInfo.blockBiome?.mainBlock?.native ?: BuiltinMetaMaterials.EMPTY.ref
		val drops = HashMap<Vector2i, Float>()

		run {
			val openSet = HashSet(candidateNodes)

			while (openSet.isNotEmpty()) {
				val cluster = HashSet<Vector2i>()
				val openCluster = HashSet<Vector2i>()

				openCluster.add(openSet.first())

				while (openCluster.isNotEmpty()) {
					val node = openCluster.first()
					openCluster.remove(node)

					if (openSet.remove(node)) {
						cluster.add(node)

						openCluster.add(world.geometry.wrap(Vector2i(node.x, node.y + 1)))
						openCluster.add(world.geometry.wrap(Vector2i(node.x, node.y - 1)))
						openCluster.add(world.geometry.wrap(Vector2i(node.x + 1, node.y)))
						openCluster.add(world.geometry.wrap(Vector2i(node.x - 1, node.y)))
					}
				}

				var maxY = Int.MIN_VALUE
				var minY = Int.MAX_VALUE

				for (droplet in cluster) {
					if (
						solidSurroundings.contains(droplet + Vector2i.POSITIVE_X) &&
						solidSurroundings.contains(droplet + Vector2i.NEGATIVE_X) &&
						solidSurroundings.contains(droplet + Vector2i.NEGATIVE_Y)
					) {
						if (droplet.y > maxY) {
							maxY = droplet.y
						}

						if (!solidSurroundings.contains(droplet + Vector2i.POSITIVE_Y)) {
							if (droplet.y < minY) {
								minY = droplet.y
							}
						}
					} else {
						if (droplet.y <= minY) {
							minY = droplet.y - 1
						}
					}
				}

				val liquidLevel = minY.coerceAtMost(maxY)

				for (node in cluster) {
					val pressure = liquidLevel - node.y

					if (pressure >= 0)
						drops[node] = 1.0f + pressure
				}
			}
		}

		for ((position, pressure) in drops.entries) {
			val cell = world.getCell(position).mutable()

			cell.liquid.state = fillLiquid.entry!!
			cell.liquid.level = 1.0f
			cell.liquid.pressure = pressure

			if (encloseLiquids && cell.background.material.isEmptyTile) {
				cell.background.material = biomeBlock.entry ?: BuiltinMetaMaterials.EMPTY
			}

			check(world.setCell(position, cell.immutable())) {
				"Failed to set cave liquid at $position to $fillLiquid"
			}
		}
	}

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

		private val MICRODUNGEON_PLACEMENT_SHIFTS = immutableList {
			accept(0)

			for (i in 1 .. 4) accept(i)
			for (i in 1 .. 4) accept(-i)
		}
	}
}
