package ru.dbotthepony.mc.otm.recipe

import com.mojang.serialization.Codec
import com.mojang.serialization.MapCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.HolderLookup
import net.minecraft.core.component.DataComponentType
import net.minecraft.core.component.DataComponents
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.ByteBufCodecs
import net.minecraft.network.codec.StreamCodec
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.StringRepresentable
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.component.CustomData
import net.minecraft.world.item.crafting.CraftingBookCategory
import net.minecraft.world.item.crafting.CraftingInput
import net.minecraft.world.item.crafting.RecipeSerializer
import net.minecraft.world.item.crafting.ShapedRecipe
import net.minecraft.world.item.crafting.ShapedRecipePattern
import net.minecraft.world.item.enchantment.ItemEnchantments
import net.minecraft.world.level.Level
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.matter.PatternState
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.registryName
import java.util.Optional
import java.util.UUID

class UpgradeRecipe(
	group: String,
	category: CraftingBookCategory,
	pattern: ShapedRecipePattern,
	result: ItemStack,
	val operations: List<Pair<ResourceLocation, Op>>,
	showNotification: Boolean = true,
) : ShapedRecipe(group, category, pattern, result, showNotification) {
	constructor(parent: ShapedRecipe, operations: List<Pair<ResourceLocation, Op>>) : this(parent.group, parent.category(), parent.pattern, parent.result, operations, parent.showNotification())

	enum class OpType : StringRepresentable {
		COPY_COMPONENT {
			override val codec: MapCodec<CopyComponent<*>> = RecordCodecBuilder.mapCodec {
				it.group(
					DataComponentType.CODEC.fieldOf("component").forGetter(CopyComponent<*>::component)
				).apply(it) { CopyComponent(it) }
			}
		},
		COPY_TILE_COMPONENT {
			override val codec: MapCodec<CopyTileComponent> = RecordCodecBuilder.mapCodec {
				it.group(
					Codec.STRING.fieldOf("source").forGetter(CopyTileComponent::source),
					Codec.STRING.optionalFieldOf("destination").forGetter { if (it.source == it.destination) Optional.empty() else Optional.of(it.destination) },
				).apply(it) { a, b -> CopyTileComponent(a, b.orElse(a)) }
			}
		},
		COPY_ALL_COMPONENTS {
			override val codec: MapCodec<CopyAllComponents> = MapCodec.unit(CopyAllComponents)
		},
		COPY_ENCHANTMENTS {
			override val codec: MapCodec<CopyEnchantments> = MapCodec.unit(CopyEnchantments)
		},
		COPY_ENERGY_CHARGE {
			override val codec: MapCodec<CopyEnergyCharge> = MapCodec.unit(CopyEnergyCharge)
		},
		MERGE_PATTERNS {
			override val codec: MapCodec<MergePatterns> = MapCodec.unit(MergePatterns)
		}
		;

		private val serName = name.lowercase()

		override fun getSerializedName(): String {
			return serName
		}

		abstract val codec: MapCodec<out Op>
	}

	sealed class Op {
		abstract val type: OpType
		abstract fun apply(source: ItemStack, container: CraftingInput, registry: HolderLookup.Provider, destination: ItemStack)

		open fun matches(container: CraftingInput, level: Level): Boolean {
			return true
		}
	}

	data class CopyComponent<T>(val component: DataComponentType<T>) : Op() {
		override val type: OpType
			get() = OpType.COPY_COMPONENT

		override fun apply(source: ItemStack, container: CraftingInput, registry: HolderLookup.Provider, destination: ItemStack) {
			val existing = source[component]

			if (existing != null) {
				destination[component] = existing
			}
		}
	}

	data class CopyTileComponent(val source: String, val destination: String = source) : Op() {
		override val type: OpType
			get() = OpType.COPY_TILE_COMPONENT

		override fun apply(
			source: ItemStack,
			container: CraftingInput,
			registry: HolderLookup.Provider,
			destination: ItemStack
		) {
			if (!source.has(DataComponents.BLOCK_ENTITY_DATA))
				return

			val sourceTag = source[DataComponents.BLOCK_ENTITY_DATA]!!.unsafe[this.source]?.copy() ?: return

			destination[DataComponents.BLOCK_ENTITY_DATA] = destination.getOrDefault(DataComponents.BLOCK_ENTITY_DATA, CustomData.EMPTY).let {
				val copy = it.copyTag()
				copy[this.destination] = sourceTag
				CustomData.of(copy)
			}
		}
	}

	object CopyAllComponents : Op() {
		override val type: OpType
			get() = OpType.COPY_ALL_COMPONENTS

		override fun apply(source: ItemStack, container: CraftingInput, registry: HolderLookup.Provider, destination: ItemStack) {
			destination.applyComponentsAndValidate(source.componentsPatch)
		}
	}

	object CopyEnchantments : Op() {
		override val type: OpType
			get() = OpType.COPY_ENCHANTMENTS

		override fun apply(source: ItemStack, container: CraftingInput, registry: HolderLookup.Provider, destination: ItemStack) {
			val enchantments = source[DataComponents.ENCHANTMENTS]

			if (enchantments != null) {
				destination[DataComponents.ENCHANTMENTS] = ItemEnchantments.Mutable(destination.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY)).let {
					for ((key, level) in enchantments.entrySet()) {
						it.upgrade(key, level)
					}

					it.toImmutable()
				}
			}
		}
	}

	object CopyEnergyCharge : Op() {
		override val type: OpType
			get() = OpType.COPY_ENERGY_CHARGE

		override fun apply(source: ItemStack, container: CraftingInput, registry: HolderLookup.Provider, destination: ItemStack) {
			val batterySource = source.getCapability(MatteryCapability.ITEM_ENERGY) ?: return
			val batteryDestination = destination.getCapability(MatteryCapability.ITEM_ENERGY) ?: return
			batteryDestination.batteryLevel += batterySource.batteryLevel
		}
	}

	object MergePatterns : Op() {
		override val type: OpType
			get() = OpType.MERGE_PATTERNS

		override fun apply(
			source: ItemStack,
			container: CraftingInput,
			registry: HolderLookup.Provider,
			destination: ItemStack
		) {
			val srcPatterns = source.getCapability(MatteryCapability.PATTERN_ITEM) ?: return
			val dstPatterns = destination.getCapability(MatteryCapability.PATTERN_ITEM) ?: return

			for (pattern in srcPatterns.patterns) {
				val findState = dstPatterns.findPattern(pattern.item)

				val new: PatternState =
					if (findState != null) {
						PatternState(findState.id, pattern.item, (findState.researchPercent + pattern.researchPercent).coerceAtMost(1.0))
					} else {
						PatternState(UUID.randomUUID(), pattern.item, pattern.researchPercent)
					}

				dstPatterns.insertPattern(new, onlyUpdate = false, simulate = false)
			}
		}
	}

	override fun assemble(input: CraftingInput, registry: HolderLookup.Provider): ItemStack {
		val result = super.assemble(input, registry)

		if (result.isEmpty) {
			return result
		}

		for ((item, op) in operations) {
			input.items()
				.filter { it.item.registryName == item }
				.forEach { op.apply(it, input, registry, result) }
		}

		return result
	}

	override fun matches(p_345040_: CraftingInput, p_44167_: Level): Boolean {
		return super.matches(p_345040_, p_44167_) && operations.all { it.second.matches(p_345040_, p_44167_) }
	}

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

	companion object : RecipeSerializer<UpgradeRecipe> {
		private val operationCodec: MapCodec<Op> = StringRepresentable
			.fromEnum(OpType::values)
			.dispatchMap({ it.type }, { it.codec })

		private val operationEntryCodec = RecordCodecBuilder.create<Pair<ResourceLocation, Op>> {
			it.group(
				ResourceLocation.CODEC.fieldOf("item").forGetter(Pair<ResourceLocation, Op>::first),
				operationCodec.forGetter(Pair<ResourceLocation, Op>::second),
			).apply(it) { a, b -> Pair(a, b) }
		}.listOf()

		private val codec = RecordCodecBuilder.mapCodec {
			it.group(
				Serializer.CODEC.forGetter { it },
				operationEntryCodec.fieldOf("operations").forGetter(UpgradeRecipe::operations)
			).apply(it, ::UpgradeRecipe)
		}

		private val streamCodec = StreamCodec.composite(
			Serializer.STREAM_CODEC, { it },
			ByteBufCodecs.fromCodecWithRegistries(operationEntryCodec), { it.operations }, // quite lazy solution
			::UpgradeRecipe
		)

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

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