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.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
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.neoforged.neoforge.network.PacketDistributor
import net.neoforged.neoforge.network.handling.IPayloadContext
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.ResourceLocation
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) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeInt(stackID)
		buff.writeEnum(type)
		buff.writeEnum(action)
	}

	fun play(context: IPayloadContext) {
		val sender = context.player() as ServerPlayer

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

	override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
		return TYPE
	}

	companion object {
		val TYPE = CustomPacketPayload.Type<ItemViewInteractPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"item_view_interaction"
			)
		)

		val CODEC: StreamCodec<FriendlyByteBuf, ItemViewInteractPacket> =
			StreamCodec.ofMember(ItemViewInteractPacket::write, ::read)

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

abstract class NetworkedItemViewPacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		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() {
	val TYPE = CustomPacketPayload.Type<ClearItemViewPacket>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"clear_item_view"
		)
	)

	override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
		return TYPE
	}

	val CODEC: StreamCodec<FriendlyByteBuf, ClearItemViewPacket> =
		StreamCodec.ofMember({ _, _ -> }, { ClearItemViewPacket })

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

class StackAddPacket(val stackId: Int, val stack: ItemStorageStack) : NetworkedItemViewPacket() {
	fun write(buff: RegistryFriendlyByteBuf) {
		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))
	}

	override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
		return TYPE
	}

	companion object {
		val TYPE = CustomPacketPayload.Type<StackAddPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"item_view_addition"
			)
		)

		val CODEC: StreamCodec<RegistryFriendlyByteBuf, StackAddPacket> =
			StreamCodec.ofMember(StackAddPacket::write, ::read)

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

class StackChangePacket(val stackId: Int, val newCount: BigInteger) : NetworkedItemViewPacket() {
	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()
	}

	override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
		return TYPE
	}

	companion object {
		val TYPE = CustomPacketPayload.Type<StackChangePacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"item_view_change"
			)
		)

		val CODEC: StreamCodec<FriendlyByteBuf, StackChangePacket> =
			StreamCodec.ofMember(StackChangePacket::write, ::read)

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

class StackRemovePacket(val stackId: Int) : NetworkedItemViewPacket() {
	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()
	}

	override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
		return TYPE
	}

	companion object {
		val TYPE = CustomPacketPayload.Type<StackRemovePacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"item_view_remove"
			)
		)

		val CODEC: StreamCodec<FriendlyByteBuf, StackRemovePacket> =
			StreamCodec.ofMember(StackRemovePacket::write, ::read)

		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<CustomPacketPayload>()

	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

		PacketDistributor.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: () -> CustomPacketPayload) {
		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" }

		for (packet in networkBacklog) {
			PacketDistributor.sendToPlayer(ply as ServerPlayer, 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()
		}
	}
}
