package ru.dbotthepony.mc.otm.recipe

import com.mojang.serialization.Codec
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2IntMaps
import net.minecraft.core.NonNullList
import net.minecraft.core.RegistryAccess
import net.minecraft.data.recipes.FinishedRecipe
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.StringRepresentable
import net.minecraft.world.Container
import net.minecraft.world.item.*
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.item.crafting.Recipe
import net.minecraft.world.item.crafting.RecipeSerializer
import net.minecraft.world.item.crafting.RecipeType
import net.minecraft.world.level.Level
import ru.dbotthepony.mc.otm.block.entity.decorative.PainterBlockEntity
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.core.isActuallyEmpty
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.tagNotNull
import ru.dbotthepony.mc.otm.data.Codec2RecipeSerializer
import ru.dbotthepony.mc.otm.data.PredicatedCodecList
import ru.dbotthepony.mc.otm.data.minRange
import ru.dbotthepony.mc.otm.registry.MItems
import ru.dbotthepony.mc.otm.registry.MRecipes
import java.util.function.Predicate

abstract class AbstractPainterRecipe(
	private val id: ResourceLocation,
	dyes: Map<out DyeColor?, Int>
) : IMatteryRecipe<Container> {
	override fun getId(): ResourceLocation {
		return id
	}

	val dyes: Object2IntMap<DyeColor?> = Object2IntMaps.unmodifiable(Object2IntArrayMap(dyes))

	abstract fun matches(value: ItemStack): Boolean

	override fun matches(contaier: Container, level: Level): Boolean {
		return !isIncomplete && matches(contaier[0])
	}

	override fun getToastSymbol(): ItemStack = ItemStack(MItems.PAINTER)

	fun canCraft(storedDyes: Map<out DyeColor?, Int>): Boolean {
		if (isIncomplete) return false
		if (dyes.isEmpty() || dyes.values.none { it > 0 }) return true
		val copy = Object2IntArrayMap(storedDyes)

		for ((dye, amount) in dyes) {
			for (i in 0 until amount) {
				if (dye != null) PainterBlockEntity.mixer(dye)?.mix(copy)
				if (copy.getInt(dye) <= 0) return false
				copy[dye] = copy.getInt(dye) - 1
			}
		}

		return true
	}

	override fun canCraftInDimensions(p_43999_: Int, p_44000_: Int): Boolean = true

	override fun isSpecial(): Boolean = true

	open fun getOutput(container: Container): ItemStack {
		if (container.isEmpty) return ItemStack.EMPTY

		return container[0].copy()
	}

	enum class DyeColorWrapper(private val refName: String, val key: DyeColor?) : StringRepresentable {
		WHITE("white", DyeColor.WHITE),
		ORANGE("orange", DyeColor.ORANGE),
		MAGENTA("magenta", DyeColor.MAGENTA),
		LIGHT_BLUE("light_blue", DyeColor.LIGHT_BLUE),
		YELLOW("yellow", DyeColor.YELLOW),
		LIME("lime", DyeColor.LIME),
		PINK("pink", DyeColor.PINK),
		GRAY("gray", DyeColor.GRAY),
		LIGHT_GRAY("light_gray", DyeColor.LIGHT_GRAY),
		CYAN("cyan", DyeColor.CYAN),
		PURPLE("purple", DyeColor.PURPLE),
		BLUE("blue", DyeColor.BLUE),
		BROWN("brown", DyeColor.BROWN),
		GREEN("green", DyeColor.GREEN),
		RED("red", DyeColor.RED),
		BLACK("black", DyeColor.BLACK),
		WATER("water", null);

		override fun getSerializedName(): String {
			return refName
		}
	}

	companion object {
		private val wrapperCodec: Codec<DyeColorWrapper> = StringRepresentable.fromEnum(DyeColorWrapper::values)

		@JvmStatic
		protected val dyesFieldCodec: MapCodec<Map<DyeColor?, Int>> = PredicatedCodecList<Map<DyeColorWrapper, Int>>(
			wrapperCodec.xmap({ mapOf(it to 1) }, { it.keys.first() }) to Predicate { it.keys.size == 1 && it.values.first() == 1 },
			Codec.list(wrapperCodec).xmap({ it.associateWith { 1 } }, { ArrayList(it.keys) }) to Predicate { it.values.all { it == 1 } },
			Codec.unboundedMap(wrapperCodec, Codec.INT.minRange(1)) to Predicate { true }
		).fieldOf("dyes").xmap({ it.mapKeys { it.key.key } }, { it.mapKeys { k -> DyeColorWrapper.entries.first { k.key == it.key } } })
	}
}

class PainterRecipe(
	id: ResourceLocation,
	val input: Ingredient,
	val output: ItemStack,
	dyes: Map<out DyeColor?, Int>
) : AbstractPainterRecipe(id, dyes) {
	constructor(
		id: ResourceLocation,
		input: Ingredient,
		output: ItemStack,
		dyes: Set<DyeColor?>
	) : this(id, input, output, Object2IntArrayMap<DyeColor?>().also { map -> dyes.forEach { map[it] = 1 } })

	override fun matches(value: ItemStack): Boolean {
		return !isIncomplete && input.test(value)
	}

	override fun isIncomplete(): Boolean {
		return input.isActuallyEmpty || output.isEmpty
	}

	override fun getIngredients(): NonNullList<Ingredient> {
		return NonNullList.of(Ingredient.EMPTY, input)
	}

	override fun assemble(p_44001_: Container): ItemStack {
		return output.copy().also { o ->
			p_44001_[0].tag?.let {
				if (o.tag == null) {
					o.tag = it.copy()
				} else {
					for (k in it.allKeys)
						if (k !in o.tagNotNull)
							o.tagNotNull[k] = it[k]!!.copy()
				}
			}
		}
	}

	override fun getResultItem(): ItemStack {
		return output
	}

	override fun getSerializer(): RecipeSerializer<*> {
		return SERIALIZER
	}

	override fun getType(): RecipeType<*> {
		return MRecipes.PAINTER
	}

	fun toFinished(): FinishedRecipe {
		return SERIALIZER.toFinished(this)
	}

	override fun getOutput(container: Container): ItemStack {
		return output.copy()
	}

	companion object {
		val SERIALIZER = Codec2RecipeSerializer<PainterRecipe> { context ->
			RecordCodecBuilder.create {
				it.group(
					context.ingredients.fieldOf("input").forGetter(PainterRecipe::input),
					ItemStack.CODEC.fieldOf("output").forGetter(PainterRecipe::output),
					dyesFieldCodec.forGetter(AbstractPainterRecipe::dyes),
				).apply(it) { a, b, c -> PainterRecipe(context.id, a, b, c) }
			}
		}
	}
}
class PainterArmorDyeRecipe(
	id: ResourceLocation,
	dyes: Map<out DyeColor?, Int>
) : AbstractPainterRecipe(id, dyes) {
	constructor(
		id: ResourceLocation,
		dyes: Set<DyeColor?>
	) : this(id, Object2IntArrayMap<DyeColor?>().also { map -> dyes.forEach { map[it] = 1 } })

	override fun matches(value: ItemStack): Boolean {
		return !isIncomplete && value.item is DyeableArmorItem
	}

	override fun assemble(container: Container): ItemStack {
		var output = container[0].copy()

		dyes.forEach { entry ->
			if (entry.key == null) {
				(output.item as DyeableLeatherItem).clearColor(output)
			} else {
				output = DyeableLeatherItem.dyeArmor(output.copy(), listOf(DyeItem.byColor(entry.key!!)))
			}
		}

		return output
	}

	override fun getResultItem(): ItemStack = ItemStack.EMPTY

	override fun getSerializer(): RecipeSerializer<*> = SERIALIZER

	override fun getType(): RecipeType<*> = MRecipes.PAINTER

	override fun isIncomplete(): Boolean = dyes.isEmpty()

	fun toFinished(): FinishedRecipe {
		return SERIALIZER.toFinished(this)
	}

	override fun getOutput(container: Container): ItemStack {
		if (container.isEmpty) return ItemStack.EMPTY

		var output = container[0].copy()

		dyes.forEach { entry ->
			if (entry.key == null) {
				(output.item as DyeableLeatherItem).clearColor(output)
			} else {
				output = DyeableLeatherItem.dyeArmor(output, listOf(DyeItem.byColor(entry.key!!)))
			}
		}

		return output
	}

	companion object {
		val SERIALIZER = Codec2RecipeSerializer<PainterArmorDyeRecipe> { context ->
			RecordCodecBuilder.create {
				it.group(
					dyesFieldCodec.forGetter(AbstractPainterRecipe::dyes),
				).apply(it) { a -> PainterArmorDyeRecipe(context.id, a) }
			}
		}
	}
}
