package ru.dbotthepony.kstarbound.lua

import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.LuaObject
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.LuaType
import org.classdump.luna.StateContext
import org.classdump.luna.Table
import org.classdump.luna.Variable
import org.classdump.luna.env.RuntimeEnvironments
import org.classdump.luna.exec.DirectCallExecutor
import org.classdump.luna.impl.DefaultTable
import org.classdump.luna.impl.NonsuspendableFunctionException
import org.classdump.luna.lib.BasicLib
import org.classdump.luna.lib.CoroutineLib
import org.classdump.luna.lib.MathLib
import org.classdump.luna.lib.OsLib
import org.classdump.luna.lib.StringLib
import org.classdump.luna.lib.TableLib
import org.classdump.luna.lib.Utf8Lib
import org.classdump.luna.load.ChunkFactory
import org.classdump.luna.runtime.AbstractFunction1
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random

class LuaEnvironment : StateContext {
	private var nilMeta: Table? = null
	private var booleanMeta: Table? = null
	private var numberMeta: Table? = null
	private var stringMeta: Table? = null
	private var functionMeta: Table? = null
	private var threadMeta: Table? = null
	private var lightUserdataMeta: Table? = null

	override fun getNilMetatable(): Table? {
		return nilMeta
	}

	override fun getBooleanMetatable(): Table? {
		return booleanMeta
	}

	override fun getNumberMetatable(): Table? {
		return numberMeta
	}

	override fun getStringMetatable(): Table? {
		return stringMeta
	}

	override fun getFunctionMetatable(): Table? {
		return functionMeta
	}

	override fun getThreadMetatable(): Table? {
		return threadMeta
	}

	override fun getLightUserdataMetatable(): Table? {
		return lightUserdataMeta
	}

	override fun getMetatable(instance: Any?): Table? {
		if (instance is LuaObject)
			return instance.metatable

		return when (val type = LuaType.typeOf(instance)!!) {
			LuaType.NIL -> nilMeta
			LuaType.BOOLEAN -> booleanMeta
			LuaType.NUMBER -> numberMeta
			LuaType.STRING -> stringMeta
			LuaType.FUNCTION -> functionMeta
			LuaType.USERDATA -> lightUserdataMeta
			LuaType.THREAD -> threadMeta
			else -> throw IllegalArgumentException("Illegal type: $type")
		}
	}

	override fun setNilMetatable(table: Table?): Table? {
		val old = nilMeta; nilMeta = table; return old
	}

	override fun setBooleanMetatable(table: Table?): Table? {
		val old = booleanMeta; booleanMeta = table; return old
	}

	override fun setNumberMetatable(table: Table?): Table? {
		val old = numberMeta; numberMeta = table; return old
	}

	override fun setStringMetatable(table: Table?): Table? {
		val old = stringMeta; stringMeta = table; return old
	}

	override fun setFunctionMetatable(table: Table?): Table? {
		val old = functionMeta; functionMeta = table; return old
	}

	override fun setThreadMetatable(table: Table?): Table? {
		val old = threadMeta; threadMeta = table; return old
	}

	override fun setLightUserdataMetatable(table: Table?): Table? {
		val old = lightUserdataMeta; lightUserdataMeta = table; return old
	}

	override fun setMetatable(instance: Any?, table: Table?): Table? {
		if (instance is LuaObject)
			return instance.setMetatable(table)
		else
			throw IllegalArgumentException("Can not set metatable of ${LuaType.typeOf(instance)}")
	}

	override fun newTable(): Table {
		return DefaultTable()
	}

	override fun newTable(array: Int, hash: Int): Table {
		return DefaultTable()
	}

	val globals: Table = newTable()
	val executor: DirectCallExecutor = DirectCallExecutor.newExecutor()
	var random = random()

	init {
		globals["_G"] = globals

		globals["assert"] = BasicLib.assertFn()
		globals["error"] = BasicLib.error()
		globals["getmetatable"] = BasicLib.getmetatable()
		globals["ipairs"] = BasicLib.ipairs()
		globals["next"] = BasicLib.next()
		globals["pairs"] = BasicLib.pairs()
		globals["pcall"] = BasicLib.pcall()

		globals["rawequal"] = BasicLib.rawequal()
		globals["rawget"] = BasicLib.rawget()
		globals["rawlen"] = BasicLib.rawlen()
		globals["rawset"] = BasicLib.rawset()
		globals["select"] = BasicLib.select()
		globals["setmetatable"] = BasicLib.setmetatable()
		globals["tostring"] = BasicLib.tostring()
		globals["tonumber"] = BasicLib.tonumber()
		globals["type"] = BasicLib.type()
		globals["_VERSION"] = BasicLib._VERSION
		globals["xpcall"] = BasicLib.xpcall()

		globals["print"] = PrintFunction(globals)

		globals["require"] = LuaRequire()

		// why not use _ENV anyway lol
		globals["self"] = newTable()

		CoroutineLib.installInto(this, globals)
		TableLib.installInto(this, globals)
		MathLib.installInto(this, globals)
		StringLib.installInto(this, globals)
		OsLib.installInto(this, globals, RuntimeEnvironments.system())

		val math = globals["math"] as Table

		math["random"] = luaFunction { origin: Number?, bound: Number? ->
			if (origin == null && bound == null) {
				returnBuffer.setTo(random.nextDouble())
			} else if (bound == null) {
				val origin = origin!!.toLong()

				if (origin > 1L) {
					returnBuffer.setTo(random.nextLong(1L, origin))
				} else if (origin == 1L) {
					returnBuffer.setTo(1L)
				} else {
					throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)")
				}
			} else {
				val origin = origin!!.toLong()
				val bound = bound.toLong()

				if (bound > origin) {
					returnBuffer.setTo(random.nextLong(origin, bound))
				} else if (bound == origin) {
					returnBuffer.setTo(origin)
				} else {
					throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)")
				}
			}
		}

		math["randomseed"] = luaFunction { seed: Number ->
			random = random(seed.toLong())
		}

		// TODO: NYI, maybe polyfill?
		Utf8Lib.installInto(this, globals)

		provideRootBindings(this)
		provideUtilityBindings(this)
	}

	private val scripts = ObjectArraySet<ChunkFactory>()
	private var initCalled = false
	private val loadedScripts = ObjectArraySet<String>()

	inner class LuaRequire : AbstractFunction1<ByteString>() {
		override fun resume(context: ExecutionContext?, suspendedState: Any?) {
			throw NonsuspendableFunctionException(this::class.java)
		}

		override fun invoke(context: ExecutionContext, arg1: ByteString) {
			val name = arg1.decode()

			if (loadedScripts.add(name)) {
				val script = Starbound.loadScript(name)
				executor.call(this@LuaEnvironment, script.newInstance(Variable(globals)))
			}
		}
	}

	fun attach(script: ChunkFactory) {
		scripts.add(script)
	}

	fun attach(scripts: Collection<AssetPath>) {
		for (script in scripts) {
			attach(Starbound.loadScript(script.fullPath))
		}
	}

	fun run(chunk: ChunkFactory): Array<out Any?> {
		return executor.call(this, chunk.newInstance(Variable(globals)))
	}

	var errorState = false
		private set

	fun markErrored() {
		errorState = true
	}

	fun init(callInit: Boolean = true): Boolean {
		check(!initCalled) { "Already called init()" }
		initCalled = true

		for (script in scripts) {
			try {
				executor.call(this, script.newInstance(Variable(globals)))
			} catch (err: Throwable) {
				errorState = true
				LOGGER.error("Failed to attach script to environment", err)
				scripts.clear()
				return false
			}
		}

		scripts.clear()

		if (callInit) {
			val init = globals["init"]

			if (init is LuaFunction<*, *, *, *, *>) {
				try {
					executor.call(this, init)
				} catch (err: Throwable) {
					errorState = true
					LOGGER.error("Exception on init()", err)
					return false
				}
			}
		}

		return true
	}

	fun invokeGlobal(name: String, vararg arguments: Any?): Array<out Any?> {
		if (errorState)
			return arrayOf()

		val load = globals[name]

		if (load is LuaFunction<*, *, *, *, *>) {
			return try {
				executor.call(this, load, *arguments)
			} catch (err: Throwable) {
				errorState = true
				LOGGER.error("Exception while calling global $name", err)
				arrayOf()
			}
		}

		return arrayOf()
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
	}
}
