package ru.dbotthepony.kstarbound.world.entities.player

import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.guava.immutableMap
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.actor.EquipmentSlot
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.io.readInternedString
import java.io.DataInputStream
import java.io.DataOutputStream

// sigh
// must do this to remain compatible with original network protocol
sealed class InventoryIndex {
	abstract fun get(inventory: PlayerInventory): ItemStack
	abstract fun set(inventory: PlayerInventory, value: ItemStack)
	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)

	open fun isValid(inventory: PlayerInventory): Boolean {
		return true
	}

	private data class BagIndex(val bag: String, val slot: Int) : InventoryIndex() {
		init {
			require(slot >= 0) { "Negative slot index: $slot" }
		}

		override fun get(inventory: PlayerInventory): ItemStack {
			return (inventory.bags[bag] ?: throw NoSuchElementException("No such bag $bag"))[slot]
		}

		override fun set(inventory: PlayerInventory, value: ItemStack) {
			(inventory.bags[bag] ?: throw NoSuchElementException("No such bag $bag"))[slot] = value
		}

		override fun isValid(inventory: PlayerInventory): Boolean {
			val bag = inventory.bags[bag] ?: return false
			return slot in 0 until bag.size
		}

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)

			stream.writeBinaryString(bag)

			// god help us if new client joins original server
			// with mods which make inventory size absurd
			if (isLegacy)
				stream.writeByte(slot)
			else
				stream.writeVarInt(slot)
		}
	}

	private data class EquipmentIndex(val slot: EquipmentSlot) : InventoryIndex() {
		override fun get(inventory: PlayerInventory): ItemStack {
			return inventory[slot]
		}

		override fun set(inventory: PlayerInventory, value: ItemStack) {
			inventory[slot] = value
		}

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(0)
			stream.writeByte(slot.ordinal)
		}
	}

	private object Hand : InventoryIndex() {
		override fun get(inventory: PlayerInventory): ItemStack {
			return inventory.handSlot
		}

		override fun set(inventory: PlayerInventory, value: ItemStack) {
			inventory.handSlot = value
		}

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(2)
		}
	}

	private object Trash : InventoryIndex() {
		override fun get(inventory: PlayerInventory): ItemStack {
			return inventory.trashSlot
		}

		override fun set(inventory: PlayerInventory, value: ItemStack) {
			inventory.trashSlot = value
		}

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(3)
		}
	}

	companion object {
		private val bagIndexCache by lazy {
			immutableMap {
				Globals.player.inventory.itemBags.keys.forEach { n ->
					put(n, immutableList(256) {
						BagIndex(n, it)
					})
				}
			}
		}

		private val equipment = immutableList(EquipmentSlot.entries.size) {
			EquipmentIndex(EquipmentSlot.entries[it])
		}

		fun hand(): InventoryIndex {
			return Hand
		}

		fun trash(): InventoryIndex {
			return Trash
		}

		fun equipment(slot: EquipmentSlot): InventoryIndex {
			return equipment[slot.ordinal]!!
		}

		fun bag(name: String, index: Int): InventoryIndex {
			return bagIndexCache[name]?.getOrNull(index) ?: BagIndex(name, index)
		}

		fun read(stream: DataInputStream, isLegacy: Boolean): InventoryIndex {
			return when (val type = stream.readUnsignedByte()) {
				0 -> equipment[stream.readUnsignedByte()]!!
				1 -> bag(stream.readInternedString(), if (isLegacy) stream.readUnsignedByte() else stream.readVarInt())
				2 -> hand()
				3 -> trash()
				else -> throw IllegalArgumentException("Unknown InventoryIndex type $type!")
			}
		}
	}
}
