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

import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.item.IContainer
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedBytes
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.util.ManualLazy
import ru.dbotthepony.kstarbound.util.RelativeClock
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator

class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
	var opened by networkedSignedInt().also { networkGroup.upstream.add(it) }
	var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) }
	var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear }
	val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) }

	override fun parametersUpdated() {
		super.parametersUpdated()
		items.size = lookupProperty("slotCount").asInt
	}

	// whenever set loot seed, put initial items, etc.
	private var isInitialized = false

	private val lostItems = ArrayList<ItemStack>()
	private val ageItemsTimer = RelativeClock()

	private val ageItemsEvery = ManualLazy {
		lookupProperty("ageItemsEvery") { JsonPrimitive(10.0) }.asDouble
	}.also { parametersLazies.add(it) }

	private val itemAgeMultiplier = ManualLazy {
		lookupProperty("itemAgeMultiplier") { JsonPrimitive(1.0) }.asDouble
	}.also { parametersLazies.add(it) }

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

		if (world.isServer) {
			for (item in lostItems) {
				val entity = ItemDropEntity(item)
				entity.position = position
				entity.joinWorld(world)
			}

			lostItems.clear()
			ageItemsTimer.update(world.sky.time)

			if (ageItemsTimer.time >= ageItemsEvery.value) {
				items.ageItems(ageItemsTimer.time * itemAgeMultiplier.value)
				ageItemsTimer.set(0.0)
			}
		}
	}

	override fun interact(request: InteractRequest): InteractAction {
		return InteractAction(InteractAction.Type.OPEN_CONTAINER, entityID)
	}

	override fun deserialize(data: JsonObject) {
		super.deserialize(data)

		opened = data.get("opened", 0)
		isCrafting = data.get("crafting", false)
		craftingProgress = data.get("craftingProgress", 0.0)
		isInitialized = data.get("initialized", true)
		items.fromJson(data.get("items", JsonArray()), resize = true)
	}

	override fun serialize(): JsonObject {
		val data = super.serialize()

		// required by original engine
		data["currentState"] = 0
		data["opened"] = opened
		data["crafting"] = isCrafting
		data["craftingProgress"] = craftingProgress
		data["initialized"] = isInitialized
		data["items"] = items.toJson(true)

		return data
	}

	private fun randomizeContents(random: RandomGenerator, threatLevel: Double) {
		var level = threatLevel
		level = lookupProperty("level") { JsonPrimitive(level) }.asDouble
		level += lookupProperty("levelAdjustment") { JsonPrimitive(0.0) }.asDouble

		val initialItems = lookupProperty("initialItems")

		if (!initialItems.isJsonNull) {
			for (item in initialItems.asJsonArray) {
				items.add(ItemDescriptor(item).build(level, random.nextLong(), random))
			}
		}

		val treasurePools = lookupProperty("treasurePools")

		if (!treasurePools.isJsonNull) {
			val get = treasurePools.asJsonArray.random(random).asString
			val treasurePool = Registries.treasurePools[get]

			if (treasurePool == null) {
				LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.")
			} else {
				for (item in treasurePool.value.evaluate(random, level)) {
					val leftover = items.add(item)

					if (leftover.isNotEmpty) {
						LOGGER.warn("Tried to overfill container at $tilePosition")
						lostItems.add(leftover)
					}
				}
			}
		}
	}

	override fun randomize(random: RandomGenerator, threatLevel: Double) {
		super.randomize(random, threatLevel)
		randomizeContents(random, threatLevel)
	}

	override fun onJoinWorld(world: World<*, *>) {
		if (!isRemote)
			isInteractive = true

		super.onJoinWorld(world)

		if (!isRemote) {
			if (isInitialized) return
			isInitialized = true

			val seed = lookupProperty("treasureSeed")

			if (seed.isJsonNull) {
				randomizeContents(world.random, world.template.threatLevel)
			} else {
				randomizeContents(random(seed.asLong), world.template.threatLevel)
			}
		}
	}

	// Networking of this container to legacy clients is incredibly stupid,
	// and networks entire state each time something has changed.
	inner class Container(size: Int) : NetworkedElement(), IContainer {
		private var items = Array(size) { ItemStack.EMPTY }
		private var itemNetVersions = LongArray(size)
		private var itemObservedVersion = LongArray(size)
		private var lastSizeChange = 0L

		private fun observeItems() {
			for (i in 0 until size) {
				if (itemObservedVersion[i] != items[i].changeset) {
					itemObservedVersion[i] = items[i].changeset
					itemNetVersions[i] = currentVersion()
				}
			}
		}

		override var size: Int = size
			set(value) {
				if (field == value) return
				lastSizeChange = currentVersion()

				if (value > field) {
					items = items.copyOf(value) as Array<ItemStack>

					for (i in field until value)
						items[i] = ItemStack.EMPTY
				} else {
					for (i in value until field) {
						lostItems.add(items[i])
					}

					items = items.copyOf(value) as Array<ItemStack>
				}

				field = value
				itemObservedVersion = itemObservedVersion.copyOf(value)
				itemNetVersions = itemNetVersions.copyOf(value)
			}

		override fun get(index: Int): ItemStack {
			return items[index]
		}

		override fun set(index: Int, value: ItemStack) {
			items[index] = value.copy()
		}

		override fun clear() {
			items.fill(ItemStack.EMPTY)
		}

		override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
			if (isLegacy) {
				val stream = DataInputStream(FastByteArrayInputStream(data.readByteArray()))

				size = stream.readVarInt()

				val setItemsSize = stream.readVarInt()
				items.fill(ItemStack.EMPTY)

				for (i in 0 until setItemsSize) {
					items[i] = ItemDescriptor(stream).build()
				}
			} else {
				size = data.readVarInt()
				items.fill(ItemStack.EMPTY)

				while (true) {
					val index = data.readVarInt() - 1
					if (index == -1) break
					items[index] = ItemDescriptor(data).build()
				}
			}
		}

		override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
			if (isLegacy) {
				val wrapper = FastByteArrayOutputStream()
				val stream = DataOutputStream(wrapper)
				stream.writeVarInt(size)
				val setItemsSize = items.indexOfLast { it.isNotEmpty } + 1

				stream.writeVarInt(setItemsSize)

				for (i in 0 until setItemsSize) {
					items[i].write(stream)
				}

				data.writeByteArray(wrapper.array, 0, wrapper.length)
			} else {
				data.writeVarInt(size)

				for (i in 0 until size) {
					if (items[i].isNotEmpty) {
						data.writeVarInt(i + 1)
						items[i].write(data)
					}
				}

				data.writeVarInt(0)
			}
		}

		override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
			if (isLegacy) {
				readInitial(data, true)
			} else {
				if (data.readBoolean()) {
					readInitial(data, false)
				} else {
					while (true) {
						val index = data.readVarInt() - 1
						if (index == -1) break
						items[index] = ItemDescriptor(data).build()
					}
				}
			}
		}

		override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
			if (isLegacy) {
				writeInitial(data, true)
			} else {
				if (lastSizeChange >= remoteVersion) {
					data.writeBoolean(true)
					writeInitial(data, false)
				} else {
					observeItems()
					data.writeBoolean(false)

					for (i in 0 until size) {
						if (itemNetVersions[i] >= remoteVersion) {
							data.writeVarInt(i + 1)
							items[i].write(data)
						}
					}

					data.writeVarInt(0)
				}
			}
		}

		override fun hasChangedSince(version: Long): Boolean {
			observeItems()
			return lastSizeChange >= version || itemNetVersions.any { it >= version }
		}

		override fun readBlankDelta(interpolationDelay: Double) {}
		override fun enableInterpolation(extrapolation: Double) {}
		override fun disableInterpolation() {}
		override fun tickInterpolation(delta: Double) {}
	}

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