package ru.dbotthepony.kstarbound.lua.bindings

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.AbstractFunction1
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.UnresolvedControlThrowable
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
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.toAABBi
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.util.sbIntern
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit

private val LOGGER = LogManager.getLogger()

private class LoadUniqueEntityFunction(val self: ServerWorld) : AbstractFunction1<ByteString>() {
	override fun invoke(context: ExecutionContext, arg1: ByteString) {
		// no, I give up, this shit is beyond bonkers
		// I don't care that mods could break
		// mods WILL break
		// just please, I beg you, for the love of God and Jesus,
		// stop fucking designing async functions as sync ones
		LOGGER.warn("Lua function world.loadUniqueEntity() has been called. If after this line world stop responding, this is the reason. Called in ${LuaRuntimeException("Calling world.loadUniqueEntity()").errorLocation}")
		LOGGER.warn("To modders: Please refer to Lua docs for information on non-busted version of this function, world.loadUniqueEntityAsync()")

		val promise = self.loadUniqueEntity(arg1.decode())

		if (promise.isDone) {
			context.returnBuffer.setTo(promise.get()?.entityID)
		} else {
			try {
				context.pause()
			} catch (err: UnresolvedControlThrowable) {
				err.resolve(this, promise)
			}
		}
	}

	override fun resume(context: ExecutionContext, suspendedState: Any) {
		val promise = suspendedState as CompletableFuture<AbstractEntity?>

		if (promise.isDone) {
			context.returnBuffer.setTo(promise.get()?.entityID)
		} else {
			try {
				context.pause()
			} catch (err: UnresolvedControlThrowable) {
				err.resolve(this, promise)
			}
		}
	}
}

private fun ExecutionContext.placeDungeonImpl(self: ServerWorld, force: Boolean, async: Boolean, name: ByteString, position: Table, dungeonID: Number?, seed: Number?) {
	val pos = toVector2i(position)
	val actualSeed = seed?.toLong() ?: staticRandom64(pos.x, pos.y, "DungeonPlacement")
	val dungeonDef = Registries.dungeons[name.decode()]

	if (dungeonDef == null) {
		LOGGER.error("Lua script tried to spawn dungeon $name, however, no such dungeon exist")

		if (async) {
			returnBuffer.setTo(LuaFuture(
				future = CompletableFuture.failedFuture(NoSuchElementException("No such dungeon: $name")),
				isLocal = false
			))
		} else {
			returnBuffer.setTo(false)
		}
	} else {
		val future = dungeonDef.value.generate(
			self,
			random(actualSeed),
			pos.x, pos.y,
			markSurfaceAndTerrain = false,
			forcePlacement = force,
			dungeonID = dungeonID?.toInt() ?: NO_DUNGEON_ID,
			// Setting scope to eventloop will make dungeon generate synchronous if it is possible
			// It is still possible, though, for dungeon to not generate synchronously if it need to load chunks
			// which are currently not loaded
			scope = if (async) Starbound.GLOBAL_SCOPE else self.eventLoop.scope).thenApply { it.hasGenerated }

		if (async) {
			returnBuffer.setTo(LuaFuture(
				future = future,
				isLocal = false
			))
		} else {
			returnBuffer.setTo(future.getNow(true))
		}
	}
}

fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) {
	callbacks["breakObject"] = luaFunction { id: Number, smash: Boolean? ->
		val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(false)

		if (entity.isRemote) {
			// we can't break objects now owned by us
			returnBuffer.setTo(false)
		} else {
			if (smash == true)
				entity.health = 0.0

			entity.remove(AbstractEntity.RemovalReason.DYING)
			returnBuffer.setTo(true)
		}
	}

	callbacks["isVisibleToPlayer"] = luaFunction { region: Table ->
		val parse = toAABB(region)
		returnBuffer.setTo(self.clients.any { it.isTracking(parse) })
	}

	callbacks["loadRegion"] = luaFunction { region: Table ->
		// FIXME: in original engine this supposedly blocks everything else from happening until region is loaded
		//  And we can't block event loop, because this will make region be unable to be loaded in first place
		//  god damn it
		val parse = toAABB(region)
		// keep in ram for at most 2400 ticks
		val tickets = self.temporaryChunkTicket(parse, 2400).get()
		val future = CompletableFuture.allOf(*tickets.map { it.chunk }.toTypedArray())

		future.thenApply {
			self.eventLoop.schedule(Runnable {
				tickets.forEach { it.cancel() }
			}, 4L, TimeUnit.SECONDS)
		}.exceptionally {
			self.eventLoop.schedule(Runnable {
				tickets.forEach { it.cancel() }
			}, 4L, TimeUnit.SECONDS)
		}

		returnBuffer.setTo(LuaFuture(
			future = future,
			isLocal = false
		))
	}

	callbacks["regionActive"] = luaFunction { region: Table ->
		val chunks = self.geometry.region2Chunks(toAABB(region))
		// TODO: i have no idea what mods expect this to return, so i'll have to guess
		returnBuffer.setTo(chunks.all { self.chunkMap[it]?.state == ChunkState.FULL })
	}

	callbacks["setTileProtection"] = luaFunction { id: Number, enable: Boolean ->
		self.switchDungeonIDProtection(id.toInt(), enable)
	}

	callbacks["isPlayerModified"] = luaFunction { region: Table ->
		returnBuffer.setTo(self.isPlayerModified(toAABBi(region)))
	}

	callbacks["forceDestroyLiquid"] = luaFunction { position: Table ->
		val parse = toVector2i(position)
		val cell = self.getCell(parse).mutable()

		if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) {
			returnBuffer.setTo(cell.liquid.level.toDouble())

			cell.liquid.reset()
			self.setCell(parse, cell.immutable())
		}
	}

	callbacks["loadUniqueEntity"] = LoadUniqueEntityFunction(self)

	callbacks["loadUniqueEntityAsync"] = luaFunction { name: ByteString ->
		returnBuffer.setTo(LuaFuture(
			future = self.loadUniqueEntity(name.decode()),
			isLocal = false
		))
	}

	callbacks["setUniqueId"] = luaFunction { id: Number, name: ByteString ->
		val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("No such entity with ID $id (tried to set unique id to $name)")

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

		if (
			entity.type == EntityType.NPC ||
			entity.type == EntityType.STAGEHAND ||
			entity.type == EntityType.MONSTER ||
			entity.type == EntityType.OBJECT
		) {
			entity.uniqueID.accept(name.decode().sbIntern())
		} else {
			throw LuaRuntimeException("Entity type is restricted from having unique ID: $entity (tried to set unique id to $name)")
		}
	}

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

		if (!entity.isRemote && entity.canTake) {
			returnBuffer.setTo(entity.take(takenBy?.toInt() ?: 0).toTable(this))
		}
	}

	callbacks["setPlayerStart"] = luaFunction { position: Table, respawnInWorld: Boolean? ->
		self.setPlayerSpawn(toVector2d(position), respawnInWorld == true)
	}

	callbacks["players"] = luaFunction {
		returnBuffer.setTo(tableOf(*self.clients.map { it.client.playerID }.toTypedArray()))
	}

	callbacks["fidelity"] = luaFunction {
		returnBuffer.setTo("high")
	}

	callbacks["setSkyTime"] = luaFunction { newTime: Number ->
		val cast = newTime.toDouble()

		if (cast < 0.0)
			throw LuaRuntimeException("Negative time? $cast")

		self.sky.time = cast
	}

	callbacks["setUniverseFlag"] = luaFunction { flag: ByteString ->
		returnBuffer.setTo(self.server.addUniverseFlag(flag.decode()))
	}

	callbacks["unsetUniverseFlag"] = luaFunction { flag: ByteString ->
		returnBuffer.setTo(self.server.removeUniverseFlag(flag.decode()))
	}

	callbacks["universeFlags"] = luaFunction {
		returnBuffer.setTo(tableOf(*self.server.getUniverseFlags().toTypedArray()))
	}

	callbacks["universeFlagSet"] = luaFunction { flag: ByteString ->
		returnBuffer.setTo(self.server.hasUniverseFlag(flag.decode()))
	}

	callbacks["placeDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? ->
		placeDungeonImpl(self, true, false, name, position, dungeonID, seed)
	}

	callbacks["tryPlaceDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? ->
		placeDungeonImpl(self, false, false, name, position, dungeonID, seed)
	}

	callbacks["placeDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? ->
		placeDungeonImpl(self, true, true, name, position, dungeonID, seed)
	}

	callbacks["tryPlaceDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? ->
		placeDungeonImpl(self, false, true, name, position, dungeonID, seed)
	}

	// terraforming
	callbacks["addBiomeRegion"] = luaStub("addBiomeRegion")
	callbacks["expandBiomeRegion"] = luaStub("expandBiomeRegion")
	callbacks["pregenerateAddBiome"] = luaStub("pregenerateAddBiome")
	callbacks["pregenerateExpandBiome"] = luaStub("pregenerateExpandBiome")
	callbacks["setLayerEnvironmentBiome"] = luaStub("setLayerEnvironmentBiome")
	callbacks["setPlanetType"] = luaStub("setPlanetType")

	callbacks["setDungeonGravity"] = luaFunction { id: Number, gravity: Any? ->
		if (gravity == null) {
			self.setDungeonGravity(id.toInt(), null)
		} else if (gravity is Table) {
			self.setDungeonGravity(id.toInt(), toVector2d(gravity))
		} else if (gravity is Number) {
			self.setDungeonGravity(id.toInt(), gravity.toDouble())
		} else {
			throw LuaRuntimeException("Illegal argument to setDungeonGravity: $gravity")
		}
	}

	callbacks["setDungeonBreathable"] = luaFunction { id: Number, breathable: Boolean? ->
		self.setDungeonBreathable(id.toInt(), breathable)
	}

	callbacks["setDungeonId"] = luaFunction { region: Table, id: Number ->
		val parse = toAABBi(region)
		val actualId = id.toInt()

		for (x in parse.mins.x .. parse.maxs.x) {
			for (y in parse.mins.y .. parse.maxs.y) {
				val cell = self.getCell(x, y)

				if (cell.dungeonId != actualId) {
					self.setCell(x, y, cell.mutable().also { it.dungeonId = actualId })
				}
			}
		}
	}

	callbacks["enqueuePlacement"] = luaFunction { distributions: Table, dungeonId: Number? ->
		val items = ArrayList<BiomePlaceables.DistributionItem>()

		for ((_, v) in distributions) {
			// original engine treats distributions table as if it was originating from biome json files
			val unprepared = Starbound.gson.fromJson(toJsonFromLua(v), BiomePlaceablesDefinition.DistributionItem::class.java)
			val prepared = unprepared.create(BiomeDefinition.CreationParams(hueShift = 0.0, random = lua.random))
			items.add(prepared)
		}

		// TODO: Enqueued placements are lost on world shutdown.
		//  Shouldn't we serialize them to persistent storage?
		returnBuffer.setTo(
			LuaFuture(
				future = self.enqueuePlacement(items, dungeonId?.toInt()),
				isLocal = false
			)
		)
	}
}
