package ru.dbotthepony.kstarbound.lua.bindings

import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kommons.collect.toList
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.contains
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaFunctionN
import ru.dbotthepony.kstarbound.lua.luaStub
import ru.dbotthepony.kstarbound.lua.nextOptionalInteger
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLine2d
import ru.dbotthepony.kstarbound.lua.toPoly
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.unpackAsArray
import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayFilterResult
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.TileRayFilter
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.Collections
import java.util.EnumSet
import java.util.function.Predicate
import java.util.stream.Collectors
import kotlin.math.PI

private val directionalAngles = intArrayOf(4, 8, 12, 0, 2, 6, 10, 14, 1, 3, 7, 5, 15, 13, 9, 11).let { arr ->
	Array(arr.size) { Vector2d.angle(arr[it] * PI / 8.0) }
}

private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPoly: Poly, position: Vector2d, maximumCorrection: Double, collisions: Set<CollisionType>) {
	val poly = originalPoly + position

	data class Entry(val poly: Poly, val center: Vector2d, var distance: Double = 0.0) : Comparable<Entry> {
		override fun compareTo(other: Entry): Int {
			return distance.compareTo(other.distance)
		}
	}

	val tiles = ObjectArrayList<Entry>()

	for (tile in self.queryTileCollisions(poly.aabb.enlarge(maximumCorrection + 1.0, maximumCorrection + 1.0))) {
		if (tile.type in collisions) {
			tiles.add(Entry(tile.poly, tile.poly.centre))
		}
	}

	fun separate(loops: Int, axis: Vector2d?): Vector2d? {
		var body = poly
		var correction = Vector2d.ZERO

		for (i in 0 until loops) {
			val center = body.centre

			for (tile in tiles)
				tile.distance = tile.center.distanceSquared(center)

			tiles.sort()

			var anyIntersects = false

			for (tile in tiles) {
				val intersection = tile.poly.intersect(body, axis)

				if (intersection != null) {
					anyIntersects = true
					body += intersection.vector
					correction += intersection.vector

					if (correction.length >= maximumCorrection)
						return null
				}
			}

			if (!anyIntersects)
				return correction
		}

		for (tile in tiles) {
			if (tile.poly.intersect(body) != null) {
				return null
			}
		}

		return correction
	}

	var trySeparate = separate(10, null)

	// First try any-directional SAT separation for two loops
	if (trySeparate != null) {
		returnBuffer.setTo(from(position + trySeparate))
		return
	}

	// Then, try direction-limiting SAT in cardinals, then 45 degs, then in
	// between, for 16 total angles in a circle.
	// TODO: if "any direction" separation fails, then this will surely fail too?
	for (axis in directionalAngles) {
		trySeparate = separate(4, axis)

		if (trySeparate != null) {
			returnBuffer.setTo(from(position + trySeparate))
			return
		}
	}

	returnBuffer.setTo()
}

private fun ExecutionContext.returnLiquid(liquid: AbstractLiquidState, returnNames: Boolean?) {
	if (returnNames == true) {
		returnBuffer.setTo(tableOf(liquid.state.key, liquid.level))
	} else if (liquid.state.id != null) {
		returnBuffer.setTo(tableOf(liquid.state.id, liquid.level))
	}
}

private val LOGGER = LogManager.getLogger()

fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
	val callbacks = lua.newTable()
	lua.globals["world"] = callbacks

	callbacks["flyingType"] = luaFunction {
		returnBuffer.setTo(self.sky.flyingType.jsonName)
	}

	callbacks["warpPhase"] = luaFunction {
		returnBuffer.setTo(self.sky.warpPhase.jsonName)
	}

	callbacks["skyTime"] = luaFunction {
		returnBuffer.setTo(self.sky.time)
	}

	callbacks["magnitude"] = luaFunction { arg1: Table, arg2: Table? ->
		if (arg2 == null) {
			returnBuffer.setTo(toVector2d(arg1).length)
		} else {
			returnBuffer.setTo(self.geometry.diff(toVector2d(arg1), toVector2d(arg2)).length)
		}
	}

	callbacks["polyContains"] = luaFunction { poly: Table, position: Table ->
		returnBuffer.setTo(self.geometry.polyContains(toPoly(poly), toVector2d(position)))
	}

	callbacks["xwrap"] = luaFunction { position: Any ->
		if (position is Number) {
			returnBuffer.setTo(self.geometry.x.cell(position.toDouble()))
		} else {
			returnBuffer.setTo(from(self.geometry.wrap(toVector2d(position))))
		}
	}

	callbacks["ywrap"] = luaFunction { position: Any ->
		if (position is Number) {
			returnBuffer.setTo(self.geometry.y.cell(position.toDouble()))
		} else {
			returnBuffer.setTo(from(self.geometry.wrap(toVector2d(position))))
		}
	}

	callbacks["nearestTo"] = luaFunction { pos0: Any, pos1: Any ->
		if (pos1 is Number) {
			val source = if (pos0 is Number) pos0.toDouble() else toVector2d(pos0).x
			returnBuffer.setTo(self.geometry.x.nearestTo(source, pos1.toDouble()))
		} else {
			val source = if (pos0 is Number) Vector2d(pos0.toDouble()) else toVector2d(pos0)
			returnBuffer.setTo(self.geometry.nearestTo(source, toVector2d(pos1)))
		}
	}

	callbacks["rectCollision"] = luaFunction { rect: Table, collisions: Table? ->
		if (collisions == null) {
			returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type.isSolidCollision }).findAny().isPresent)
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent)
		}
	}

	callbacks["pointCollision"] = luaFunction { rect: Table, collisions: Table? ->
		if (collisions == null) {
			returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type.isSolidCollision }))
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type in actualCollisions }))
		}
	}

	callbacks["pointTileCollision"] = luaFunction { rect: Table, collisions: Table? ->
		val cell = self.getCell(toVector2i(rect))

		if (collisions == null) {
			returnBuffer.setTo(cell.foreground.material.value.collisionKind.isSolidCollision)
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			returnBuffer.setTo(cell.foreground.material.value.collisionKind in actualCollisions)
		}
	}

	callbacks["lineTileCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
		if (collisions == null) {
			returnBuffer.setTo(self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind.isSolidCollision) RayFilterResult.HIT else RayFilterResult.SKIP }).traversedTiles.isNotEmpty())
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			returnBuffer.setTo(self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in actualCollisions) RayFilterResult.HIT else RayFilterResult.SKIP }).traversedTiles.isNotEmpty())
		}
	}

	callbacks["lineTileCollisionPoint"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
		val result = if (collisions == null) {
			self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind.isSolidCollision) RayFilterResult.HIT else RayFilterResult.SKIP })
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in actualCollisions) RayFilterResult.HIT else RayFilterResult.SKIP })
		}

		if (result.hitTile == null) {
			returnBuffer.setTo()
		} else {
			returnBuffer.setTo(tableOf(from(result.hitTile.borderCross), from(result.hitTile.normal.normal)))
		}
	}

	callbacks["rectTileCollision"] = luaFunction { rect: Table, collisions: Table? ->
		if (collisions == null) {
			returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }))
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions }))
		}
	}

	callbacks["lineCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
		val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())

		val result = self.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions }

		if (result == null) {
			returnBuffer.setTo()
		} else {
			returnBuffer.setTo(from(result.border), from(result.normal))
		}
	}

	callbacks["polyCollision"] = luaFunction { rect: Table, translate: Table?, collisions: Table? ->
		if (collisions == null) {
			returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent)
		} else {
			val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
			returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent)
		}
	}

	callbacks["collisionBlocksAlongLine"] = luaFunction {  pos0: Table, pos1: Table, collisions: Table?, limit: Number? ->
		val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
		var actualLimit = limit?.toInt() ?: Int.MAX_VALUE

		if (actualLimit < 0)
			actualLimit = Int.MAX_VALUE

		val result = self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY ->
			if (cell.foreground.material.value.collisionKind in actualCollisions) {
				val tlimit = --actualLimit

				if (tlimit > 0) {
					RayFilterResult.CONTINUE
				} else if (tlimit == 0) {
					RayFilterResult.HIT
				} else {
					RayFilterResult.BREAK
				}
			} else {
				RayFilterResult.SKIP
			}
		}

		returnBuffer.setTo(tableOf(*result.traversedTiles.map { from(it.pos) }.toTypedArray()))
	}

	callbacks["liquidAlongLine"] = luaFunction { pos0: Table, pos1: Table ->
		val liquid = newTable()
		var i = 1L

		self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY ->
			if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f && cell.liquid.state.id != null) {
				liquid[i++] = tableOf(tableOf(x, y), tableOf(cell.liquid.state.id, cell.liquid.level.toDouble()))
			}

			RayFilterResult.SKIP
		}

		returnBuffer.setTo(liquid)
	}

	callbacks["liquidNamesAlongLine"] = luaFunction { pos0: Table, pos1: Table ->
		val liquid = newTable()
		var i = 1L

		self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY ->
			if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) {
				liquid[i++] = tableOf(tableOf(x, y), tableOf(cell.liquid.state.key, cell.liquid.level.toDouble()))
			}

			RayFilterResult.SKIP
		}

		returnBuffer.setTo(liquid)
	}

	callbacks["resolvePolyCollision"] = luaFunction { poly: Table, position: Table, maximumCorrection: Number, collisions: Table? ->
		val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
		resolvePolyCollision(self, toPoly(poly), toVector2d(position), maximumCorrection.toDouble(), actualCollisions)
	}

	callbacks["tileIsOccupied"] = luaFunction { pos: Table, isForeground: Boolean?, includeEmphemeral: Boolean? ->
		val cell = self.getCell(toVector2i(pos))

		if (cell.tile(isForeground == false).material.isNotEmptyTile) {
			returnBuffer.setTo(true)
		} else {
			returnBuffer.setTo(self.entityIndex.tileEntitiesAt(toVector2i(pos)).any { !it.isEphemeral || includeEmphemeral == true })
		}
	}

	callbacks["placeObject"] = luaFunction { type: ByteString, pos0: Table, objectDirection: Number?, parameters: Table? ->
		val pos = toVector2i(pos0)

		try {
			val prototype = Registries.worldObjects[type.decode()] ?: throw LuaRuntimeException("No such object $type")
			var direction = Direction.RIGHT

			if (objectDirection != null && objectDirection.toLong() < 0L)
				direction = Direction.LEFT

			val json = if (parameters == null) JsonObject() else parameters.toJson(true) as JsonObject
			val orientation = prototype.value.findValidOrientation(self, pos, direction)

			if (orientation == -1) {
				LOGGER.debug("Lua script tried to place object {} at {}, but it can't be placed there!", prototype.key, pos)
				returnBuffer.setTo(false)
			} else {
				val create = WorldObject.create(prototype, pos, json)
				create?.orientationIndex = orientation.toLong()
				create?.joinWorld(self)
				returnBuffer.setTo(create != null)
			}
		} catch (err: Throwable) {
			LOGGER.error("Exception while placing world object $type at $pos", err)
			returnBuffer.setTo(false)
		}
	}

	callbacks["spawnItem"] = luaFunctionN("spawnItem") {
		val itemType = toJsonFromLua(it.nextAny())
		val pos = toVector2d(it.nextTable())
		val inputCount = it.nextOptionalInteger() ?: 1L
		val inputParameters = toJsonFromLua(it.nextOptionalAny(null))
		val initialVelocity = toVector2d(it.nextOptionalAny(tableOf(0L, 0L)))
		val intangibleTime = it.nextOptionalAny(null)

		try {
			val descriptor: ItemDescriptor

			if (itemType is JsonObject) {
				descriptor = ItemDescriptor(itemType)
			} else {
				descriptor = ItemDescriptor(itemType.asString, inputCount, if (inputParameters.isJsonNull) JsonObject() else inputParameters.asJsonObject)
			}

			if (descriptor.isEmpty) {
				LOGGER.debug("Lua script tried to create non existing item {} at {}", itemType, pos)
				returnBuffer.setTo()
			} else {
				val create = ItemDropEntity(descriptor)
				create.movement.velocity = initialVelocity

				if (intangibleTime is Number) {
					create.intangibleTimer = GameTimer(intangibleTime.toDouble())
				}

				create.joinWorld(self)
				returnBuffer.setTo(create.entityID)
			}
		} catch (err: Throwable) {
			LOGGER.error("Exception while creating item $itemType at $pos", err)
			returnBuffer.setTo()
		}
	}

	callbacks["spawnTreasure"] = luaFunction { position: Table, pool: ByteString, level: Number, seed: Number? ->
		val entities = IntArrayList()

		try {
			val items = Registries.treasurePools
				.getOrThrow(pool.decode())
				.value
				// not using lua.random because we are, well, world's bindings
				.evaluate(if (seed != null) random(seed.toLong()) else self.random, level.toDouble())

			val pos = toVector2d(position)

			for (item in items) {
				val entity = ItemDropEntity(item)
				entity.position = pos
				entity.joinWorld(self)
				entities.add(entity.entityID)
			}
		} catch (err: Throwable) {
			LOGGER.error("Exception while spawning treasure from $pool at $position", err)
		}

		returnBuffer.setTo(tableOf(*entities.toTypedArray()))
	}

	callbacks["spawnMonster"] = luaFunction {
		// TODO
		returnBuffer.setTo(0)
	}
	callbacks["spawnNpc"] = luaStub("spawnNpc")
	callbacks["spawnStagehand"] = luaStub("spawnStagehand")
	callbacks["spawnProjectile"] = luaStub("spawnProjectile")
	callbacks["spawnVehicle"] = luaStub("spawnVehicle")

	callbacks["threatLevel"] = luaFunction { returnBuffer.setTo(self.template.threatLevel) }
	callbacks["time"] = luaFunction { returnBuffer.setTo(self.sky.time) }
	callbacks["day"] = luaFunction { returnBuffer.setTo(self.sky.day) }
	callbacks["timeOfDay"] = luaFunction { returnBuffer.setTo(self.sky.timeOfDay) }
	callbacks["dayLength"] = luaFunction { returnBuffer.setTo(self.sky.dayLength) }

	callbacks["getProperty"] = luaFunction { name: ByteString, orElse: Any? ->
		returnBuffer.setTo(from(self.getProperty(name.decode()) { toJsonFromLua(orElse) }))
	}

	callbacks["setProperty"] = luaFunction { name: ByteString, value: Any? ->
		self.setProperty(name.decode(), toJsonFromLua(value))
	}

	callbacks["liquidAt"] = luaFunction { posOrRect: Table ->
		if (posOrRect[1L] is Number) {
			val cell = self.getCell(toVector2i(posOrRect))

			if (cell.liquid.state.isNotEmptyLiquid) {
				returnLiquid(cell.liquid, false)
			}
		} else {
			val level = self.averageLiquidLevel(toAABB(posOrRect))

			if (level != null && level.type.id != null) {
				returnBuffer.setTo(tableOf(level.type.id, level.average))
			}
		}
	}

	callbacks["liquidNameAt"] = luaFunction { posOrRect: Table  ->
		if (posOrRect[1L] is Number) {
			val cell = self.getCell(toVector2i(posOrRect))

			if (cell.liquid.state.isNotEmptyLiquid) {
				returnLiquid(cell.liquid, true)
			}
		} else {
			val level = self.averageLiquidLevel(toAABB(posOrRect))

			if (level != null) {
				returnBuffer.setTo(tableOf(level.type.key, level.average))
			}
		}
	}

	callbacks["gravity"] = luaFunction { pos: Table ->
		returnBuffer.setTo(self.gravityAt(toVector2d(pos)).y)
	}

	callbacks["gravityVector"] = luaFunction { pos: Table ->
		returnBuffer.setTo(from(self.gravityAt(toVector2d(pos))))
	}

	callbacks["spawnLiquidPromise"] = luaFunction { pos: Table, liquid: Any, quantity: Number ->
		val action = TileModification.Pour(if (liquid is ByteString) Registries.liquid.ref(liquid.decode()) else Registries.liquid.ref((liquid as Number).toInt()), quantity.toFloat())

		returnBuffer.setTo(
			LuaFuture(
				future = self.applyTileModifications(listOf(toVector2i(pos) to action), false)
					.thenApply { it.isEmpty() },
				isLocal = false
			)
		)
	}

	callbacks["spawnLiquid"] = luaFunction { pos: Table, liquid: Any, quantity: Number ->
		val action = TileModification.Pour(if (liquid is ByteString) Registries.liquid.ref(liquid.decode()) else Registries.liquid.ref((liquid as Number).toInt()), quantity.toFloat())
		returnBuffer.setTo(self.applyTileModifications(listOf(toVector2i(pos) to action), false).thenApply { it.isEmpty() }.getNow(true))
	}

	callbacks["destroyLiquid"] = luaFunction { pos: Table ->
		val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f)
		val cell = self.getCell(toVector2i(pos))
		self.applyTileModifications(listOf(toVector2i(pos) to action), false)

		if (cell.liquid.state.isNotEmptyLiquid)
			returnLiquid(cell.liquid, false)
	}

	callbacks["destroyNamedLiquid"] = luaFunction { pos: Table ->
		val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f)
		val cell = self.getCell(toVector2i(pos))
		self.applyTileModifications(listOf(toVector2i(pos) to action), false)

		if (cell.liquid.state.isNotEmptyLiquid)
			returnLiquid(cell.liquid, true)
	}

	callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.isDungeonIDProtected(self.getCell(toVector2i(pos)).dungeonId)) }

	callbacks["findPlatformerPath"] = luaStub("findPlatformerPath")
	callbacks["platformerPathStart"] = luaStub("platformerPathStart")

	callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName ?: "unknown") }
	callbacks["size"] = luaFunction { returnBuffer.setTo(from(self.geometry.size)) }
	callbacks["inSurfaceLayer"] = luaFunction { pos: Table -> returnBuffer.setTo(self.template.isSurfaceLayer(toVector2i(pos))) }
	callbacks["surfaceLevel"] = luaFunction { returnBuffer.setTo(self.template.surfaceLevel) }
	callbacks["terrestrial"] = luaFunction { returnBuffer.setTo(self.template.worldParameters is TerrestrialWorldParameters) }

	callbacks["itemDropItem"] = luaFunction { id: Number ->
		returnBuffer.setTo(from((self.entities[id.toInt()] as? ItemDropEntity)?.item?.toJson()))
	}

	callbacks["biomeBlocksAt"] = luaFunction { pos: Table, returnNames: Boolean? ->
		val info = self.template.cellInfo(toVector2i(pos))
		val blocks = tableOf()
		var i = 1L

		val biome = info.blockBiome

		if (biome != null) {
			biome.mainBlock.native.entry?.id?.let { blocks[i++] = it }
			biome.subBlocks.forEach { it.native.entry?.id?.let { blocks[i++] = it } }
		}

		returnBuffer.setTo(blocks)
	}

	callbacks["biomeBlockNamesAt"] = luaFunction { pos: Table, returnNames: Boolean? ->
		val info = self.template.cellInfo(toVector2i(pos))
		val blocks = tableOf()
		var i = 1L

		val biome = info.blockBiome

		if (biome != null) {
			biome.mainBlock.native.entry?.key?.let { blocks[i++] = it }
			biome.subBlocks.forEach { it.native.entry?.key?.let { blocks[i++] = it } }
		}

		returnBuffer.setTo(blocks)
	}

	callbacks["dungeonId"] = luaFunction { pos: Table -> returnBuffer.setTo(self.getCell(toVector2i(pos)).dungeonId) }

	provideWorldEntitiesBindings(self, callbacks, lua)
	provideWorldEnvironmentalBindings(self, callbacks, lua)

	if (self is ServerWorld) {
		provideServerWorldBindings(self, callbacks, lua)
	}
}
