package ru.dbotthepony.kstarbound.lua.bindings

import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
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.lib.ArgumentIterator
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.item.RecipeRegistry
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.VersionRegistry
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.lua.LUA_HINT_ARRAY
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.StateMachine
import ru.dbotthepony.kstarbound.lua.createJsonArray
import ru.dbotthepony.kstarbound.lua.createJsonObject
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.indexSetNoYield
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaFunctionN
import ru.dbotthepony.kstarbound.lua.luaFunctionNS
import ru.dbotthepony.kstarbound.lua.luaStub
import ru.dbotthepony.kstarbound.lua.nextOptionalFloat
import ru.dbotthepony.kstarbound.lua.nextOptionalInteger
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLuaInteger
import ru.dbotthepony.kstarbound.util.random.random
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.isNotEmpty
import kotlin.collections.random
import kotlin.collections.set
import kotlin.collections.withIndex
import kotlin.math.max

private val LOGGER = LogManager.getLogger()

private fun <T : Any> lookup(registry: Registry<T>, key: Any?): Registry.Entry<T>? {
	if (key is ByteString) {
		return registry[key.decode()]
	} else if (key is Number) {
		return registry[key.toInt()]
	} else {
		return null
	}
}

private fun <T : Any> lookupStrict(registry: Registry<T>, key: Any?): Registry.Entry<T> {
	if (key is ByteString) {
		return registry[key.decode()] ?: throw LuaRuntimeException("No such ${registry.name}: $key")
	} else if (key is Number) {
		return registry[key.toInt()] ?: throw LuaRuntimeException("No such ${registry.name}: $key")
	} else {
		throw LuaRuntimeException("No such ${registry.name}: $key")
	}
}

private val evalFunction = luaFunction { name: ByteString, value: Double ->
	val fn = Registries.jsonFunctions[name.decode()] ?: throw LuaRuntimeException("No such function $name")
	returnBuffer.setTo(fn.value.evaluate(value))
}

private val evalFunction2 = luaFunction { name: ByteString, value: Double, value2: Double ->
	val fn = Registries.json2Functions[name.decode()] ?: throw LuaRuntimeException("No such function $name")
	returnBuffer.setTo(fn.value.evaluate(value, value2))
}

private val imageSize = luaFunction { name: ByteString ->
	val ref = SpriteReference.create(name.decode())
	val sprite = ref.sprite ?: throw LuaRuntimeException("No such image or sprite $ref")
	returnBuffer.setTo(tableOf(sprite.width, sprite.height))
}

private fun imageSpaces(context: ExecutionContext, arguments: ArgumentIterator): StateMachine {
	val machine = StateMachine()

	val name = arguments.nextString()
	val image = Image.get(name.decode()) ?: throw LuaRuntimeException("No such image $name")

	val pixelOffset = machine.loadVector2i(arguments.nextTable())
	val fillFactor = arguments.nextFloat()
	val flip = arguments.nextOptionalBoolean(false)

	return machine
		.add {
			val values = image.worldSpaces(pixelOffset.get(), fillFactor, flip)
			val table = context.newTable(values.size, 0)

			for ((i, value) in values.withIndex()) {
				table.rawset(i.toLong() + 1, value)
			}
		}
}

private fun nonEmptyRegion(context: ExecutionContext, name: ByteString) {
	val image = Image.get(name.decode()) ?: throw LuaRuntimeException("No such image $name")
	context.returnBuffer.setTo(context.from(image.nonEmptyRegion))
}

private fun registryDef(registry: Registry<*>): LuaFunction<ByteString, *, *, *, *> {
	return luaFunction { name ->
		val value = registry[name.decode()] ?: throw LuaRuntimeException("No such NPC type $name")
		returnBuffer.setTo(from(value.json))
	}
}

private fun registryDef2(registry: Registry<*>): LuaFunction<Any?, *, *, *, *> {
	return luaFunction { name ->
		val def = lookup(registry, name)

		if (def != null) {
			returnBuffer.setTo(newTable(0, 2).also {
				it["path"] = def.file?.computeFullPath()
				it["config"] = from(def.json)
			})
		} else {
			returnBuffer.setTo()
		}
	}
}

private fun registryDefExists(registry: Registry<*>): LuaFunction<ByteString, *, *, *, *> {
	return luaFunction { name ->
		returnBuffer.setTo(name.decode() in registry)
	}
}

private val recipesForItem = luaFunction { name: ByteString ->
	val list = RecipeRegistry.output2recipes[name.decode()]

	if (list == null) {
		returnBuffer.setTo(newTable())
	} else {
		returnBuffer.setTo(newTable(list.size, 0).also {
			for ((i, v) in list.withIndex()) {
				it.rawset(i + 1L, from(v.json))
			}
		})
	}
}

private fun getMatchingTenants(context: ExecutionContext, tags: Table) {
	val actualTags = Object2IntOpenHashMap<String>()

	for ((k, v) in tags) {
		if (k is ByteString && v is Number) {
			actualTags[k.decode()] = v.toInt()
		}
	}

	val result = Registries.tenants.keys.values
		.stream()
		.filter { it.value.test(actualTags) }
		.sorted { a, b -> b.value.compareTo(a.value) }
		.map { context.from(it.json) }
		.toList()

	context.returnBuffer.setTo(context.newTable(result.size, 0).also {
		for ((k, v) in result.withIndex()) {
			it[k + 1] = v
		}
	})
}

private val liquidStatusEffects = luaFunction { id: Any ->
	val liquid = lookup(Registries.liquid, id)

	if (liquid == null) {
		returnBuffer.setTo(tableOf())
	} else {
		returnBuffer.setTo(tableOf(*liquid.value.statusEffects.map { it.key.map({ it }, { it }) }.toTypedArray()))
	}
}

private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIterator) {
	val tile = lookup(Registries.tiles, arguments.nextAny())
	val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null))

	if (mod != null && mod.value.miningSounds.map({ it.isNotEmpty() }, { true })) {
		context.returnBuffer.setTo(mod.value.miningSounds.map({ it.random() }, { it }))
		return
	}

	if (tile != null && tile.value.miningSounds.map({ it.isNotEmpty() }, { true })) {
		context.returnBuffer.setTo(tile.value.miningSounds.map({ it.random() }, { it }))
		return
	}

	// original engine parity
	context.returnBuffer.setTo("")
}

private fun materialFootstepSound(context: ExecutionContext, arguments: ArgumentIterator) {
	val tile = lookup(Registries.tiles, arguments.nextAny())
	val mod = lookup(Registries.tiles, arguments.nextOptionalAny(null))

	if (mod != null && mod.value.footstepSound.map({ it.isNotEmpty() }, { true })) {
		context.returnBuffer.setTo(mod.value.footstepSound.map({ it.random() }, { it }))
		return
	}

	if (tile != null && tile.value.footstepSound.map({ it.isNotEmpty() }, { true })) {
		context.returnBuffer.setTo(tile.value.footstepSound.map({ it.random() }, { it }))
		return
	}

	context.returnBuffer.setTo(Globals.client.defaultFootstepSound.map({ it }, { it.random() }))
}

private val materialHealth = luaFunction { id: Any ->
	returnBuffer.setTo(lookupStrict(Registries.tiles, id).value.actualDamageTable.totalHealth)
}

private val liquidName = luaFunction { id: Any ->
	returnBuffer.setTo(lookupStrict(Registries.liquid, id).key)
}

private val liquidId = luaFunction { id: Any ->
	returnBuffer.setTo(lookupStrict(Registries.liquid, id).id)
}

private val techType = luaFunction { id: Any ->
	returnBuffer.setTo(lookupStrict(Registries.techs, id).value.type)
}

private val techConfig = luaFunction { id: Any ->
	returnBuffer.setTo(lookupStrict(Registries.techs, id).json)
}

private val jobject = luaFunction { returnBuffer.setTo(createJsonObject()) }
private val jarray = luaFunction { returnBuffer.setTo(createJsonArray()) }

private val jremove = luaFunction {  self: Table, key: Any ->
	val nils = self.metatable?.rawget("__nils") as? Table

	if (nils != null) {
		nils[key] = 0L
	}

	self[key] = null as Any?
}

private val jsize = luaFunction { self: Table ->
	var elemCount = 0L
	var highestIndex = 0L
	var hintList = false

	val meta = self.metatable

	if (meta != null) {
		if (meta["__typehint"] == LUA_HINT_ARRAY) {
			hintList = true
		}

		val nils = meta["__nils"]

		if (nils is Table) {
			for ((k, v) in nils) {
				val ik = k.toLuaInteger()

				if (ik != null) {
					highestIndex = max(ik, highestIndex)
				} else {
					hintList = false
				}
			}
		}
	}

	for ((k, v) in self) {
		val ik = k.toLuaInteger()

		if (ik != null) {
			highestIndex = max(ik, highestIndex)
		} else {
			hintList = false
		}

		elemCount++
	}

	if (hintList) {
		returnBuffer.setTo(highestIndex)
	} else {
		returnBuffer.setTo(elemCount)
	}
}

// why is this a thing?
private val jresize = luaFunction { self: Table, target: Long ->
	val nils = self.metatable?.rawget("__nils") as? Table

	if (nils != null) {
		val keysToRemove = ArrayList<Any>()

		for ((k, v) in nils) {
			val ik = k.toLuaInteger()

			if (ik != null && ik > 0L && ik > target)
				keysToRemove.add(k)
		}

		for (k in keysToRemove) {
			nils[k] = null as Any?
		}
	}

	val keysToRemove = ArrayList<Any>()

	for ((k, v) in self) {
		val ik = k.toLuaInteger()

		if (ik != null && ik > 0L && ik > target)
			keysToRemove.add(k)
	}

	for (k in keysToRemove) {
		self[k] = null as Any?
	}

	indexSetNoYield(self, target, indexNoYield(self, target))
}

private val assetJson = luaFunction { path: ByteString ->
	returnBuffer.setTo(from(Starbound.loadJsonAsset(path.decode()).get()))
}

private val makeCurrentVersionedJson = luaFunction { identifier: ByteString, content: Any? ->
	returnBuffer.setTo(from(VersionRegistry.make(identifier.decode(), toJsonFromLua(content)).toJson()))
}

private val loadVersionedJson = luaFunction { data: Any?, identifier: ByteString ->
	returnBuffer.setTo(from(VersionRegistry.load(identifier.decode(), toJsonFromLua(data))))
}

private val createBiome = luaFunction { name: ByteString, seed: Number, verticalMidPoint: Number, threatLevel: Number ->
	try {
		val biome = Registries.biomes
			.getOrThrow(name.decode())
			.value
			.create(random(seed.toLong()), verticalMidPoint.toInt(), threatLevel.toDouble())

		returnBuffer.setTo(from(Starbound.gson.toJsonTree(biome)))
	} catch (err: Throwable) {
		LOGGER.error("Exception while creating biome for Lua script of name $name with seed $seed, with verticalMidPoint $verticalMidPoint, with threatLevel $threatLevel", err)
	}
}

private val treeStemDirectory = luaFunction { name: ByteString ->
	returnBuffer.setTo(Registries.treeStemVariants[name.decode()]?.file?.computeDirectory(true) ?: "/")
}

private val treeFoliageDirectory = luaFunction { name: ByteString ->
	returnBuffer.setTo(Registries.treeFoliageVariants[name.decode()]?.file?.computeDirectory(true) ?: "/")
}

private val itemConfig = luaFunction { descriptor: Any, level: Number?, seed: Number? ->
	val desc = ItemDescriptor(descriptor)

	if (desc.name !in ItemRegistry) {
		returnBuffer.setTo()
	} else {
		val (config, params) = desc.buildConfig(level?.toDouble(), seed?.toLong())

		returnBuffer.setTo(tableMapOf(
			"directory" to ItemRegistry[desc.name].directory,
			"config" to config.map({ from(it) }, { it }),
			"parameters" to params.map({ from(it) }, { it })
		))
	}
}

// why
private val createItem = luaFunction { descriptor: Any, level: Number?, seed: Number? ->
	val desc = ItemDescriptor(descriptor)

	if (desc.name !in ItemRegistry) {
		throw LuaRuntimeException("No such item ${desc.name}")
	} else {
		val (_, params) = desc.buildConfig(level?.toDouble(), seed?.toLong())
		val tab = desc.toTable(this)

		if (tab != null)
			tab["parameters"] = params.map({ from(it) }, { it })

		returnBuffer.setTo(tab)
	}
}

private val itemType = luaFunction { identifier: ByteString ->
	returnBuffer.setTo(ItemRegistry[identifier.decode()].type.jsonName)
}

private val itemTags = luaFunction { identifier: ByteString ->
	returnBuffer.setTo(tableOf(*ItemRegistry[identifier.decode()].itemTags.toTypedArray()))
}

private val itemHasTag = luaFunction { identifier: ByteString, tag: ByteString ->
	returnBuffer.setTo(tag.decode() in ItemRegistry[identifier.decode()].itemTags)
}

fun provideRootBindings(lua: LuaEnvironment) {
	val table = lua.newTable()
	lua.globals["root"] = table
	lua.globals["jobject"] = jobject
	lua.globals["jarray"] = jarray
	lua.globals["jremove"] = jremove
	lua.globals["jsize"] = jsize
	lua.globals["jresize"] = jresize

	table["assetJson"] = assetJson
	table["makeCurrentVersionedJson"] = makeCurrentVersionedJson
	table["loadVersionedJson"] = loadVersionedJson

	table["evalFunction"] = evalFunction
	table["evalFunction2"] = evalFunction2
	table["imageSize"] = imageSize
	table["imageSpaces"] = luaFunctionNS("imageSpaces", ::imageSpaces)
	table["nonEmptyRegion"] = luaFunction(::nonEmptyRegion)
	table["npcConfig"] = registryDef(Registries.npcTypes)

	table["npcVariant"] = luaStub("npcVariant")
	table["projectileGravityMultiplier"] = luaStub("projectileGravityMultiplier")
	table["projectileConfig"] = registryDef(Registries.projectiles)

	table["recipesForItem"] = recipesForItem
	table["itemType"] = itemType
	table["itemTags"] = itemTags
	table["itemHasTag"] = itemHasTag
	table["itemConfig"] = itemConfig
	table["createItem"] = createItem

	table["tenantConfig"] = registryDef(Registries.tenants)

	table["getMatchingTenants"] = luaFunction(::getMatchingTenants)
	table["liquidStatusEffects"] = liquidStatusEffects

	table["generateName"] = luaFunction { asset: ByteString, seed: Number? ->
		returnBuffer.setTo(Starbound.generateName(asset.decode(), if (seed == null) lua.random else random(seed.toLong())))
	}

	table["questConfig"] = registryDef(Registries.questTemplates)

	table["npcPortrait"] = luaStub("npcPortrait")
	table["monsterPortrait"] = luaStub("monsterPortrait")
	table["npcPortrait"] = luaStub("npcPortrait")

	table["isTreasurePool"] = registryDefExists(Registries.treasurePools)

	table["createTreasure"] = luaFunction { pool: ByteString, level: Number, seed: Number? ->
		val get = Registries.treasurePools[pool.decode()] ?: throw LuaRuntimeException("No such treasure pool $pool")
		val random = if (seed == null) lua.random else random(seed.toLong())
		returnBuffer.setTo(tableOf(*get.value.evaluate(random, level.toDouble()).filter { it.isNotEmpty }.map { from(it.toJson()) }.toTypedArray()))
	}

	table["materialMiningSound"] = luaFunctionN("materialMiningSound", ::materialMiningSound)
	table["materialFootstepSound"] = luaFunctionN("materialFootstepSound", ::materialFootstepSound)
	table["materialHealth"] = materialHealth

	table["materialConfig"] = registryDef2(Registries.tiles)
	table["modConfig"] = registryDef2(Registries.tileModifiers)

	table["liquidName"] = liquidName
	table["liquidId"] = liquidId

	table["createBiome"] = createBiome

	table["monsterSkillParameter"] = luaStub("monsterSkillParameter")
	table["monsterParameters"] = luaStub("monsterParameters")
	table["monsterMovementSettings"] = luaStub("monsterMovementSettings")

	table["hasTech"] = registryDefExists(Registries.techs)
	table["techType"] = techType
	table["techConfig"] = techConfig

	table["treeStemDirectory"] = treeStemDirectory
	table["treeFoliageDirectory"] = treeFoliageDirectory

	table["collection"] = luaStub("collection")
	table["collectables"] = luaStub("collectables")
	table["elementalResistance"] = luaStub("elementalResistance")
	table["dungeonMetadata"] = luaStub("dungeonMetadata")
	table["behavior"] = luaStub("behavior")
}
