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

import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.core.BlockPos
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.CraftingInput
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.state.BlockState
import net.neoforged.neoforge.capabilities.Capabilities
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.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
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.capability.matter.MatterStorageImpl
import ru.dbotthepony.mc.otm.capability.matter.ProfiledMatterStorage
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.MatteryCraftingContainer
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
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.graph.matter.MatterNode
import ru.dbotthepony.mc.otm.menu.matter.MatterEntanglerMenu
import ru.dbotthepony.mc.otm.registry.game.MBlockEntities
import ru.dbotthepony.mc.otm.registry.game.MRecipes

class MatterEntanglerBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryWorkerBlockEntity<MatterEntanglerBlockEntity.Job>(
	MBlockEntities.MATTER_ENTANGLER, blockPos, blockState, Job.CODEC) {
	class Job(itemStack: ItemStack, val matter: Decimal, ticks: Double, experience: Float) : ItemJob(itemStack, ticks, MachinesConfig.MATTER_ENTANGLER.energyConsumption, experience = experience) {
		val matterPerTick = matter / ticks

		companion object {
			val CODEC: Codec<Job> = RecordCodecBuilder.create {
				it.group(
					ItemStack.CODEC.fieldOf("itemStack").forGetter(ItemJob::itemStack),
					DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(Job::matter),
					Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(ItemJob::ticks),
					Codec.FLOAT.minRange(0f).optionalFieldOf("experience", 0f).forGetter(Job::experience)
				).apply(it, ::Job)
			}
		}
	}

	override val upgrades = makeUpgrades(3, UpgradeType.BASIC_MATTER)
	override val energy = ProfiledEnergyStorage(WorkerEnergyStorage(::energyLevelUpdated, upgrades.transform(MachinesConfig.MATTER_ENTANGLER)))
	val matter = ProfiledMatterStorage(MatterStorageImpl(::markDirtyFast, FlowDirection.INPUT, upgrades.matterCapacity(MachinesConfig.MATTER_ENTANGLER::matterCapacity)))
	val node = MatterNode()

	val experience = ExperienceStorage(MachinesConfig.MATTER_ENTANGLER::maxExperienceStored).also(::addNeighbourListener)
	val energyConfig = ConfigurableEnergy(energy)

	val inputs = object : MatteryCraftingContainer(::itemContainerUpdated, 3, 3) {
		override fun getMaxStackSize(): Int {
			return 1
		}
	}

	val output = object : MatteryContainer(::itemContainerUpdated, 1) {
		override fun getMaxStackSize(slot: Int, itemStack: ItemStack): Int {
			return Int.MAX_VALUE
		}
	}

	val itemConfig = ConfigurableItemHandler(
		input = inputs.handler(object : HandlerFilter {
			override fun canInsert(slot: Int, stack: ItemStack): Boolean {
				val list = inputs.toList()
				list[slot] = stack
				val shadow = CraftingInput.ofPositioned(3, 3, list)

				return (level ?: return false)
					.recipeManager
					.byType(MRecipes.MATTER_ENTANGLER)
					.any { it.value.preemptivelyMatches(shadow, level!!, 3, 3) }
			}

			override fun canExtract(slot: Int, amount: Int, stack: ItemStack): Boolean {
				return false
			}
		}),
		output = output.handler(HandlerFilter.OnlyOut)
	)

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

		savetables.stateful(::energy, ENERGY_KEY)
		savetables.stateful(::matter, MATTER_STORAGE_KEY)
		savetables.stateful(::upgrades)
		savetables.stateful(::inputs)
		savetables.stateful(::output)
		savetables.stateful(::experience)

		exposeGlobally(MatteryCapability.MATTER_NODE, node)
		exposeGlobally(MatteryCapability.MATTER_BLOCK, matter)
	}

	override fun setLevel(level: Level) {
		super.setLevel(level)
		node.discover(this)
	}

	override fun setRemoved() {
		super.setRemoved()
		node.isValid = false
	}

	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu? {
		return MatterEntanglerMenu(containerID, inventory, this)
	}

	override fun onJobTick(status: JobStatus<Job>, id: Int) {
		val required = status.job.matterPerTick * status.ticksAdvanced

		if (matter.storedMatter < required) {
			matter.receiveMatter(node.graph.extractMatter(status.job.matterPerTick.coerceAtLeast(Decimal.TEN).coerceAtMost(matter.missingMatter), false), false)
		}

		status.scale(matter.extractMatter(required, false) / required)
	}

	override fun tick() {
		super.tick()

		if (jobEventLoops[0].currentJob == null) {
			matter.extractMatter(node.graph.receiveMatter(matter.storedMatter, false), false)
		}
	}

	override fun onJobFinish(status: JobStatus<Job>, id: Int) {
		if (!output.fullyAddItem(status.job.itemStack)) {
			status.noItem()
		} else {
			experience.storeExperience(status.job.experience, this)
		}
	}

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

		val inputs = CraftingInput.of(3, 3, inputs.toList())

		val recipe = (level ?: return JobContainer.failure())
			.recipeManager
			.byType(MRecipes.MATTER_ENTANGLER)
			.firstOrNull { it.value.matches(inputs, level!!) } ?: return JobContainer.noItem()

		val result = recipe.value.assemble(inputs, level!!.registryAccess())

		this.inputs.forEach { it.shrink(1) }
		this.inputs.setChanged()

		return JobContainer.success(
			Job(
				result,
				recipe.value.matter,
				recipe.value.ticks * MachinesConfig.MATTER_ENTANGLER.workTimeMultiplier,
				recipe.value.experience
			)
		)
	}
}
