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

import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.item.ItemEntity
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.level.block.state.BlockState
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.fluids.capability.IFluidHandler
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.block.entity.MatteryDeviceBlockEntity
import ru.dbotthepony.mc.otm.capability.CombinedItemHandler
import ru.dbotthepony.mc.otm.capability.fluid.BlockMatteryFluidHandler
import ru.dbotthepony.mc.otm.capability.moveFluid
import ru.dbotthepony.mc.otm.config.ItemsConfig
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.core.ifPresentK
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.orNull
import ru.dbotthepony.mc.otm.core.util.FluidStackValueCodec
import ru.dbotthepony.mc.otm.menu.decorative.FluidTankMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities

class FluidTankBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryDeviceBlockEntity(MBlockEntities.FLUID_TANK, blockPos, blockState) {
	val fluid = BlockMatteryFluidHandler(::onChanged, ItemsConfig::FLUID_TANK_CAPACITY)
	var synchronizedFluid by synchronizer.Field(FluidStack.EMPTY, FluidStackValueCodec, setter = { value, access, remote ->
		access.write(value)

		level?.lightEngine?.checkBlock(blockPos)
	})

	val fillInput = MatteryContainer(::setChangedLight, 1).also(::addDroppableContainer)
	val drainInput = MatteryContainer(::setChangedLight, 1).also(::addDroppableContainer)
	val output = MatteryContainer(::setChangedLight, 1).also(::addDroppableContainer)

	val itemConfig = ConfigurableItemHandler(
		input = CombinedItemHandler(
			drainInput.handler(HandlerFilter.DrainableFluidContainers),
			fillInput.handler(object : HandlerFilter {
				override fun canInsert(slot: Int, stack: ItemStack): Boolean {
					if (fluid.isEmpty) {
						return stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).map { it.tanks > 0 }.orElse(false)
					}

					return stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).map { it.fill(fluid[0], IFluidHandler.FluidAction.SIMULATE) > 0 }.orElse(false)
				}

				override fun canExtract(slot: Int, amount: Int, stack: ItemStack): Boolean {
					return !canInsert(slot, stack)
				}
			})
		),
		output = output.handler(HandlerFilter.OnlyOut),
		frontDefault = ItemHandlerMode.INPUT_OUTPUT,
		backDefault = ItemHandlerMode.INPUT_OUTPUT,
		leftDefault = ItemHandlerMode.INPUT_OUTPUT,
		rightDefault = ItemHandlerMode.INPUT_OUTPUT,
		topDefault = ItemHandlerMode.INPUT_OUTPUT,
		bottomDefault = ItemHandlerMode.INPUT_OUTPUT,
	)

	val fluidConfig = ConfigurableFluidHandler(fluid)

	init {
		savetables.stateful(::fluid, FLUID_KEY)
		savetables.stateful(::fillInput)
		savetables.stateful(::drainInput)
		savetables.stateful(::output)
	}

	private fun onChanged(new: FluidStack, old: FluidStack) {
		synchronizedFluid = new.copy()
		setChangedLight()
	}

	private fun drainItem() {
		val item = drainInput[0]

		if (item.isNotEmpty) {
			val cap = (if (item.count == 1) item else item.copyWithCount(1)).getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orNull()

			if (cap == null) {
				if (output.consumeItem(item, simulate = false)) {
					drainInput.setChanged(0)
				}

				return
			}

			if (fluid.isNotFull) {
				if (item.count == 1) {
					val moved0 = moveFluid(source = cap, destination = fluid)

					if (moved0.isNotEmpty) {
						drainInput[0] = cap.container

						if (output.consumeItem(drainInput[0], simulate = false)) {
							drainInput.setChanged(0)
						}
					}
				} else {
					val moved0 = moveFluid(source = cap, destination = fluid, actuallyFill = false)

					if (moved0.isNotEmpty) {
						if (output.consumeItem(cap.container, simulate = true)) {
							val cap1 = item.copyWithCount(1).getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orNull() ?: throw ConcurrentModificationException()

							val moved1 = moveFluid(source = cap1, destination = fluid)

							if (moved1 != moved0 || moved1.amount != moved0.amount) {
								LOGGER.error("Error moving fluids in Fluid tank at $blockPos: moved $moved0 during simulation from ${cap.container}, moved $moved1 from ${cap1.container} during execution. This is likely a bug in OTM or other mod!")
							} else {
								item.count--
								drainInput.setChanged(0)

								if (!output.consumeItem(cap1.container, simulate = false)) {
									LOGGER.error("Unable to insert ${cap1.container} into output slot of Fluid tank at $blockPos, popping item in world instead to avoid item loss! This is likely a bug in OTM or other mod!")
									(level as? ServerLevel)?.addFreshEntity(ItemEntity(level!!, blockPos.x.toDouble(), blockPos.y.toDouble(), blockPos.z.toDouble(), cap1.container))
								}
							}
						}
					}
				}
			}
		}
	}

	private fun fillItem() {
		val item = fillInput[0]

		if (item.isNotEmpty) {
			val cap = (if (item.count == 1) item else item.copyWithCount(1)).getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orNull()

			if (cap == null) {
				if (output.consumeItem(item, simulate = false)) {
					fillInput.setChanged(0)
				}

				return
			}

			if (fluid.isNotEmpty) {
				if (item.count == 1) {
					val moved0 = moveFluid(source = fluid, destination = cap)

					if (moved0.isNotEmpty) {
						fillInput[0] = cap.container

						if (output.consumeItem(fillInput[0], simulate = false)) {
							fillInput.setChanged(0)
						}
					}
				} else {
					val moved0 = moveFluid(source = fluid, destination = cap, actuallyDrain = false)

					if (moved0.isNotEmpty) {
						if (output.consumeItem(cap.container, simulate = true)) {
							val cap1 = item.copyWithCount(1).getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orNull() ?: throw ConcurrentModificationException()

							val moved1 = moveFluid(source = fluid, destination = cap1)

							if (moved1 != moved0 || moved1.amount != moved0.amount) {
								LOGGER.error("Error moving fluids in Fluid tank at $blockPos: moved $moved0 during simulation from ${cap.container}, moved $moved1 from ${cap1.container} during execution. This is likely a bug in OTM or other mod!")
							} else {
								item.count--
								fillInput.setChanged(0)

								if (!output.consumeItem(cap1.container, simulate = false)) {
									LOGGER.error("Unable to insert ${cap1.container} into output slot of Fluid tank at $blockPos, popping item in world instead to avoid item loss! This is likely a bug in OTM or other mod!")
									(level as? ServerLevel)?.addFreshEntity(ItemEntity(level!!, blockPos.x.toDouble(), blockPos.y.toDouble(), blockPos.z.toDouble(), cap1.container))
								}
							}
						}
					}
				}
			}
		}
	}

	override fun tick() {
		super.tick()

		drainItem()
		fillItem()
	}

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

	companion object {
		const val FLUID_KEY = "fluid"
		private val LOGGER = LogManager.getLogger()
	}
}
