package ru.dbotthepony.mc.otm.block.entity.tech

import it.unimi.dsi.fastutil.ints.IntList
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.AbstractCookingRecipe
import net.minecraft.world.item.crafting.BlastingRecipe
import net.minecraft.world.item.crafting.RecipeType
import net.minecraft.world.item.crafting.SingleRecipeInput
import net.minecraft.world.item.crafting.SmeltingRecipe
import net.minecraft.world.item.crafting.SmokingRecipe
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.neoforged.neoforge.capabilities.Capabilities
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.mc.otm.block.entity.ExperienceStorage
import ru.dbotthepony.mc.otm.block.entity.ItemJob
import ru.dbotthepony.mc.otm.block.entity.JobContainer
import ru.dbotthepony.mc.otm.block.entity.JobStatus
import ru.dbotthepony.mc.otm.block.entity.MatteryWorkerBlockEntity
import ru.dbotthepony.mc.otm.capability.UpgradeType
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.config.WorkerBalanceValues
import ru.dbotthepony.mc.otm.container.CombinedContainer
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.balance
import ru.dbotthepony.mc.otm.core.collect.filter
import ru.dbotthepony.mc.otm.core.collect.maybe
import ru.dbotthepony.mc.otm.core.immutableList
import ru.dbotthepony.mc.otm.core.otmRandom
import ru.dbotthepony.mc.otm.menu.tech.PoweredFurnaceMenu
import ru.dbotthepony.mc.otm.recipe.MatteryCookingRecipe
import ru.dbotthepony.mc.otm.recipe.MicrowaveRecipe
import ru.dbotthepony.mc.otm.registry.game.MBlockEntities
import ru.dbotthepony.mc.otm.registry.game.MRecipes

sealed class AbstractPoweredFurnaceBlockEntity<P : AbstractCookingRecipe, S : MatteryCookingRecipe>(
	type: BlockEntityType<*>,
	blockPos: BlockPos,
	blockState: BlockState,
	val recipeType: RecipeType<P>,
	val secondaryRecipeType: RecipeType<S>?,
	val config: WorkerBalanceValues,
	maxJobs: Int = 2
) : MatteryWorkerBlockEntity<ItemJob>(type, blockPos, blockState, ItemJob.CODEC, maxJobs) {
	final override val upgrades = makeUpgrades(2, UpgradeType.BASIC_PROCESSING)
	final override val energy = ProfiledEnergyStorage(WorkerEnergyStorage(this::energyLevelUpdated, upgrades.transform(config)))

	val inputs = MatteryContainer(this::itemContainerUpdated, maxJobs)
	val outputs = MatteryContainer(this::itemContainerUpdated, maxJobs)

	init {
		addDroppableContainer(inputs)
		addDroppableContainer(outputs)
	}

	inner class SyncSlot {
		var inputItem by syncher.item()
		var outputItem by syncher.item()
		var progress by syncher.float()
		var visualRotation = 0f
	}

	var lastRender = 0L

	val syncSlots = immutableList(maxJobs) { SyncSlot() }

	val experience = ExperienceStorage(config::maxExperienceStored).also(::addNeighbourListener)
	val energyConfig = ConfigurableEnergy(energy)
	val itemConfig = ConfigurableItemHandler(
		input = inputs.handler(HandlerFilter.OnlyIn),
		output = outputs.handler(HandlerFilter.OnlyOut),
		battery = batteryItemHandler
	)

	init {
		exposeGlobally(Capabilities.FluidHandler.BLOCK, experience)

		savetables.stateful(::upgrades)
		savetables.stateful(::energy)

		savetables.stateful(::experience)

		savetables.stateful(inputs, "input")
		savetables.stateful(outputs, "output")
	}

	private val combined = CombinedContainer(inputs)

	override fun tick() {
		if (balanceInputs) {
			combined.balance()
		}

		super.tick()
	}

	override fun onJobFinish(status: JobStatus<ItemJob>, id: Int) {
		if (outputs.fullyAddItem(status.job.itemStack, slots = IntList.of(id))) {
			experience.storeExperience(status.experience, this)
			syncSlots[id].inputItem = ItemStack.EMPTY
			syncSlots[id].outputItem = ItemStack.EMPTY
			syncSlots[id].progress = 0f
		} else {
			status.noItem()
		}
	}

	override fun onJobTick(status: JobStatus<ItemJob>, id: Int) {
		super.onJobTick(status, id)
		syncSlots[id].progress = status.workProgress
	}

	override fun computeNextJob(id: Int): JobContainer<ItemJob> {
		if (!energy.batteryLevel.isPositive)
			return JobContainer.noEnergy()

		if (inputs[id].isEmpty)
			return JobContainer.noItem()

		val level = level as? ServerLevel ?: return JobContainer.failure()

		if (secondaryRecipeType != null) {
			val recipe = level.recipeManager
				.byType(secondaryRecipeType)
				.iterator()
				.filter { it.value.matches(SingleRecipeInput(inputs[id]), level) }
				.maybe()?.value

			if (recipe != null) {
				val toProcess = inputs[id].count.coerceAtMost(1 + upgrades.processingItems)

				val output = recipe.getResultItem(level.registryAccess()).copyWithCount(toProcess)
				val inputItem = inputs[id].copyWithCount(1)

				inputs[id].shrink(toProcess)
				inputs.setChanged(id)

				syncSlots[id].inputItem = inputItem
				syncSlots[id].outputItem = output.copy()
				syncSlots[id].progress = 0f

				return JobContainer.success(
					ItemJob(
						recipe.getResultItem(level.registryAccess()).copyWithCount(toProcess),
						recipe.workTime * config.workTimeMultiplier,
						config.energyConsumption * toProcess,
						experience = recipe.experience.sample(level.otmRandom) * toProcess))
			}
		}

		return level.recipeManager.getRecipeFor(recipeType, SingleRecipeInput(inputs[id]), level).map {
			val output = it.value.assemble(SingleRecipeInput(inputs[id]), level.registryAccess())
			val inputItem = inputs[id].copyWithCount(1)
			val toProcess = inputs[id].count.coerceAtMost(upgrades.processingItems + 1)
			inputs[id].shrink(toProcess)

			syncSlots[id].inputItem = inputItem
			syncSlots[id].outputItem = output.copy()
			syncSlots[id].progress = 0f

			JobContainer.success(ItemJob(
				output.copyWithCount(toProcess), it.value.cookingTime * config.workTimeMultiplier, config.energyConsumption * toProcess, it.value.experience * toProcess
			))
		}.orElse(JobContainer.noItem())
	}
}

class PoweredFurnaceBlockEntity(blockPos: BlockPos, blockState: BlockState) : AbstractPoweredFurnaceBlockEntity<SmeltingRecipe, MatteryCookingRecipe>(
	MBlockEntities.POWERED_FURNACE, blockPos, blockState, RecipeType.SMELTING, null, MachinesConfig.POWERED_FURNACE) {
	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu {
		return PoweredFurnaceMenu.furnace(containerID, inventory, this)
	}
}

class PoweredBlastFurnaceBlockEntity(blockPos: BlockPos, blockState: BlockState) : AbstractPoweredFurnaceBlockEntity<BlastingRecipe, MatteryCookingRecipe>(
	MBlockEntities.POWERED_BLAST_FURNACE, blockPos, blockState, RecipeType.BLASTING, null, MachinesConfig.POWERED_FURNACE) {
	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu {
		return PoweredFurnaceMenu.blasting(containerID, inventory, this)
	}
}

class PoweredSmokerBlockEntity(blockPos: BlockPos, blockState: BlockState) : AbstractPoweredFurnaceBlockEntity<SmokingRecipe, MicrowaveRecipe>(
	MBlockEntities.POWERED_SMOKER, blockPos, blockState, RecipeType.SMOKING, MRecipes.MICROWAVE, MachinesConfig.POWERED_FURNACE) {
	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu {
		return PoweredFurnaceMenu.smoking(containerID, inventory, this)
	}
}
