package ru.dbotthepony.mc.otm.triggers

import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.Hash.Strategy
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
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.DeserializationContext
import net.minecraft.advancements.critereon.InventoryChangeTrigger
import net.minecraft.advancements.critereon.MinMaxBounds
import net.minecraft.resources.ResourceLocation
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.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.collect.flatMap
import ru.dbotthepony.mc.otm.core.collect.toList
import ru.dbotthepony.mc.otm.core.isNotEmpty
import java.util.stream.Collectors

/**
 * 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 by using search tree
 */
object MatteryInventoryChangeTrigger : CriterionTrigger<InventoryChangeTrigger.TriggerInstance> {
	private object BoundsStrategy : Strategy<MinMaxBounds.Ints?> {
		override fun equals(a: MinMaxBounds.Ints?, b: MinMaxBounds.Ints?): Boolean {
			return a?.min == b?.min && a?.max == b?.max
		}

		override fun hashCode(o: MinMaxBounds.Ints?): Int {
			return o?.let { Integer.rotateLeft(it.min.hashCode(), 16) xor it.max.hashCode() } ?: 0
		}
	}

	private object DefaultStrategy : Strategy<Any?> {
		override fun equals(a: Any?, b: Any?): Boolean {
			return a == b
		}

		override fun hashCode(o: Any?): Int {
			return o.hashCode()
		}
	}

	private fun interface Tester<T> {
		fun test(value: T, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int): Boolean
	}

	private fun interface Hint<T> {
		fun getHints(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int): Collection<T>?
	}

	private class Node<T>(
		val strategy: Strategy<in T?>,
		private val getter: InventoryChangeTrigger.TriggerInstance.() -> Collection<T>,
		val test: Tester<T?>, // if hint is present, tester is always going to be called with `null` value once
		val hint: Hint<T>? = null
	) {
		fun getValues(instance: InventoryChangeTrigger.TriggerInstance): Set<T> {
			val result = ObjectArraySet<T>()
			result.addAll(getter.invoke(instance))
			return result
		}
	}

	private val nodes = ArrayList<Node<*>>()

	init {
		nodes.add(Node(BoundsStrategy, { listOf(slotsOccupied) }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(slotsOccupied) }))
		nodes.add(Node(BoundsStrategy, { listOf(slotsFull) }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(slotsFull) }))
		nodes.add(Node(BoundsStrategy, { listOf(slotsEmpty) }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(slotsEmpty) }))

		nodes.add(Node(
			DefaultStrategy,
			{ predicates.iterator().flatMap { (it.items ?: listOf(null)).iterator() }.toList() },
			{ v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v == null || item.item == v },
			{ inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> mutableListOf(item.item) }))

		nodes.add(Node(
			DefaultStrategy,
			{ predicates.map { it.tag } },
			{ v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v == null || item.`is`(v) },
			{ inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> item.tags.collect(Collectors.toCollection(::ArrayList)) }))

		nodes.add(Node(BoundsStrategy, { predicates.map { it.count } }, { v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.matches(item.count) }))

		nodes.add(Node(
			BoundsStrategy,
			{ predicates.map { it.durability } },
			{ v, inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int -> v!!.isAny || item.isDamageableItem && v.matches(item.maxDamage - item.damageValue) }))
	}

	private class ListenerTree(private val advancements: PlayerAdvancements) {
		private val set = ObjectOpenHashSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>()
		private val tree = Object2ObjectOpenCustomHashMap<Any?, Any>(nodes.first().strategy as Strategy<in Any?>)

		private fun search(instance: InventoryChangeTrigger.TriggerInstance, tree: MutableMap<Any?, Any>, nodeId: Int): Collection<MutableSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>> {
			val node = nodes[nodeId]

			if (nodeId + 1 != nodes.size) {
				val result = ArrayList<MutableSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>>()

				for (v in node.getValues(instance)) {
					result.addAll(
						search(
							instance,
							tree.computeIfAbsent(v, Object2ObjectFunction { Object2ObjectOpenCustomHashMap<Any, Any>(4, nodes[nodeId + 1].strategy as Strategy<in Any?>) }) as MutableMap<Any?, Any>,
							nodeId + 1))
				}

				return result
			} else {
				val result = ArrayList<MutableSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>>()

				for (v in node.getValues(instance)) {
					result.add(tree.computeIfAbsent(v, Object2ObjectFunction { ObjectOpenHashSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>(4) }) as MutableSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>)
				}

				return result
			}
		}

		fun add(listener: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>) {
			if (set.add(listener)) {
				search(listener.triggerInstance, tree, 0).forEach { it.add(listener) }
			}
		}

		fun remove(listener: CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>) {
			if (set.remove(listener)) {
				search(listener.triggerInstance, tree, 0).forEach { it.remove(listener) }
			}
		}

		private fun addNull(input: Collection<Any?>): Collection<Any?> {
			return if (null in input)
				input
			else if (input is ArrayList) {
				input.add(null)
				input
			} else {
				ArrayList(input).also { it.add(null) }
			}
		}

		private fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int, tree: MutableMap<Any?, Any>, nodeId: Int, set: MutableSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>) {
			val node = nodes[nodeId] as Node<Any?>
			val keys = node.hint?.getHints(inventory, item, slotsFull, slotsEmpty, slotsOccupied)?.let(::addNull) ?: tree.keys

			for (k in keys) {
				val v = tree[k] ?: continue

				if (node.test.test(k, inventory, item, slotsFull, slotsEmpty, slotsOccupied)) {
					if (nodeId + 1 == nodes.size) {
						for (l in v as Set<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>) {
							// переделываем matches у InventoryTriggerInstance
							with (l.triggerInstance) {
								if (
									this.slotsFull.matches(slotsFull) &&
									this.slotsEmpty.matches(slotsEmpty) &&
									this.slotsOccupied.matches(slotsOccupied)
								) {
									if (this.predicates.isEmpty() || this.predicates.size == 1 && item.isNotEmpty && this.predicates[0].matches(item)) {
										set.add(l)
									} else if (this.predicates.size > 1) {
										val unsatisfied = ObjectArrayList(this.predicates)
										unsatisfied.removeIf { it.matches(ItemStack.EMPTY) }

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

										if (unsatisfied.isEmpty) {
											set.add(l)
										}
									}
								}
							}
						}
					} else {
						trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, v as MutableMap<Any?, Any>, nodeId + 1, set)
					}
				}
			}
		}

		fun trigger(inventory: Container, item: ItemStack, slotsFull: Int, slotsEmpty: Int, slotsOccupied: Int) {
			val matches = ObjectOpenHashSet<CriterionTrigger.Listener<InventoryChangeTrigger.TriggerInstance>>()

			trigger(inventory, item, slotsFull, slotsEmpty, slotsOccupied, tree, 0, 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 getId(): ResourceLocation {
		return CriteriaTriggers.INVENTORY_CHANGED.id
	}

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

	override fun createInstance(data: JsonObject, context: DeserializationContext): InventoryChangeTrigger.TriggerInstance {
		return CriteriaTriggers.INVENTORY_CHANGED.createInstance(data, context)
	}

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

			if (mattery != null) {
				return trigger(player, mattery.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)
	}
}
