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

import com.mojang.blaze3d.platform.InputConstants
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
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.PacketDistributor
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.addSorted
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.map
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.util.ItemStorageStackSorter
import ru.dbotthepony.mc.otm.storage.*
import java.math.BigInteger
import java.util.*
import java.util.function.Supplier
import kotlin.collections.ArrayList

interface INetworkedItemViewProvider {
	val networkedItemView: NetworkedItemView
}

private fun all(stack: ItemStack): Int = stack.maxStackSize.coerceAtMost(stack.count)
private fun half(stack: ItemStack): Int = (stack.maxStackSize.coerceAtMost(stack.count) / 2).coerceAtLeast(1)

data 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: MNetworkContext) {
		val sender = context.sender ?: return

		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))
		}
	}
}

abstract class NetworkedItemViewPacket : MatteryPacket {
	final override fun play(context: MNetworkContext) {
		val get = Minecraft.getInstance().player?.containerMenu ?: return
		val view = (get as? INetworkedItemViewProvider)?.networkedItemView ?: throw IllegalStateException("No NetworkedItemView is present in currently open menu")
		action(view)
	}

	protected abstract fun action(view: NetworkedItemView)
}

object ClearItemViewPacket : NetworkedItemViewPacket() {
	override fun write(buff: FriendlyByteBuf) {}

	override fun action(view: NetworkedItemView) {
		view.clear()
	}
}

class StackAddPacket(val stackId: Int, val stack: ItemStorageStack) : NetworkedItemViewPacket() {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(stackId)
		stack.write(buff)
	}

	override fun action(view: NetworkedItemView) {
		if (view.id2tuple.containsKey(stackId)) {
			throw IllegalStateException("NetworkedItemView $view already contains stack with id $stackId")
		}

		val tuple = NetworkedItemView.Tuple(stackId, stack)
		view.id2tuple[stackId] = tuple
		view.sortedView.addSorted(tuple, view.sorter.map(NetworkedItemView.Tuple::stack))
	}

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

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

	override fun action(view: NetworkedItemView) {
		val tuple = view.id2tuple[stackId] ?: throw IllegalStateException("No such stack with id $stackId in $view")
		tuple.stack = tuple.stack.copy(newCount)
		view.resort()
	}

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

class StackRemovePacket(val stackId: Int) : NetworkedItemViewPacket() {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(stackId)
	}

	override fun action(view: NetworkedItemView) {
		val obj = view.id2tuple.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 stackID = buffer.readInt()
			return StackRemovePacket(stackID)
		}
	}
}

/**
 * Creates a virtual, slotless container for Player to interaction with.
 */
class NetworkedItemView(val ply: Player, val menu: MatteryMenu, val isRemote: Boolean) : IStorageEventConsumer<ItemStorageStack> {
	data class Tuple(val networkId: Int, var stack: ItemStorageStack, val upstreamId: UUID? = null) : Supplier<ItemStorageStack> {
		override fun get(): ItemStorageStack {
			return stack
		}
	}

	override val storageType: StorageStack.Type<ItemStorageStack>
		get() = StorageStack.ITEMS

	val id2tuple = Int2ObjectOpenHashMap<Tuple>()
	val sortedView = ArrayList<Tuple>()
	var sorter: Comparator<ItemStorageStack> = ItemStorageStackSorter.DEFAULT
		set(value) {
			if (field != value) {
				field = value
				resort()
			}
		}

	val itemCount get() = id2tuple.size

	private var nextItemID = 0
	private val uuid2tuple = Object2ObjectOpenHashMap<UUID, Tuple>()
	private val networkBacklog = ArrayList<Any>()

	operator fun get(id: Int): Tuple? = id2tuple[id]

	fun resort() {
		if (isRemote) {
			sortedView.sortWith(sorter.map(Tuple::stack))
		}
	}

	var component: IStorageComponent<ItemStorageStack>? = null
		set(provider) {
			if (provider === field) return

			field?.removeListenerAndNotify(this)
			field = provider
			provider?.addListenerAndNotify(this)
		}

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

		MenuNetworkChannel.sendToServer(ItemViewInteractPacket(
			sortedView.getOrNull(index)?.networkId ?: -1,
			if (mouseButton == InputConstants.MOUSE_BUTTON_MIDDLE) ClickType.CLONE else if (Screen.hasShiftDown()) ClickType.QUICK_MOVE else ClickType.PICKUP,
			if (mouseButton == InputConstants.MOUSE_BUTTON_LEFT) ClickAction.PRIMARY else ClickAction.SECONDARY
		))
	}

	fun removed() {
		component?.removeListenerAndNotify(this)
	}

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

		val state = Tuple(nextItemID++, stack, id)

		this.id2tuple[state.networkId] = state
		uuid2tuple[id] = state
		network { StackAddPacket(state.networkId, state.stack) }
	}

	override fun onStackChanged(stack: ItemStorageStack, id: UUID) {
		val get = uuid2tuple[id] ?: throw IllegalStateException("Unknown ItemStack with upstream id $id!")
		get.stack = stack
		network { StackChangePacket(get.networkId, stack.count) }
	}

	override fun onStackRemoved(id: UUID) {
		val get = uuid2tuple[id] ?: throw IllegalStateException("Unknown ItemStack with upstream id $id!")
		uuid2tuple.remove(id)
		id2tuple.remove(get.networkId)
		network { StackRemovePacket(get.networkId) }
	}

	private inline fun network(fn: () -> Any) {
		if (!isRemote) {
			networkBacklog.add(fn())
		}
	}

	fun clear() {
		sortedView.clear()
		uuid2tuple.clear()
		id2tuple.clear()

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

	fun network() {
		check(!isRemote) { "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 component = component ?: return
		val (stackId, type, action) = packet

		if (type == ClickType.CLONE) {
			if (!ply.abilities.instabuild) return
			menu.syncCarried((get(stackId) ?: return).stack.toItemStack().also { it.count = it.maxStackSize })
			return
		}

		// забираем из системы с зажатым shift
		if (type == ClickType.QUICK_MOVE) {
			val tuple = get(stackId) ?: return
			val stack = tuple.stack.toItemStack()

			var amount = if (action == ClickAction.PRIMARY) all(stack) else half(stack)
			amount -= menu.quickMoveToInventory(tuple.stack.toItemStack(amount), true).count
			if (amount == 0) return

			menu.quickMoveToInventory(component.extractStack(tuple.upstreamId!!, amount.toBigInteger(), false).toItemStack(), false)
			return
		}

		if (menu.carried.isNotEmpty) {
			if (action == ClickAction.PRIMARY) {
				menu.syncCarried(component.insertStack(ItemStorageStack(menu.carried), false).toItemStack())
			} else if (component.insertStack(ItemStorageStack(menu.carried.copyWithCount(1)), false).isEmpty) {
				menu.carried.shrink(1)
				menu.syncCarried()
			}
		} else if (stackId > -1) {
			val state = get(stackId) ?: return
			val stack = state.stack.toItemStack()
			val amount = if (action == ClickAction.PRIMARY) all(stack) else half(stack)
			menu.carried = component.extractStack(state.upstreamId!!, amount.toBigInteger(), false).toItemStack()
			menu.syncCarried()
		}
	}
}
