package ru.dbotthepony.kstarbound.world.entities.tile

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
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.TypeAdapter
import com.google.gson.reflect.TypeToken
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.Table
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.guava.immutableSet
import ru.dbotthepony.kommons.io.RGBACodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.Vector2iCodec
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.JsonElementCodec
import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
import ru.dbotthepony.kstarbound.network.syncher.UnsignedShortCodec
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement
import ru.dbotthepony.kstarbound.network.syncher.networkedPointer
import ru.dbotthepony.kstarbound.network.syncher.networkedString
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.LuaMessageHandler
import ru.dbotthepony.kstarbound.lua.LuaUpdateComponent
import ru.dbotthepony.kstarbound.lua.bindings.provideAnimatorBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideEntityBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideWorldBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideWorldObjectBindings
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.asStringOrNull
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection
import java.io.DataOutputStream
import java.util.HashMap
import java.util.random.RandomGenerator

open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity() {
	open fun deserialize(data: JsonObject) {
		direction = data.get("direction", directions) { Direction.LEFT }
		orientationIndex = data.get("orientationIndex", -1).toLong()
		isInteractive = data.get("interactive", false)
		tilePosition = data.get("tilePosition", vectors)

		lua.globals["storage"] = lua.from(data.get("scriptStorage") { JsonObject() })

		uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))

		loadParameters(data.get("parameters") { JsonObject() })
	}

	open fun loadParameters(parameters: JsonObject) {
		for ((k, v) in parameters.entrySet()) {
			this.parameters[k] = v
		}
	}

	open fun serialize(): JsonObject {
		val into = JsonObject()
		into["name"] = config.key
		into["tilePosition"] = vectors.toJsonTree(tilePosition)
		into["direction"] = directions.toJsonTree(direction)
		into["orientationIndex"] = orientationIndex
		into["interactive"] = isInteractive

		val scriptStorage = lua.globals["storage"]

		if (scriptStorage != null && scriptStorage is Table) {
			into["scriptStorage"] = scriptStorage.toJson()
		}

		uniqueID.get().ifPresent {
			into["uniqueId"] = it
		}

		into["parameters"] = JsonObject().also {
			for ((k, v) in parameters) {
				it[k] = v.deepCopy()
			}
		}

		return into
	}

	/**
	 * called by DungeonWorld to deterministically randomize parameters
	 */
	open fun randomize(random: RandomGenerator, threatLevel: Double) {

	}

	protected val orientationLazies = ArrayList<ManualLazy<*>>()
	protected val parametersLazies = ArrayList<ManualLazy<*>>()
	protected val spacesLazies = ArrayList<ManualLazy<*>>()

	override val metaBoundingBox: AABB by ManualLazy {
		(orientation?.metaBoundBox ?: orientation?.let { AABB(it.boundingBox.mins.toDoubleVector() + Vector2d.NEGATIVE_XY, it.boundingBox.maxs.toDoubleVector() + Vector2d(2.0, 2.0)) } ?: AABB.ZERO) + position
	}.also { orientationLazies.add(it); spacesLazies.add(it) }

	val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also {
		networkGroup.upstream.add(it)
		it.addListener { parametersUpdated() }
	}

	val orientation: ObjectOrientation? get() {
		return config.value.orientations.getOrNull(orientationIndex.toInt())
	}

	protected val mergedJson = ManualLazy {
		val orientation = orientation

		if (orientation == null) {
			mergeJson(config.jsonObject.deepCopy(), parameters) as JsonObject
		} else {
			mergeJson(mergeJson(config.jsonObject.deepCopy(), orientation.json), parameters) as JsonObject
		}
	}.also { orientationLazies.add(it); parametersLazies.add(it) }

	fun lookupProperty(path: JsonPath, orElse: () -> JsonElement): JsonElement {
		return path.get(mergedJson.value, orElse)
	}

	fun lookupProperty(path: String, orElse: () -> JsonElement): JsonElement {
		return mergedJson.value[path] ?: orElse.invoke()
	}

	fun lookupProperty(path: String): JsonElement {
		return mergedJson.value[path] ?: JsonNull.INSTANCE
	}

	init {
		networkGroup.upstream.add(uniqueID)
	}

	var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
	val declaredMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) }

	private val materialSpaces0 = ManualLazy {
		declaredMaterialSpaces.map { world.geometry.wrap(it.first + tilePosition) to it.second }
	}.also { orientationLazies.add(it); spacesLazies.add(it) }

	override val materialSpaces by materialSpaces0

	override val occupySpaces: ImmutableSet<Vector2i> by ManualLazy {
		(orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet())
	}.also { orientationLazies.add(it); spacesLazies.add(it) }

	override val roots: Set<Vector2i>
		get() = setOf()

	val anchorPositions: ImmutableSet<Vector2i> by ManualLazy {
		immutableSet {
			orientation?.anchors?.forEach { accept(it.position + tilePosition) }
		}
	}.also { orientationLazies.add(it); spacesLazies.add(it) }

	init {
		networkGroup.upstream.add(xTilePositionNet)
		networkGroup.upstream.add(yTilePositionNet)

		declaredMaterialSpaces.addListener {
			materialSpaces0.invalidate()
			markSpacesDirty()
		}
	}

	var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) }
	var health by networkedFloat(config.value.health).also { networkGroup.upstream.add(it) }

	var orientationIndex by networkedPointer(-1L).also {
		networkGroup.upstream.add(it)
		it.addListener(Runnable { orientationUpdated() })
	}

	private val networkedRenderKeys = NetworkedMap(InternedStringCodec, InternedStringCodec).also { networkGroup.upstream.add(it) }
	private val localRenderKeys = HashMap<String, String>()

	var soundEffectEnabled by networkedBoolean(true).also { networkGroup.upstream.add(it) }
	var lightSourceColor by networkedData(RGBAColor.TRANSPARENT_BLACK, RGBACodec).also { networkGroup.upstream.add(it) }
	val newChatMessageEvent = networkedEventCounter().also { networkGroup.upstream.add(it) }
	val chatMessage by networkedString().also { networkGroup.upstream.add(it) }
	val chatPortrait by networkedString().also { networkGroup.upstream.add(it) }
	val chatConfig by networkedJsonElement().also { networkGroup.upstream.add(it) }

	inner class WireNode(val position: Vector2i, val isInput: Boolean) {
		val connections = NetworkedList(WireConnection.CODEC).also { networkGroup.upstream.add(it) }
		var state by networkedBoolean().also { networkGroup.upstream.add(it) }

		fun addConnection(connection: WireConnection) {
			if (connection !in connections) {
				connections.add(connection)
			}
		}
	}

	val inputNodes: ImmutableList<WireNode> = lookupProperty("inputNodes") { JsonArray() }
		.asJsonArray
		.stream()
		.map { WireNode(vectors.fromJsonTree(it), true) }
		.collect(ImmutableList.toImmutableList())

	val outputNodes: ImmutableList<WireNode> = lookupProperty("outputNodes") { JsonArray() }
		.asJsonArray
		.stream()
		.map { WireNode(vectors.fromJsonTree(it), false) }
		.collect(ImmutableList.toImmutableList())

	val offeredQuests = NetworkedList(QuestArcDescriptor.CODEC, QuestArcDescriptor.LEGACY_CODEC).also { networkGroup.upstream.add(it) }
	val turnInQuests = NetworkedList(InternedStringCodec).also { networkGroup.upstream.add(it) }
	val damageSources = NetworkedList(DamageSource.CODEC, DamageSource.LEGACY_CODEC).also { networkGroup.upstream.add(it) }

	// don't interpolate scripted animation parameters
	val scriptedAnimationParameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { networkGroup.upstream.add(it, false) }

	// Why is this a thing when we have 'health' field??????? Hello chucklefish again?
	val tileHealth = TileHealth.TileEntity().also { networkGroup.upstream.add(it.networkGroup) }
	val animator: Animator

	init {
		if (config.value.animation?.value != null) {
			if (config.value.animationCustom.size() > 0 && config.value.animation!!.json != null) {
				animator = Animator(Starbound.gson.fromJson(mergeJson(config.value.animation!!.json!!, config.value.animationCustom), AnimationDefinition::class.java))
			} else {
				animator = Animator(config.value.animation!!.value!!)
			}
		} else {
			animator = Animator()
		}

		networkGroup.upstream.add(animator.networkGroup)
	}

	val lua = LuaEnvironment()
	val luaUpdate = LuaUpdateComponent(lua)
	val luaMessageHandler = LuaMessageHandler(lua)

	init {
		lua.globals["storage"] = lua.newTable()
	}

	val unbreakable by ManualLazy {
		lookupProperty("unbreakable") { JsonPrimitive(false) }.asBoolean
	}.also { parametersLazies.add(it) }

	override val type: EntityType
		get() = EntityType.OBJECT

	override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
		stream.writeBinaryString(config.key)
		stream.writeJsonElement(JsonObject().also {
			for ((k, v) in parameters) {
				it[k] = v.deepCopy()
			}
		})
	}

	private var frameTimer = 0.0
	private var frame = 0

	val flickerPeriod = config.value.flickerPeriod?.copy()

	// ????????
	val volumeBoundingBox: AABB get() {
		val orientation = orientation

		if (orientation == null) {
			return AABB(position, position + Vector2d.POSITIVE_XY)
		} else {
			return AABB(orientation.boundingBox.mins.toDoubleVector(), orientation.boundingBox.maxs.toDoubleVector() + Vector2d.POSITIVE_XY)
		}
	}

	protected open fun parametersUpdated() {
		parametersLazies.forEach { it.invalidate() }
	}

	protected open fun orientationUpdated() {
		parametersUpdated()
		spacesLazies.forEach { it.invalidate() }
		orientationLazies.forEach { it.invalidate() }

		val orientation = orientation

		if (orientation != null) {
			if (orientation.directionAffinity != null) {
				direction = orientation.directionAffinity
			}

			declaredMaterialSpaces.clear()
			declaredMaterialSpaces.addAll(orientation.materialSpaces)
		} else {
			declaredMaterialSpaces.clear()
		}
	}

	override fun onPositionUpdated() {
		super.onPositionUpdated()
		spacesLazies.forEach { it.invalidate() }

		if (isInWorld && world.isServer) {
			updateMaterialSpaces(listOf()) // remove old spaces after moving before updating orientation

			// update orientation
			updateOrientation()

			// reapply world spaces
			updateMaterialSpacesNow()
		}
	}

	private fun updateOrientation() {
		orientationIndex = config.value.findValidOrientation(world, tilePosition, direction).toLong()
	}

	override fun onJoinWorld(world: World<*, *>) {
		super.onJoinWorld(world)

		val orientation = orientation

		if (orientation == null)
			updateOrientation() // try to find valid orientation
		else {
			if (orientation.directionAffinity != null) {
				direction = orientation.directionAffinity
			}

			declaredMaterialSpaces.clear()
			declaredMaterialSpaces.addAll(orientation.materialSpaces)
		}

		if (isRemote) {

		} else {
			setImageKey("color", lookupProperty("color") { JsonPrimitive("default") }.asString)

			for ((k, v) in lookupProperty("animationParts") { JsonObject() }.asJsonObject.entrySet()) {
				animator.setPartTag(k, "partImage", v.asString)
			}

			updateMaterialSpacesNow()

			provideWorldBindings(world, lua)
			provideWorldObjectBindings(this, lua)
			provideEntityBindings(this, lua)
			provideAnimatorBindings(animator, lua)
			lua.attach(config.value.scripts)
			luaUpdate.stepCount = lookupProperty(JsonPath("scriptDelta")) { JsonPrimitive(5) }.asDouble
			lua.init()
		}

		// as original code puts it:
		// Don't animate the initial state when first spawned IF you're dumb, which by default
		// you would be, and don't know how to use transition and static states properly. Someday
		// I'll be brave and delete shit garbage entirely and we'll see what breaks.
		if (lookupProperty("forceFinishAnimationsInInit") { JsonPrimitive(true) }.asBoolean) {
			animator.finishAnimations()
		}
	}

	fun getRenderParam(key: String): String {
		return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default"
	}

	protected fun setImageKey(key: String, value: String) {
		val old = localRenderKeys.put(key, value)

		if (!isRemote && networkedRenderKeys[key] != value) {
			networkedRenderKeys[key] = value
		}
	}

	private val interactAction by ManualLazy {
		lookupProperty("interactAction") { JsonNull.INSTANCE }
	}.also { parametersLazies.add(it) }

	private val interactData by ManualLazy {
		lookupProperty("interactData") { JsonNull.INSTANCE }
	}.also { parametersLazies.add(it) }

	override fun interact(request: InteractRequest): InteractAction {
		val diff = world.geometry.diff(request.sourcePos, position)

		val result = lua.invokeGlobal("onInteraction", lua.newTable().apply {
			this["source"] = lua.tableOf(diff.x, diff.y)
			this["sourceId"] = request.source
		})

		if (result.isNotEmpty()) {
			if (result[0] == null)
				return InteractAction.NONE
			else if (result[0] is ByteString) {
				val decoded = (result[0] as ByteString).decode()
				return InteractAction(InteractAction.Type.entries.firstOrNull { it.jsonName == decoded } ?: throw NoSuchElementException("Unknown interaction action type $decoded!"))
			} else {
				val data = result[0] as Table
				val decoded = (data[1L] as ByteString).decode()
				return InteractAction(InteractAction.Type.entries.firstOrNull { it.jsonName == decoded } ?: throw NoSuchElementException("Unknown interaction action type $decoded!"), 0, toJsonFromLua(data[2L]))
			}
		}

		if (!interactAction.isJsonNull) {
			return InteractAction(interactAction.asString, entityID, interactData)
		}

		return super.interact(request)
	}

	fun callBreak(smash: Boolean = false) {

	}

	fun addChatMessage(message: String, config: JsonElement, portrait: String? = null) {

	}

	override fun tick(delta: Double) {
		super.tick(delta)

		flickerPeriod?.update(delta, world.random)

		if (!isRemote) {
			tileHealth.tick(config.value.damageConfig, delta)
			animator.tick(delta)

			val orientation = orientation

			if (orientation != null) {
				frameTimer = (frameTimer + delta) % orientation.animationCycle
				val oldFrame = frame
				frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt().coerceIn(0, orientation.frames)

				if (oldFrame != frame) {
					setImageKey("frame", frame.toString())
				}
			}

			try {
				luaUpdate.update(delta)
			} catch (err: Throwable) {
				LogManager.getLogger().error("Error running update", err)
			}
		}

		if (world.isServer && !unbreakable) {
			var shouldBreak = false

			if (health <= 0.0)
				shouldBreak = true

			if (!shouldBreak && tileHealth.isDead)
				shouldBreak = true

			if (!shouldBreak) {
				val orientation = orientation

				if (orientation != null && !orientation.anchorsValid(world, tilePosition)) {
					shouldBreak = true
				}
			}

			if (shouldBreak) {
				callBreak(false)
			}
		}
	}

	override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
		if (unbreakable)
			return false

		tileHealth.damage(config.value.damageConfig, source, damage)
		return tileHealth.isDead
	}

	companion object {
		private val lightColorPath = JsonPath("lightColor")
		private val lightColorsPath = JsonPath("lightColors")
		private val materialSpacesCodec = StreamCodec.Pair(Vector2iCodec, InternedStringCodec.map({ Registries.tiles.ref(this) }, { entry?.key ?: key.left.orElse("") }))
		private val materialSpacesCodecLegacy = StreamCodec.Pair(Vector2iCodec, UnsignedShortCodec.map({ Registries.tiles.ref(this) }, { entry?.id ?: 65534 }))

		private val colors1 by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, String::class.java, RGBAColor::class.java)) as TypeAdapter<ImmutableMap<String, RGBAColor>> }
		private val colors0 by lazy { Starbound.gson.getAdapter(RGBAColor::class.java) }
		private val strings by lazy { Starbound.gson.getAdapter(String::class.java) }
		private val directions by lazy { Starbound.gson.getAdapter(Direction::class.java) }
		private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }

		fun fromJson(content: JsonObject): WorldObject {
			val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'")

			val result = when (prototype.value.objectType) {
				ObjectType.OBJECT -> WorldObject(prototype)
				ObjectType.LOUNGEABLE -> LoungeableObject(prototype)
				ObjectType.CONTAINER -> ContainerObject(prototype)
				ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE")
				ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER")
				ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS")
			}

			result.deserialize(content)
			return result
		}

		fun create(prototype: Registry.Entry<ObjectDefinition>, position: Vector2i, parameters: JsonObject = JsonObject()): WorldObject? {
			val result = when (prototype.value.objectType) {
				ObjectType.OBJECT -> WorldObject(prototype)
				ObjectType.LOUNGEABLE -> LoungeableObject(prototype)
				ObjectType.CONTAINER -> ContainerObject(prototype)
				//ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE")
				//ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER")
				//ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS")
				else -> null
			}

			result?.loadParameters(parameters)
			result?.tilePosition = position
			return result
		}
	}
}
