package ru.dbotthepony.mc.otm.matter

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.Streams
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonSyntaxException
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import com.mojang.serialization.JsonOps
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Object2BooleanFunction
import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import it.unimi.dsi.fastutil.objects.Reference2BooleanFunction
import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap
import it.unimi.dsi.fastutil.objects.Reference2ObjectFunction
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import net.minecraft.ChatFormatting
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.MinecraftServer
import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
import net.minecraft.tags.TagKey
import net.minecraft.util.profiling.ProfilerFiller
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.TransientCraftingContainer
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.Recipe
import net.minecraft.world.item.crafting.RecipeType
import net.minecraft.world.level.ItemLike
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.common.crafting.IShapedRecipe
import net.minecraftforge.event.AddReloadListenerEvent
import net.minecraftforge.event.OnDatapackSyncEvent
import net.minecraftforge.event.RegisterCommandsEvent
import net.minecraftforge.event.entity.player.ItemTooltipEvent
import net.minecraftforge.event.server.ServerStartedEvent
import net.minecraftforge.eventbus.api.IEventBus
import net.minecraftforge.fml.ModList
import net.minecraftforge.network.NetworkEvent
import net.minecraftforge.network.PacketDistributor
import net.minecraftforge.registries.DeferredRegister
import net.minecraftforge.registries.ForgeRegistries
import net.minecraftforge.server.command.EnumArgument
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.config.ClientConfig
import ru.dbotthepony.mc.otm.MINECRAFT_SERVER
import ru.dbotthepony.mc.otm.NULLABLE_MINECRAFT_SERVER
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.SystemTime
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.drive.IMatteryDrive
import ru.dbotthepony.mc.otm.client.isShiftDown
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.container.set
import ru.dbotthepony.mc.otm.container.stream
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.TextComponent
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.core.filterNotNull
import ru.dbotthepony.mc.otm.core.util.formatMatter
import ru.dbotthepony.mc.otm.core.util.formatMatterFull
import ru.dbotthepony.mc.otm.core.util.formatSiComponent
import ru.dbotthepony.mc.otm.core.util.formatTickDuration
import ru.dbotthepony.mc.otm.core.ifPresentK
import ru.dbotthepony.mc.otm.core.isActuallyEmpty
import ru.dbotthepony.mc.otm.core.math.isZero
import ru.dbotthepony.mc.otm.core.orNull
import ru.dbotthepony.mc.otm.core.readItemType
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.stream
import ru.dbotthepony.mc.otm.core.util.readBinaryComponent
import ru.dbotthepony.mc.otm.core.util.readCollection
import ru.dbotthepony.mc.otm.core.util.writeBinaryComponent
import ru.dbotthepony.mc.otm.core.util.writeCollection
import ru.dbotthepony.mc.otm.core.writeItemType
import ru.dbotthepony.mc.otm.milliTime
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.network.RegistryNetworkChannel
import ru.dbotthepony.mc.otm.registry.RegistryDelegate
import ru.dbotthepony.mc.otm.secondTime
import ru.dbotthepony.mc.otm.storage.ItemStackWrapper
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.OutputStream
import java.math.BigInteger
import java.util.*
import java.util.function.BooleanSupplier
import java.util.function.Supplier
import java.util.stream.Stream
import java.util.zip.Deflater
import java.util.zip.Inflater
import kotlin.ConcurrentModificationException
import kotlin.collections.ArrayDeque
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.LinkedHashMap
import kotlin.jvm.optionals.getOrNull
import kotlin.math.pow
import kotlin.math.roundToInt

private fun transformQuotes(it: String?): String = if (it != null) '"' + it.replace("\"", "\"\"") + '"' else "NULL"

object MatterManager {
	const val MATTER_DIRECTORY = "otm_matter"
	const val FINDER_DIRECTORY = "otm_recipe_finder"
	private val LOGGER = LogManager.getLogger()

	private object Registry : SimpleJsonResourceReloadListener(GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(), MATTER_DIRECTORY) {
		val keyEntries = HashMap<ResourceLocation, IRegistryEntry>()
		val tagEntries = LinkedHashMap<TagKey<Item>, IRegistryEntry>()

		val keyEntriesBlacklist = HashMap<ResourceLocation, BlacklistAction>()
		val tagEntriesBlacklist = LinkedHashMap<TagKey<Item>, BlacklistAction>()

		val computeKeys = HashMap<ResourceLocation, ComputeAction>()
		val computeTags = HashMap<TagKey<Item>, ComputeAction>()

		var ready = false
			private set

		/**
		 * Get matter value for item defined by `insert` or `update` registry actions
		 */
		fun direct(value: Item): IMatterValue {
			check(ready) { "Not ready" }

			if (isBlacklisted(value))
				return IMatterValue.Companion

			val key = value.registryName ?: throw NullPointerException("$value has no registry name!")

			val keyEntry = keyEntries[key]

			if (keyEntry != null) {
				return keyEntry
			}

			val reverse = ForgeRegistries.ITEMS.tags()!!.getReverseTag(value).orElse(null)

			if (reverse != null) {
				return reverse.tagKeys
					.map { tagEntries[it] }
					.filterNotNull()
					.sorted { o1, o2 -> o1.priority.compareTo(o2.priority) }
					.findFirst()
					.orElse(null) ?: IMatterValue.Companion
			}

			return IRegistryEntry.Companion
		}

		/**
		 * Get matter value for item defined by `compute` registry action
		 */
		fun indirect(value: Item): Result {
			check(ready) { "Not ready" }

			if (isBlacklisted(value))
				return Result.MISSING

			var compute = computeKeys[value.registryName!!]

			if (compute == null) {
				val reverse = ForgeRegistries.ITEMS.tags()!!.getReverseTag(value).orElse(null)

				if (reverse != null) {
					compute = reverse.tagKeys
						.map { computeTags[it] }
						.filterNotNull()
						.sorted()
						.findFirst()
						.orElse(null)
				}
			}

			if (compute != null) {
				return compute.tryCompute()
			}

			return Result.MISSING
		}

		private val blacklistCacheKey = Reference2BooleanOpenHashMap<Item>()
		private val blacklistCacheTag = Object2BooleanOpenHashMap<TagKey<Item>>() // despite them being interned as of 1.19.2, i consider this internal detail

		fun isBlacklisted(value: Item): Boolean {
			return blacklistCacheKey.computeIfAbsent(value, Reference2BooleanFunction fn@{
				val key = value.registryName ?: throw NullPointerException("$value has no registry name!")

				if (key in keyEntriesBlacklist)
					return@fn true

				val reverse = ForgeRegistries.ITEMS.tags()!!.getReverseTag(value).orElse(null)

				if (reverse != null) {
					return@fn reverse.tagKeys.anyMatch { it in tagEntriesBlacklist }
				}

				return@fn false
			})
		}

		fun isBlacklisted(value: TagKey<Item>): Boolean {
			return blacklistCacheTag.computeIfAbsent(value, Object2BooleanFunction fn@{
				return@fn value in tagEntriesBlacklist || ForgeRegistries.ITEMS.tags()!!.getTag(value).let {
					if (it.isEmpty || !it.isBound) {
						false
					} else {
						it.stream().anyMatch { item -> !isBlacklisted(item) }
					}
				}
			})
		}

		override fun prepare(resourceManager: ResourceManager, profilerFiller: ProfilerFiller): MutableMap<ResourceLocation, JsonElement> {
			ready = false
			registryIsBuilt = false
			val time = System.nanoTime()
			LOGGER.info("Slicing matter values...")
			profilerFiller.push("otm_matter_registry_prepare")

			try {
				val prep = super.prepare(resourceManager, profilerFiller)
				val diff = System.nanoTime() - time
				LOGGER.info("Slicing matter values took ${diff / 1_000_000L}ms")
				return prep
			} finally {
				profilerFiller.pop()
			}
		}

		override fun apply(map: MutableMap<ResourceLocation, JsonElement>, resourceManager: ResourceManager, profilerFiller: ProfilerFiller) {
			registryIsBuilt = false
			val time = System.nanoTime()
			LOGGER.info("Stitching matter values")
			profilerFiller.push("otm_matter_registry")

			try {
				keyEntries.clear()
				tagEntries.clear()

				computeKeys.clear()
				computeTags.clear()

				blacklistCacheKey.clear()
				blacklistCacheTag.clear()

				keyEntriesBlacklist.clear()
				tagEntriesBlacklist.clear()

				val add = ArrayList<Pair<InsertAction, ResourceLocation>>()
				val addOrReplace = ArrayList<Pair<InsertAction, ResourceLocation>>()
				val update = ArrayList<Pair<UpdateAction, ResourceLocation>>()
				val delete = ArrayList<Pair<DeleteAction, ResourceLocation>>()
				val sortedKeys = ArrayList<ResourceLocation>(map.keys)
				val inverse = Object2IntOpenHashMap<String>()
				inverse.defaultReturnValue(Int.MAX_VALUE) // выталкиваем неизвестные имена в самый конец

				for ((i, info) in ModList.get().mods.withIndex()) {
					inverse[info.namespace] = i
				}

				inverse[OverdriveThatMatters.MOD_ID] = -1 // загружаем файлы самого мода всегда раньше, чем всё остальное

				sortedKeys.sortWith { a, b -> inverse.getInt(a.namespace).compareTo(inverse.getInt(b.namespace)) }

				for (key in sortedKeys) {
					val json = map[key]!!

					if (json !is JsonObject) {
						throw JsonParseException("Matter value $key has invalid type: ${json::class.qualifiedName} ($json)")
					}

					val result = AbstractRegistryAction.CODEC
						.decode(JsonOps.INSTANCE, json)
						.getOrThrow(false) { throw JsonSyntaxException("Failed to deserialize matter registry entry $key: $it") }
						.first

					when (result) {
						is InsertAction -> {
							if (result.replaceIfExists) {
								addOrReplace.add(result to key)
							} else {
								add.add(result to key)
							}
						}

						is UpdateAction -> update.add(result to key)
						is ComputeAction -> {
							result.id.map(
								{
									computeKeys.compute(it) { _, existing -> if (result.canReplace(existing)) result else existing }
								},
								{
									computeTags.compute(it) { _, existing -> if (result.canReplace(existing)) result else existing }
								}
							)
						}

						is BlacklistAction -> {
							result.id.map(
								{
									keyEntriesBlacklist[it] = result
								},
								{
									tagEntriesBlacklist[it] = result
								}
							)

							delete.add(result to key)
						}

						is DeleteAction -> delete.add(result to key)

						else -> throw JsonParseException("Unknown action ${result::class.qualifiedName} in $key")
					}
				}

				val registry = ArrayList<IMutableRegistryEntry>()

				for ((action, key) in add) action.update(registry, key)
				for ((action, key) in addOrReplace) action.update(registry, key)
				for ((action, key) in delete) action.update(registry, key)
				for ((action, key) in update) action.update(registry, key)

				for (key in keyEntriesBlacklist.keys) computeKeys.remove(key)
				for (key in tagEntriesBlacklist.keys) computeTags.remove(key)

				val tags = HashMap<TagKey<Item>, IMutableRegistryEntry>()
				val keys = HashMap<ResourceLocation, IMutableRegistryEntry>()

				for (entry in registry) {
					entry.name.map(
						{
							val existing = keys[it]

							if (existing == null || existing.priority < entry.priority) {
								keys[it] = entry
							}
						},
						{
							val existing = tags[it]

							if (existing == null || existing.priority < entry.priority) {
								tags[it] = entry
							}
						}
					)
				}

				for (key in keys.values) {
					keyEntries[key.name.left().orElseThrow()] = key.freeze()
				}

				val tagEntries = ArrayList<IRegistryEntry>()

				for (tag in tags.values) {
					tagEntries.add(tag.freeze())
				}

				tagEntries.sortBy { it.priority }

				for (entry in tagEntries) {
					this.tagEntries[entry.name.right().orElseThrow()] = entry
				}
			} finally {
				profilerFiller.pop()
			}

			val diff = System.nanoTime() - time
			LOGGER.info("Stitching matter values took ${diff / 1_000_000L}ms")

			ready = true
			finishUpIfRequiredAndPossible(NULLABLE_MINECRAFT_SERVER)
		}
	}

	internal data class Result(val type: Type, val value: IMatterValue? = null) {
		constructor(value: IMatterValue) : this(Type.RESOLVED, value)

		val isSkipped get() = type === Type.SKIPPED
		val isMissing get() = type === Type.MISSING

		enum class Type {
			RESOLVED, SKIPPED, MISSING
		}

		companion object {
			// recursive recipes should be ignored until we resolve everything else
			val SKIPPED = Result(Type.SKIPPED)

			// missing matter values are fatal, whole recipe chain is then considered defunct
			val MISSING = Result(Type.MISSING)
		}
	}

	private object Resolver : SimpleJsonResourceReloadListener(GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(), FINDER_DIRECTORY) {
		val delegate = RegistryDelegate<Finder>("recipe_finder") {
			disableSync()
			disableSaving()
		}

		var ready = false
			private set

		val registrar: DeferredRegister<Finder> = DeferredRegister.create(delegate.key, OverdriveThatMatters.MOD_ID)

		init {
			registrar.register("simple") {
				Finder { server, data ->
					val location = (data["recipe_type"] ?: throw JsonSyntaxException("Missing recipe type")).let { ResourceLocation.tryParse(it.asString) } ?: throw JsonSyntaxException("Invalid recipe type: ${data["recipe_type"]}")

					if (!ForgeRegistries.RECIPE_TYPES.containsKey(location)) {
						LOGGER.error("Invalid or missing recipe category: $location!")
						return@Finder Stream.empty()
					}

					val findRecipeType = ForgeRegistries.RECIPE_TYPES.getValue(location) as RecipeType<Recipe<Container>>? ?: throw ConcurrentModificationException()

					val isCritical = data["is_critical"]?.asBoolean ?: true
					val ignoreDamageables = data["ignore_damageables"]?.asBoolean ?: false
					val allowBacktrack = data["allow_backtrack"]?.asBoolean ?: true

					var stream = server.recipeManager.byType(findRecipeType).values.stream().filter { !it.isIncomplete }

					if (ignoreDamageables) {
						stream = stream.filter { it.ingredients.stream().flatMap { it.items.stream() }.noneMatch { it.isDamageableItem } }
					}

					stream.map {
						ResolvedRecipe(
							it.ingredients.stream()
								.filter { !it.isActuallyEmpty }
								.map { it.items.stream().map(::RecipeEntry) },
							ImmutableStack(it.getResultItem(server.registryAccess())),
							isCritical = isCritical,
							name = it.id,
							allowBacktrack = allowBacktrack
						)
					}
					.filter {
						if (it.inputs.isEmpty()) {
							LOGGER.warn("${it.formattedName} with output '${it.output.item.registryName}' ended up with no inputs!")
							false
						} else {
							true
						}
					}
				}
			}

			registrar.register("crafting") {
				Finder { server, data ->
					val location = (data["recipe_type"] ?: throw JsonSyntaxException("Missing recipe type")).let { ResourceLocation.tryParse(it.asString) } ?: throw JsonSyntaxException("Invalid recipe type: ${data["recipe_type"]}")

					if (!ForgeRegistries.RECIPE_TYPES.containsKey(location)) {
						LOGGER.error("Invalid or missing recipe category: $location!")
						return@Finder Stream.empty()
					}

					val findRecipeType = ForgeRegistries.RECIPE_TYPES.getValue(location) as RecipeType<Recipe<Container>>? ?: throw ConcurrentModificationException()

					val allowBacktrack = data["allow_backtrack"]?.asBoolean ?: true
					val ignoreDamageables = data["ignore_damageables"]?.asBoolean ?: false
					val isCritical = data["is_critical"]?.asBoolean ?: true
					var stream = server.recipeManager.byType(findRecipeType).values.stream().filter { !it.isIncomplete }

					if (ignoreDamageables) {
						stream = stream.filter { it.ingredients.stream().flatMap { it.items.stream() }.noneMatch { it.isDamageableItem } }
					}

					stream.map {
						// avoid reality snap when recipe has no output
						val resultItem = it.getResultItem(server.registryAccess())

						if (resultItem.isEmpty) {
							null
						} else {
							var width: Int
							var height: Int

							if (it is IShapedRecipe<*>) {
								width = it.recipeWidth
								height = it.recipeHeight
							} else {
								width = 3
								height = 3
							}

							if (width * height < it.ingredients.size) {
								width = it.ingredients.size.coerceAtLeast(width)
								height = it.ingredients.size.coerceAtLeast(height)
							}

							val container = TransientCraftingContainer(object : AbstractContainerMenu(null, 0) {
								override fun quickMoveStack(pPlayer: Player, pIndex: Int): ItemStack {
									return ItemStack.EMPTY
								}

								override fun stillValid(pPlayer: Player): Boolean {
									return false
								}
							}, width, height)

							val realIngredients = ArrayList<ArrayList<RecipeEntry>>()

							for (c in it.ingredients.indices) {
								if (it.ingredients[c].isActuallyEmpty) {
									continue
								}

								for ((i, ingredient) in it.ingredients.withIndex()) {
									if (i != c) {
										container[i] = if (ingredient.isActuallyEmpty) ItemStack.EMPTY else ingredient.items.firstOrNull() ?: ItemStack.EMPTY
									}
								}

								val result = ArrayList<RecipeEntry>()

								for (item in it.ingredients[c].items) {
									container[c] = item

									if (!it.assemble(container, server.registryAccess()).isEmpty) {
										val residue = it.getRemainingItems(container)

										val thisResidue = residue[c]

										if (thisResidue.isEmpty) {
											result.add(RecipeEntry(item))
										} else {
											result.add(RecipeEntry(ImmutableStack(item), ImmutableStack(thisResidue)))
										}
									}
								}

								realIngredients.add(result)
							}

							ResolvedRecipe(
								realIngredients.stream().map { it.stream() },
								ImmutableStack(resultItem),
								isCritical = isCritical,
								name = it.id,
								allowBacktrack = allowBacktrack
							)
						}
					}.filterNotNull().filter {
						if (it.inputs.isEmpty()) {
							LOGGER.warn("${it.formattedName} with output '${it.output.item.registryName}' ended up with no inputs!")
							false
						} else {
							true
						}
					}
				}
			}
		}

		private var foundResolvers: Map<ResourceLocation, Pair<Finder, JsonObject>> = ImmutableMap.of()

		override fun prepare(
			p_10771_: ResourceManager,
			p_10772_: ProfilerFiller
		): MutableMap<ResourceLocation, JsonElement> {
			registryIsBuilt = false
			return super.prepare(p_10771_, p_10772_)
		}

		override fun apply(values: Map<ResourceLocation, JsonElement>, resourceManager: ResourceManager, profilerFiller: ProfilerFiller) {
			registryIsBuilt = false
			determinedValues.clear()
			commentary.clear()

			val builder = ImmutableMap.Builder<ResourceLocation, Pair<Finder, JsonObject>>()

			for ((key, json) in values) {
				if (json !is JsonObject) {
					throw JsonParseException("$key is not a json object")
				}

				val location = (json["type"] ?: throw JsonSyntaxException("Missing resolver type")).let { ResourceLocation.tryParse(it.asString) } ?: throw JsonSyntaxException("Invalid resolver type: ${json["type"]}")

				if (!recipeFinders.containsKey(location)) {
					throw JsonParseException("Resolver type $location does not exist (in $key)")
				}

				val resolver = recipeFinders.getValue(location) ?: throw ConcurrentModificationException()
				builder.put(key, resolver to json)
			}

			foundResolvers = builder.build()
			ready = true
			finishUpIfRequiredAndPossible(NULLABLE_MINECRAFT_SERVER)
		}

		private data class Accumulator(
			val input2Recipes: Reference2ObjectOpenHashMap<Item, ReferenceOpenHashSet<ResolvedRecipe>> = Reference2ObjectOpenHashMap(),
			val output2Recipes: Reference2ObjectOpenHashMap<Item, ReferenceOpenHashSet<ResolvedRecipe>> = Reference2ObjectOpenHashMap(),
		) {
			val heuristicsSize: Long
				get() {
					return input2Recipes.size.toLong() + output2Recipes.size.toLong()
				}

			fun combine(other: Accumulator) {
				for ((k, v) in other.input2Recipes) {
					val existing = input2Recipes[k]

					if (existing == null) {
						input2Recipes[k] = v
					} else {
						existing.addAll(v)
					}
				}

				for ((k, v) in other.output2Recipes) {
					val existing = output2Recipes[k]

					if (existing == null) {
						output2Recipes[k] = v
					} else {
						existing.addAll(v)
					}
				}
			}

			fun add(recipe: ResolvedRecipe) {
				output2Recipes.computeIfAbsent(recipe.output.item, Reference2ObjectFunction { ReferenceOpenHashSet() }).add(recipe)

				for (input1 in recipe.inputs) {
					for (input in input1) {
						input2Recipes.computeIfAbsent(input.input.item, Reference2ObjectFunction { ReferenceOpenHashSet() }).add(recipe)
					}
				}
			}
		}

		private var input2Recipes: Map<Item, Collection<ResolvedRecipe>> = mapOf()
		private var output2Recipes: Map<Item, Collection<ResolvedRecipe>> = mapOf()

		private val determinedValues = Reference2ObjectOpenHashMap<Item, IMatterValue>()
		private val seenItems = ArrayDeque<Item>()

		private var changes = false
		private var iteration = 0
		private var lenientStage = false
		private var cachedIterationResults = Reference2ObjectOpenHashMap<Item, Result>()

		private fun doTryToBacktrack(item: Item, makeCommentary: Boolean): Result {
			val recipes = input2Recipes[item]

			if (recipes == null || recipes.isEmpty() || !recipes.all { it.allowBacktrack } || !recipes.all { it.inputs.all { it.all { it.input.item == item } } }) {
				if (makeCommentary)
					comment(item, TextComponent("Item '${item.registryName}' has no recipes"))

				return Result.MISSING
			}

			var minimal: IMatterValue? = null
			var minimalMultiplier = 0.0
			var foundRecipe: ResolvedRecipe? = null

			for (recipe in recipes) {
				val value = doDetermineValue(recipe.output.item)

				if (value.value != null) {
					if (minimal == null || minimal < value.value) {
						minimal = value.value
						minimalMultiplier = recipe.output.multiplier / recipe.inputs.size
						foundRecipe = recipe
					}
				} else if (!value.isSkipped) {
					LOGGER.error("${item.registryName} cant resolve ${recipe.output.item}")
					minimal = null
					break
				}
			}

			if (minimal == null) {
				if (makeCommentary)
					comment(item, TextComponent("Item '${item.registryName}' has no valid backtracking recipes, and no output recipes"))

				return Result.MISSING
			}

			val result = MatterValue(minimal.matter * minimalMultiplier, minimal.complexity * minimalMultiplier)

			changes = true
			determinedValues[item] = result
			comment(item, TextComponent("Matter value backtracked from ${foundRecipe!!.formattedName} at iteration $iteration"))
			return Result(result)
		}

		private fun tryToBacktrack(item: Item, asFallback: Boolean): Result {
			val getResult = cachedIterationResults[item]

			if (getResult != null) {
				return getResult
			}

			if (Registry.isBlacklisted(item))
				return Result.MISSING

			val value = Registry.direct(item)

			if (value.hasMatterValue)
				return Result(value)

			val value2 = determinedValues[item]
			if (value2?.hasMatterValue == true)
				return Result(value2)

			if (item in seenItems)
				if (!asFallback || item == seenItems.last() && seenItems.count { it == item } == 1)
					return Result.SKIPPED

			seenItems.addLast(item)

			try {
				val value3 = Registry.indirect(item)
				if (value3.value != null)
					return value3

				val result = doTryToBacktrack(item, !asFallback)
				cachedIterationResults[item] = result
				return result
			} finally {
				seenItems.removeLast()
			}
		}

		private fun doDetermineValue(item: Item): Result {
			var minimalMatter: Decimal? = null
			var minimalComplexity: Double? = null

			val recipes = output2Recipes[item]

			if (recipes == null || recipes.isEmpty()) {
				return tryToBacktrack(item, false)
			}

			var hadSkips = false
			var minimalRecipe: ResolvedRecipe? = null

			recipesLoop@ for (recipe in recipes) {
				if (recipe.inputs.isEmpty()) {
					// TODO: should we ignore empty recipes?
					comment(item, TextComponent("${recipe.formattedName} is empty"))

					continue
				}

				var accumulatedMatter: Decimal? = null
				var accumulatedComplexity: Double? = null

				inputsLoop@ for ((i, inputs) in recipe.inputs.withIndex()) {
					var minimal: IMatterValue? = null
					var minimalMultiplier = 0.0
					val skips = ArrayList<RecipeEntry>()
					val recursiveSkips = ArrayList<RecipeEntry>()
					var hadResidue = false

					innerInputsLoop@ for (entry in inputs) {
						val ivalue = determineValue(entry.input.item)

						if (ivalue.isMissing) {
							comment(item, TextComponent("Input $entry at input slot $i in ${recipe.formattedName} has no matter value"))

							if (recipe.isCritical && !lenientStage) {
								return Result.MISSING
							} else {
								if (recipe.isCritical) {
									skips.add(entry)
									continue@innerInputsLoop
								} else {
									continue@recipesLoop
								}
							}
						} else if (ivalue.isSkipped) {
							comment(item, TextComponent("Input $entry at input slot $i in ${recipe.formattedName} is recursive"))
							recursiveSkips.add(entry)

							if (inputs.size == 1) {
								hadSkips = true
								continue@recipesLoop
							} else {
								continue@innerInputsLoop
							}
						}

						var realValue = ivalue.value!!
						val ivalue2 = entry.remainder?.item?.let(::determineValue)

						if (ivalue2 != null) {
							if (ivalue2.isMissing) {
								comment(item, TextComponent("Remainder of $entry at input slot $i in ${recipe.formattedName} has no matter value"))

								if (recipe.isCritical && !lenientStage) {
									return Result.MISSING
								} else {
									if (recipe.isCritical) {
										skips.add(entry)
										continue@innerInputsLoop
									} else {
										continue@recipesLoop
									}
								}
							} else if (ivalue2.isSkipped) {
								comment(item, TextComponent("Remainder of $entry at input slot $i in ${recipe.formattedName} is recursive"))
								recursiveSkips.add(entry)

								if (inputs.size == 1) {
									hadSkips = true
									continue@recipesLoop
								} else {
									continue@innerInputsLoop
								}
							}

							realValue = MatterValue(realValue.matter - ivalue2.value!!.matter, realValue.complexity - ivalue2.value.complexity)
							hadResidue = true

							if (realValue.matter.isNegative || realValue.complexity < 0.0) {
								comment(item, TextComponent("Entry $entry at input slot $i in ${recipe.formattedName} ended up with negative matter value or/and complexity"))
								return Result.MISSING
							}
						}

						if (minimal == null || minimal > realValue) {
							minimal = realValue
							minimalMultiplier = entry.input.multiplier
						}
					}

					if (skips.size > inputs.size / 2 || skips.isNotEmpty() && inputs.size == 1) {
						comment(item, TextComponent("More than half inputs (${skips.joinToString(", ")}) at input slot $i in ${recipe.formattedName} have no matter values"))
						return Result.MISSING
					} else if (skips.isNotEmpty()) {
						/**
						 * Эвристический анализ тегов
						 *
						 * Если у 60% <= предметов со значением материи есть тег, и он есть у всех
						 * предметов без значения материи, то предметы без материи игнорируются
						 */
						val manager = ForgeRegistries.ITEMS.tags()!!
						val tagSetsPresent = Object2IntArrayMap<TagKey<Item>>()
						val tagSetsMissing = Object2IntArrayMap<TagKey<Item>>()

						for (input in inputs) {
							if (input in recursiveSkips) {
								continue
							}

							val list = manager.getReverseTag(input.input.item).getOrNull()?.tagKeys?.collect(ImmutableList.toImmutableList()) ?: ImmutableList.of()

							if (input !in skips) {
								for (tag in list)
									tagSetsPresent.computeInt(tag) { _, e -> (e ?: 0) + 1 }
							} else {
								for (tag in list)
									tagSetsMissing.computeInt(tag) { _, e -> (e ?: 0) + 1 }
							}
						}

						val filtered = tagSetsPresent.object2IntEntrySet()
							.filter { it.intValue.toDouble() >= (inputs.size - skips.size - recursiveSkips.size) * 0.6 }

						val result = filtered.all { tagSetsMissing.getInt(it.key) == skips.size }

						if (!result) {
							comment(item, TextComponent("More than half inputs (${skips.joinToString(", ")}) at input slot $i in ${recipe.formattedName} have no matter values"))
							return Result.MISSING
						} else {
							comment(item, TextComponent("${recipe.formattedName} with inputs at slot $i without matter values were allowed to be skipped due to next tags:"))

							for (tag in filtered) {
								comment(item, TextComponent(tag.key.location.toString()))
							}
						}
					}

					if (minimal != null && !minimal.hasMatterValue && hadResidue && inputs.size == 1) {
						// ингредиент ничего не стоит ибо не потребляется в крафте
						continue@inputsLoop
					}

					if (minimal == null || !minimal.hasMatterValue) {
						comment(item, TextComponent("'${recipe.formattedName}' has invalid input at slot $i (possible inputs: ${inputs.joinToString(", ")}) (???)"))
						return Result.MISSING
					}

					if (accumulatedMatter == null || accumulatedComplexity == null) {
						accumulatedMatter = minimal.matter * minimalMultiplier
						accumulatedComplexity = minimal.complexity * minimalMultiplier
					} else {
						accumulatedMatter += minimal.matter * minimalMultiplier
						accumulatedComplexity += minimal.complexity * minimalMultiplier
					}
				}

				if (accumulatedMatter == null || accumulatedComplexity == null) {
					throw RuntimeException("This piece should be unreachable")
				}

				if (!accumulatedMatter.isPositive || accumulatedComplexity <= 0.0) {
					comment(item, TextComponent("${recipe.formattedName} ended up with negative matter value and/or complexity (???)"))
					return Result.MISSING
				}

				if (minimalMatter == null || minimalMatter > accumulatedMatter) {
					minimalMatter = recipe.transformMatterValue(accumulatedMatter * recipe.output.multiplier)
					minimalComplexity = recipe.transformComplexity(accumulatedComplexity * recipe.output.multiplier)
					minimalRecipe = recipe
				}
			}

			if (minimalMatter == null || minimalComplexity == null) {
				if (hadSkips) {
					val backtrack = tryToBacktrack(item, true)

					if (backtrack.value == null) {
						return Result.SKIPPED
					} else {
						return backtrack
					}
				}

				comment(item, TextComponent("'${item.registryName}' ended up with no valid recipes (???)"))

				return Result.MISSING
			}

			val result = MatterValue(minimalMatter, minimalComplexity)

			changes = true
			determinedValues[item] = result
			comment(item, TextComponent("Matter value derived from ${minimalRecipe!!.formattedName} at iteration $iteration"))

			return Result(result)
		}

		fun determineValue(item: Item): Result {
			val getResult = cachedIterationResults[item]

			if (getResult != null)
				return getResult

			if (Registry.isBlacklisted(item))
				return Result.MISSING

			val direct = Registry.direct(item)
			if (direct.hasMatterValue)
				return Result(direct)

			val determined = determinedValues[item]
			if (determined?.hasMatterValue == true)
				return Result(determined)

			if (item in seenItems)
				return Result.SKIPPED

			seenItems.addLast(item)

			try {
				val indirect = Registry.indirect(item)
				if (indirect.value != null)
					return indirect

				val result = doDetermineValue(item)
				cachedIterationResults[item] = result
				return result
			} finally {
				seenItems.removeLast()
			}
		}

		fun resolve(server: MinecraftServer) {
			var time = SystemTime()
			LOGGER.info("Finding recipes...")

			determinedValues.clear()
			seenItems.clear()

			val foundStreams = foundResolvers.map {
				try {
					it.value.first.find(server, it.value.second)
				} catch(err: Throwable) {
					LOGGER.fatal("Recipe resolver ${it.key} experienced internal error", err)
					throw RuntimeException("Recipe resolver ${it.key} experienced internal error", err)
				}
			}

			val sequentialStreams = foundStreams.filter { !it.isParallel }
			val parallelStreams = foundStreams.filter { it.isParallel }

			val streamA = Streams.concat(*sequentialStreams.toTypedArray())
			val streamB = Streams.concat(*parallelStreams.toTypedArray())

			val resultA = streamA.collect(::Accumulator, Accumulator::add, Accumulator::combine)
			val resultB = streamB.collect(::Accumulator, Accumulator::add, Accumulator::combine)

			val result: Accumulator

			if (resultA.heuristicsSize > resultB.heuristicsSize) {
				result = resultA
				resultA.combine(resultB)
			} else {
				result = resultB
				resultB.combine(resultA)
			}

			val (input2Recipes, output2Recipes) = result

			LOGGER.info("Finding recipes took ${time.millis}ms, involving ${input2Recipes.keys.size} unique inputs and ${output2Recipes.keys.size} unique outputs")
			LOGGER.info("Resolving recipes...")

			this.input2Recipes = input2Recipes
			this.output2Recipes = output2Recipes

			changes = true
			iteration = 0
			var ops = 0

			val toDetermine = ReferenceLinkedOpenHashSet<Item>()
			toDetermine.addAll(input2Recipes.keys)
			toDetermine.addAll(output2Recipes.keys)

			if (LOGGER.isDebugEnabled) {
				LOGGER.debug("Gonna try to search for matter values for next items:")

				val names = ArrayList<String>()
				var length = 0

				for (value in toDetermine) {
					val name = value.registryName!!.toString()

					if (length != 0 && length + name.length < 400) {
						names.add(name)
						length += name.length
					} else if (length == 0) {
						names.add(name)
						length = name.length
					} else {
						LOGGER.debug(names.joinToString(", "))
						names.clear()
						length = 0
					}
				}

				if (names.isNotEmpty()) {
					LOGGER.debug(names.joinToString(", "))
				}
			}

			time = SystemTime()

			lenientStage = false

			while (changes) {
				ops += cachedIterationResults.size
				cachedIterationResults = Reference2ObjectOpenHashMap()
				changes = false
				iteration++

				val iterator = toDetermine.iterator()

				for (value in iterator) {
					if (determineValue(value).value?.hasMatterValue == true) {
						iterator.remove()
					}
				}
			}

			lenientStage = true
			changes = true

			while (changes) {
				ops += cachedIterationResults.size
				cachedIterationResults = Reference2ObjectOpenHashMap()
				changes = false
				iteration++

				val iterator = toDetermine.iterator()

				for (value in iterator) {
					if (determineValue(value).value?.hasMatterValue == true) {
						iterator.remove()
					}
				}
			}

			for (item in toDetermine) {
				if (item !in commentary || commentary[item]!!.isEmpty()) {
					if (Registry.isBlacklisted(item)) {
						comment(item, TextComponent("Item ${item.registryName} is blacklisted from having matter value"))
					} else {
						comment(item, TextComponent("Item ${item.registryName} was never visited"))
					}
				}
			}

			LOGGER.info("Resolving recipes took ${time.millis}ms in $iteration iterations with ~$ops operations, determined ${determinedValues.size} matter values")
		}
	}

	internal fun compute(value: Item): Result {
		return Resolver.determineValue(value)
	}

	private val seenTags = ArrayDeque<TagKey<Item>>()

	internal fun compute(value: TagKey<Item>): Result {
		if (value in seenTags) {
			return Result.SKIPPED
		}

		seenTags.addLast(value)

		try {
			val result = Registry.tagEntries[value]

			if (result != null) {
				return Result(result)
			}

			val computed = Registry.computeTags[value]

			if (computed != null) {
				return computed.tryCompute()
			}

			return Result.MISSING
		} finally {
			seenTags.removeLast()
		}
	}

	@JvmStatic
	fun hasMatterValue(value: Item): Boolean {
		return get(value).hasMatterValue
	}

	private fun reduce(a: IMatterValue, b: IMatterValue): IMatterValue {
		if (a.hasMatterValue && b.hasMatterValue) {
			return MatterValue(a.matter + b.matter, a.complexity + b.complexity)
		} else if (a.hasMatterValue) {
			return a
		} else if (b.hasMatterValue) {
			return b
		} else {
			return IMatterValue.Companion
		}
	}

	private fun get(value: ItemStack, level: Int, accountForStackSize: Boolean): IMatterValue {
		if (value.isEmpty) {
			return IMatterValue.Companion
		}

		var matter: IMatterValue
		var undamagedMatterValue: IMatterValue

		if (value.item is IMatterItem)
			matter = (value.item as IMatterItem).getMatterValue(value) ?: IMatterValue.Companion
		else
			matter = get(value.item)

		if (matter.hasMatterValue && value.isDamageableItem) {
			val severity = 1.0 - value.damageValue.toDouble() / value.maxDamage.toDouble()
			undamagedMatterValue = matter
			matter = MatterValue(matter.matter * severity, matter.complexity * (0.5 + severity / 2))
		} else {
			undamagedMatterValue = matter
		}

		val matterCap = value.getCapability(MatteryCapability.MATTER).orNull()

		if (matterCap != null) {
			matter = MatterValue(matter.matter + matterCap.storedMatter, matter.complexity.coerceAtLeast(1.0))
			undamagedMatterValue = MatterValue(undamagedMatterValue.matter + matterCap.storedMatter, undamagedMatterValue.complexity)
		}

		val drive = value.getCapability(MatteryCapability.DRIVE).orNull()

		if (drive != null && drive.storageType == OverdriveThatMatters.INSTANCE.ITEM_STORAGE()) {
			(drive as IMatteryDrive<ItemStackWrapper>).stacks
				.map { it.stack.stack }
				.filter { !it.isEmpty }
				.map { get(it, level + 1, true) }
				.reduce(::reduce)
				.ifPresent {
					matter += it
					undamagedMatterValue += it
				}
		}

		value.getCapability(ForgeCapabilities.ITEM_HANDLER).ifPresentK {
			it.stream().filter { !it.isEmpty }.map { get(it, level + 1, true) }.reduce(::reduce).ifPresent {
				matter += it
				undamagedMatterValue += it
			}
		}

		if (matter.hasMatterValue && value.count > 1 && accountForStackSize) {
			return MatterValue(matter.matter + undamagedMatterValue.matter * (value.count - 1), matter.complexity + undamagedMatterValue.complexity * (value.count - 1))
		}

		return matter
	}

	/**
	 * Matter value for itemstack, accounting for capabilities and stack size
	 */
	@JvmStatic
	@JvmOverloads
	fun get(value: ItemStack, accountForStackSize: Boolean = true): IMatterValue {
		return get(value, 0, accountForStackSize)
	}

	@JvmStatic
	fun hasMatterValue(value: ItemStack): Boolean {
		return get(value).hasMatterValue
	}

	@JvmStatic
	fun canDecompose(value: Item): Boolean {
		if (value is IMatterItem)
			return value.canDecompose(ItemStack.EMPTY)

		return get(value).hasMatterValue
	}

	@JvmStatic
	fun canDecompose(value: ItemStack): Boolean {
		var can = true

		if (value.item is IMatterItem)
			can = (value.item as IMatterItem).canDecompose(value)

		can = can && (value.getCapability(MatteryCapability.MATTER).orNull()?.storedMatter ?: Decimal.ZERO).isZero
		can = can && (value.getCapability(MatteryCapability.DRIVE).orNull()?.storedCount ?: BigInteger.ZERO).isZero

		return can && get(value).hasMatterValue
	}

	@JvmStatic
	fun getResearchAdvance(complexity: Double): Double {
		if (complexity <= 0.0) {
			return 0.0
		}

		return (1.0 / (complexity.pow(1.0 / 3.0) * 1.25).coerceAtLeast(0.01)).coerceIn(0.025, 0.2)
	}

	@JvmStatic
	fun getResearchAdvance(stack: ItemStack): Double {
		return getResearchAdvance(get(stack).complexity)
	}

	@JvmStatic
	fun getResearchAdvance(stack: ItemLike): Double {
		return getResearchAdvance(this@MatterManager.get(stack.asItem()).complexity)
	}

	private val formatMatterAsReadable = BooleanSupplier { if (ClientConfig.ALWAYS_DISPLAY_MATTER_VALUE) minecraft.window.isShiftDown else minecraft.window.isShiftDown && milliTime % 2_000L > 1_000L }

	fun tooltipEvent(event: ItemTooltipEvent) {
		if (ClientConfig.ALWAYS_DISPLAY_MATTER_VALUE || minecraft.window.isShiftDown) {
			val matter = get(event.itemStack, accountForStackSize = false)

			if (matter.hasMatterValue) {
				if (matter.complexity >= 1.0E-3) {
					val matterSized = get(event.itemStack, accountForStackSize = true)

					if (matter.matter != matterSized.matter || matter.complexity != matterSized.complexity) {
						event.toolTip.add(
							TranslatableComponent("otm.gui.matter.format_and_complexity2",
								matter.matter.formatMatterFull(formatAsReadable = formatMatterAsReadable),
								matterSized.matter.formatMatter(formatAsReadable = formatMatterAsReadable),
								if (matter.complexity > 1000.0) formatTickDuration(matter.complexity.roundToInt(), true) else matter.complexity.formatSiComponent(TranslatableComponent("otm.gui.ticks")),
								if (matterSized.complexity > 1000.0) formatTickDuration(matterSized.complexity.roundToInt(), true) else matterSized.complexity.formatSiComponent(TranslatableComponent("otm.gui.ticks")),
							).withStyle(ChatFormatting.AQUA))
					} else {
						event.toolTip.add(
							TranslatableComponent("otm.gui.matter.format_and_complexity",
								matter.matter.formatMatterFull(formatAsReadable = formatMatterAsReadable),
								if (matter.complexity > 1000.0) formatTickDuration(matter.complexity.roundToInt(), true) else matter.complexity.formatSiComponent(TranslatableComponent("otm.gui.ticks")),
							).withStyle(ChatFormatting.AQUA))
					}
				} else {
					event.toolTip.add(matter.matter.formatMatterFull(formatAsReadable = formatMatterAsReadable).withStyle(ChatFormatting.AQUA))
				}
			}

			if (event.flags.isAdvanced) {
				val commentary = getCommentary(event.itemStack.item)

				if (commentary != null) {
					if (commentary.size > 3) {
						var index = ((secondTime / 3L) % commentary.size).toInt()

						for (i in 0 .. 2) {
							if (index == commentary.size) {
								event.toolTip.add(TextComponent("=========").withStyle(ChatFormatting.DARK_GRAY))
								index = 0
							}

							event.toolTip.add(commentary[index++].withStyle(ChatFormatting.DARK_GRAY))
						}
					} else {
						for (comment in commentary) {
							event.toolTip.add(comment.withStyle(ChatFormatting.DARK_GRAY))
						}
					}
				}
			}
		}
	}

	/**
	 * because it is immutable
	 */
	data class ImmutableStack(
		val item: Item,
		val multiplier: Double = 1.0
	) {
		init {
			require(!multiplier.isNaN()) { "Provided stack cost multiplier is NaN" }
			require(!multiplier.isInfinite()) { "Provided stack cost multiplier is infinite" }
		}

		constructor(item: Item, count: Int) : this(item, 1.0 / count.toDouble())
		constructor(item: ItemStack) : this(item.item, item.count)
	}

	data class RecipeEntry(
		val input: ImmutableStack,
		val remainder: ImmutableStack? = null
	) {
		constructor(item: Item, count: Int) : this(ImmutableStack(item, count))
		constructor(item: ItemStack) : this(ImmutableStack(item))

		override fun toString(): String {
			return "RecipeEntry[${input.item.registryName} to ${remainder?.item?.registryName}]"
		}
	}

	class ResolvedRecipe(
		inputs: Stream<Stream<RecipeEntry>>,
		val output: ImmutableStack,
		val transformMatterValue: (Decimal) -> Decimal = { it },
		val transformComplexity: (Double) -> Double = { it },

		/**
		 * if we can't resolve one of ingredient's matter value - consider output also having no matter value
		 */
		val isCritical: Boolean = true,
		val name: ResourceLocation? = null,

		/**
		 * Whenever to allow "backtracking" recipe ingredients to recipe outputs.
		 *
		 * When an item has no recipes, but takes part in recipes with output of already determined matter value
		 * (e.g. 4 rabbit hides -> 1 leather), we can assume that we can determine matter value of ingredients based on matter value of output
		 *
		 * "Backtrack" can happen if everything of the next holds true:
		 * * All recipes with item in question contains only that item;
		 * * All recipes allow backtracking
		 */
		val allowBacktrack: Boolean = true,
	) {
		val inputs: List<List<RecipeEntry>> = inputs
			.map { it.filter { it.input.multiplier > 0.0 }.collect(ImmutableList.toImmutableList()) }
			.filter { it.isNotEmpty() }
			.collect(ImmutableList.toImmutableList())

		val formattedName: String
			get() = if (name == null) "One of recipes" else "Recipe '$name'"

		override fun equals(other: Any?): Boolean {
			if (this === other) return true
			if (other !is ResolvedRecipe) return false

			if (output != other.output) return false
			if (transformMatterValue != other.transformMatterValue) return false
			if (transformComplexity != other.transformComplexity) return false
			if (isCritical != other.isCritical) return false
			if (allowBacktrack != other.allowBacktrack) return false
			if (inputs != other.inputs) return false

			return true
		}

		override fun hashCode(): Int {
			var result = output.hashCode()
			result = 31 * result + transformMatterValue.hashCode()
			result = 31 * result + transformComplexity.hashCode()
			result = 31 * result + isCritical.hashCode()
			result = 31 * result + allowBacktrack.hashCode()
			result = 31 * result + inputs.hashCode()
			return result
		}
	}

	fun interface Finder {
		/**
		 * Returned stream can be either parallel or sequential
		 *
		 * Parallel streams will _greatly_ improve performance on mid-end range computers,
		 * but you need to make sure your stream has no side effects (such as binding late-binding tags)
		 *
		 * Internally, OTM separate parallel streams from sequential so sequential streams happen strictly on server thread
		 * (if you can't avoid side effects, or don't bother with synchronization).
		 *
		 * You can safely call [MatterManager.getDirect] inside returned stream, both parallel and sequential.
		 */
		fun find(server: MinecraftServer, data: JsonObject): Stream<ResolvedRecipe>
	}

	/**
	 * Please use exclusively inside recipe finder
	 */
	@JvmStatic
	fun getDirect(value: Item): IMatterValue {
		return Registry.direct(value)
	}

	/**
	 * Access recipe finders registry
	 *
	 * @throws IllegalStateException if calling too early
	 */
	@JvmStatic val recipeFinders get() = Resolver.delegate.get()

	/**
	 * Access recipe finders registry key
	 *
	 * Use this with your [DeferredRegister]
	 */
	@JvmStatic val recipeFindersKey get() = Resolver.delegate.key

	private val commentary = Reference2ObjectOpenHashMap<Item, ArrayList<Component>>()

	@JvmStatic
	fun getCommentary(item: Item): List<MutableComponent>? {
		return commentary[item]?.let { source ->
			ArrayList<MutableComponent>(source.size)
				.also { for (value in source) it.add(value.copy()) }
		}
	}

	private fun comment(item: Item, comment: Component) {
		val target = commentary.computeIfAbsent(item, Reference2ObjectFunction { ArrayList(1) })

		if (comment !in target) {
			target.add(comment)
		}
	}

	fun reloadEvent(event: AddReloadListenerEvent) {
		event.addListener(Registry)
		event.addListener(Resolver)
	}

	fun initialize(bus: IEventBus) {
		bus.addListener(Resolver.delegate::build)
		Resolver.registrar.register(bus)
	}

	private enum class DumpFilter {
		ALL {
			override fun filter(input: Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>>): Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>> {
				return input
			}
		},
		ONLY_MISSING {
			override fun filter(input: Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>>): Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>> {
				return input.filter { !it.second.hasMatterValue }
			}
		},
		ONLY_VALID {
			override fun filter(input: Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>>): Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>> {
				return input.filter { it.second.hasMatterValue }
			}
		},
		ONLY_BLACKLISTED {
			override fun filter(input: Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>>): Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>> {
				return input.filter { Registry.isBlacklisted(it.first.value) }
			}
		},
		;

		abstract fun filter(input: Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>>): Stream<Pair<Map.Entry<ResourceKey<Item>, Item>, IMatterValue>>
	}

	private fun dumpRegistry(stack: CommandContext<CommandSourceStack>, filter: DumpFilter = DumpFilter.ALL, mod: String? = null): Int {
		val targetFile = File(MINECRAFT_SERVER.serverDirectory, "otm/registry_dumps/${filter.name.lowercase()}_${System.currentTimeMillis() / 1_000L}.csv")
		File(MINECRAFT_SERVER.serverDirectory, "otm/registry_dumps").mkdirs()

		stack.source.sendSuccess({ TranslatableComponent("otm.dumping_matter_registry", targetFile.absolutePath) }, false)

		val writer = targetFile.bufferedWriter(Charsets.UTF_8)

		writer.write(arrayOf("Registry ID", "Matter Value", "Complexity", "Commentary").joinToString(";", transform = ::transformQuotes))
		writer.write("\n")

		var stream = ForgeRegistries.ITEMS.entries.stream()

		if (mod != null) {
			stream = stream.filter { it.key.location().namespace == mod }
		}

		if (filter != DumpFilter.ALL && filter != DumpFilter.ONLY_BLACKLISTED) {
			stream = stream.filter { !Registry.isBlacklisted(it.value) }
		}

		val stream2 = stream.map { it to get(it.value) }

		for ((entry, value) in filter.filter(stream2)) {
			val (key, item) = entry
			val isBlacklisted: Boolean

			if (filter == DumpFilter.ONLY_BLACKLISTED) {
				isBlacklisted = true
			} else if (filter == DumpFilter.ALL) {
				isBlacklisted = Registry.isBlacklisted(item)
			} else {
				isBlacklisted = false
			}

			if (isBlacklisted) {
				writer.write(arrayOf(key.location().toString(), "---", "---", "Item is blacklisted from having matter value").joinToString(";", transform = ::transformQuotes))
			} else {
				if (!value.hasMatterValue) {
					writer.write(arrayOf(key.location().toString(), "", "", commentary[item]?.joinToString("\n", transform = { transformQuotes(it.string) }) ?: "").joinToString(";", transform = ::transformQuotes))
				} else {
					writer.write(arrayOf(key.location().toString(), value.matter.toString(), value.complexity.toString(), commentary[item]?.joinToString("\n", transform = { transformQuotes(it.string) }) ?: "").joinToString(";", transform = ::transformQuotes))
				}
			}

			writer.write("\n")
		}

		writer.close()

		stack.source.sendSuccess({ TranslatableComponent("otm.dumped_matter_registry", targetFile.absolutePath) }, true)

		return 0
	}

	fun addCommands(event: RegisterCommandsEvent) {
		event.dispatcher.register(
			Commands.literal("dump_matter_registry")
				.requires { it.hasPermission(Commands.LEVEL_OWNERS) }
				.executes(::dumpRegistry)
				.then(
					Commands.argument("mode", EnumArgument.enumArgument(DumpFilter::class.java)).executes { dumpRegistry(it, it.getArgument("mode", DumpFilter::class.java)) }
						.then(Commands.argument("mod", StringArgumentType.string())
							.executes { dumpRegistry(it, it.getArgument("mode", DumpFilter::class.java), StringArgumentType.getString(it, "mod")) }
							.suggests { context, builder ->
								val startingWith = builder.input.substring(builder.start).lowercase()
								ModList.get().mods.stream()
									.filter { it.modId.startsWith(startingWith) }
									.filter { key -> ForgeRegistries.ITEMS.entries.stream()
										.filter { it.key.location() == key }
										.anyMatch { !Registry.isBlacklisted(it.value) } }
									.forEach { builder.suggest(it.modId, TextComponent(it.displayName)) }
								builder.buildFuture()
							})
				)
		)
	}

	private val matterValues = Reference2ObjectOpenHashMap<Item, IMatterValue>()

	val map: Map<Item, IMatterValue> = Collections.unmodifiableMap(matterValues)

	private var registryIsBuilt = false

	private fun finishUpIfRequiredAndPossible(server: MinecraftServer?) {
		if (server == null)
			return

		if (registryIsBuilt || !Resolver.ready || !Registry.ready)
			return

		registryIsBuilt = true

		commentary.clear()
		matterValues.clear()

		Resolver.resolve(server)

		for (item in ForgeRegistries.ITEMS) {
			val value = compute(item).value

			if (value?.hasMatterValue == true)
				matterValues[item] = value
		}

		syncRegistry(PacketDistributor.ALL.noArg())
	}

	fun onDataPackSync(event: OnDatapackSyncEvent) {
		if (!registryIsBuilt)
			return

		if (event.player == null) {
			syncRegistry(PacketDistributor.ALL.noArg())
		} else {
			syncRegistry(PacketDistributor.PLAYER.with { event.player ?: throw ConcurrentModificationException() })
		}
	}

	fun onServerStarted(event: ServerStartedEvent) {
		check(Resolver.ready) { "Recipe resolver is not ready somehow" }
		check(Registry.ready) { "Matter registry is not ready somehow" }
		finishUpIfRequiredAndPossible(event.server)
	}

	fun get(value: Item): IMatterValue {
		return matterValues[value] ?: IMatterValue.Companion
	}

	private val receivedPackets = ArrayList<SyncPacket>()

	private fun syncRegistry(distributor: PacketDistributor.PacketTarget) {
		val time = SystemTime()
		val stream = FastByteArrayOutputStream()
		val data = DataOutputStream(stream)

		var commentsSize = commentary.size
		data.writeInt(matterValues.size)

		for ((k, v) in matterValues) {
			data.writeItemType(k)
			data.writeMatterValue(v)

			val comment = commentary[k]

			if (comment != null) {
				commentsSize--
				data.write(1)
				data.writeCollection(comment, OutputStream::writeBinaryComponent)
			} else {
				data.write(0)
			}
		}

		data.writeInt(commentsSize)

		for ((k, v) in commentary) {
			if (!matterValues.containsKey(k)) {
				data.writeItemType(k)
				data.writeCollection(v, OutputStream::writeBinaryComponent)
			}
		}

		val deflater = Deflater(5)
		deflater.setInput(stream.array, 0, stream.length)
		deflater.finish()

		val chunks = ArrayList<SyncPacket>()
		var totalSize = 0
		var first = true

		while (true) {
			val bytes = ByteArray(2 shl 20 - 1024)
			val written = deflater.deflate(bytes)

			if (written == 0) {
				break
			} else {
				totalSize += written
				chunks.add(SyncPacket(bytes, written, if (first) { first = false; FIRST } else NORMAL))
			}
		}

		if (chunks.size == 1) {
			chunks[0].mode = FIRST_AND_LAST
		} else if(chunks.size > 1) {
			chunks.last().mode = LAST
		}

		LOGGER.debug("Encoding matter registry packet took ${time.millis}ms, (${stream.length} bytes total, $totalSize bytes compressed)")

		for (chunk in chunks) {
			RegistryNetworkChannel.send(distributor, chunk)
		}
	}

	private fun playRegistryPackets() {
		val time = SystemTime()
		var totalCompressedSize = 0

		for (chunk in receivedPackets) {
			totalCompressedSize += chunk.length
		}

		if (totalCompressedSize == 0) {
			return // what.
		}

		val compressed = ByteArray(totalCompressedSize)
		var pointer = 0

		for (chunk in receivedPackets) {
			for (i in 0 until chunk.length) {
				compressed[pointer++] = chunk.payload[i]
			}
		}

		receivedPackets.clear()

		val chunks = ArrayList<ByteArray>()
		var size = 0
		val inflater = Inflater()
		inflater.setInput(compressed)

		while (!inflater.finished()) {
			val chunk = ByteArray(2 shl 16)
			val inflated = inflater.inflate(chunk)

			if (inflated == 0) {
				break
			} else {
				size += inflated

				if (size >= 1 shl 24 /* 16 MiB */) {
					throw IndexOutOfBoundsException("Pipe Bomb")
				}

				if (inflated == 2 shl 16) {
					chunks.add(chunk)
				} else {
					chunks.add(chunk.copyOfRange(0, inflated))
				}
			}
		}

		val spliced = ByteArray(size)
		var pointer2 = 0

		for (chunk in chunks) {
			for (i in chunk.indices) {
				spliced[pointer2++] = chunk[i]
			}
		}

		val stream = FastByteArrayInputStream(spliced)
		val data = DataInputStream(stream)

		val valuesSize = data.readInt()

		matterValues.clear()
		commentary.clear()

		for (i in 0 until valuesSize) {
			val type = data.readItemType()
			check(matterValues.put(type, data.readMatterValue()) == null) { "Duplicate item type $type" }

			if (data.read() > 0) {
				commentary[type] = data.readCollection { readBinaryComponent()!! }
			}
		}

		val commentsSize = data.readInt()

		for (i in 0 until commentsSize) {
			val type = data.readItemType()
			check(commentary.put(type, data.readCollection { readBinaryComponent()!! }) == null) { "Duplicate commentary item type $type" }
		}

		LOGGER.debug("Decoding matter registry packets took ${time.millis}ms ($totalCompressedSize bytes compressed, $size bytes total)")
	}

	fun readSyncPacket(buff: FriendlyByteBuf): SyncPacket {
		LOGGER.info("Received matter registry packet, ${buff.readableBytes()} bytes in size")

		val mode = buff.readByte()
		val bytes = ByteArray(buff.readableBytes())
		buff.readBytes(bytes)
		return SyncPacket(bytes, bytes.size, mode.toInt())
	}

	private const val FIRST = 0
	private const val NORMAL = 1
	private const val LAST = 2
	private const val FIRST_AND_LAST = 3

	class SyncPacket(val payload: ByteArray, val length: Int, var mode: Int) : MatteryPacket {
		override fun write(buff: FriendlyByteBuf) {
			buff.writeByte(mode)
			buff.writeBytes(payload, 0, length)
		}

		override fun play(context: Supplier<NetworkEvent.Context>) {
			if (SERVER_IS_LIVE)
				return // singleplayer or LAN host

			if (mode == FIRST) {
				receivedPackets.clear()
				receivedPackets.add(this)
			} else if (mode == LAST) {
				receivedPackets.add(this)
				playRegistryPackets()
			} else if (mode == FIRST_AND_LAST) {
				receivedPackets.clear()
				receivedPackets.add(this)
				playRegistryPackets()
			} else {
				receivedPackets.add(this)
			}
		}
	}
}
