package ru.dbotthepony.mc.otm.menu.data

import com.mojang.blaze3d.platform.InputConstants
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.ClickAction
import net.minecraft.world.inventory.ClickType
import net.minecraft.world.item.ItemStack
import net.minecraftforge.network.NetworkEvent
import net.minecraftforge.network.PacketDistributor
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.readBigInteger
import ru.dbotthepony.mc.otm.core.writeBigInteger
import ru.dbotthepony.mc.otm.menu.MatteryMenu
import ru.dbotthepony.mc.otm.network.*
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.storage.*
import java.math.BigInteger
import java.util.*
import java.util.function.Supplier

interface INetworkedItemViewProvider {
	val networkedItemView: NetworkedItemView
}

class ItemViewInteractPacket(val stackID: Int, val type: ClickType, val action: ClickAction) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(stackID)
		buff.writeEnum(type)
		buff.writeEnum(action)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val sender = context.sender ?: return@enqueueWork

			if (!sender.isSpectator) {
				sender.resetLastActionTime()
				(sender.containerMenu as? INetworkedItemViewProvider)?.networkedItemView?.playerInteract(this)
			}
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): ItemViewInteractPacket {
			return ItemViewInteractPacket(buff.readInt(), buff.readEnum(ClickType::class.java), buff.readEnum(ClickAction::class.java))
		}
	}
}

object ClearItemViewPacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		// NO-OP
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			(minecraft.player?.containerMenu as? INetworkedItemViewProvider)?.networkedItemView?.clear()
		}
	}

	fun read(buff: FriendlyByteBuf): ClearItemViewPacket {
		return ClearItemViewPacket
	}

	fun send(ply: ServerPlayer) {
		MenuNetworkChannel.send(ply, this)
	}
}

class StackAddPacket(val containerId: Int, val id: Int, val stack: ItemStackWrapper) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(containerId)
		buff.writeInt(id)
		buff.writeBigItem(stack)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true
		context.get().enqueueWork {
			val get = Minecraft.getInstance().player?.containerMenu ?: return@enqueueWork

			if (get.containerId != containerId)
				return@enqueueWork

			val view = (get as? INetworkedItemViewProvider)?.networkedItemView ?: throw IllegalStateException("No such item tracker with id $containerId")

			if (view.localState.containsKey(id)) {
				throw IllegalStateException("Item tracker $containerId already has stack with id of $id")
			}

			val state = NetworkedItemView.NetworkedItem(id, stack)
			view.localState[id] = state

			/*val iterator = view.sortedView.iterator().withIndex()
			var lastCompare = 0
			var hit = false

			while (iterator.hasNext()) {
				val (i, existing) = iterator.next()
				val cmp = view.sorter.compare(existing.stack, stack)

				if (cmp != lastCompare && lastCompare != 0) {
					hit = true
					view.sortedView.add(i, state)
				} else if (cmp != lastCompare) {
					lastCompare = cmp
				}
			}

			if (!hit) {*/
				view.sortedView.add(state)
			//}

			view.resort()
		}
	}

	companion object {
		fun read(buffer: FriendlyByteBuf): StackAddPacket {
			val containerId = buffer.readInt()
			val id = buffer.readInt()
			val item = buffer.readBigItem()
			return StackAddPacket(containerId, id, item)
		}
	}
}

class StackChangePacket(val id: Int, val stackID: Int, val newCount: BigInteger) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(id)
		buff.writeInt(stackID)
		buff.writeBigInteger(newCount)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true
		context.get().enqueueWork {
			val get = Minecraft.getInstance().player?.containerMenu ?: return@enqueueWork

			if (get.containerId != id)
				return@enqueueWork

			val view = (get as? INetworkedItemViewProvider)?.networkedItemView ?: throw IllegalStateException("No such item tracker with id $id")
			val state = view.localState[stackID] ?: throw IllegalStateException("No such stack with id $stackID in $view")

			state.stack.count = newCount
			view.resort()
		}
	}

	companion object {
		fun read(buffer: FriendlyByteBuf): StackChangePacket {
			val id = buffer.readInt()
			val stackID = buffer.readInt()
			val newCount = buffer.readBigInteger()
			return StackChangePacket(id, stackID, newCount)
		}
	}
}

class StackRemovePacket(val id: Int, val stackID: Int) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(id)
		buff.writeInt(stackID)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true
		context.get().enqueueWork {
			val get = Minecraft.getInstance().player?.containerMenu ?: return@enqueueWork

			if (get.containerId != id)
				return@enqueueWork

			val view = (get as? INetworkedItemViewProvider)?.networkedItemView ?: throw IllegalStateException("No such item tracker with id $id")
			val obj = view.localState.remove(stackID) ?: throw IllegalStateException("No such stack with id $stackID in $view")
			view.sortedView.remove(obj)
			view.resort()
		}
	}

	companion object {
		fun read(buffer: FriendlyByteBuf): StackRemovePacket {
			val id = buffer.readInt()
			val stackID = buffer.readInt()
			return StackRemovePacket(id, stackID)
		}
	}
}

/**
 * Creates a virtual, slotless container for Player to interaction with.
 */
open class NetworkedItemView(val ply: Player, val menu: MatteryMenu, val remote: Boolean) : IStorageEventConsumer<ItemStackWrapper> {
	data class NetworkedItem constructor(val id: Int, val stack: ItemStackWrapper, val upstreamId: UUID? = null)

	override val storageType: StorageStackType<ItemStackWrapper>
		get() = ITEM_STORAGE

	// this (how client see and interact with)
	val localState = Int2ObjectOpenHashMap<NetworkedItem>()

	val sortedView = LinkedList<NetworkedItem>()
	var sorter: Comparator<ItemStack> = NAME_SORTER

	companion object {
		val NAME_SORTER = Comparator<ItemStack> { o1, o2 ->
			val cmp = o1.displayName.string.compareTo(o2.displayName.string)

			if (cmp != 0)
				return@Comparator cmp

			return@Comparator o1.item.registryName.toString().compareTo(o2.item.registryName.toString())
		}

		val COUNT_SORTER = Comparator<ItemStack> { o1, o2 ->
			val cmp = o1.count.compareTo(o2.count)

			if (cmp != 0)
				return@Comparator cmp

			return@Comparator o1.item.registryName.toString().compareTo(o2.item.registryName.toString())
		}
	}

	fun resort() {
		sortedView.sortWith { a, b ->
			return@sortWith sorter.compare(a.stack.item, b.stack.item)
		}
	}

	// parent (e.g. VirtualComponent)
	protected val upstreamState = HashMap<UUID, NetworkedItem>()
	protected val networkBacklog = ArrayList<Any>()

	operator fun get(id: Int): NetworkedItem? = localState[id]

	var provider: IStorageComponent<ItemStackWrapper>? = null
		private set

	fun mouseClick(index: Int, mouseButton: Int) {
		if (minecraft.player?.isSpectator == true) {
			return
		}

		val list = sortedView

		val action =
			if (mouseButton == InputConstants.MOUSE_BUTTON_LEFT) ClickAction.PRIMARY else ClickAction.SECONDARY

		val type =
			if (mouseButton == InputConstants.MOUSE_BUTTON_MIDDLE) ClickType.CLONE else if (Screen.hasShiftDown()) ClickType.QUICK_MOVE else ClickType.PICKUP

		MenuNetworkChannel.sendToServer(
			ItemViewInteractPacket(if (index >= list.size) -1 else list[index].id, type, action)
		)
	}

	fun setComponent(provider: IStorageComponent<ItemStackWrapper>?) {
		if (provider === this.provider) return

		this.provider?.removeListenerAuto(this)
		this.provider = provider
		provider?.addListenerAuto(this)
	}

	fun removed() {
		provider?.removeListenerAuto(this)
	}

	val itemCount get() = localState.values.size

	override fun addStack(stack: ItemStackWrapper, id: UUID, provider: IStorageProvider<ItemStackWrapper>) {
		check(!upstreamState.containsKey(id)) { "Already tracking ItemStack with upstream id $id!" }

		val state = NetworkedItem(nextItemID++, stack.copy(), id)

		this.localState[state.id] = state
		upstreamState[id] = state
		network { StackAddPacket(menu.containerId, state.id, state.stack) }
	}

	override fun changeStack(stack: ItemStackWrapper, id: UUID, oldCount: BigInteger) {
		val get = upstreamState[id] ?: throw IllegalStateException("Unknown ItemStack with upstream id $id!")
		get.stack.count = stack.count
		network { StackChangePacket(menu.containerId, get.id, stack.count) }
	}

	protected fun network(fn: () -> Any) {
		if (!remote) {
			networkBacklog.add(fn())
		}
	}

	override fun removeStack(stack: ItemStackWrapper, id: UUID) {
		val get = upstreamState[id] ?: throw IllegalStateException("Unknown ItemStack with upstream id $id!")
		upstreamState.remove(id)
		localState.remove(get.id)
		network { StackRemovePacket(menu.containerId, get.id) }
	}

	protected var nextItemID = 0

	fun clear() {
		sortedView.clear()
		upstreamState.clear()
		localState.clear()

		if (!remote) {
			networkBacklog.clear()
			networkBacklog.add(ClearItemViewPacket)
		}
	}

	fun network() {
		check(!remote) { "Not a server" }
		val consumer = PacketDistributor.PLAYER.with { ply as ServerPlayer }

		for (packet in networkBacklog) {
			MenuNetworkChannel.send(consumer, packet)
		}

		networkBacklog.clear()
	}

	fun playerInteract(packet: ItemViewInteractPacket) {
		val provider = provider ?: return

		val click = packet.type
		val action = packet.action
		val stackId = packet.stackID

		if (click == ClickType.CLONE) {
			if (stackId < 0 || !ply.abilities.instabuild) return

			val state = get(stackId) ?: return
			val copy = state.stack.stack.also { it.count = it.maxStackSize }

			ply.containerMenu.carried = copy
			MenuNetworkChannel.send(ply as ServerPlayer, SetCarriedPacket(ply.containerMenu.carried))
			ply.containerMenu.setRemoteCarried(ply.containerMenu.carried.copy())

			return
		}

		if (click == ClickType.QUICK_MOVE && stackId > -1) {
			val state = get(stackId) ?: return

			val amount =
				if (action == ClickAction.PRIMARY)
					state.stack.item.maxStackSize
				else
					1.coerceAtLeast(state.stack.item.maxStackSize / 2)

			val extracted = provider.extractStack(state.upstreamId!!, amount.toBigInteger(), true)

			if (!extracted.isEmpty) {
				val remaining = menu.quickMoveToInventory(extracted.stack, false)

				if (remaining.count != extracted.count.toInt()) {
					provider.extractStack(state.upstreamId, (extracted.count.toInt() - remaining.count).toBigInteger(), false)
				}
			}

			return
		}

		if (!menu.carried.isEmpty && click != ClickType.QUICK_MOVE) {
			// try to put
			if (action == ClickAction.PRIMARY) {
				val carried = menu.carried
				val amount = carried.count

				if (amount == carried.count) {
					val stack = provider.insertStack(ItemStackWrapper(menu.carried), false).stack
					menu.carried = stack
					MenuNetworkChannel.send(ply as ServerPlayer, SetCarriedPacket(menu.carried))
					menu.setRemoteCarried(menu.carried.copy())
				}
			} else {
				val copy = menu.carried.copy()
				copy.count = 1

				if (provider.insertStack(ItemStackWrapper(copy), false).isEmpty) {
					menu.carried.shrink(1)
					MenuNetworkChannel.send(ply as ServerPlayer, SetCarriedPacket(menu.carried))
					menu.setRemoteCarried(menu.carried.copy())
				}
			}
		} else if (stackId > -1) {
			val state = get(stackId) ?: return

			val amount =
				if (action == ClickAction.PRIMARY)
					state.stack.item.maxStackSize
				else
					(state.stack.stack.count / 2).coerceAtMost(state.stack.item.maxStackSize / 2).coerceAtLeast(1)

			val extracted = provider.extractStack(state.upstreamId!!, amount.toBigInteger(), false)
			menu.carried = extracted.stack
			MenuNetworkChannel.send(ply as ServerPlayer, SetCarriedPacket(menu.carried))
			menu.setRemoteCarried(menu.carried.copy())
		}
	}
}
