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.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.luaStub
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
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.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
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.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
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 fun ExecutionContext.entityQueryImpl(self: World<*, *>, options: Table, predicate: Predicate<AbstractEntity> = Predicate { true }): Table {
	val withoutEntityId = (indexNoYield(options, "withoutEntityId") as Number?)?.toInt()

	val includedTypes = EnumSet.allOf(EntityType::class.java)
	val getIncludedTypes = indexNoYield(options, "includedTypes") 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, "callScript") as ByteString?)?.decode()
	val callScriptArgs = (indexNoYield(options, "callScriptArgs") as Table?)?.unpackAsArray() ?: arrayOf()
	val callScriptResult = indexNoYield(options, "callScriptResult") ?: true

	val lineQuery = (indexNoYield(options, "line") as Table?)?.let { toLine2d(it) }
	val polyQuery = (indexNoYield(options, "poly") as Table?)?.let { toPoly(it) }
	val rectQuery = (indexNoYield(options, "rect") as Table?)?.let { toAABB(it) }

	val radius = indexNoYield(options, "radius")

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

	val boundMode = EntityBoundMode.entries.valueOf((indexNoYield(options, "boundMode") 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, "order") 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["center"] = pos1
		actualOptions["radius"] = pos2OrRadius
	} else {
		pos2OrRadius as Table
		actualOptions["rect"] = 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["line"] = 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()))
	}
}
