package ru.dbotthepony.kstarbound.lua.bindings

import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.LoungeOrientation
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
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.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
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.vector.Vector2d
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.stream
import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ActorEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.InspectableEntity
import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject
import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.*
import java.util.function.Predicate

private enum class EntityBoundMode(override val jsonName: String) : IStringSerializable {
	META_BOUNDING_BOX("MetaBoundBox"),
	COLLISION_AREA("CollisionArea"),
	POSITION("Position")
}

private val callScriptStr = ByteString.of("callScript")
private val callScriptArgsStr = ByteString.of("callScriptArgs")
private val callScriptResultStr = ByteString.of("callScriptResult")
private val lineStr = ByteString.of("line")
private val polyStr = ByteString.of("poly")
private val rectStr = ByteString.of("rect")
private val withoutEntityIdStr = ByteString.of("withoutEntityId")
private val includedTypesStr = ByteString.of("includedTypes")
private val radiusStr = ByteString.of("radius")
private val centerStr = ByteString.of("center")
private val boundModeStr = ByteString.of("boundMode")
private val orderStr = ByteString.of("order")

private fun ExecutionContext.entityQueryImpl(self: World<*, *>, options: Table, predicate: Predicate<AbstractEntity> = Predicate { true }): Table {
	val withoutEntityId = (indexNoYield(options, withoutEntityIdStr) as Number?)?.toInt()

	val includedTypes = EnumSet.allOf(EntityType::class.java)
	val getIncludedTypes = indexNoYield(options, includedTypesStr) as Table?

	if (getIncludedTypes != null) {
		includedTypes.clear()

		for ((_, v) in getIncludedTypes) {
			when (val t = (v as ByteString).decode()) {
				"mobile" -> {
					includedTypes.add(EntityType.PLAYER)
					includedTypes.add(EntityType.MONSTER)
					includedTypes.add(EntityType.NPC)
					includedTypes.add(EntityType.PROJECTILE)
					includedTypes.add(EntityType.ITEM_DROP)
					includedTypes.add(EntityType.VEHICLE)
				}

				"creature" -> {
					includedTypes.add(EntityType.PLAYER)
					includedTypes.add(EntityType.MONSTER)
					includedTypes.add(EntityType.NPC)
				}

				else -> {
					includedTypes.add(EntityType.entries.valueOf(t))
				}
			}
		}
	}

	val callScript = (indexNoYield(options, callScriptStr) as ByteString?)?.decode()
	val callScriptArgs = (indexNoYield(options, callScriptArgsStr) as Table?)?.unpackAsArray() ?: arrayOf()
	val callScriptResult = indexNoYield(options, callScriptResultStr) ?: true

	val lineQuery = (indexNoYield(options, lineStr) as Table?)?.let { toLine2d(it) }
	val polyQuery = (indexNoYield(options, polyStr) as Table?)?.let { toPoly(it) }
	val rectQuery = (indexNoYield(options, rectStr) as Table?)?.let { toAABB(it) }

	val radius = indexNoYield(options, radiusStr)

	val radiusQuery = if (radius is Number) {
		val center = toVector2d(indexNoYield(options, centerStr) ?: throw LuaRuntimeException("Specified 'radius', but not 'center'"))
		center to radius.toDouble()
	} else {
		null
	}

	val boundMode = EntityBoundMode.entries.valueOf((indexNoYield(options, boundModeStr) as ByteString?)?.decode() ?: "CollisionArea")

	val innerPredicate = Predicate<AbstractEntity> {
		if (!predicate.test(it)) return@Predicate false
		if (it.type !in includedTypes) return@Predicate false
		if (it.entityID == withoutEntityId) return@Predicate false

		if (callScript != null) {
			if (it !is ScriptedEntity || it.isRemote) return@Predicate false
			val call = it.callScript(callScript, *callScriptArgs)
			if (call.isEmpty() || call[0] != callScriptResult) return@Predicate false
		}

		when (boundMode) {
			EntityBoundMode.META_BOUNDING_BOX -> {
				// If using MetaBoundBox, the regular line / box query methods already
				// enforce collision with MetaBoundBox

				if (radiusQuery != null) {
					return@Predicate self.geometry.rectIntersectsCircle(it.metaBoundingBox, radiusQuery.first, radiusQuery.second)
				}
			}

			EntityBoundMode.COLLISION_AREA -> {
				// Collision area queries either query based on the collision area if
				// that's given, or as a fallback the regular bound box.

				var collisionArea = it.collisionArea

				if (collisionArea.isEmpty)
					collisionArea = it.metaBoundingBox

				if (lineQuery != null)
					return@Predicate self.geometry.lineIntersectsRect(lineQuery, collisionArea)

				if (polyQuery != null)
					return@Predicate self.geometry.polyIntersectsPoly(polyQuery, Poly(collisionArea))

				if (rectQuery != null)
					return@Predicate self.geometry.rectIntersectsRect(rectQuery, collisionArea)

				if (radiusQuery != null)
					return@Predicate self.geometry.rectIntersectsCircle(collisionArea, radiusQuery.first, radiusQuery.second)
			}

			EntityBoundMode.POSITION -> {
				if (lineQuery != null)
					return@Predicate self.geometry.lineIntersectsRect(lineQuery, AABB.rectangle(it.position, 0.0))

				if (polyQuery != null)
					return@Predicate self.geometry.polyContains(polyQuery, it.position)

				if (rectQuery != null)
					return@Predicate self.geometry.rectContains(rectQuery, it.position)

				if (radiusQuery != null)
					return@Predicate self.geometry.diff(radiusQuery.first, it.position).length <= radiusQuery.second
			}
		}

		true
	}

	val entitites = if (lineQuery != null) {
		// TODO: this is wildly inefficient
		self.entityIndex.query(AABB(lineQuery), innerPredicate)
	} else if (polyQuery != null) {
		self.entityIndex.query(polyQuery.aabb, innerPredicate)
	} else if (rectQuery != null) {
		self.entityIndex.query(rectQuery, innerPredicate)
	} else if (radiusQuery != null) {
		self.entityIndex.query(AABB.withSide(radiusQuery.first, radiusQuery.second), innerPredicate)
	} else {
		mutableListOf()
	}

	when (val order = (indexNoYield(options, orderStr) as ByteString?)?.decode()?.lowercase()) {
		null -> {} // do nothing
		"random" -> entitites.shuffle(self.random)
		"nearest" -> {
			val nearestPosition = lineQuery?.p0 ?: polyQuery?.centre ?: rectQuery?.centre ?: radiusQuery?.first ?: Vector2d.ZERO

			entitites.sortWith { o1, o2 ->
				self.geometry.diff(o1.position, nearestPosition).lengthSquared.compareTo(self.geometry.diff(o2.position, nearestPosition).lengthSquared) }
		}
		else -> throw LuaRuntimeException("Unknown entity sort order $order!")
	}

	return tableOf(*entitites.map { it.entityID.toLong() }.toTypedArray())
}

private fun ExecutionContext.intermediateQueryFunction(self: World<*, *>, pos1: Table, pos2OrRadius: Any, options: Table?, predicate: Predicate<AbstractEntity>) {
	val actualOptions = options ?: tableOf()

	if (pos2OrRadius is Number) {
		actualOptions[centerStr] = pos1
		actualOptions[radiusStr] = pos2OrRadius
	} else {
		pos2OrRadius as Table
		actualOptions[rectStr] = tableOf(pos1[1L], pos1[2L], pos2OrRadius[1L], pos2OrRadius[2L])
	}

	returnBuffer.setTo(entityQueryImpl(self, actualOptions, predicate))
}

private fun ExecutionContext.intermediateLineQueryFunction(self: World<*, *>, pos1: Table, pos2: Table, options: Table?, predicate: Predicate<AbstractEntity>) {
	val actualOptions = options ?: tableOf()
	actualOptions[lineStr] = tableOf(pos1, pos2)
	returnBuffer.setTo(entityQueryImpl(self, actualOptions, predicate))
}

private inline fun <reified T : AbstractEntity> createQueryFunction(self: World<*, *>) = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? ->
	intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate { it is T })
}

private inline fun <reified T : AbstractEntity> createLineQueryFunction(self: World<*, *>) = luaFunction { pos1: Table, pos2: Table, options: Table? ->
	intermediateLineQueryFunction(self, pos1, pos2, options, Predicate { it is T })
}

fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) {
	callbacks["entityQuery"] = createQueryFunction<AbstractEntity>(self)
	callbacks["monsterQuery"] = createQueryFunction<AbstractEntity>(self) // TODO
	callbacks["npcQuery"] = createQueryFunction<AbstractEntity>(self) // TODO
	callbacks["itemDropQuery"] = createQueryFunction<ItemDropEntity>(self)
	callbacks["playerQuery"] = createQueryFunction<PlayerEntity>(self)

	callbacks["entityLineQuery"] = createLineQueryFunction<AbstractEntity>(self)
	callbacks["monsterLineQuery"] = createLineQueryFunction<AbstractEntity>(self) // TODO
	callbacks["npcLineQuery"] = createLineQueryFunction<AbstractEntity>(self) // TODO
	callbacks["itemDropLineQuery"] = createLineQueryFunction<ItemDropEntity>(self)
	callbacks["playerLineQuery"] = createLineQueryFunction<PlayerEntity>(self)

	callbacks["objectQuery"] = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? ->
		var objectName: String? = null

		if (options != null)
			objectName = (indexNoYield(options, "name") as ByteString?)?.decode()

		intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate {
			it is WorldObject && (objectName == null || it.config.key == objectName)
		})
	}

	callbacks["objectLineQuery"] = luaFunction { pos1: Table, pos2: Table, options: Table? ->
		var objectName: String? = null

		if (options != null)
			objectName = (indexNoYield(options, "name") as ByteString?)?.decode()

		intermediateLineQueryFunction(self, pos1, pos2, options, Predicate {
			it is WorldObject && (objectName == null || it.config.key == objectName)
		})
	}

	callbacks["loungeableQuery"] = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? ->
		var orientationName: String? = null

		if (options != null)
			orientationName = (indexNoYield(options, "orientation") as ByteString?)?.decode()

		val orientation = when (orientationName) {
			null -> LoungeOrientation.NONE
			else -> LoungeOrientation.entries.valueOf(orientationName)
		}

		intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate {
			it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation)
		})
	}

	callbacks["loungeableLineQuery"] = luaFunction { pos1: Table, pos2: Table, options: Table? ->
		var orientationName: String? = null

		if (options != null)
			orientationName = (indexNoYield(options, "orientation") as ByteString?)?.decode()

		val orientation = when (orientationName) {
			null -> LoungeOrientation.NONE
			else -> LoungeOrientation.entries.valueOf(orientationName!!)
		}

		intermediateLineQueryFunction(self, pos1, pos2, options, Predicate {
			it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation)
		})
	}

	callbacks["objectAt"] = luaFunction { pos: Table ->
		returnBuffer.setTo(self.entityIndex.tileEntityAt(toVector2i(pos))?.entityID)
	}

	callbacks["entityExists"] = luaFunction { id: Number ->
		returnBuffer.setTo(id.toInt() in self.entities)
	}

	callbacks["entityCanDamage"] = luaFunction { source: Number, target: Number ->
		val a = self.entities[source.toInt()]
		val b = self.entities[target.toInt()]

		returnBuffer.setTo(a != null && b != null && a.team.get().canDamage(b.team.get(), a == b))
	}

	callbacks["entityDamageTeam"] = luaFunction { id: Number ->
		returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.entities[id]?.team?.get())))
	}

	callbacks["entityAggressive"] = luaStub("entityAggressive")

	callbacks["entityType"] = luaFunction { id: Number -> returnBuffer.setTo(self.entities[id.toInt()]?.type?.jsonName) }
	callbacks["entityPosition"] = luaFunction { id: Number -> returnBuffer.setTo(from(self.entities[id.toInt()]?.position)) }
	callbacks["entityVelocity"] = luaFunction { id: Number -> returnBuffer.setTo(from((self.entities[id.toInt()] as? DynamicEntity)?.movement?.velocity)) }
	callbacks["entityMetaBoundBox"] = luaFunction { id: Number -> returnBuffer.setTo(from(self.entities[id.toInt()]?.metaBoundingBox)) }
	callbacks["entityCurrency"] = luaFunction { id: Number, currency: ByteString -> returnBuffer.setTo((self.entities[id.toInt()] as? PlayerEntity)?.inventory?.currencies?.get(currency.decode())) }
	callbacks["entityHasCountOfItem"] = luaFunction { id: Number, descriptor: Any, exact: Boolean? ->
		val player = self.entities[id.toInt()] as? PlayerEntity ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(player.inventory.hasCountOfItem(ItemDescriptor(descriptor), exact ?: false))
	}

	callbacks["entityHealth"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? ActorEntity ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(tableOf(entity.health, entity.maxHealth))
	}

	callbacks["entitySpecies"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(entity.species)
	}

	callbacks["entityGender"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(entity.gender.jsonName)
	}

	callbacks["entityName"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()

		// TODO
		when (entity) {
			is ActorEntity -> returnBuffer.setTo(entity.name)
			is WorldObject -> returnBuffer.setTo(entity.config.key)
		}
	}

	callbacks["entityDescription"] = luaFunction { id: Number, species: ByteString? ->
		val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()

		if (entity is InspectableEntity) {
			returnBuffer.setTo(entity.inspectionDescription(species?.decode()))
		} else {
			returnBuffer.setTo(entity.description)
		}
	}

	callbacks["entityPortrait"] = luaFunction { id: Number, mode: ByteString ->
		val entity = self.entities[id.toInt()] as? ActorEntity ?: return@luaFunction returnBuffer.setTo()

		returnBuffer.setTo(tableOf(*entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode.decode())).map { from(it.toJson()) }.toTypedArray()))
	}

	callbacks["entityHandItem"] = luaFunction { id: Number, hand: ByteString ->
		val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()

		when (val gethand = hand.decode().lowercase()) {
			"primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull)
			"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull)
			else -> throw LuaRuntimeException("Unknown tool hand $gethand")
		}
	}

	callbacks["entityHandItemDescriptor"] = luaFunction { id: Number, hand: ByteString ->
		val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()

		when (val gethand = hand.decode().lowercase()) {
			"primary" -> returnBuffer.setTo(entity.primaryHandItem.toTable(this))
			"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.toTable(this))
			else -> throw LuaRuntimeException("Unknown tool hand $gethand")
		}
	}

	callbacks["entityUniqueId"] = luaFunction { id: Number ->
		returnBuffer.setTo(self.entities[id.toInt()]?.uniqueID?.get())
	}

	callbacks["getObjectParameter"] = luaFunction { id: Number, parameter: ByteString, defaultValue: Any? ->
		// FIXME: this is stupid (defaultValue is ignored when we lookup parameter on non existing entity),
		//  but we must retain original behavior
		val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo()
		val result = entity.lookupProperty(parameter.decode())

		if (result.isJsonNull) {
			returnBuffer.setTo(defaultValue)
		} else {
			returnBuffer.setTo(from(result))
		}
	}

	callbacks["getNpcScriptParameter"] = luaStub("getNpcScriptParameter")

	callbacks["objectSpaces"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(tableOf())
		returnBuffer.setTo(tableOf(*entity.occupySpaces.map { from(it - entity.tilePosition) }.toTypedArray()))
	}

	callbacks["farmableStage"] = luaStub("farmableStage")

	callbacks["containerSize"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(entity.items.size)
	}

	callbacks["containerClose"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false)
		// FIXME: this doesn't get networked if called on client
		//  (AND this is the reason why in multiplayer player can't see chest/container open animations when
		//  other players open them)
		entity.closeContainer()
		returnBuffer.setTo(true)
	}

	callbacks["containerOpen"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false)
		// FIXME: this doesn't get networked if called on client
		//  (AND this is the reason why in multiplayer player can't see chest/container open animations when
		//  other players open them)
		entity.openContainer()
		returnBuffer.setTo(true)
	}

	callbacks["containerItems"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(tableOf(*entity.items.filter { it.isNotEmpty }.map { it.toTable(this) }.toTypedArray()))
	}

	callbacks["containerItemAt"] = luaFunction { id: Number, index: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()

		if (index.toInt() < entity.items.size)
			returnBuffer.setTo(entity.items[index.toInt()].toTable(this))
	}

	callbacks["containerConsume"] = luaFunction { id: Number, desc: Any, exactMatch: Boolean? ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(entity.takeItem(ItemDescriptor(desc), exact = exactMatch ?: false).getNow(null))
	}

	callbacks["containerConsumeAt"] = luaFunction { id: Number, slot: Number, amount: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null))
	}

	callbacks["containerAvailable"] = luaFunction { id: Number, desc: Any ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(entity.items.take(ItemDescriptor(desc), simulate = true))
	}

	// why we have containerItems + containerTakeAll, when we could have containerItems + containerClear?????????
	callbacks["containerTakeAll"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		returnBuffer.setTo(tableOf(*entity.clearContainer().getNow(listOf()).map { it.toTable(this) }.toTypedArray()))
	}

	callbacks["containerTakeAt"] = luaFunction { id: Number, slot: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()

		if (slot.toInt() < entity.items.size)
			returnBuffer.setTo(entity.takeItemAt(slot.toInt()).getNow(null))
	}

	callbacks["containerTakeNumItemsAt"] = luaFunction { id: Number, slot: Number, amount: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()

		if (slot.toInt() < entity.items.size)
			returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null))
	}

	callbacks["containerItemsCanFit"] = luaFunction { id: Number, desc: Any ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		val item = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(item.size - entity.items.add(item, simulate = true).first.size)
	}

	callbacks["containerItemsFitWhere"] = luaFunction { id: Number, desc: Any ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
		val item = ItemDescriptor(desc).build(random = lua.random)
		val (leftover, touched) = entity.items.add(item, simulate = true)

		returnBuffer.setTo(tableMapOf(
			"leftover" to leftover.size,
			"slots" to tableOf(*touched.toIntArray())
		))
	}

	callbacks["containerAddItems"] = luaFunction { id: Number, desc: Any ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
		val build = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(entity.addItems(build).getNow(build)?.toTable(this))
	}

	callbacks["containerStackItems"] = luaFunction { id: Number, desc: Any ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
		val build = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(entity.stackWithExisting(build).getNow(build)?.toTable(this))
	}

	callbacks["containerPutItemsAt"] = luaFunction { id: Number, desc: Any, slot: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
		val build = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(entity.putItems(slot.toInt(), build).getNow(build)?.toTable(this))
	}

	callbacks["containerSwapItems"] = luaFunction { id: Number, desc: Any, slot: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
		val build = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = true).getNow(build)?.toTable(this))
	}

	callbacks["containerSwapItemsNoCombine"] = luaFunction { id: Number, desc: Any, slot: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
		val build = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = false).getNow(build)?.toTable(this))
	}

	callbacks["containerItemApply"] = luaFunction { id: Number, desc: Any, slot: Number ->
		val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
		val build = ItemDescriptor(desc).build(random = lua.random)
		returnBuffer.setTo(entity.combineItems(slot.toInt(), build).getNow(build)?.toTable(this))
	}

	callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") {
		val id = it.nextInteger()
		val function = it.nextString().decode()
		val entity = self.entities[id.toInt()] ?: return@luaFunctionN //?: throw LuaRuntimeException("Entity with ID $id does not exist")

		if (entity !is ScriptedEntity)
			throw LuaRuntimeException("$entity is not scripted entity")

		if (entity.isRemote)
			throw LuaRuntimeException("$entity is not owned by this side")

		returnBuffer.setToContentsOf(entity.callScript(function, *it.copyRemaining()))
	}

	callbacks["findUniqueEntity"] = luaFunction { id: ByteString ->
		returnBuffer.setTo(LuaFuture(
			future = self.findUniqueEntity(id.decode()).thenApply { from(it) },
			isLocal = self.isServer
		))
	}

	callbacks["findUniqueEntityAsync"] = luaFunction { id: ByteString ->
		returnBuffer.setTo(LuaFuture(
			future = self.findUniqueEntity(id.decode()).thenApply { from(it) },
			isLocal = false
		))
	}

	callbacks["sendEntityMessage"] = luaFunctionN("sendEntityMessage") {
		val id = it.nextAny()
		val func = it.nextString().decode()

		if (id is Number) {
			val entityID = id.toInt()

			returnBuffer.setTo(LuaFuture(
				future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining()
					.stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) },
				isLocal = Connection.connectionForEntityID(entityID) == self.connectionID
			))
		} else {
			id as ByteString
			val entityID = id.decode()
			val findAlreadyLoaded = self.entities.values.find { it.uniqueID.get() == entityID }

			val isLocal = if (findAlreadyLoaded == null)
				self.isServer
			else
				!findAlreadyLoaded.isRemote

			returnBuffer.setTo(LuaFuture(
				future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining()
					.stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) },
				isLocal = isLocal
			))
		}
	}

	callbacks["loungeableOccupied"] = luaStub("loungeableOccupied")
	callbacks["isMonster"] = luaStub("isMonster")
	callbacks["monsterType"] = luaStub("monsterType")
	callbacks["npcType"] = luaStub("npcType")
	callbacks["stagehandType"] = luaStub("stagehandType")
	callbacks["isNpc"] = luaStub("isNpc")

	callbacks["isEntityInteractive"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()]

		if (entity is InteractiveEntity)
			returnBuffer.setTo(entity.isInteractive)
	}

	callbacks["entityMouthPosition"] = luaFunction { id: Number ->
		val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()
		// original entine returns non nil only for "Chatty entity"
		returnBuffer.setTo(from(entity.mouthPosition))
	}

	callbacks["entityTypeName"] = luaStub("entityTypeName")
}
