package ru.dbotthepony.mc.otm.menu

import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Pair
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ReferenceArrayList
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.InventoryMenu
import net.minecraft.world.inventory.MenuType
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.enchantment.EnchantmentHelper.hasBindingCurse
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraftforge.network.NetworkEvent
import net.minecraftforge.network.PacketDistributor
import ru.dbotthepony.mc.otm.capability.IMatteryUpgrade
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.UpgradeType
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.compat.cos.cosmeticArmorSlots
import ru.dbotthepony.mc.otm.compat.curios.curiosSlots
import ru.dbotthepony.mc.otm.compat.curios.isCurioSlot
import ru.dbotthepony.mc.otm.container.ItemFilter
import ru.dbotthepony.mc.otm.container.ItemFilterNetworkSlot
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.UpgradeContainer
import ru.dbotthepony.mc.otm.core.GetterSetter
import ru.dbotthepony.mc.otm.core.collect.ConditionalEnumSet
import ru.dbotthepony.mc.otm.core.collect.ConditionalSet
import ru.dbotthepony.mc.otm.core.immutableList
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.util.BigDecimalValueCodec
import ru.dbotthepony.mc.otm.core.util.BinaryStringCodec
import ru.dbotthepony.mc.otm.core.util.BooleanValueCodec
import ru.dbotthepony.mc.otm.core.util.IStreamCodec
import ru.dbotthepony.mc.otm.core.util.ItemValueCodec
import ru.dbotthepony.mc.otm.core.util.NullValueCodec
import ru.dbotthepony.mc.otm.core.util.VarIntValueCodec
import ru.dbotthepony.mc.otm.menu.input.InstantBooleanInput
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.network.MatteryPlayerNetworkChannel
import ru.dbotthepony.mc.otm.network.MenuFieldPacket
import ru.dbotthepony.mc.otm.network.MenuNetworkChannel
import ru.dbotthepony.mc.otm.network.SetInventoryFilterPacket
import ru.dbotthepony.mc.otm.network.enqueueWork
import ru.dbotthepony.mc.otm.network.packetHandled
import ru.dbotthepony.mc.otm.network.sender
import ru.dbotthepony.mc.otm.network.synchronizer.FieldSynchronizer
import ru.dbotthepony.mc.otm.network.synchronizer.IField
import ru.dbotthepony.mc.otm.network.synchronizer.IMutableBooleanField
import java.io.DataInputStream
import java.io.DataOutputStream
import java.math.BigDecimal
import java.util.*
import java.util.function.BooleanSupplier
import java.util.function.DoubleSupplier
import java.util.function.IntSupplier
import java.util.function.Predicate
import java.util.function.Supplier

data class PlayerSlot<A : Slot, B : Slot>(val functional: A, val cosmetic: B? = null)

data class EquipmentSlots(
	val armorSlots: List<PlayerSlot<MatteryMenu.EquipmentSlot, Slot>>,
	val curiosSlots: List<PlayerSlot<Slot, Slot>>
)

/**
 * [openState] **is clientside only**, attempting to use it on server will result
 * in classloading exceptions.
 */
data class UpgradeSlots(
	val slots: List<MatterySlot>,
	val allowedTypes: Set<UpgradeType>,
	val openState: GetterSetter<Boolean>,
	val currentStats: IMatteryUpgrade
)

abstract class MatteryMenu @JvmOverloads protected constructor(
	menuType: MenuType<*>?,
	containerId: Int,
	val inventory: Inventory,
	val tile: BlockEntity? = null
) : AbstractContainerMenu(menuType, containerId) {
	/**
	 * Server->Client synchronizer
	 */
	val mSynchronizer = FieldSynchronizer()
	val ply: Player get() = inventory.player

	private val _playerInventorySlots = ArrayList<InventorySlot>()
	private val _playerInventorySlots2 = ArrayList<InventorySlot>()
	private val _playerHotbarSlots = ArrayList<InventorySlot>()
	private val _playerMainSlots = ArrayList<InventorySlot>()
	private val _playerExoSuitSlots = ArrayList<InventorySlot>()
	private val _playerCombinedInventorySlots = ArrayList<InventorySlot>()

	private val playerInputs = ArrayList<PlayerInput<*>>()

	class PlayerInputPacket(val containerId: Int, val inputId: Int, val payload: ByteArray) : MatteryPacket {
		constructor(buff: FriendlyByteBuf) : this(buff.readVarInt(), buff.readVarInt(), ByteArray(buff.readableBytes()).also { buff.readBytes(it) })

		override fun write(buff: FriendlyByteBuf) {
			buff.writeVarInt(containerId)
			buff.writeVarInt(inputId)
			buff.writeBytes(payload)
		}

		override fun play(context: Supplier<NetworkEvent.Context>) {
			context.packetHandled = true

			context.enqueueWork {
				val menu = context.sender?.containerMenu as? MatteryMenu ?: return@enqueueWork
				if (menu.containerId != containerId || !menu.stillValid(context.sender!!)) return@enqueueWork
				val input = menu.playerInputs.getOrNull(inputId) ?: return@enqueueWork
				if (!input.test(context.sender)) return@enqueueWork
				input.invoke(input.codec.read(DataInputStream(FastByteArrayInputStream(payload))))
			}
		}
	}

	/**
	 * Client->Server input
	 */
	inner class PlayerInput<V>(val codec: IStreamCodec<V>, allowSpectators: Boolean = false, val handler: (V) -> Unit) : Predicate<Player?> {
		val id = playerInputs.size
		var allowSpectators by mSynchronizer.bool(allowSpectators).property

		init {
			playerInputs.add(this)
		}

		private val filters = ArrayList<Predicate<Player>>()

		init {
			filters.add { allowSpectators || !it.isSpectator }
		}

		fun filter(predicate: Predicate<Player>): PlayerInput<V> {
			filters.add(predicate)
			return this
		}

		override fun test(player: Player?): Boolean {
			if (player == null) return false
			return filters.all { it.test(player) }
		}

		fun input(value: V) {
			if (test(minecraft.player as Player?)) {
				val stream = FastByteArrayOutputStream()
				codec.write(DataOutputStream(stream), value)
				MenuNetworkChannel.sendToServer(PlayerInputPacket(containerId, id, stream.array.copyOfRange(0, stream.length)))
			}
		}

		internal fun invoke(value: Any?) {
			handler.invoke(value as V)
		}
	}

	fun oneWayInput(allowSpectators: Boolean = false, handler: () -> Unit): PlayerInput<Nothing?> {
		return PlayerInput(NullValueCodec, allowSpectators) {
			handler.invoke()
		}
	}

	fun bigDecimalInput(allowSpectators: Boolean = false, handler: (BigDecimal) -> Unit) = PlayerInput(BigDecimalValueCodec, allowSpectators, handler)
	fun booleanInput(allowSpectators: Boolean = false, handler: (Boolean) -> Unit) = PlayerInput(BooleanValueCodec, allowSpectators, handler)
	fun stringInput(allowSpectators: Boolean = false, handler: (String) -> Unit) = PlayerInput(BinaryStringCodec, allowSpectators, handler)
	fun intInput(allowSpectators: Boolean = false, handler: (Int) -> Unit) = PlayerInput(VarIntValueCodec, allowSpectators, handler)

	/**
	 * inventory + exosuit + hotbar (in this order)
	 */
	val playerInventorySlots: List<InventorySlot> = Collections.unmodifiableList(_playerInventorySlots)

	/**
	 * hotbar + inventory + exosuit (in this order)
	 */
	val playerInventorySlots2: List<InventorySlot> = Collections.unmodifiableList(_playerInventorySlots2)

	/**
	 * hotbar only
	 */
	val playerHotbarSlots: List<InventorySlot> = Collections.unmodifiableList(_playerHotbarSlots)

	/**
	 * inventory only
	 */
	val playerMainSlots: List<InventorySlot> = Collections.unmodifiableList(_playerMainSlots)

	/**
	 * exosuit only
	 */
	val playerExoSuitSlots: List<InventorySlot> = Collections.unmodifiableList(_playerExoSuitSlots)

	/**
	 * inventory + exosuit (in this order)
	 */
	val playerCombinedInventorySlots: List<InventorySlot> = Collections.unmodifiableList(_playerCombinedInventorySlots)

	var autoCreateInventoryFrame = true
		private set

	protected val lockedInventorySlots: MutableSet<Int> = HashSet()
	protected open fun isInventorySlotLocked(index: Int): Boolean = lockedInventorySlots.contains(index)

	private val _filterSlots = ArrayList<ItemFilterNetworkSlot>()
	val filterSlots: List<ItemFilterNetworkSlot> = Collections.unmodifiableList(_filterSlots)

	protected var inventorySlotIndexStart = 0
	protected var inventorySlotIndexEnd = 0

	private val playerPacketDistributor = PacketDistributor.PLAYER.with { ply as ServerPlayer }

	fun addFilterSlots(slots: ItemFilter): List<ItemFilterNetworkSlot> {
		val result = ArrayList<ItemFilterNetworkSlot>(slots.size)

		for (i in 0 until slots.size) {
			_filterSlots.add(ItemFilterNetworkSlot(i, _filterSlots.size, slots).also(result::add))
		}

		return result
	}

	fun addFilterSlots(amount: Int): List<ItemFilterNetworkSlot> {
		val result = ArrayList<ItemFilterNetworkSlot>(amount)

		for (i in 0 until amount) {
			_filterSlots.add(ItemFilterNetworkSlot(i, _filterSlots.size, null).also(result::add))
		}

		return result
	}

	open inner class InventorySlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : UserFilteredSlot(container, index, x, y) {
		override fun mayPlace(itemStack: ItemStack): Boolean {
			return !isInventorySlotLocked(index) && super.mayPlace(itemStack)
		}

		override fun mayPickup(player: Player): Boolean {
			return !isInventorySlotLocked(index) && super.mayPickup(player)
		}

		override fun isSameInventory(other: Slot): Boolean {
			if (container === inventory || container === ply.matteryPlayer?.exoPackContainer)
				return (other.container === inventory || other.container === ply.matteryPlayer?.exoPackContainer) && isSameFilter(other)

			return super.isSameInventory(other)
		}

		var chargeFlag: GetterSetter<Boolean>? = null
			private set

		init {
			val mattery = ply.matteryPlayer

			if (mattery != null) {
				if (container === inventory) {
					if (slotIndex in mattery.regularSlotFilters.indices)
						filter = GetterSetter.of(
							getter = { mattery.regularSlotFilters[slotIndex].value },
							setter = { MatteryPlayerNetworkChannel.sendToServer(SetInventoryFilterPacket(SetInventoryFilterPacket.Type.INVENTORY, slotIndex, it)) }
						)

					if (slotIndex in mattery.regularSlotChargeFlag.indices) {
						val input = booleanInput(true) { mattery.regularSlotChargeFlag[slotIndex].boolean = it }

						chargeFlag = GetterSetter.of(
							getter = { mattery.regularSlotChargeFlag[slotIndex].boolean },
							setter = input::input
						)
					}
				} else if (container === mattery.exoPackContainer) {
					filter = GetterSetter.of(
						getter = { mattery.exoPackContainer.getSlotFilter(slotIndex) },
						setter = { MatteryPlayerNetworkChannel.sendToServer(SetInventoryFilterPacket(SetInventoryFilterPacket.Type.EXOPACK, slotIndex, it)) }
					)

					val input = booleanInput(true) { if (it) mattery.exoPackSlotsChargeFlag.add(slotIndex) else mattery.exoPackSlotsChargeFlag.remove(slotIndex) }

					chargeFlag = GetterSetter.of(
						getter = { slotIndex in mattery.exoPackSlotsChargeFlag },
						setter = input::input
					)
				} else {
					filter = null
				}
			} else {
				filter = null
			}
		}
	}

	open inner class EquipmentSlot(container: Container, index: Int, val type: net.minecraft.world.entity.EquipmentSlot, x: Int = 0, y: Int = 0) : InventorySlot(container, index, x, y) {
		constructor(type: net.minecraft.world.entity.EquipmentSlot, x: Int = 0, y: Int = 0) : this(
			inventory, 34 + type.ordinal, type, x, y
		)

		override fun mayPlace(itemStack: ItemStack): Boolean {
			return super.mayPlace(itemStack) && itemStack.canEquip(type, inventory.player)
		}

		override fun mayPickup(player: Player): Boolean {
			return super.mayPickup(player) && !item.isEmpty && (player.isCreative || !hasBindingCurse(item))
		}

		override fun getMaxStackSize(): Int {
			return 1
		}

		override fun getNoItemIcon(): Pair<ResourceLocation, ResourceLocation>? {
			return Pair.of(InventoryMenu.BLOCK_ATLAS, TEXTURE_EMPTY_SLOTS[type.index])
		}
	}

	protected fun addInventorySlots(autoFrame: Boolean = !ply.isSpectator) {
		check(_playerInventorySlots.isEmpty()) { "Already created inventory slots" }
		autoCreateInventoryFrame = autoFrame

		for (i in 9 .. 35) {
			val slot = InventorySlot(inventory, i)

			_playerInventorySlots.add(slot)
			_playerInventorySlots2.add(slot)
			_playerMainSlots.add(slot)
			_playerCombinedInventorySlots.add(slot)

			mapQuickMove(slot, equipmentSlots)
			mapQuickMoveToExternal(slot)
			addSlot(slot)
		}

		val mattery = ply.matteryPlayer

		if (mattery != null && mattery.hasExoPack) {
			for (i in 0 until mattery.exoPackContainer.containerSize) {
				val slot = InventorySlot(mattery.exoPackContainer, i)

				_playerInventorySlots.add(slot)
				_playerInventorySlots2.add(slot)
				_playerExoSuitSlots.add(slot)
				_playerCombinedInventorySlots.add(slot)

				mapQuickMove(slot, equipmentSlots)
				mapQuickMoveToExternal(slot)
				addSlot(slot)
			}
		}

		for (i in 0..8) {
			val slot = InventorySlot(inventory, i)

			_playerInventorySlots2.add(i, slot)
			_playerInventorySlots.add(slot)
			_playerHotbarSlots.add(slot)

			mapQuickMove(slot, equipmentSlots)
			mapQuickMoveToExternal(slot)
			addSlot(slot)
		}
	}

	override fun broadcastChanges() {
		val payload = mSynchronizer.collectNetworkPayload()

		if (payload != null) {
			MenuNetworkChannel.send(ply, MenuFieldPacket(containerId, payload))
		}

		super.broadcastChanges()

		val consumer = PacketDistributor.PLAYER.with { ply as ServerPlayer }

		for (slot in _filterSlots) {
			val packet = slot.sendChanges()

			if (packet != null) {
				MenuNetworkChannel.send(consumer, packet)
			}
		}
	}

	override fun broadcastFullState() {
		mSynchronizer.invalidate()
		val payload = mSynchronizer.collectNetworkPayload()

		if (payload != null) {
			MenuNetworkChannel.send(ply, MenuFieldPacket(containerId, payload))
		}

		super.broadcastFullState()

		val consumer = PacketDistributor.PLAYER.with { ply as ServerPlayer }

		for (slot in _filterSlots) {
			val packet = slot.sendChanges(true)

			if (packet != null) {
				MenuNetworkChannel.send(consumer, packet)
			}
		}
	}

	override fun stillValid(player: Player): Boolean {
		if (tile == null) return true

		if (player.level().getBlockEntity(tile.blockPos) !== tile) {
			return false
		}

		val pos = tile.blockPos

		return player.distanceToSqr(pos.x.toDouble() + 0.5, pos.y.toDouble() + 0.5, pos.z.toDouble() + 0.5) <= 64.0
	}

	private val externalSlots = ConditionalSet<Slot>()
	private val quickMoveMapping = Reference2ObjectOpenHashMap<Slot, ReferenceArrayList<Collection<Slot>>>()

	override fun addSlot(pSlot: Slot): Slot {
		if (pSlot in slots) {
			return pSlot
		}

		if (pSlot is UserFilteredSlot && !pSlot.hasSetFilter) {
			val container = pSlot.container

			val input: PlayerInput<Item?>
			val field: IField<Item?>

			if (container is MatteryContainer) {
				input = PlayerInput(ItemValueCodec.nullable, handler = { container.setSlotFilter(pSlot.slotIndex, it) })
				field = mSynchronizer.ComputedField(getter = { container.getSlotFilter(pSlot.slotIndex) }, ItemValueCodec.nullable)
			} else {
				input = PlayerInput(ItemValueCodec.nullable, handler = { throw UnsupportedOperationException() })
				field = mSynchronizer.ComputedField(getter = { null }, ItemValueCodec.nullable)
			}

			pSlot.filter = GetterSetter.of(getter = field::value, setter = input::input)
		}

		return super.addSlot(pSlot)
	}

	/**
	 * Adds slot to "storage slots" list (utilized by quick move) and calls [addSlot]
	 *
	 * [condition] allows to specify when slot is invisible on GUI (hence being ignored by quick move)
	 */
	protected fun <T : Slot> addStorageSlot(slot: T, addMapping: Boolean = true, prepend: Boolean = false, condition: BooleanSupplier = BooleanSupplier { true }): T {
		if (!externalSlots.actuallyContains(slot)) {
			addSlot(slot)

			if (prepend)
				externalSlots.replaceFirst(slot, condition)
			else
				externalSlots.replace(slot, condition)

			if (addMapping) {
				mapQuickMove(slot, equipmentSlots)
				mapQuickMoveToInventory(slot)
			}
		}

		return slot
	}

	protected fun addStorageSlot(slot: Iterable<Slot>, addMapping: Boolean = true, prepend: Boolean = false, condition: BooleanSupplier = BooleanSupplier { true }) {
		for (value in slot)
			addStorageSlot(value, addMapping, prepend, condition)
	}

	/**
	 * Marks slot as "storage" - shift clicking it will move its contents to Player's inventory
	 */
	protected fun mapQuickMoveToInventory(slot: Slot, prepend: Boolean = false) {
		mapQuickMove(slot, playerInventorySlots, prepend = prepend)
	}

	/**
	 * Marks slot as "inventory" - shift clicking it will move its contents to menu's storage slots
	 */
	protected fun mapQuickMoveToExternal(slot: Slot, prepend: Boolean = false) {
		mapQuickMove(slot, externalSlots, prepend = prepend)
	}

	protected fun mapQuickMove(slot: Slot, target: Collection<Slot>, prepend: Boolean = false, condition: BooleanSupplier = BooleanSupplier { true }) {
		val listing = quickMoveMapping.computeIfAbsent(slot, Reference2ObjectFunction { ReferenceArrayList(1) /* ReferenceArrayList ибо мы используем его в некотором смысле как множество */ })
		listing.remove(target)

		if (prepend)
			listing.add(0, target)
		else
			listing.add(target)
	}

	protected fun mapQuickMove(slot: Slot, vararg target: Collection<Slot>) {
		for (value in target) {
			mapQuickMove(slot, value)
		}
	}

	// This method receive Player interactor and slot_index where Shift + Right click occurred
	// It shall return item stack that got moved
	override fun quickMoveStack(ply: Player, slotIndex: Int): ItemStack {
		val slot = slots[slotIndex]

		if (!slot.hasItem() || !slot.mayPickup(ply)) {
			return ItemStack.EMPTY
		}

		val target = quickMoveMapping[slot] ?: return ItemStack.EMPTY
		val copy = slot.item.copy()
		var any = false

		if (target.any { it.any { it is UserFilteredSlot && it.filter != null } }) {
			for (collection in target) {
				if (moveItemStackTo(slot, collection, onlyFiltered = true)) {
					any = true

					if (!slot.hasItem()) {
						return copy
					}
				}
			}
		}

		for (collection in target) {
			if (moveItemStackTo(slot, collection)) {
				any = true

				if (!slot.hasItem()) {
					return copy
				}
			}
		}

		if (any) {
			return copy
		} else {
			return ItemStack.EMPTY
		}
	}

	fun quickMoveToInventory(stack: ItemStack, simulate: Boolean): ItemStack {
		if (_playerInventorySlots.isEmpty()) {
			return stack
		}

		return moveItemStackToSlots(stack, _playerInventorySlots, simulate = simulate)
	}

	override fun canTakeItemForPickAll(itemStack: ItemStack, slot: Slot): Boolean {
		if (slot is EquipmentSlot)
			return false

		return super.canTakeItemForPickAll(itemStack, slot) && (slot !is MatterySlot || slot.canTakeItemForPickAll()) && !slot.isCurioSlot
	}

	override fun moveItemStackTo(
		item: ItemStack,
		initialSlot: Int,
		finalSlot: Int,
		inverse: Boolean
	): Boolean {
		val remainder = moveItemStackToSlots(item, initialSlot, finalSlot, inverse)

		if (remainder.count == item.count) {
			return false
		}

		item.count = remainder.count
		return true
	}

	fun moveItemStackTo(
		source: Slot,
		slots: Collection<Slot>,
		onlyFiltered: Boolean = false
	): Boolean {
		val remainder = moveItemStackToSlots(source.item, slots, onlyFiltered = onlyFiltered)

		if (remainder.count == source.item.count) {
			return false
		}

		val copy = source.item.copy()

		if (remainder.isEmpty) {
			source.set(ItemStack.EMPTY)
			source.onTake(ply, copy)
		} else {
			copy.count = source.item.count - remainder.count
			source.item.count = remainder.count
			source.onTake(ply, copy)
		}

		return true
	}

	fun moveItemStackToSlots(item: ItemStack, slots: Collection<Slot>, simulate: Boolean = false, onlyFiltered: Boolean = false): ItemStack {
		if (item.isEmpty) {
			return ItemStack.EMPTY
		}

		val copy = item.copy()

		// first pass - stack with existing slots
		if (copy.isStackable) {
			for (slot in slots) {
				if (onlyFiltered && (slot !is UserFilteredSlot || slot.filter == null || slot.filter!!.get() != item.item)) {
					continue
				} else if (!onlyFiltered && slot is UserFilteredSlot && slot.filter != null && slot.filter!!.get() != null && slot.filter!!.get() != item.item) {
					continue
				}

				val limit = slot.getMaxStackSize(copy)

				if (limit > slot.item.count && slot.mayPlace(item) && ItemStack.isSameItemSameTags(slot.item, copy)) {
					val newCount = (slot.item.count + copy.count).coerceAtMost(limit)
					val diff = newCount - slot.item.count
					copy.count -= diff

					if (!simulate) {
						slot.item.count += diff
						slot.setChanged()
					}

					if (copy.isEmpty) {
						return copy
					}
				}
			}
		}

		// second pass - drop stack into first free slot
		for (slot in slots) {
			if (onlyFiltered && (slot !is UserFilteredSlot || slot.filter == null || slot.filter!!.get() != item.item)) {
				continue
			} else if (!onlyFiltered && slot is UserFilteredSlot && slot.filter != null && slot.filter!!.get() != null && slot.filter!!.get() != item.item) {
				continue
			}

			val limit = slot.getMaxStackSize(copy)

			if (!slot.hasItem() && slot.mayPlace(item)) {
				val newCount = copy.count.coerceAtMost(limit)

				if (!simulate) {
					slot.set(copy.copy().also { it.count = newCount })
					slot.setChanged()
				}

				copy.count -= newCount

				if (copy.isEmpty) {
					return copy
				}
			}
		}

		return copy
	}

	fun moveItemStackToSlots(item: ItemStack, initialSlot: Int, finalSlot: Int, inverse: Boolean = false, simulate: Boolean = false): ItemStack {
		if (initialSlot > finalSlot) {
			return item
		}

		require(initialSlot >= 0) { "Invalid initial slot $initialSlot" }
		require(finalSlot < slots.size) { "Final slot $finalSlot is bigger than total size of array of ${slots.size}" }

		val slots = ArrayList<Slot>(finalSlot - initialSlot + 1)
		var filters = false

		for (i in (if (inverse) finalSlot downTo initialSlot else initialSlot .. finalSlot)) {
			val slot = slots[i]
			slots.add(slot)

			if (slot is InventorySlot && slot.filter != null) {
				filters = true
			}
		}

		if (filters) {
			return moveItemStackToSlots(moveItemStackToSlots(item, slots, simulate, onlyFiltered = true), slots, simulate, onlyFiltered = false)
		}

		return moveItemStackToSlots(item, slots, simulate)
	}

	private var armorSlots: ImmutableList<PlayerSlot<EquipmentSlot, Slot>>? = null
	private var curiosSlots: ImmutableList<PlayerSlot<Slot, Slot>>? = null
	private val equipmentSlots = ArrayList<Slot>()

	fun makeArmorSlots(mapMoveToExternal: Boolean = false): List<PlayerSlot<EquipmentSlot, Slot>> {
		if (armorSlots != null) {
			return armorSlots!!
		}

		val cosmetic = ply.cosmeticArmorSlots

		return ImmutableList.of(
			PlayerSlot(EquipmentSlot(net.minecraft.world.entity.EquipmentSlot.HEAD), cosmetic?.get(net.minecraft.world.entity.EquipmentSlot.HEAD)),
			PlayerSlot(EquipmentSlot(net.minecraft.world.entity.EquipmentSlot.CHEST), cosmetic?.get(net.minecraft.world.entity.EquipmentSlot.CHEST)),
			PlayerSlot(EquipmentSlot(net.minecraft.world.entity.EquipmentSlot.LEGS), cosmetic?.get(net.minecraft.world.entity.EquipmentSlot.LEGS)),
			PlayerSlot(EquipmentSlot(net.minecraft.world.entity.EquipmentSlot.FEET), cosmetic?.get(net.minecraft.world.entity.EquipmentSlot.FEET)),
		).also {
			armorSlots = it

			for ((a, b) in it) {
				equipmentSlots.add(a)
				addSlot(a)
				if (b != null) addSlot(b)

				if (mapMoveToExternal) {
					mapQuickMoveToExternal(a)
				}

				mapQuickMoveToInventory(a)

				if (b != null) {
					if (mapMoveToExternal) {
						mapQuickMoveToExternal(b)
					}

					mapQuickMoveToInventory(b)
				}
			}
		}
	}

	fun makeEquipmentSlots(mapMoveToExternal: Boolean = false): EquipmentSlots {
		return EquipmentSlots(
			armorSlots = makeArmorSlots(mapMoveToExternal),
			curiosSlots = curiosSlots ?: ImmutableList.copyOf(ply.curiosSlots).also {
				for ((a, b) in it) {
					equipmentSlots.add(a)
					addSlot(a)
					if (b != null) addSlot(b)

					if (mapMoveToExternal) {
						mapQuickMoveToExternal(a)
					}

					mapQuickMoveToInventory(a)

					if (b != null) {
						if (mapMoveToExternal) {
							mapQuickMoveToExternal(b)
						}

						mapQuickMoveToInventory(b)
					}
				}
			}.also { curiosSlots = it }
		)
	}

	fun makeUpgradeSlots(count: Int, container: UpgradeContainer?): UpgradeSlots {
		if (container != null) {
			require(count == container.containerSize) { "Upgrade container size ${container.containerSize} does not match with provided size $count" }
		}

		val allowedTypes = EnumMap<UpgradeType, IMutableBooleanField>(UpgradeType::class.java)

		for (value in UpgradeType.ALL) {
			allowedTypes[value] = mSynchronizer.bool()

			if (container != null) {
				allowedTypes[value]!!.boolean = value in container.allowedUpgrades
			}
		}

		val syncContainer = container ?: SimpleContainer(count)

		val isOpen = InstantBooleanInput(this)

		return UpgradeSlots(
			slots = immutableList(count) {
				object : MatterySlot(syncContainer, it) {
					init {
						mapQuickMoveToInventory(this)
					}

					override fun mayPlace(itemStack: ItemStack): Boolean {
						return super.mayPlace(itemStack) && itemStack.getCapability(MatteryCapability.UPGRADE).map { it.upgradeTypes.any { allowedTypes[it]!!.boolean } }.orElse(false)
					}
				}
			}.also { for (i in it.indices.reversed()) addStorageSlot(it[i], prepend = true, condition = isOpen) },

			allowedTypes = ConditionalEnumSet(allowedTypes),
			openState = isOpen,
			currentStats = object : IMatteryUpgrade {
				override val upgradeTypes: Set<UpgradeType> = setOf()
				override val speedBonus: Double by mSynchronizer.computedDouble(DoubleSupplier { container?.speedBonus ?: 0.0 }).property
				override val processingItems: Int by mSynchronizer.computedInt(IntSupplier { container?.processingItems ?: 0 }).property
				override val energyStorageFlat: Decimal by mSynchronizer.computedDecimal { container?.energyStorageFlat ?: Decimal.ZERO }
				override val energyStorage: Decimal by mSynchronizer.computedDecimal { container?.energyStorage ?: Decimal.ZERO }
				override val energyConsumed: Decimal by mSynchronizer.computedDecimal { container?.energyConsumed ?: Decimal.ZERO }
				override val energyThroughputFlat: Decimal by mSynchronizer.computedDecimal { container?.energyThroughputFlat ?: Decimal.ZERO }
				override val energyThroughput: Decimal by mSynchronizer.computedDecimal { container?.energyThroughput ?: Decimal.ZERO }
				override val failureMultiplier: Double by mSynchronizer.computedDouble(DoubleSupplier { container?.failureMultiplier ?: 1.0 }).property
			}
		)
	}

	companion object {
		val TEXTURE_EMPTY_SLOTS: List<ResourceLocation> = ImmutableList.of(
			InventoryMenu.EMPTY_ARMOR_SLOT_BOOTS,
			InventoryMenu.EMPTY_ARMOR_SLOT_LEGGINGS,
			InventoryMenu.EMPTY_ARMOR_SLOT_CHESTPLATE,
			InventoryMenu.EMPTY_ARMOR_SLOT_HELMET)
	}
}
