package ru.dbotthepony.mc.otm.triggers

import com.google.gson.JsonObject
import com.mojang.serialization.Codec
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import net.minecraft.advancements.CriteriaTriggers
import net.minecraft.advancements.CriterionTrigger
import net.minecraft.advancements.critereon.InventoryChangeTrigger
import net.minecraft.server.PlayerAdvancements
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.container.util.iterator
import ru.dbotthepony.mc.otm.core.isNotEmpty
import java.util.stream.Stream

private typealias TriggerSet = ObjectOpenHashSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>

private fun items(of: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>): List<Item> {
	return of.trigger.items
		.stream()
		.flatMap { it.items.map { it.stream() }.orElse(Stream.empty()) }
		.map { it.value() }
		.toList()
}

/**
 * This object detours all necessary InventoryChangeTrigger methods
 *
 * Reason behind this is to support arbitrary containers (not just [Inventory], done for Exopack) and to improve performance
 */
object MatteryInventoryChangeTrigger : CriterionTrigger<InventoryChangeTrigger.TriggerInstance> {
	private class ListenerTree(private val advancements: PlayerAdvancements) {
		// fastutil for lesser memory footprint, even if it causes lesser performance
		private val sorted = Reference2ObjectOpenHashMap<Item, TriggerSet>()
		private val unsorted = TriggerSet()
		private val tracked = TriggerSet()

		fun add(listener: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>) {
			if (tracked.add(listener)) {
				val items = items(listener)

				if (items.isEmpty()) {
					unsorted.add(listener)
				} else {
					for (item in items) {
						sorted.computeIfAbsent(item, Reference2ObjectFunction { TriggerSet() }).add(listener)
					}
				}
			}
		}

		fun remove(listener: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>) {
			if (tracked.remove(listener)) {
				unsorted.remove(listener)

				for (item in items(listener)) {
					val set = sorted[item]

					if (set != null && set.remove(listener) && set.isEmpty()) {
						sorted.remove(item)
					}
				}
			}
		}

		private fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int, elements: TriggerSet, matches: TriggerSet) {
			for (l in elements) {
				with (l.trigger) {
					if (slots.matches(slotsFull, slotsEmpty, slotsOccupied)) {
						if (items.isEmpty() || items.size == 1 && item.isNotEmpty && this.items[0].test(item)) {
							matches.add(l)
						} else if (items.size > 1) {
							val unsatisfied = ObjectArrayList(items)
							unsatisfied.removeIf { it.test(ItemStack.EMPTY) }

							for (inventoryItem in inventory) {
								unsatisfied.removeIf { it.test(inventoryItem) }
								if (unsatisfied.isEmpty) break
							}

							if (unsatisfied.isEmpty) {
								matches.add(l)
							}
						}
					}
				}
			}
		}

		fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int) {
			val matches = TriggerSet()

			trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, unsorted, matches)

			val sorted = sorted[item.item]

			if (sorted != null) {
				trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, sorted, matches)
			}

			for (l in matches) {
				l.run(advancements)
			}
		}
	}

	private val listeners = Reference2ObjectOpenHashMap<PlayerAdvancements, ListenerTree>()

	override fun addPlayerListener(advancements: PlayerAdvancements, instance: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>) {
		listeners.computeIfAbsent(advancements, Reference2ObjectFunction { ListenerTree(it as PlayerAdvancements) }).add(instance)
	}

	override fun removePlayerListener(advancements: PlayerAdvancements, instance: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>) {
		listeners[advancements]?.remove(instance)
	}

	override fun removePlayerListeners(advancements: PlayerAdvancements) {
		listeners.remove(advancements)
	}

	override fun codec(): Codec<InventoryChangeTrigger.TriggerInstance> {
		return CriteriaTriggers.INVENTORY_CHANGED.codec()
	}

	// реплицирует ванильный метод
	fun trigger(player: ServerPlayer, inventory: Container, item: ItemStack) {
		if (inventory === player.inventory) {
			return trigger(player, player.matteryPlayer.combinedInventory, item)
		}

		var slotsFull = 0
		var slotsEmpty = 0
		var slotsOccupied = 0

		for (slot in 0 until inventory.containerSize) {
			val slotItem = inventory[slot]

			if (slotItem.isEmpty)
				slotsEmpty++
			else {
				slotsOccupied++

				if (slotItem.count >= inventory.maxStackSize)
					slotsFull++
			}
		}

		trigger(player, inventory, item, slotsFull, slotsEmpty, slotsOccupied)
	}

	// реплицирует ванильный метод
	fun trigger(player: ServerPlayer, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int) {
		listeners[player.advancements]?.trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied)
	}
}
