package ru.dbotthepony.kstarbound.lua.bindings

import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.util.XXHash32
import ru.dbotthepony.kommons.util.XXHash64
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.lua.LuaThread
import ru.dbotthepony.kstarbound.lua.LuaType
import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise
import ru.dbotthepony.kstarbound.lua.userdata.LuaRandom
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextNormalDouble
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.toBytes
import ru.dbotthepony.kstarbound.util.toStarboundString
import java.io.StringWriter
import java.util.*

// TODO: Lua-side implementation for better performance?
private fun replaceTags(args: LuaThread.ArgStack): Int {
	val string = args.nextString()
	val tagsList = args.lua
		.readTable(
			args.position++,
			{ getString(it) ?: throw IllegalStateException("Tags table contain non-string keys") },
			{ getString(it) ?: throw IllegalStateException("Tags table contain non-string values") }
		) ?: throw IllegalArgumentException("bad argument #2: table expected, got ${args.peek(args.position - 1)}")

	val tags = tagsList.toMap()

	args.lua.push(SBPattern.of(string).resolveOrSkip({ tags[it] }))
	return 1
}

private fun hash32(args: LuaThread.ArgStack): Int {
	val digest = XXHash32(2938728349.toInt())

	while (args.hasNext) {
		when (args.peek()) {
			LuaType.BOOLEAN -> digest.update(if (args.nextBoolean()) 1 else 0)
			LuaType.NUMBER -> toBytes(digest::update, args.nextDouble())
			LuaType.STRING -> digest.update(args.nextString().toByteArray(Charsets.UTF_8))
			else -> throw IllegalArgumentException("bad argument #${args.position} to staticRandomI32")
		}
	}

	return digest.digestAsInt()
}

private fun hash64(args: LuaThread.ArgStack): Long {
	val digest = XXHash64(1997293021376312589L)

	while (args.hasNext) {
		when (args.peek()) {
			LuaType.BOOLEAN -> digest.update(if (args.nextBoolean()) 1 else 0)
			LuaType.NUMBER -> toBytes(digest::update, args.nextDouble())
			LuaType.STRING -> digest.update(args.nextString().toByteArray(Charsets.UTF_8))
			else -> throw IllegalArgumentException("bad argument #${args.position} to staticRandomI32")
		}
	}

	return digest.digestAsLong()
}

private fun staticRandomI32(args: LuaThread.ArgStack): Int {
	args.lua.push(hash32(args).toLong())
	return 1
}

private fun staticRandomI64(args: LuaThread.ArgStack): Int {
	args.lua.push(hash64(args))
	return 1
}

private fun staticRandomDouble(args: LuaThread.ArgStack): Int {
	args.lua.push(hash64(args).ushr(11) * 1.1102230246251565E-16)
	return 1
}

private fun staticRandomDoubleRange(args: LuaThread.ArgStack): Int {
	val min = args.nextDouble()
	val max = args.nextDouble()
	val double = hash64(args).ushr(11) * 1.1102230246251565E-16
	args.lua.push(double * (max - min) + min)
	return 1
}

// FIXME: incorrect.
private fun staticRandomI64Range(args: LuaThread.ArgStack): Int {
	val min = args.nextDouble()
	val max = args.nextDouble()
	val double = hash64(args).ushr(11) * 1.1102230246251565E-16
	args.lua.push((min + (max - min + 1L) * double).toLong())
	return 1
}

// FIXME: incorrect.
private fun staticRandomI32Range(args: LuaThread.ArgStack): Int {
	val min = args.nextDouble()
	val max = args.nextDouble()
	val double = hash64(args).ushr(11) * 1.1102230246251565E-16
	args.lua.push((min + (max - min + 1L) * double).toInt().toLong())
	return 1
}

// TODO: Lua-side implementation for better performance?
private fun makeUuid(args: LuaThread.ArgStack): Int {
	args.lua.push(UUID(args.lua.random.nextLong(), args.lua.random.nextLong()).toStarboundString())
	return 1
}

private fun nrand(args: LuaThread.ArgStack): Int {
	val stdev = args.nextOptionalDouble() ?: 1.0
	val mean = args.nextOptionalDouble() ?: 0.0
	args.lua.push(args.lua.random.nextNormalDouble(stdev, mean))
	return 1
}

// TODO: Lua-side implementation for better performance?
private fun jsonMerge(args: LuaThread.ArgStack): Int {
	val a = args.nextOptionalJson() ?: JsonNull.INSTANCE
	val b = args.nextOptionalJson() ?: JsonNull.INSTANCE

	args.lua.push(ru.dbotthepony.kstarbound.json.mergeJson(a, b))
	return 1
}

// TODO: Lua-side implementation for better performance?
private fun jsonQuery(args: LuaThread.ArgStack): Int {
	val json = args.nextOptionalJson() ?: JsonNull.INSTANCE
	val path = args.nextString()
	val default = args.nextOptionalJson() ?: JsonNull.INSTANCE

	args.lua.push(JsonPath.query(path).get(json, default))
	return 1
}

private fun printJson(args: LuaThread.ArgStack): Int {
	val json = args.nextJson()
	val pretty = args.nextOptionalBoolean() ?: false

	val strBuilder = StringWriter()
	val writer = JsonWriter(strBuilder)
	writer.isLenient = true

	if (pretty) {
		writer.setIndent("    ")
	}

	TypeAdapters.JSON_ELEMENT.write(writer, json)
	args.lua.push(strBuilder.toString())

	return 1
}

fun provideUtilityBindings(lua: LuaThread) {
	lua.push {
		LuaThread.LOGGER.info(it.nextString())
		0
	}

	lua.storeGlobal("__print")

	lua.push {
		LuaThread.LOGGER.warn(it.nextString())
		0
	}

	lua.storeGlobal("__print_warn")

	lua.push {
		LuaThread.LOGGER.error(it.nextString())
		0
	}

	lua.storeGlobal("__print_error")

	lua.push {
		LuaThread.LOGGER.fatal(it.nextString())
		0
	}

	lua.storeGlobal("__print_fatal")

	lua.push {
		val path = it.nextString()

		try {
			it.lua.load(Starbound.readLuaScript(path).join(), "@$path")
			1
		} catch (err: Exception) {
			LuaThread.LOGGER.error("Exception loading Lua script $path", err)
			throw err
		}
	}

	lua.storeGlobal("__require")

	lua.push {
		it.lua.push(it.lua.random.nextDouble())
		1
	}

	lua.storeGlobal("__random_double")

	lua.push {
		it.lua.push(it.lua.random.nextLong(it.nextLong(), it.nextLong()))
		1
	}

	lua.storeGlobal("__random_long")

	lua.push {
		it.lua.random = random(it.nextLong())
		0
	}

	lua.storeGlobal("__random_seed")

	lua.push {
		it.lua.push(it.lua.getNamedHandle(it.nextString()))
		1
	}

	lua.storeGlobal("gethandle")

	lua.push {
		val find = it.lua.findNamedHandle(it.nextString())

		if (find == null) {
			0
		} else {
			it.lua.push(find)
			1
		}
	}

	lua.storeGlobal("findhandle")

	lua.pushTable()
	lua.dup()
	lua.storeGlobal("sb")

	lua.setTableValue("makeUuid", ::makeUuid)
	lua.setTableValue("nrand", ::nrand)
	lua.setTableValue("jsonMerge", ::jsonMerge)
	lua.setTableValue("jsonQuery", ::jsonQuery)

	lua.setTableValue("replaceTags", ::replaceTags)

	lua.setTableValue("staticRandomI32", ::staticRandomI32)
	lua.setTableValue("staticRandomI32Range", ::staticRandomI32Range)
	lua.setTableValue("staticRandomI64", ::staticRandomI64)
	lua.setTableValue("staticRandomI64Range", ::staticRandomI64Range)
	lua.setTableValue("staticRandomDouble", ::staticRandomDouble)
	lua.setTableValue("staticRandomDoubleRange", ::staticRandomDoubleRange)

	lua.pushTable()
	val randomMeta = lua.createHandle("RandomGenerator")

	lua.pushBinding("init", LuaRandom::init)
	lua.pushBinding("addEntropy", LuaRandom::addEntropy)
	lua.pushBinding("randu32", LuaRandom::randu32)
	lua.pushBinding("randi32", LuaRandom::randi32)
	lua.pushBinding("randu64", LuaRandom::randu64)
	lua.pushBinding("randi64", LuaRandom::randi64)
	lua.pushBinding("randf", LuaRandom::randf)
	lua.pushBinding("randd", LuaRandom::randd)
	lua.pushBinding("randInt", LuaRandom::randLong)
	lua.pushBinding("randUInt", LuaRandom::randLong)
	lua.pushBinding("randLong", LuaRandom::randLong)
	lua.pushBinding("randULong", LuaRandom::randLong)
	lua.pushBinding("randn", LuaRandom::randn)

	lua.pop()

	lua.push("makeRandomSource")
	lua.push { args ->
		val seed = args.nextOptionalLong() ?: args.lua.random.nextLong()
		lua.pushTable()
		lua.push("__index")
		lua.push(randomMeta)
		lua.setTableValue()
		lua.pushObject(LuaRandom(random(seed)))
		1
	}

	lua.setTableValue()

	lua.pushTable()
	val noiseMeta = lua.createHandle("PerlinSource")

	lua.pushBinding("get", LuaPerlinNoise::get)
	lua.pushBinding("seed", LuaPerlinNoise::seed)
	lua.pushBinding("parameters", LuaPerlinNoise::parameters)
	lua.pushBinding("init", LuaPerlinNoise::init)

	lua.pop()

	lua.push("makePerlinSource")
	lua.push { args ->
		val params = AbstractPerlinNoise.of(Starbound.gson.fromJson(args.nextJson(), PerlinNoiseParameters::class.java))
		lua.pushTable()
		lua.push("__index")
		lua.push(noiseMeta) // cyclic reference through GC root
		lua.setTableValue()
		lua.pushObject(LuaPerlinNoise(params))
		1
	}

	lua.setTableValue()

	lua.setTableValue("printJson", ::printJson)

	lua.pop()
}

private typealias ConfigLookup = (path: JsonPath) -> JsonElement?

private fun lookupConfigValue(self: ConfigLookup, args: LuaThread.ArgStack): Int {
	val parameter = args.nextString()
	val lookup = self(JsonPath.query(parameter))

	if (lookup == null) {
		// TODO: while this is considerably faster, it does not correspond to original engine behavior where "default value" is always copied (lua -> json -> lua)
		if (args.top == 1) {
			args.lua.push()
		} else if (args.top > 2) {
			args.lua.dup(2)
		}
	} else {
		args.lua.push(lookup)
	}

	return 1
}

fun provideConfigBinding(lua: LuaThread, lookup: ConfigLookup) {
	lua.pushTable()
	lua.dup()
	lua.storeGlobal("config")
	lua.pushBinding(lookup, "getParameter", ::lookupConfigValue)
	lua.pop()
}

fun createConfigBinding(lua: LuaThread, lookup: ConfigLookup) {
	lua.push { lookupConfigValue(lookup, it) }
}