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.lib.ArgumentIterator
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kommons.collect.toList
import ru.dbotthepony.kstarbound.Registries
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.isEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
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.tableFrom
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.tileAreaBrush
import java.util.concurrent.CompletableFuture

private val foregroundStr = ByteString.of("foreground")
private val backgroundStr = ByteString.of("background")

private fun isBackground(layer: ByteString): Boolean {
	return if (layer == backgroundStr)
		true
	else if (layer == foregroundStr)
		false
	else
		throw LuaRuntimeException("Invalid tile layer $layer")
}

private fun ExecutionContext.damageTilesImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<TileDamageResult> {
	val positions = it.nextTable().iterator().map { toVector2i(it.value) }.toList()
	val isBackground = isBackground(it.nextString())

	val sourcePosition = toVector2d(it.nextTable())
	val damageType = TileDamageType.entries.valueOf(it.nextString().decode())
	val damage = it.nextFloat()
	val harvestLevel = it.nextOptionalInteger()?.toInt() ?: 999
	val sourceEntity = self.entities[it.nextOptionalInteger()?.toInt() ?: 0]

	return self.damageTiles(positions, isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity)
}

private fun ExecutionContext.damageTileAreaImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<TileDamageResult> {
	val center = toVector2i(it.nextTable())
	val radius = it.nextFloat()
	val isBackground = isBackground(it.nextString())

	val sourcePosition = toVector2d(it.nextTable())
	val damageType = TileDamageType.entries.valueOf(it.nextString().decode())
	val damage = it.nextFloat()
	val harvestLevel = it.nextOptionalInteger()?.toInt() ?: 999
	val sourceEntity = self.entities[it.nextOptionalInteger()?.toInt() ?: 0]

	return self.damageTiles(tileAreaBrush(center, radius), isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity)
}

private fun ExecutionContext.placeMaterialImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<List<Vector2i>> {
	val pos = toVector2i(it.nextTable())
	val isBackground = isBackground(it.nextString())
	val material = Registries.tiles.getOrThrow(it.nextString().decode())
	val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f }
	val allowOverlap = it.nextBoolean()

	val action = TileModification.PlaceMaterial(isBackground, material.ref, hueShift)

	// original engine return here doesn't make any sense whatsoever
	return self.applyTileModifications(listOf(pos to action), allowOverlap, false).thenApply { it.map { it.first } }
}

private fun ExecutionContext.placeModImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<List<Vector2i>> {
	val pos = toVector2i(it.nextTable())
	val isBackground = isBackground(it.nextString())
	val material = Registries.tileModifiers.getOrThrow(it.nextString().decode())
	val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f }
	val allowOverlap = it.nextBoolean()

	val action = TileModification.PlaceModifier(isBackground, material.ref, hueShift)

	// original engine return here doesn't make any sense whatsoever
	return self.applyTileModifications(listOf(pos to action), allowOverlap, false).thenApply { it.map { it.first } }
}

fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) {
	callbacks["lightLevel"] = luaStub("lightLevel")
	callbacks["windLevel"] = luaStub("windLevel")

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

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

	callbacks["material"] = luaFunction { pos: Table, layer: ByteString ->
		val isBackground = isBackground(layer)
		val tile = self.getCell(toVector2i(pos)).tile(isBackground)

		if (tile.material.isNullTile) {
			returnBuffer.setTo()
		} else if (tile.material.isEmptyTile) {
			returnBuffer.setTo(false)
		} else {
			returnBuffer.setTo(tile.material.key)
		}

	}
	callbacks["mod"] = luaFunction { pos: Table, layer: ByteString ->
		val isBackground = isBackground(layer)
		val tile = self.getCell(toVector2i(pos)).tile(isBackground)

		if (tile.modifier.isNotEmptyModifier) {
			returnBuffer.setTo(tile.modifier.key)
		}
	}

	callbacks["materialHueShift"] = luaFunction { pos: Table, layer: ByteString ->
		val isBackground = isBackground(layer)
		val tile = self.getCell(toVector2i(pos)).tile(isBackground)
		returnBuffer.setTo(tile.hueShift)
	}

	callbacks["modHueShift"] = luaFunction { pos: Table, layer: ByteString ->
		val isBackground = isBackground(layer)
		val tile = self.getCell(toVector2i(pos)).tile(isBackground)
		returnBuffer.setTo(tile.modifierHueShift)
	}

	callbacks["materialColor"] = luaFunction { pos: Table, layer: ByteString ->
		val isBackground = isBackground(layer)
		val tile = self.getCell(toVector2i(pos)).tile(isBackground)
		returnBuffer.setTo(tile.color.ordinal)
	}

	callbacks["materialColorName"] = luaFunction { pos: Table, layer: ByteString ->
		val isBackground = isBackground(layer)
		val tile = self.getCell(toVector2i(pos)).tile(isBackground)
		returnBuffer.setTo(tile.color.jsonName)
	}

	callbacks["setMaterialColor"] = luaFunction { pos: Table, layer: ByteString, color: Any ->
		val isBackground = isBackground(layer)

		val actualColor = if (color is Number)
			TileColor.entries[color.toInt()]
		else if (color is ByteString)
			TileColor.entries.valueOf(color.decode())
		else
			throw LuaRuntimeException("Unknown tile color $color")

		val actualPos = toVector2i(pos)
		val cell = self.getCell(actualPos).mutable()
		cell.tile(isBackground).color = actualColor
		returnBuffer.setTo(self.setCell(actualPos, cell))
	}

	callbacks["oceanLevel"] = luaFunction { pos: Table ->
		returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel)
	}

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

	callbacks["damageTiles"] = luaFunctionN("damageTiles") {
		returnBuffer.setTo(damageTilesImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE)
	}

	callbacks["damageTilesPromise"] = luaFunctionN("damageTilesPromise") {
		returnBuffer.setTo(
			LuaFuture(
				future = damageTilesImpl(self, it).thenApply { it.jsonName },
				isLocal = false
			)
		)
	}

	callbacks["damageTileArea"] = luaFunctionN("damageTileArea") {
		returnBuffer.setTo(damageTileAreaImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE)
	}

	callbacks["damageTileAreaPromise"] = luaFunctionN("damageTileAreaPromise") {
		returnBuffer.setTo(
			LuaFuture(
				future = damageTileAreaImpl(self, it).thenApply { it.jsonName },
				isLocal = false
			)
		)
	}

	callbacks["placeMaterial"] = luaFunctionN("placeMaterial") {
		returnBuffer.setTo(placeMaterialImpl(self, it).getNow(listOf()).isEmpty())
	}

	callbacks["placeMaterialPromise"] = luaFunctionN("placeMaterialPromise") {
		returnBuffer.setTo(
			LuaFuture(
				future = placeMaterialImpl(self, it).thenApply { tableFrom(it.map { from(it) }) },
				isLocal = false
			)
		)
	}

	callbacks["placeMod"] = luaFunctionN("placeMod") {
		returnBuffer.setTo(placeModImpl(self, it).getNow(listOf()).isEmpty())
	}

	callbacks["placeModPromise"] = luaFunctionN("placeModPromise") {
		returnBuffer.setTo(
			LuaFuture(
				future = placeModImpl(self, it).thenApply { tableFrom(it.map { from(it) }) },
				isLocal = false
			)
		)
	}
}
