package ru.dbotthepony.mc.otm.compat

import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ReferenceArraySet
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.world.SimpleContainer
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.InventoryMenu
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraftforge.network.NetworkEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.menu.MatteryMenu
import ru.dbotthepony.mc.otm.menu.MatterySlot
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.network.packetHandled
import ru.dbotthepony.mc.otm.network.sender
import java.util.*
import java.util.function.Supplier

private val menuConfigurations = WeakHashMap<AbstractContainerMenu, MenuConfiguration>()

private class MenuConfiguration(
	private val player: Player,
	private val menu: AbstractContainerMenu,
	originalInventorySlots: Collection<Slot>,
	private val indices: Collection<Int>
) {
	private data class SlotPos(val x: Int, val y: Int) {
		constructor(slot: Slot) : this(slot.x, slot.y)
	}

	private val slotPositions = Int2ObjectArrayMap<List<SlotPos>>()
	private val rows = Int2ObjectOpenHashMap<List<Slot>>()
	val isIncomplete: Boolean

	private fun getRow(index: Int, matteryPlayer: MatteryPlayerCapability) = rows.computeIfAbsent(index, Int2ObjectFunction {
		val row = ArrayList<Slot>(9)
		val offset = (it - 3) * 9

		for (i in 0 .. 8) {
			if (matteryPlayer.exoPackContainer.containerSize > i + offset) {
				row.add(MatterySlot(matteryPlayer.exoPackContainer, i + offset))
			} else {
				row.add(FakeSlot())
			}
		}

		row
	})

	var scroll = 0
		set(value) {
			if (field == value || isIncomplete || value < 0) {
				return
			}

			val matteryPlayer = player.matteryPlayer ?: return

			if (!matteryPlayer.hasExoPack || matteryPlayer.exoPackContainer.containerSize <= (value - 1) * 9) {
				return
			}

			for (rowIndex in 0 .. 2) {
				for (slot in getRow(rowIndex + field, matteryPlayer)) {
					if (slot.container === matteryPlayer.exoPackContainer) {
						@Suppress("name_shadowing")
						val indexOf = matteryPlayer.exoPackMenu.slots.indexOfFirst { it.slotIndex == slot.slotIndex }

						if (indexOf != -1) {
							if (slot.item.isEmpty) {
								matteryPlayer.exoPackMenu.setRemoteSlotNoCopy(indexOf, ItemStack(Items.BARRIER))
							} else {
								matteryPlayer.exoPackMenu.setRemoteSlotNoCopy(indexOf, ItemStack.EMPTY)
							}
						}
					}
				}
			}

			field = value
			val indices = this.indices.iterator()

			for (rowIndex in 0 .. 2) {
				val row = getRow(rowIndex + field, matteryPlayer)
				val positions = slotPositions[rowIndex] ?: throw IndexOutOfBoundsException("$rowIndex")

				for (column in 0 .. 8) {
					val slot = row[column]
					val (x, y) = positions[column]

					val index = indices.next()
					menu.slots[index] = slot
					slot.x = x
					slot.y = y

					if (slot.item.isEmpty) {
						menu.setRemoteSlotNoCopy(index, ItemStack(Items.BARRIER))
					} else {
						menu.setRemoteSlotNoCopy(index, ItemStack.EMPTY)
					}

					slot.index = index
				}
			}
		}

	init {
		kotlin.run {
			for (i in 0 .. 2) {
				val offset = 9 + i * 9
				val row = ArrayList<Slot>(9)
				val slotPositions = ArrayList<SlotPos>(9)

				for (i2 in 0 .. 8) {
					val slot = originalInventorySlots.firstOrNull { it.slotIndex == i2 + offset }

					if (slot == null) {
						this.isIncomplete = true

						if (loggedClasses.add(menu::class.java)) {
							LOGGER.error("Menu $menu <${menu::class.qualifiedName}> is incomplete, missing inventory slot at ${i2 + offset}")
						}

						return@run
					}

					row.add(slot)
					slotPositions.add(SlotPos(slot))
				}

				rows[i] = row
				this.slotPositions[i] = slotPositions
			}

			this.isIncomplete = false
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
		private val loggedClasses = ReferenceArraySet<Class<*>>()
	}
}

private class FakeSlot : MatterySlot(SimpleContainer(1), 0, 0, 0) {
	override fun mayPickup(player: Player): Boolean {
		return false
	}

	override fun mayPlace(itemStack: ItemStack): Boolean {
		return false
	}

	override fun getItem(): ItemStack {
		return ItemStack.EMPTY
	}

	override fun hasItem(): Boolean {
		return false
	}

	override fun set(p_40240_: ItemStack) {
		// no op
	}

	override fun remove(p_40227_: Int): ItemStack {
		return ItemStack.EMPTY
	}

	override fun setChanged() {
		// no op
	}

	override fun isSameInventory(other: Slot): Boolean {
		return false
	}

	override fun allowModification(p_150652_: Player): Boolean {
		return false
	}

	override fun isActive(): Boolean {
		return false
	}

	override fun getMaxStackSize(pStack: ItemStack): Int {
		return 0
	}

	override fun getMaxStackSize(): Int {
		return 0
	}
}

class InventoryScrollPacket(val scroll: Int) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeVarInt(scroll)
	}

	fun play(player: Player) {
		val containerMenu = player.containerMenu ?: return

		if (containerMenu is InventoryMenu || containerMenu is MatteryMenu) {
			return
		}

		menuConfigurations.computeIfAbsent(containerMenu) {
			// do not use hash map because we need keys be iterated in natural order
			val originalSlots = Int2ObjectAVLTreeMap<Slot>()

			for (slot in it.slots) {
				if (slot.container === player.inventory && slot.slotIndex in 9 .. 35) {
					originalSlots[slot.index] = slot
				}
			}

			MenuConfiguration(player, containerMenu, originalSlots.values, originalSlots.keys)
		}.scroll = scroll
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		play(context.sender ?: throw IllegalStateException("Illegal side"))
	}

	companion object {
		fun read(buff: FriendlyByteBuf): InventoryScrollPacket {
			return InventoryScrollPacket(buff.readVarInt())
		}
	}
}
