package ru.dbotthepony.kstarbound.lua.bindings

import com.google.common.collect.ImmutableList
import com.google.gson.JsonNull
import com.google.gson.JsonPrimitive
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.quest.QuestDescriptor
import ru.dbotthepony.kstarbound.json.JsonPath
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.set
import ru.dbotthepony.kstarbound.lua.toColor
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject

fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
	val config = lua.newTable()
	lua.globals["config"] = config

	config["getParameter"] = luaFunction { name: ByteString, default: Any? ->
		val path = JsonPath.query(name.decode())
		val find = self.lookupProperty(path) { JsonNull.INSTANCE }

		if (find.isJsonNull) {
			returnBuffer.setTo(default)
		} else {
			returnBuffer.setTo(from(find))
		}
	}

	val table = lua.newTable()
	lua.globals["object"] = table

	table["name"] = luaFunction { returnBuffer.setTo(self.config.key) }
	table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) }
	table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) }
	table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive }
	table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().orNull()) }
	table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(KOptional.ofNullable(id?.decode())) }
	table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) }

	// original engine parity, it returns occupied spaces in local coordinates
	table["spaces"] = luaFunction { returnBuffer.setTo(from(self.occupySpaces.map { from(it - self.tilePosition) })) }

	table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() }
	table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state }
	table["smash"] = luaFunction { smash: Boolean? -> self.callBreak(smash ?: false) }
	table["level"] = luaFunction { returnBuffer.setTo(self.lookupProperty(JsonPath("level")) { JsonPrimitive(self.world.template.threatLevel) }.asDouble) }
	table["toAbsolutePosition"] = luaFunction { pos: Table -> returnBuffer.setTo(from(toVector2d(pos) + self.position)) }

	table["say"] = luaFunction { line: ByteString, tags: Table?, config: Table ->
		if (tags == null) {
			if (line.isEmpty) {
				returnBuffer.setTo(false)
			} else {
				self.addChatMessage(line.decode(), config.toJson())
				returnBuffer.setTo(true)
			}
		} else {
			if (line.isEmpty) {
				returnBuffer.setTo(false)
			} else {
				self.addChatMessage(SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }), config.toJson())
				returnBuffer.setTo(true)
			}
		}
	}

	table["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table?, config: Table ->
		if (tags == null) {
			if (line.isEmpty) {
				returnBuffer.setTo(false)
			} else {
				self.addChatMessage(line.decode(), config.toJson(), portrait.decode())
				returnBuffer.setTo(true)
			}
		} else {
			if (line.isEmpty) {
				returnBuffer.setTo(false)
			} else {
				self.addChatMessage(SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() }), config.toJson(), portrait.decode())
				returnBuffer.setTo(true)
			}
		}
	}

	table["isTouching"] = luaFunction { entity: Number ->
		val find = self.world.entities[entity]

		if (find != null) {
			returnBuffer.setTo(find.collisionArea.intersect(self.volumeBoundingBox))
		} else {
			returnBuffer.setTo(false)
		}
	}

	table["setLightColor"] = luaFunction { color: Table ->
		self.lightSourceColor = toColor(color)
	}

	table["getLightColor"] = luaFunction {
		returnBuffer.setTo(from(self.lightSourceColor))
	}

	table["inputNodeCount"] = luaFunction { returnBuffer.setTo(self.inputNodes.size) }
	table["outputNodeCount"] = luaFunction { returnBuffer.setTo(self.outputNodes.size) }

	table["getInputNodePosition"] = luaFunction { index: Long ->
		returnBuffer.setTo(from(self.inputNodes[index.toInt()].position))
	}

	table["getOutputNodePosition"] = luaFunction { index: Long ->
		returnBuffer.setTo(from(self.outputNodes[index.toInt()].position))
	}

	table["getInputNodeLevel"] = luaFunction { index: Long ->
		returnBuffer.setTo(self.inputNodes[index.toInt()].state)
	}

	table["getOutputNodeLevel"] = luaFunction { index: Long ->
		returnBuffer.setTo(self.outputNodes[index.toInt()].state)
	}

	table["isInputNodeConnected"] = luaFunction { index: Long ->
		returnBuffer.setTo(self.inputNodes[index.toInt()].connections.isNotEmpty())
	}

	table["isOutputNodeConnected"] = luaFunction { index: Long ->
		returnBuffer.setTo(self.outputNodes[index.toInt()].connections.isNotEmpty())
	}

	table["getInputNodeIds"] = luaFunction { index: Long ->
		val results = newTable()

		for (connection in self.inputNodes[index.toInt()].connections) {
			val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation) as? WorldObject

			if (entity != null) {
				results[entity.entityID] = connection.index
			}
		}

		returnBuffer.setTo(results)
	}

	table["getOutputNodeIds"] = luaFunction { index: Long ->
		val results = newTable()

		for (connection in self.outputNodes[index.toInt()].connections) {
			val entity = self.world.entityIndex.tileEntityAt(connection.entityLocation) as? WorldObject

			if (entity != null) {
				results[entity.entityID] = connection.index
			}
		}

		returnBuffer.setTo(results)
	}

	table["setOutputNodeLevel"] = luaFunction { index: Long, state: Boolean ->
		self.outputNodes[index.toInt()].state = state
	}

	table["setAllOutputNodes"] = luaFunction { state: Boolean ->
		self.outputNodes.forEach { it.state = state }
	}

	table["setOfferedQuests"] = luaFunction { quests: Table? ->
		self.offeredQuests.clear()

		if (quests != null) {
			for ((_, v) in quests) {
				if (v is Table) {
					self.offeredQuests.add(Starbound.gson.fromJson(v.toJson(), QuestArcDescriptor::class.java))
				} else if (v is ByteString) {
					self.offeredQuests.add(QuestArcDescriptor(ImmutableList.of(QuestDescriptor(v.decode()))))
				} else {
					throw LuaRuntimeException("Unknown quest arc descriptor type: $v")
				}
			}
		}
	}

	table["setTurnInQuests"] = luaFunction { quests: Table? ->
		self.turnInQuests.clear()

		if (quests != null) {
			for ((_, v) in quests) {
				self.turnInQuests.add((v as ByteString).decode())
			}
		}
	}

	table["setConfigParameter"] = luaFunction { key: ByteString, value: Any? ->
		self.parameters[key.decode()] = toJsonFromLua(value)
	}

	table["setAnimationParameter"] = luaFunction { key: ByteString, value: Any? ->
		self.scriptedAnimationParameters[key.decode()] = toJsonFromLua(value)
	}

	table["setMaterialSpaces"] = luaFunction { spaces: Table ->
		self.declaredMaterialSpaces.clear()

		for ((i, pair) in spaces) {
			pair as Table

			val position = toVector2i(indexNoYield(pair, 1L) ?: throw NullPointerException("invalid space at $i"))
			val material = indexNoYield(pair, 2L) as? ByteString ?: throw NullPointerException("invalid space at $i")

			self.declaredMaterialSpaces.add(position to Registries.tiles.ref(material.decode()))
		}
	}

	table["setDamageSources"] = luaFunction { sources: Table? ->
		self.damageSources.clear()

		if (sources != null) {
			for ((_, v) in sources) {
				self.damageSources.add(Starbound.gson.fromJson((v as Table).toJson(), DamageSource::class.java))
			}
		}
	}

	table["health"] = luaFunction { returnBuffer.setTo(self.health) }
	table["setHealth"] = luaFunction { health: Double -> self.health = health }
}
