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

import com.mojang.serialization.Codec
import it.unimi.dsi.fastutil.ints.IntArrayList
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.Containers
import net.minecraft.world.MenuProvider
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.RecipeType
import net.minecraft.world.item.crafting.SingleRecipeInput
import net.minecraft.world.item.crafting.SmokingRecipe
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
import ru.dbotthepony.kommons.util.Delegate
import ru.dbotthepony.mc.otm.block.IBlockWithCustomName
import ru.dbotthepony.mc.otm.block.decorative.GrillBlock
import ru.dbotthepony.mc.otm.block.entity.ExperienceStorage
import ru.dbotthepony.mc.otm.block.entity.MatteryBlockEntity
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.core.TextComponent
import ru.dbotthepony.mc.otm.core.get
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.math.component1
import ru.dbotthepony.mc.otm.core.math.component2
import ru.dbotthepony.mc.otm.core.math.component3
import ru.dbotthepony.mc.otm.core.set
import ru.dbotthepony.mc.otm.core.util.countingLazy
import ru.dbotthepony.mc.otm.data.codec.minRange
import ru.dbotthepony.mc.otm.menu.decorative.GrillMenu
import ru.dbotthepony.mc.otm.registry.game.MBlockEntities

class GrillBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryBlockEntity(MBlockEntities.GRILL, blockPos, blockState), MenuProvider, IBlockWithCustomName {
	val fuelSlot = object : MatteryContainer(this@GrillBlockEntity::markDirtyFast, 1) {
		override fun getMaxStackSize(): Int {
			return 4
		}
	}

	val inputSlots = object : MatteryContainer(this@GrillBlockEntity::markDirtyFast, SLOTS) {
		override fun getMaxStackSize(): Int {
			return 1
		}

		private fun clearSlot(slot: Int) {
			inputProgress[slot] = 0
			outputs[slot] = null
			activeSlots.rem(slot)
		}

		override fun setChanged(slot: Int, new: ItemStack, old: ItemStack) {
			super.setChanged(slot, new, old)

			if (new.isEmpty || new.count > 1) {
				clearSlot(slot)
			} else {
				val level = level

				if (level == null) {
					clearSlot(slot)
					return
				}

				val input = SingleRecipeInput(new)
				val result = level.recipeManager
					.byType(RecipeType.SMOKING)
					.firstOrNull { it.value.matches(input, level) }

				if (result == null) {
					clearSlot(slot)
				} else {
					if (outputs[slot] != result.value)
						inputProgress[slot] = 0

					outputs[slot] = result.value
					activeSlots.add(slot)
				}
			}
		}
	}

	override var customDisplayName: Component? = null

	override fun createMenu(p_39954_: Int, p_39955_: Inventory, p_39956_: Player): AbstractContainerMenu {
		return GrillMenu(p_39954_, p_39955_, this)
	}

	override fun getDisplayName(): Component {
		return customDisplayName ?: level?.getBlockState(blockPos)?.block?.name ?: TextComponent("null at $blockPos")
	}

	private val outputs = arrayOfNulls<SmokingRecipe>(SLOTS)
	private val activeSlots = IntArrayList()
	private val inputProgress = IntArray(SLOTS)
	val experience = ExperienceStorage().also(::addNeighbourListener)

	var fuelTicks = 0
		private set
	var fuelTotalTicks = 0
		private set

	fun totalTicksRequired(slot: Int): Int {
		return outputs[slot]?.cookingTime?.times(3) ?: 0
	}

	fun ticksPassed(slot: Int): Int {
		return inputProgress[slot]
	}

	override fun saveShared(nbt: CompoundTag, registry: HolderLookup.Provider) {
		super.saveShared(nbt, registry)
		saveCustomDisplayName(nbt, registry)
	}

	override fun loadAdditional(nbt: CompoundTag, registry: HolderLookup.Provider) {
		super.loadAdditional(nbt, registry)
		loadCustomDisplayName(nbt, registry)
	}

	init {
		experience.addListener(::markDirtyFast)
		savetablesLevel.int(::fuelTicks)
		savetablesLevel.int(::fuelTotalTicks)
		savetables.stateful(::inputSlots)
		savetables.stateful(::fuelSlot)
		savetables.stateful(::experience)

		// TODO: could have been done as list (and get more space efficient storage), but we use an array, oh well
		for (i in inputProgress.indices) {
			savetables.codec(Delegate.Of({ inputProgress[i] }, { inputProgress[i] = it }), progressCodec, "inputProgress${i}")
		}

		// addDroppableContainer(inputSlots)
		// addDroppableContainer(fuelSlot)
	}

	private val state by countingLazy(blockStateChangesCounter) {
		this.blockState[GrillBlock.STATE_PROPERTY]
	}

	override fun tick() {
		super.tick()

		if (fuelTicks <= 0 && activeSlots.isNotEmpty()) {
			val fuel = fuelSlot[0].getBurnTime(null) * MachinesConfig.BriefcaseGrill.FUEL_EFFICIENCY

			if (fuel > 0) {
				fuelTicks = fuel
				fuelTotalTicks = fuel
				val split = fuelSlot[0].split(1)
				val remaining = split.craftingRemainingItem

				if (remaining.isEmpty) {
					fuelSlot.setChanged(0)
				} else if (fuelSlot[0].isNotEmpty) {
					val level = level as ServerLevel
					val (x, y, z) = blockPos.center
					Containers.dropItemStack(level, x, y, z, remaining)
				} else {
					fuelSlot[0] = remaining
				}
			}
		}

		if (fuelTicks > 0) {
			if (activeSlots.isNotEmpty()) {
				fuelTicks -= MachinesConfig.BriefcaseGrill.FUEL_CONSUMPTION

				for (slot in activeSlots.toIntArray()) {
					val recipe = outputs[slot]!!

					if (recipe.cookingTime * MachinesConfig.BriefcaseGrill.COOK_TIME <= ++inputProgress[slot]) {
						experience.storeExperience(recipe.experience)
						inputSlots[slot] = recipe.assemble(SingleRecipeInput(inputSlots[slot]), level!!.registryAccess())
					}
				}

				if (state !== GrillBlock.State.GRILLING) {
					level!!.setBlock(blockPos, blockState.set(GrillBlock.STATE_PROPERTY, GrillBlock.State.GRILLING), Block.UPDATE_ALL)
				}
			} else {
				fuelTicks--

				if (state !== GrillBlock.State.FUELED) {
					level!!.setBlock(blockPos, blockState.set(GrillBlock.STATE_PROPERTY, GrillBlock.State.FUELED), Block.UPDATE_ALL)
				}
			}

			markDirtyFast()
		} else {
			fuelTotalTicks = 0

			if (state !== GrillBlock.State.IDLE) {
				level!!.setBlock(blockPos, blockState.set(GrillBlock.STATE_PROPERTY, GrillBlock.State.IDLE), Block.UPDATE_ALL)
			}
		}
	}

	companion object {
		const val SLOTS = 6

		private val progressCodec = Codec.INT.minRange(0)
	}
}
