package ru.dbotthepony.kstarbound.defs.item

import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
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.TableFactory
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.lua.StateMachine
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.VersionRegistry
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Supplier
import java.util.random.RandomGenerator

private val EMPTY_JSON = JsonObject()

fun ItemDescriptor(data: JsonElement): ItemDescriptor {
	if (data is JsonPrimitive) {
		return ItemDescriptor(data.asString, 1L)
	} else if (data is JsonArray) {
		val name = data[0].asString
		val count = data.get(1, 1L)
		val parameters = data.get(2, ::JsonObject)
		return ItemDescriptor(name, count, parameters)
	} else if (data is JsonObject) {
		if ("id" in data && "version" in data && "content" in data) {
			// loading versioned json from original engine
			if (data["id"].asString != "Item")
				throw JsonSyntaxException("Expected id to be 'Item', ${data["id"]} given")

			return ItemDescriptor(data["content"])
		} else {
			// loading regular json
			val name = (data.get("name") ?: data.get("item") ?: throw JsonSyntaxException("Missing item name")).asString
			val count = data.get("count", 1L)
			val parameters = data.get("parameters") { data.get("parameters", ::JsonObject) }
			return ItemDescriptor(name, count, parameters)
		}
	} else if (data is JsonNull) {
		return ItemDescriptor.EMPTY
	} else {
		throw JsonSyntaxException("Invalid item descriptor: $data")
	}
}

fun ItemDescriptor(data: Table, stateMachine: StateMachine): Supplier<ItemDescriptor> {
	val name = stateMachine.index(data, 1L, "name", "item")
	val count = stateMachine.optionalIndex(data, 2L, "count")
	val parameters = stateMachine.optionalIndex(data, 3L, "parameters", "data")
	var result: KOptional<ItemDescriptor> = KOptional.empty()

	stateMachine.add {
		val iname = name.get()
		val icount = count.get().orElse(1L)
		val iparameters = parameters.get().map { if (it is Table) it.toJson(true) else throw LuaRuntimeException("Invalid item descriptor parameters ($it)") }.orElse { JsonObject() }

		if (iname !is ByteString) throw LuaRuntimeException("Invalid item descriptor name (${iname})")
		if (icount !is Number) throw LuaRuntimeException("Invalid item descriptor count (${icount})")

		result = KOptional(ItemDescriptor(iname.decode(), icount.toLong(), iparameters as JsonObject))
	}

	return Supplier { result.value }
}

fun ExecutionContext.ItemDescriptor(data: Table): ItemDescriptor {
	val name = indexNoYield(data, 1L) ?: indexNoYield(data, "name") ?: indexNoYield(data, "item")
	val count = indexNoYield(data, 2L) ?: indexNoYield(data, "count") ?: 1L
	val parameters = indexNoYield(data, 3L) ?: indexNoYield(data, "parameters") ?: indexNoYield(data, "data")

	if (name !is ByteString) throw LuaRuntimeException("Invalid item descriptor name (${name})")
	if (count !is Number) throw LuaRuntimeException("Invalid item descriptor count (${count})")

	if (parameters == null) {
		return ItemDescriptor(name.decode(), count.toLong())
	} else if (parameters is Table) {
		return ItemDescriptor(name.decode(), count.toLong(), parameters.toJson(true) as JsonObject)
	} else {
		throw LuaRuntimeException("Invalid item descriptor parameters ($parameters)")
	}
}

@Deprecated("Does not obey meta methods, need to find replacement where possible")
fun ItemDescriptor(data: Table): ItemDescriptor {
	val name = data[1L] ?: data["name"] ?: data["item"]
	val count = data[2L] ?: data["count"] ?: 1L
	val parameters = data[3L] ?: data["parameters"] ?: data["data"]

	if (name !is ByteString) throw LuaRuntimeException("Invalid item descriptor name (${name})")
	if (count !is Number) throw LuaRuntimeException("Invalid item descriptor count (${count})")

	if (parameters == null) {
		return ItemDescriptor(name.decode(), count.toLong())
	} else if (parameters is Table) {
		return ItemDescriptor(name.decode(), count.toLong(), parameters.toJson(true) as JsonObject)
	} else {
		throw LuaRuntimeException("Invalid item descriptor parameters ($parameters)")
	}
}

fun ItemDescriptor(stream: DataInputStream): ItemDescriptor {
	val name = stream.readInternedString()
	val count = stream.readVarLong()
	val params = stream.readJsonElement()

	if (name == "")
		return ItemDescriptor.EMPTY

	return ItemDescriptor(name, count, if (params === JsonNull.INSTANCE || params == EMPTY_JSON) EMPTY_JSON else params as JsonObject)
}

/**
 * Describes a prototype of item
 *
 * [parameters] is considered to be immutable and should not be modified
 * directly (must be copied for mutable context)
 */
@JsonAdapter(ItemDescriptor.Adapter::class)
data class ItemDescriptor(
	val name: String,
	val count: Long,
	val parameters: JsonObject = EMPTY_JSON
) {
	val isEmpty get() = count <= 0L || name == "" || ref.isEmpty
	val isNotEmpty get() = !isEmpty
	val ref by lazy { if (name == "") ItemRegistry.AIR else ItemRegistry[name] }

	override fun toString(): String {
		return "ItemDescriptor[$name, $count, $parameters]"
	}

	private fun toJsonStruct() = JsonObject().also {
		it.add("name", JsonPrimitive(name))
		it.add("count", JsonPrimitive(count))
		it.add("parameters", parameters.deepCopy())
	}

	fun toJson(): JsonObject? {
		if (Starbound.IS_STORE_JSON) {
			return VersionRegistry.make("Item", toJsonStruct()).toJson()
		} else {
			if (isEmpty) {
				return null
			}

			return toJsonStruct()
		}
	}

	fun toTable(allocator: TableFactory): Table? {
		if (isEmpty) {
			return null
		}

		return allocator.newTable(0, 3).also {
			it.rawset("name", name)
			it.rawset("count", count)
			it.rawset("parameters", allocator.from(parameters))
		}
	}

	/**
	 * This is fragile, to some extent, because it is SOLE RESPONSIBILITY of builder script to store
	 * [level] and/or [seed] inside item parameters once it is built (otherwise we will get different or
	 * reset stats on next item load from serialized state)
	 */
	fun build(level: Double? = null, seed: Long? = null, random: RandomGenerator? = null): ItemStack {
		val builder = ref.json["builder"]?.asString ?: return ref.type.factory(ref, ref.json, parameters.deepCopy(), count)

		try {
			val lua = LuaEnvironment()
			lua.attach(Starbound.loadScript(builder))
			lua.random = random ?: lua.random
			lua.init(false)

			val (config, parameters) = lua.invokeGlobal("build", ref.directory + "/", lua.from(ref.json), lua.from(parameters), level, seed)

			val jConfig = toJsonFromLua(config).asJsonObject
			val jParameters = toJsonFromLua(parameters).asJsonObject

			return ref.type.factory(ref, jConfig, jParameters, count)
		} catch (err: Throwable) {
			LOGGER.error("Error while building item '$name' using script $builder", err)
			return ref.type.factory(ref, ref.json, parameters.deepCopy(), count)
		}
	}

	fun write(stream: DataOutputStream) {
		stream.writeBinaryString(name)
		stream.writeVarLong(count.coerceAtLeast(0L))
		stream.writeJsonElement(parameters)
	}

	class Adapter(gson: Gson) : TypeAdapter<ItemDescriptor>() {
		override fun write(out: JsonWriter, value: ItemDescriptor) {
			if (value.isEmpty)
				out.nullValue()
			else
				out.value(value.toJson())
		}

		override fun read(`in`: JsonReader): ItemDescriptor {
			return ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`))
		}
	}

	companion object {
		val EMPTY = ItemDescriptor("", 0)
		val CODEC = StreamCodec.Impl(::ItemDescriptor, { a, b -> b.write(a) })
		private val LOGGER = LogManager.getLogger()
	}
}
