package ru.dbotthepony.mc.otm.recipe

import com.google.common.collect.ImmutableList
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.NonNullList
import net.minecraft.core.RegistryAccess
import net.minecraft.nbt.CompoundTag
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.StringRepresentable
import net.minecraft.world.inventory.CraftingContainer
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.CraftingBookCategory
import net.minecraft.world.item.crafting.CraftingRecipe
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.item.crafting.RecipeSerializer
import net.minecraft.world.item.crafting.RecipeType
import net.minecraft.world.item.crafting.ShapedRecipe
import net.minecraft.world.level.Level
import net.minecraftforge.common.crafting.IShapedRecipe
import ru.dbotthepony.mc.otm.container.util.stream
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.tagNotNull
import ru.dbotthepony.mc.otm.data.Codec2RecipeSerializer
import ru.dbotthepony.mc.otm.data.RecipePair
import ru.dbotthepony.mc.otm.data.codec
import java.util.stream.Stream

class UpgradeRecipe(
	val parent: RecipePair<ShapedRecipe>,
	copyPaths: Stream<Op>,
	val source: ResourceLocation,
) : CraftingRecipe, IShapedRecipe<CraftingContainer> by parent.recipe {
	constructor(parent: RecipePair<ShapedRecipe>, copyPaths: Collection<Op>, source: ResourceLocation) : this(parent, copyPaths.stream(), source)

	override fun matches(p_44002_: CraftingContainer, p_44003_: Level): Boolean {
		return parent.recipe.matches(p_44002_, p_44003_)
	}

	override fun canCraftInDimensions(p_43999_: Int, p_44000_: Int): Boolean {
		return parent.recipe.canCraftInDimensions(p_43999_, p_44000_)
	}

	override fun getResultItem(): ItemStack {
		return parent.recipe.resultItem
	}

	override fun getRemainingItems(p_44004_: CraftingContainer): NonNullList<ItemStack> {
		return parent.recipe.getRemainingItems(p_44004_)
	}

	override fun getIngredients(): NonNullList<Ingredient> {
		return parent.recipe.ingredients
	}

	override fun isSpecial(): Boolean {
		return parent.recipe.isSpecial
	}

	override fun getGroup(): String {
		return parent.recipe.group
	}

	override fun getToastSymbol(): ItemStack {
		return parent.recipe.toastSymbol
	}

	override fun isIncomplete(): Boolean {
		return parent.recipe.isIncomplete
	}

	override fun getType(): RecipeType<*> {
		return parent.recipe.type
	}

	override fun category(): CraftingBookCategory {
		return parent.recipe.category()
	}

	enum class OpType : StringRepresentable {
		DIRECT {
			override val codec: Codec<Direct> = RecordCodecBuilder.create {
				it.group(
					Codec.STRING.fieldOf("path").forGetter(Direct::path)
				).apply(it, ::Direct)
			}
		}, INDIRECT {
			override val codec: Codec<Indirect> = RecordCodecBuilder.create {
				it.group(
					Codec.STRING.fieldOf("source").forGetter(Indirect::pathSource),
					Codec.STRING.fieldOf("destination").forGetter(Indirect::pathDestination),
				).apply(it, ::Indirect)
			}
		};

		override fun getSerializedName(): String {
			return name.lowercase()
		}

		abstract val codec: Codec<out Op>
	}

	sealed class Op {
		abstract val type: OpType
		abstract fun apply(source: CompoundTag, destination: CompoundTag)
	}

	data class Direct(val path: String) : Op() {
		private val split = path.split('.')
		override val type: OpType
			get() = OpType.DIRECT

		override fun apply(source: CompoundTag, destination: CompoundTag) {
			var a = source
			var b = destination

			for (i in 0 until split.size - 1) {
				val value = split[i]

				if (value !in a) {
					a[value] = CompoundTag()
				}

				if (value !in b) {
					b[value] = CompoundTag()
				}

				a = a[value] as CompoundTag
				b = b[value] as CompoundTag
			}

			val value = split.last()

			if (value in a) {
				b[value] = a[value]!!.copy()
			}
		}
	}

	data class Indirect(val pathSource: String, val pathDestination: String) : Op() {
		private val splitSource = pathSource.split('.')
		private val splitDestination = pathDestination.split('.')

		override val type: OpType
			get() = OpType.INDIRECT

		override fun apply(source: CompoundTag, destination: CompoundTag) {
			var a = source
			var b = destination

			for (i in 0 until splitSource.size - 1) {
				val value = splitSource[i]

				if (value !in a) {
					a[value] = CompoundTag()
				}

				a = a[value] as CompoundTag
			}

			for (i in 0 until splitDestination.size - 1) {
				val value = splitDestination[i]

				if (value !in b) {
					b[value] = CompoundTag()
				}

				b = b[value] as CompoundTag
			}

			val valueA = splitSource.last()
			val valueB = splitDestination.last()

			if (valueA in a) {
				b[valueB] = a[valueA]!!.copy()
			}
		}
	}

	val copyPaths: ImmutableList<Op> = copyPaths.collect(ImmutableList.toImmutableList())

	override fun assemble(pInv: CraftingContainer): ItemStack {
		val result = parent.recipe.assemble(pInv)

		if (result.isEmpty) {
			return result
		}

		val sources = pInv.stream().filter { !it.isEmpty && it.item.registryName == source }.toList()

		if (sources.size != 1) {
			return ItemStack.EMPTY
		}

		val source = sources.first()

		for (op in copyPaths) {
			op.apply(source.tagNotNull, result.tagNotNull)
		}

		return result
	}

	override fun getSerializer(): RecipeSerializer<UpgradeRecipe> {
		return CODEC
	}

	companion object {
		val COPY_PATHS_CODEC: Codec<List<Op>> = StringRepresentable
			.fromEnum(OpType::values)
			.dispatch({ it.type }, { it.codec })
			.listOf()

		val CODEC = Codec2RecipeSerializer<UpgradeRecipe> { p ->
			RecordCodecBuilder.create {
				it.group(
					ShapedRecipe.Serializer.SHAPED_RECIPE.codec(p).fieldOf("parent").forGetter(UpgradeRecipe::parent),
					COPY_PATHS_CODEC.fieldOf("copyPaths").forGetter(UpgradeRecipe::copyPaths),
					ResourceLocation.CODEC.fieldOf("source").forGetter(UpgradeRecipe::source)
				).apply(it, ::UpgradeRecipe)
			}
		}
	}
}
