package ru.dbotthepony.kstarbound.lua

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 it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap
import org.classdump.luna.ByteString
import org.classdump.luna.Conversions
import org.classdump.luna.Table
import org.classdump.luna.TableFactory
import org.classdump.luna.impl.NonsuspendableFunctionException
import org.classdump.luna.runtime.AbstractFunction3
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.IStruct3i
import ru.dbotthepony.kommons.util.IStruct4i
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.world.physics.Poly

fun ExecutionContext.toVector2i(table: Any): Vector2i {
	val x = indexNoYield(table, 1L)
	val y = indexNoYield(table, 2L)

	if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x")
	if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y")

	return Vector2i(x.toInt(), y.toInt())
}

fun ExecutionContext.toVector2d(table: Any): Vector2d {
	val x = indexNoYield(table, 1L)
	val y = indexNoYield(table, 2L)

	if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x")
	if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y")

	return Vector2d(x.toDouble(), y.toDouble())
}

fun ExecutionContext.toPoly(table: Table): Poly {
	val vertices = ArrayList<Vector2d>()

	for ((_, v) in table) {
		vertices.add(toVector2d(v))
	}

	return Poly(vertices)
}

fun ExecutionContext.toVector2f(table: Any): Vector2f {
	val x = indexNoYield(table, 1L)
	val y = indexNoYield(table, 2L)

	if (x !is Number) throw ClassCastException("Expected table representing a vector, but value at [1] is not a number: $x")
	if (y !is Number) throw ClassCastException("Expected table representing a vector, but value at [2] is not a number: $y")

	return Vector2f(x.toFloat(), y.toFloat())
}

fun ExecutionContext.toColor(table: Any): RGBAColor {
	val x = indexNoYield(table, 1L)
	val y = indexNoYield(table, 2L)
	val z = indexNoYield(table, 3L)
	val w = indexNoYield(table, 4L) ?: 255

	if (x !is Number) throw ClassCastException("Expected table representing a Color, but value at [1] is not a number: $x")
	if (y !is Number) throw ClassCastException("Expected table representing a Color, but value at [2] is not a number: $y")
	if (z !is Number) throw ClassCastException("Expected table representing a Color, but value at [3] is not a number: $z")
	if (w !is Number) throw ClassCastException("Expected table representing a Color, but value at [4] is not a number: $w")

	return RGBAColor(x.toInt(), y.toInt(), z.toInt(), w.toInt())
}

fun ExecutionContext.toAABB(table: Any): AABB {
	val x = indexNoYield(table, 1L)
	val y = indexNoYield(table, 2L)
	val z = indexNoYield(table, 3L)
	val w = indexNoYield(table, 4L)

	if (x !is Number) throw ClassCastException("Expected table representing a AABB, but value at [1] is not a number: $x")
	if (y !is Number) throw ClassCastException("Expected table representing a AABB, but value at [2] is not a number: $y")
	if (z !is Number) throw ClassCastException("Expected table representing a AABB, but value at [3] is not a number: $z")
	if (w !is Number) throw ClassCastException("Expected table representing a AABB, but value at [4] is not a number: $w")

	return AABB(Vector2d(x.toDouble(), y.toDouble()), Vector2d(z.toDouble(), w.toDouble()))
}

fun toJsonFromLua(value: Any?): JsonElement {
	return when (value) {
		null, is JsonNull -> JsonNull.INSTANCE
		is String -> JsonPrimitive(value)
		is ByteString -> JsonPrimitive(value.decode())
		is Number -> JsonPrimitive(value)
		is Boolean -> InternedJsonElementAdapter.of(value)
		is Table -> value.toJson()
		else -> throw IllegalArgumentException("Unable to translate $value into json!")
	}
}

fun Table.toJson(forceObject: Boolean = false): JsonElement {
	val arrayValues = Long2ObjectAVLTreeMap<JsonElement>()
	val hashValues = HashMap<String, JsonElement>()

	val meta = metatable
	var hint = LUA_HINT_NONE

	if (meta != null) {
		val getHint = meta["__typehint"]

		if (getHint is Number) {
			hint = getHint.toLong()
		}

		val nils = meta["__nils"]

		if (nils is Table) {
			// Nil entries just have a garbage integer as their value
			for ((k, v) in nils) {
				val ik = k.toLuaInteger()

				if (ik != null) {
					arrayValues[ik] = JsonNull.INSTANCE
				} else {
					hashValues[k.toString()] = JsonNull.INSTANCE
				}
			}
		}
	}

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

		if (ik != null) {
			arrayValues[ik] = toJsonFromLua(v)
		} else {
			hashValues[k.toString()] = toJsonFromLua(v)
		}
	}

	val interpretAsList = !forceObject && hashValues.isEmpty() && hint != LUA_HINT_OBJECT

	if (interpretAsList) {
		val list = JsonArray()

		for ((k, v) in arrayValues) {
			val ik = k.toInt() - 1

			while (list.size() <= ik) {
				list.add(JsonNull.INSTANCE)
			}

			list[ik] = v
		}

		return list
	} else {
		for ((k, v) in arrayValues) {
			hashValues[(k - 1L).toString()] = v
		}

		return JsonObject().apply {
			for ((k, v) in hashValues) {
				this[k] = v
			}
		}
	}
}

fun TableFactory.from(value: JsonElement?): Any? {
	when (value) {
		is JsonPrimitive -> {
			if (value.isNumber) {
				return value.asDouble
			} else if (value.isString) {
				return ByteString.of(value.asString)
			} else if (value.isBoolean) {
				return value.asBoolean
			} else {
				throw RuntimeException("unreachable code")
			}
		}

		is JsonArray -> return from(value)
		is JsonObject -> return from(value)
		null, is JsonNull -> return null
		else -> throw RuntimeException(value::class.qualifiedName)
	}
}

private data class JsonTable(val metatable: Table, val nils: Table, val data: Table)

fun Any?.toLuaInteger(): Long? {
	if (this == null)
		return null
	else if (this is Long)
		return this
	else if (this is Double) {
		if (this % 1.0 == 0.0) {
			return this.toLong()
		} else {
			return null
		}
	} else if (this is ByteString) {
		val decoded = decode()

		if (decoded.contains('.'))
			return null

		return decoded.toLongOrNull()
	} else {
		return null
	}
}

const val LUA_HINT_NONE = 0L
const val LUA_HINT_ARRAY = 1L
const val LUA_HINT_OBJECT = 2L

private object JsonTableIndex : AbstractFunction3<Table, Any, Any?>() {
	override fun resume(context: ExecutionContext?, suspendedState: Any?) {
		throw NonsuspendableFunctionException(this::class.java)
	}

	private val __nils: ByteString = ByteString.of("__nils")

	override fun invoke(context: ExecutionContext, self: Table, key: Any, value: Any?) {
		val meta = self.metatable
		val nils = meta[__nils] as Table

		// If we are setting an entry to nil, need to add a bogus integer entry
		// to the __nils table, otherwise need to set the entry *in* the __nils
		// table to nil and remove it.

		// TODO: __newindex is called only when assigning non-existing keys to values,
		// TODO: as per Lua manual.
		// TODO: Chucklefish weren't aware of this?

		if (value == null) {
			nils[key] = 0L
		} else {
			nils[key] = null as Any?
		}

		self[key] = value
	}
}

private fun TableFactory.createJsonTable(typeHint: Long, size: Int, hash: Int): JsonTable {
	val metatable = newTable()
	val nils = newTable()
	val data = newTable(size, hash)

	metatable["__newindex"] = JsonTableIndex
	metatable["__nils"] = nils
	metatable["__typehint"] = typeHint

	data.metatable = metatable
	return JsonTable(metatable, nils, data)
}

fun TableFactory.from(value: JsonObject): Table {
	val (_, nils, data) = createJsonTable(LUA_HINT_OBJECT, 0, value.size())

	for ((k, v) in value.entrySet()) {
		if (v.isJsonNull) {
			nils[k] = 0L
		} else {
			data[k] = from(v)
		}
	}

	return data
}

fun TableFactory.from(value: JsonArray): Table {
	val (_, nils, data) = createJsonTable(LUA_HINT_ARRAY, 0, value.size())

	for ((i, v) in value.withIndex()) {
		if (v.isJsonNull) {
			nils[i + 1L] = 0L
		} else {
			data[i + 1L] = from(v)
		}
	}

	return data
}

fun TableFactory.createJsonObject(): Table {
	return createJsonTable(LUA_HINT_OBJECT, 0, 0).data
}

fun TableFactory.createJsonArray(): Table {
	return createJsonTable(LUA_HINT_ARRAY, 0, 0).data
}

fun TableFactory.from(value: IStruct2d): Table {
	return newTable(2, 0).apply {
		this[1L] = value.component1()
		this[2L] = value.component2()
	}
}

fun TableFactory.from(value: Poly): Table {
	return newTable(value.vertices.size, 0).apply {
		value.vertices.withIndex().forEach { (i, v) -> this[i + 1L] = from(v) }
	}
}

fun TableFactory.fromCollection(value: Collection<JsonElement?>): Table {
	val table = newTable(value.size, 0)

	for ((k, v) in value.withIndex()) {
		table.rawset(Conversions.normaliseKey(k + 1), from(v))
	}

	return table
}

fun TableFactory.from(value: IStruct2i): Table {
	return newTable(2, 0).also {
		it.rawset(1L, value.component1())
		it.rawset(2L, value.component2())
	}
}

fun TableFactory.from(value: IStruct3i): Table {
	return newTable(3, 0).also {
		it.rawset(1L, value.component1())
		it.rawset(2L, value.component2())
		it.rawset(3L, value.component3())
	}
}

fun TableFactory.from(value: IStruct4i): Table {
	return newTable(3, 0).also {
		it.rawset(1L, value.component1())
		it.rawset(2L, value.component2())
		it.rawset(3L, value.component3())
		it.rawset(4L, value.component4())
	}
}

fun TableFactory.from(value: RGBAColor): Table {
	return newTable(3, 0).also {
		it.rawset(1L, value.redInt.toLong())
		it.rawset(2L, value.greenInt.toLong())
		it.rawset(3L, value.blueInt.toLong())
		it.rawset(4L, value.alphaInt.toLong())
	}
}

fun TableFactory.from(value: Collection<Any>): Table {
	return newTable(value.size, 0).also {
		for ((i, v) in value.withIndex()) {
			it.rawset(i + 1L, v)
		}
	}
}

fun TableFactory.from(value: AABB): Table {
	return newTable(3, 0).also {
		it.rawset(1L, value.mins.x)
		it.rawset(2L, value.mins.y)
		it.rawset(3L, value.maxs.x)
		it.rawset(4L, value.maxs.y)
	}
}
