
package ru.dbotthepony.mc.otm.datagen.recipes

import com.google.gson.JsonObject
import net.minecraft.advancements.CriterionTriggerInstance
import net.minecraft.data.recipes.FinishedRecipe
import net.minecraft.data.recipes.RecipeCategory
import net.minecraft.data.recipes.ShapedRecipeBuilder
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.item.crafting.RecipeSerializer
import net.minecraft.world.level.ItemLike
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.core.collect.JsonArrayCollector
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.set
import ru.dbotthepony.mc.otm.datagen.modLocation
import ru.dbotthepony.mc.otm.recipe.EnergyContainerRecipe
import ru.dbotthepony.mc.otm.recipe.UpgradeRecipe
import java.util.function.Consumer

private interface RecipeCell {
	val value: Ingredient
}

private data class IngredientRecipeCell(override val value: Ingredient) : RecipeCell

@Suppress("EqualsOrHashCode")
private data class ItemRecipeCell(private val item: ItemLike) : RecipeCell {
	override val value: Ingredient by lazy { Ingredient.of(item) }

	override fun equals(other: Any?): Boolean {
		if (other is ItemLike) {
			return other.asItem() == item
		}

		if (other is ItemRecipeCell) {
			return item == other.item
		}

		return false
	}
}

private data class TagRecipeCell(private val item: TagKey<Item>) : RecipeCell {
	override val value: Ingredient by lazy { Ingredient.of(item) }
}

private data class RecipeRow(
	val a: RecipeCell?,
	val b: RecipeCell?,
	val c: RecipeCell?,
)

/**
 * [ShapedRecipeBuilder] that doesn't suck
 */
@Suppress("unused")
class MatteryRecipe(val result: ItemLike, val count: Int = 1, val category: RecipeCategory = RecipeCategory.MISC) {
	private val rows = arrayOfNulls<RecipeRow>(3)
	private var index = 0

	private val unlockedBy = ArrayList<Pair<String, CriterionTriggerInstance>>().also {
		it.add("has_result" to has(result))
	}

	fun unlockedBy(name: String, trigger: CriterionTriggerInstance): MatteryRecipe {
		unlockedBy.add(name to trigger)
		return this
	}

	fun unlockedBy(item: ItemLike): MatteryRecipe {
		val location = item.asItem().registryName!!
		return unlockedBy("has_${location.namespace}_${location.path}", has(item))
	}

	fun unlockedBy(item: TagKey<Item>): MatteryRecipe {
		return unlockedBy("has_${item.location.namespace}_${item.location.path}", has(item))
	}

	companion object {
		private val charlist = charArrayOf(
			'A',
			'B',
			'C',

			'D',
			'E',
			'F',

			'G',
			'K',
			'M',
		)

		private fun findOrGetNextChar(
			iterator: Iterator<Char>,
			input: RecipeCell?,
			list: MutableList<Pair<Char, RecipeCell>>,
			builder: ShapedRecipeBuilder
		): Char {
			if (input == null) {
				return ' '
			}

			val find = list.firstOrNull { it.second == input }

			if (find != null) {
				return find.first
			}

			val char = iterator.next()
			list.add(char to input)
			builder.define(char, input.value)
			return char
		}
	}

	private fun buildRegular(): ShapedRecipeBuilder {
		if (index == 0) {
			throw NoSuchElementException("No recipe rows were defined")
		}

		val builder = ShapedRecipeBuilder(category, result, count)

		val pairs = ArrayList<Pair<Char, RecipeCell>>()
		val iterator = charlist.iterator()

		for (row in rows) {
			row ?: break

			val a = findOrGetNextChar(iterator, row.a, pairs, builder)
			val b = findOrGetNextChar(iterator, row.b, pairs, builder)
			val c = findOrGetNextChar(iterator, row.c, pairs, builder)

			builder.pattern("$a$b$c")
		}

		for ((a, b) in unlockedBy) {
			builder.unlockedBy(a, b)
		}

		return builder
	}

	private var upgradeSource: ResourceLocation? = null
	private val copyPaths = ArrayList<UpgradeRecipe.Op>()

	fun setUpgradeSource(source: ResourceLocation): MatteryRecipe {
		this.upgradeSource = source
		return this
	}

	fun setUpgradeSource(source: ItemLike): MatteryRecipe {
		this.upgradeSource = source.asItem().registryName!!
		return this
	}

	fun addUpgradeOps(vararg operations: UpgradeRecipe.Op): MatteryRecipe {
		for (op in operations) copyPaths.add(op)
		return this
	}

	private fun filter(consumer: Consumer<FinishedRecipe>): Consumer<FinishedRecipe> {
		if (upgradeSource != null) {
			check(copyPaths.isNotEmpty()) { "Defined upgrade recipe without nbt migration operations" }

			return Consumer {
				consumer.accept(object : FinishedRecipe by it {
					override fun serializeRecipeData(pJson: JsonObject) {
						it.serializeRecipeData(pJson)

						pJson["copyPaths"] = copyPaths.stream().map { it.serialize() }.collect(JsonArrayCollector)
						pJson["source"] = upgradeSource!!.toString()
					}

					override fun getType(): RecipeSerializer<*> {
						return UpgradeRecipe.Companion
					}
				})
			}
		}

		return consumer
	}

	fun build(consumer: Consumer<FinishedRecipe>, name: String? = null) {
		val builder = buildRegular()

		if (name != null) {
			builder.save(filter(consumer), modLocation(
				if (result.asItem().registryName!!.namespace == OverdriveThatMatters.MOD_ID)
					"${result.asItem().registryName!!.path}_$name"
				else
					"${result.asItem().registryName!!.namespace}_${result.asItem().registryName!!.path}_$name"
			))
		} else {
			builder.save(filter(consumer))
		}
	}

	fun build(consumer: Consumer<FinishedRecipe>, name: ResourceLocation) {
		buildRegular().save(filter(consumer), name)
	}

	fun buildEnergetic(consumer: Consumer<FinishedRecipe>, name: String? = null) {
		build({
			consumer.accept(object : FinishedRecipe by it {
				override fun getType(): RecipeSerializer<*> {
					return EnergyContainerRecipe.Companion
				}
			})
		}, name)
	}

	fun row(): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(null, null, null)

		return this
	}

	fun row(a: Ingredient? = null, b: Ingredient? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: Ingredient? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: Ingredient? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: ItemLike? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::ItemRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: ItemLike? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::ItemRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: ItemLike? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::ItemRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: TagKey<Item>? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::TagRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: TagKey<Item>? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::TagRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: Ingredient? = null, b: TagKey<Item>? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::IngredientRecipeCell), b?.let(::TagRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: Ingredient? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: Ingredient? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: Ingredient? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: ItemLike? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::ItemRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: ItemLike? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::ItemRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: ItemLike? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::ItemRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: TagKey<Item>? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::TagRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: TagKey<Item>? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::TagRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: ItemLike? = null, b: TagKey<Item>? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::ItemRecipeCell), b?.let(::TagRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: Ingredient? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: Ingredient? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: Ingredient? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::IngredientRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: ItemLike? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::ItemRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: ItemLike? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::ItemRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: ItemLike? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::ItemRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: TagKey<Item>? = null, c: Ingredient? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::TagRecipeCell), c?.let(::IngredientRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: TagKey<Item>? = null, c: ItemLike? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::TagRecipeCell), c?.let(::ItemRecipeCell))

		return this
	}

	fun row(a: TagKey<Item>? = null, b: TagKey<Item>? = null, c: TagKey<Item>? = null): MatteryRecipe {
		if (index == 3) {
			throw IllegalStateException("Already have all rows defined")
		}

		rows[index++] = RecipeRow(a?.let(::TagRecipeCell), b?.let(::TagRecipeCell), c?.let(::TagRecipeCell))

		return this
	}

	fun rowA(a: Ingredient?): MatteryRecipe {
		return this.row(a, null as Ingredient?, null as Ingredient?)
	}

	fun rowA(a: ItemLike?): MatteryRecipe {
		return this.row(a, null as ItemLike?, null as ItemLike?)
	}

	fun rowA(a: TagKey<Item>?): MatteryRecipe {
		return this.row(a, null as TagKey<Item>?, null as TagKey<Item>?)
	}

	fun rowB(b: Ingredient?): MatteryRecipe {
		return this.row(null as Ingredient?, b, null as Ingredient?)
	}

	fun rowB(b: ItemLike?): MatteryRecipe {
		return this.row(null as ItemLike?, b, null as ItemLike?)
	}

	fun rowB(b: TagKey<Item>?): MatteryRecipe {
		return this.row(null as TagKey<Item>?, b, null as TagKey<Item>?)
	}

	fun rowC(c: Ingredient?): MatteryRecipe {
		return this.row(null as Ingredient?, null as Ingredient?, c)
	}

	fun rowC(c: ItemLike?): MatteryRecipe {
		return this.row(null as ItemLike?, null as ItemLike?, c)
	}

	fun rowC(c: TagKey<Item>?): MatteryRecipe {
		return this.row(null as TagKey<Item>?, null as TagKey<Item>?, c)
	}

	fun rowAB(a: Ingredient?, b: Ingredient?): MatteryRecipe {
		return this.row(a, b, null as Ingredient?)
	}

	fun rowAB(a: Ingredient?, b: ItemLike?): MatteryRecipe {
		return this.row(a, b, null as Ingredient?)
	}

	fun rowAB(a: Ingredient?, b: TagKey<Item>?): MatteryRecipe {
		return this.row(a, b, null as Ingredient?)
	}

	fun rowAB(a: ItemLike?, b: Ingredient?): MatteryRecipe {
		return this.row(a, b, null as ItemLike?)
	}

	fun rowAB(a: ItemLike?, b: ItemLike?): MatteryRecipe {
		return this.row(a, b, null as ItemLike?)
	}

	fun rowAB(a: ItemLike?, b: TagKey<Item>?): MatteryRecipe {
		return this.row(a, b, null as ItemLike?)
	}

	fun rowAB(a: TagKey<Item>?, b: Ingredient?): MatteryRecipe {
		return this.row(a, b, null as TagKey<Item>?)
	}

	fun rowAB(a: TagKey<Item>?, b: ItemLike?): MatteryRecipe {
		return this.row(a, b, null as TagKey<Item>?)
	}

	fun rowAB(a: TagKey<Item>?, b: TagKey<Item>?): MatteryRecipe {
		return this.row(a, b, null as TagKey<Item>?)
	}

	fun rowAC(a: Ingredient?, c: Ingredient?): MatteryRecipe {
		return this.row(a, null as Ingredient?, c)
	}

	fun rowAC(a: Ingredient?, c: ItemLike?): MatteryRecipe {
		return this.row(a, null as Ingredient?, c)
	}

	fun rowAC(a: Ingredient?, c: TagKey<Item>?): MatteryRecipe {
		return this.row(a, null as Ingredient?, c)
	}

	fun rowAC(a: ItemLike?, c: Ingredient?): MatteryRecipe {
		return this.row(a, null as ItemLike?, c)
	}

	fun rowAC(a: ItemLike?, c: ItemLike?): MatteryRecipe {
		return this.row(a, null as ItemLike?, c)
	}

	fun rowAC(a: ItemLike?, c: TagKey<Item>?): MatteryRecipe {
		return this.row(a, null as ItemLike?, c)
	}

	fun rowAC(a: TagKey<Item>?, c: Ingredient?): MatteryRecipe {
		return this.row(a, null as TagKey<Item>?, c)
	}

	fun rowAC(a: TagKey<Item>?, c: ItemLike?): MatteryRecipe {
		return this.row(a, null as TagKey<Item>?, c)
	}

	fun rowAC(a: TagKey<Item>?, c: TagKey<Item>?): MatteryRecipe {
		return this.row(a, null as TagKey<Item>?, c)
	}

	fun rowBC(b: Ingredient?, c: Ingredient?): MatteryRecipe {
		return this.row(null as Ingredient?, b, c)
	}

	fun rowBC(b: Ingredient?, c: ItemLike?): MatteryRecipe {
		return this.row(null as Ingredient?, b, c)
	}

	fun rowBC(b: Ingredient?, c: TagKey<Item>?): MatteryRecipe {
		return this.row(null as Ingredient?, b, c)
	}

	fun rowBC(b: ItemLike?, c: Ingredient?): MatteryRecipe {
		return this.row(null as ItemLike?, b, c)
	}

	fun rowBC(b: ItemLike?, c: ItemLike?): MatteryRecipe {
		return this.row(null as ItemLike?, b, c)
	}

	fun rowBC(b: ItemLike?, c: TagKey<Item>?): MatteryRecipe {
		return this.row(null as ItemLike?, b, c)
	}

	fun rowBC(b: TagKey<Item>?, c: Ingredient?): MatteryRecipe {
		return this.row(null as TagKey<Item>?, b, c)
	}

	fun rowBC(b: TagKey<Item>?, c: ItemLike?): MatteryRecipe {
		return this.row(null as TagKey<Item>?, b, c)
	}

	fun rowBC(b: TagKey<Item>?, c: TagKey<Item>?): MatteryRecipe {
		return this.row(null as TagKey<Item>?, b, c)
	}
}
