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

import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.io.LongValueCodec
import ru.dbotthepony.kommons.io.UnsignedVarLongCodec
import ru.dbotthepony.kommons.io.readKOptional
import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.actor.EquipmentSlot
import ru.dbotthepony.kstarbound.defs.actor.EssentialSlot
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedItem
import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt
import ru.dbotthepony.kstarbound.item.IContainer
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.NetworkedItemStack
import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
import java.io.DataInputStream
import java.io.DataOutputStream

class PlayerInventory {
	inner class Bag(size: Int) : IContainer {
		override var size: Int = size
			set(value) { throw UnsupportedOperationException() }

		override fun clear() {
			slots.forEach { it.accept(ItemStack.EMPTY) }
		}

		val slots = immutableList(size) { networkedItem() }

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

		override fun set(index: Int, value: ItemStack) {
			slots[index].accept(value)
		}
	}

	data class HotbarSlot(val left: KOptional<InventoryIndex> = KOptional(), val right: KOptional<InventoryIndex> = KOptional()) {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readKOptional { InventoryIndex.read(stream, isLegacy) }, stream.readKOptional { InventoryIndex.read(stream, isLegacy) })

		fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeKOptional(left) { it.write(this, isLegacy) }
			stream.writeKOptional(right) { it.write(this, isLegacy) }
		}

		companion object {
			val CODEC = nativeCodec(::HotbarSlot, HotbarSlot::write)
			val LEGACY_CODEC = legacyCodec(::HotbarSlot, HotbarSlot::write)
		}
	}

	val networkGroup = NetworkedGroup()

	// here it gets interesting, original code is using List#sorted, which itself uses Star::sort,
	// which is just an alias for std::sort, and std::sort is ***not*** stable sort, meaning
	// if bags have same priority, PlayerInventory behavior becomes undefined
	// We, on the other hand, use stable sort provided by Java
	// (and if bags have the same priority they will appear in order they are defined in json)
	val bags: ImmutableMap<String, Bag> = Globals.player.inventory.itemBags.entries
		.stream()
		.sorted { o1, o2 -> o1.value.priority.compareTo(o2.value.priority) }
		.map { it.key to Bag(it.value.size) }
		.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))

	val equipment: ImmutableMap<EquipmentSlot, NetworkedItemStack> = EquipmentSlot.entries
		.stream()
		.map { it to networkedItem() }
		.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))

	val essentialSlots: ImmutableMap<EssentialSlot, NetworkedItemStack> = EssentialSlot.entries
		.stream()
		.map { it to networkedItem() }
		.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))

	private val currencies = NetworkedMap(keyCodec = InternedStringCodec, valueCodec = UnsignedVarLongCodec to LongValueCodec, isDumb = true)

	init {
		// this is required for original engine
		// because otherwise
		// it will throw "element not found"
		// when trying to update currencies
		for (key in Globals.currencies.keys) {
			currencies[key] = 0L
		}
	}

	operator fun get(index: EquipmentSlot): ItemStack {
		return equipment[index]!!.get()
	}

	operator fun set(index: EquipmentSlot, value: ItemStack) {
		equipment[index]!!.accept(value)
	}

	operator fun get(index: EssentialSlot): ItemStack {
		return essentialSlots[index]!!.get()
	}

	operator fun set(index: EssentialSlot, value: ItemStack) {
		essentialSlots[index]!!.accept(value)
	}

	init {
		equipment.values.forEach {
			networkGroup.add(it)
		}

		bags.values.forEach {
			it.slots.forEach {
				networkGroup.add(it)
			}
		}
	}

	// "swap slot" in original sources
	var handSlot by networkGroup.add(networkedItem())
	var trashSlot by networkGroup.add(networkedItem())

	init {
		networkGroup.add(currencies)
	}

	var hotbarGroup by networkGroup.add(networkedUnsignedInt())
	val hotbarSlots = Object2DArray(Globals.player.inventory.customBarIndexes, Globals.player.inventory.customBarGroups) { _, _ ->
		networkedData(HotbarSlot(), HotbarSlot.CODEC, HotbarSlot.LEGACY_CODEC)
	}

	init {
		for (x in 0 until hotbarSlots.rows) {
			for (y in 0 until hotbarSlots.columns) {
				networkGroup.add(hotbarSlots[y, x])
			}
		}
	}

	var hotbarIndex by networkGroup.add(networkedData(HotbarIndex.nothing(), HotbarIndex.CODEC))

	init {
		essentialSlots.values.forEach { networkGroup.add(it) }
	}
}
