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.NonNullList
import net.minecraft.core.UUIDUtil
import net.minecraft.core.component.DataComponentType
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.CraftingInput
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.ShapedRecipePattern
import net.minecraft.world.level.Level
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.matteryEnergy
import ru.dbotthepony.mc.otm.core.collect.filterNotNull
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.data.codec.DecimalCodec
import ru.dbotthepony.mc.otm.data.codec.minRange
import ru.dbotthepony.mc.otm.network.StreamCodecs
import ru.dbotthepony.mc.otm.network.optional
import ru.dbotthepony.mc.otm.network.wrap
import ru.dbotthepony.mc.otm.registry.game.MItems
import ru.dbotthepony.mc.otm.registry.game.MRecipes
import java.util.Optional
import java.util.UUID
import kotlin.jvm.optionals.getOrElse

interface IMatterEntanglerRecipe : IMatteryRecipe<CraftingInput> {
	val matter: Decimal
	val ticks: Double
	val ingredients: ShapedRecipePattern
	val result: ItemStack
	val experience: Float

	fun preemptivelyMatches(container: CraftingInput.Positioned, level: Level, craftingWidth: Int, craftingHeight: Int): Boolean
}

open class MatterEntanglerRecipe(
	override val ingredients: ShapedRecipePattern,
	override val matter: Decimal,
	override val ticks: Double,
	override val result: ItemStack,
	override val experience: Float = 0f,
	val uuidKey: Optional<DataComponentType<UUID>> = Optional.empty(),
	val fixedUuid: Optional<UUID> = Optional.empty()
) : IMatterEntanglerRecipe {
	override fun matches(container: CraftingInput, level: Level): Boolean {
		if (isIncomplete) return false
		return ingredients.test(container)
	}

	override fun preemptivelyMatches(container: CraftingInput.Positioned, level: Level, craftingWidth: Int, craftingHeight: Int): Boolean {
		if (isIncomplete) return false
		return ingredients.preemptiveTest(container, craftingWidth, craftingHeight)
	}

	override fun assemble(container: CraftingInput, registry: HolderLookup.Provider): ItemStack {
		return result.copy().also {
			uuidKey.ifPresent { id ->
				it[id] = fixedUuid.getOrElse { UUID.randomUUID() }
			}
		}
	}

	override fun canCraftInDimensions(width: Int, height: Int): Boolean {
		return width >= ingredients.width && height >= ingredients.height
	}

	override fun getResultItem(registry: HolderLookup.Provider): ItemStack {
		return result
	}

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

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

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

	override fun isIncomplete(): Boolean {
		return result.isEmpty || ingredients.isIncomplete
	}

	override fun isSpecial(): Boolean {
		return true
	}

	override fun getToastSymbol(): ItemStack {
		return ItemStack(MItems.MATTER_ENTANGLER)
	}

	fun energetic() = Energy(this)
	fun matter() = Matter(this)

	open class Energy(val parent: MatterEntanglerRecipe) : IMatterEntanglerRecipe by parent {
		override fun assemble(container: CraftingInput, registry: HolderLookup.Provider): ItemStack {
			return parent.assemble(container, registry).also { result ->
				container.items().iterator().map { it.matteryEnergy }.filterNotNull().forEach {
					result.matteryEnergy!!.batteryLevel += it.batteryLevel
				}
			}
		}

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

	open class Matter(val parent: MatterEntanglerRecipe) : IMatterEntanglerRecipe by parent {
		override fun assemble(container: CraftingInput, registry: HolderLookup.Provider): ItemStack {
			return parent.assemble(container, registry).also { result ->
				container.items().iterator().map { it.getCapability(MatteryCapability.MATTER_ITEM) }.filterNotNull().forEach {
					result.getCapability(MatteryCapability.MATTER_ITEM)!!.storedMatter += it.storedMatter
				}
			}
		}

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

	object EnergySerializer : RecipeSerializer<Energy> {
		private val energyCodec: MapCodec<Energy> = codec.xmap(::Energy, Energy::parent)
		private val energyStreamCodec: StreamCodec<RegistryFriendlyByteBuf, Energy> = streamCodec.map(::Energy, Energy::parent)

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

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

	object MatterSerializer : RecipeSerializer<Matter> {
		private val matterCodec: MapCodec<Matter> = codec.xmap(::Matter, Matter::parent)
		private val matterStreamCodec: StreamCodec<RegistryFriendlyByteBuf, Matter> = streamCodec.map(::Matter, Matter::parent)

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

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

	companion object : RecipeSerializer<MatterEntanglerRecipe> {
		val codec: MapCodec<MatterEntanglerRecipe> = RecordCodecBuilder.mapCodec {
			it.group(
				ShapedRecipePattern.MAP_CODEC.fieldOf("ingredients").forGetter(MatterEntanglerRecipe::ingredients),
				DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(MatterEntanglerRecipe::matter),
				Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(MatterEntanglerRecipe::ticks),
				ItemStack.CODEC.fieldOf("result").forGetter(MatterEntanglerRecipe::result),
				Codec.FLOAT.minRange(0f).optionalFieldOf("experience", 0f).forGetter(MatterEntanglerRecipe::experience),
				(DataComponentType.CODEC as Codec<DataComponentType<UUID>>).optionalFieldOf("uuidKey").forGetter(MatterEntanglerRecipe::uuidKey),
				UUIDUtil.STRING_CODEC.optionalFieldOf("fixedUuid").forGetter(MatterEntanglerRecipe::fixedUuid)
			).apply(it, ::MatterEntanglerRecipe)
		}

		val streamCodec: StreamCodec<RegistryFriendlyByteBuf, MatterEntanglerRecipe> = StreamCodecs.composite(
			ShapedRecipePattern.STREAM_CODEC, MatterEntanglerRecipe::ingredients,
			StreamCodecs.DECIMAL, { it.matter },
			StreamCodecs.DOUBLE, MatterEntanglerRecipe::ticks,
			ItemStack.STREAM_CODEC, MatterEntanglerRecipe::result,
			StreamCodecs.FLOAT, MatterEntanglerRecipe::experience,
			DataComponentType.STREAM_CODEC.wrap().optional() as StreamCodec<RegistryFriendlyByteBuf, Optional<DataComponentType<UUID>>>, MatterEntanglerRecipe::uuidKey,
			UUIDUtil.STREAM_CODEC.wrap().optional(), MatterEntanglerRecipe::fixedUuid,
			::MatterEntanglerRecipe
		)

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

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