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.HolderLookup
import net.minecraft.core.NonNullList
import net.minecraft.core.component.DataComponents
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.ByteBufCodecs
import net.minecraft.network.codec.StreamCodec
import net.minecraft.tags.ItemTags
import net.minecraft.util.StringRepresentable
import net.minecraft.world.item.DyeColor
import net.minecraft.world.item.DyeItem
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.component.DyedItemColor
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.item.crafting.RecipeInput
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.core.get
import ru.dbotthepony.mc.otm.data.codec.PredicatedCodecList
import ru.dbotthepony.mc.otm.data.codec.minRange
import ru.dbotthepony.mc.otm.network.MatteryStreamCodec
import ru.dbotthepony.mc.otm.network.nullable
import ru.dbotthepony.mc.otm.registry.game.MItems
import ru.dbotthepony.mc.otm.registry.game.MRecipes
import java.util.function.Predicate
import kotlin.collections.ArrayList
import kotlin.collections.Map
import kotlin.collections.Set
import kotlin.collections.all
import kotlin.collections.associateWith
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.first
import kotlin.collections.forEach
import kotlin.collections.iterator
import kotlin.collections.map
import kotlin.collections.mapKeys
import kotlin.collections.mapOf
import kotlin.collections.none
import kotlin.collections.set

abstract class AbstractPainterRecipe(
	dyes: Map<out DyeColor?, Int>
) : IMatteryRecipe<RecipeInput> {
	val dyes: Object2IntMap<DyeColor?> = Object2IntMaps.unmodifiable(Object2IntArrayMap(dyes))

	abstract fun matches(value: ItemStack): Boolean

	override fun matches(contaier: RecipeInput, 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: RecipeInput): 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 dyesFieldStreamCodec: StreamCodec<FriendlyByteBuf, Map<DyeColor?, Int>> = ByteBufCodecs.map(
			{ Object2IntArrayMap(it) },
			MatteryStreamCodec.Enum<FriendlyByteBuf, DyeColor>(DyeColor::class.java).nullable(),
			ByteBufCodecs.INT)

		@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(
	val input: Ingredient,
	val output: ItemStack,
	dyes: Map<out DyeColor?, Int>
) : AbstractPainterRecipe(dyes) {
	constructor(
		input: Ingredient,
		output: ItemStack,
		dyes: Set<DyeColor?>
	) : this(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.hasNoItems() || output.isEmpty
	}

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

	override fun assemble(input: RecipeInput, registries: HolderLookup.Provider): ItemStack {
		return output.copy().also { o ->
			o.applyComponentsAndValidate(input[0].componentsPatch)
		}
	}

	override fun getResultItem(registries: HolderLookup.Provider): ItemStack {
		return output
	}

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

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

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

	companion object : RecipeSerializer<PainterRecipe> {
		private val codec = RecordCodecBuilder.mapCodec {
			it.group(
				Ingredient.CODEC_NONEMPTY.fieldOf("input").forGetter(PainterRecipe::input),
				ItemStack.CODEC.fieldOf("output").forGetter(PainterRecipe::output),
				dyesFieldCodec.forGetter(AbstractPainterRecipe::dyes),
			).apply(it, ::PainterRecipe)
		}

		private val streamCodec = StreamCodec.composite(
			Ingredient.CONTENTS_STREAM_CODEC, PainterRecipe::input,
			ItemStack.STREAM_CODEC, PainterRecipe::output,
			dyesFieldStreamCodec, PainterRecipe::dyes,
			::PainterRecipe
		)

		override fun codec(): MapCodec<PainterRecipe> {
			return codec
		}

		override fun streamCodec(): StreamCodec<RegistryFriendlyByteBuf, PainterRecipe> {
			return streamCodec
		}
	}
}
class PainterArmorDyeRecipe(
	dyes: Map<out DyeColor?, Int>
) : AbstractPainterRecipe(dyes) {
	constructor(
		dyes: Set<DyeColor?>
	) : this(Object2IntArrayMap<DyeColor?>().also { map -> dyes.forEach { map[it] = 1 } })

	override fun matches(value: ItemStack): Boolean {
		return !isIncomplete && value.`is`(ItemTags.DYEABLE)
	}

	private fun assemble(value: ItemStack): ItemStack {
		var output = value.copy()

		if (dyes.size == 1 && dyes.keys.first() == null) {
			output[DataComponents.DYED_COLOR] = null
		} else {
			output = DyedItemColor.applyDyes(output, dyes.keys.map { DyeItem.byColor(it!!) })
		}

		return output
	}

	override fun assemble(container: RecipeInput, registry: HolderLookup.Provider): ItemStack {
		if (container.isEmpty) return ItemStack.EMPTY
		return assemble(container[0])
	}

	override fun getResultItem(registry: HolderLookup.Provider): ItemStack = ItemStack.EMPTY

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

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

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

	override fun getOutput(container: RecipeInput): ItemStack {
		if (container.isEmpty) return ItemStack.EMPTY
		return assemble(container[0])
	}

	companion object : RecipeSerializer<PainterArmorDyeRecipe> {
		private val codec by lazy {
			RecordCodecBuilder.mapCodec {
				it.group(
					dyesFieldCodec.forGetter(AbstractPainterRecipe::dyes),
				).apply(it, ::PainterArmorDyeRecipe)
			}
		}

		private val streamCodec = dyesFieldStreamCodec.map(::PainterArmorDyeRecipe, AbstractPainterRecipe::dyes)

		override fun codec(): MapCodec<PainterArmorDyeRecipe> {
			return codec
		}

		override fun streamCodec(): StreamCodec<RegistryFriendlyByteBuf, PainterArmorDyeRecipe> {
			return streamCodec as StreamCodec<RegistryFriendlyByteBuf, PainterArmorDyeRecipe>
		}
	}
}
