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

import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import net.minecraft.core.BlockPos
import net.minecraft.network.chat.Component
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.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.items.IItemHandler
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.block.CableBlock
import ru.dbotthepony.mc.otm.block.entity.MatteryPoweredBlockEntity
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.config.EnergyBalanceValues
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.ItemFilter
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.core.orNull
import ru.dbotthepony.mc.otm.graph.storage.StorageNode
import ru.dbotthepony.mc.otm.menu.storage.StorageImporterExporterMenu
import ru.dbotthepony.mc.otm.once
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.registry.MNames
import ru.dbotthepony.mc.otm.storage.IStorageEventConsumer
import ru.dbotthepony.mc.otm.storage.IStorageProvider
import ru.dbotthepony.mc.otm.storage.ItemStorageStack
import ru.dbotthepony.mc.otm.storage.StorageStack
import java.math.BigInteger
import java.util.*
import kotlin.collections.ArrayList

abstract class AbstractStorageImportExport(
	blockType: BlockEntityType<*>,
	blockPos: BlockPos,
	blockState: BlockState,
	energyValues: EnergyBalanceValues = MachinesConfig.STORAGE_INTERFACES
) : MatteryPoweredBlockEntity(blockType, blockPos, blockState) {
	final override val energy = ProfiledEnergyStorage(WorkerEnergyStorage(::markDirtyFast, energyValues))
	val energyConfig = ConfigurableEnergy(energy, modesFront = FlowDirection.NONE)

	val cell: StorageNode = object : StorageNode(energy) {
		override fun onNeighbour(link: Link) {
			if (link is DirectionLink) {
				level?.once {
					if (!isRemoved) {
						val newState = this@AbstractStorageImportExport.blockState.setValue(CableBlock.MAPPING_CONNECTION_PROP[link.direction]!!, true)

						if (newState !== this@AbstractStorageImportExport.blockState && SERVER_IS_LIVE)
							level?.setBlock(blockPos, newState, Block.UPDATE_CLIENTS)
					}
				}
			}
		}

		override fun onUnNeighbour(link: Link) {
			if (link is DirectionLink) {
				level?.once {
					if (!isRemoved) {
						val newState = this@AbstractStorageImportExport.blockState.setValue(CableBlock.MAPPING_CONNECTION_PROP[link.direction]!!, false)

						if (newState !== this@AbstractStorageImportExport.blockState && SERVER_IS_LIVE)
							level?.setBlock(blockPos, newState, Block.UPDATE_CLIENTS)
					}
				}
			}
		}
	}

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

	init {
		savetables.stateful(::energy, ENERGY_KEY)
		exposeGlobally(MatteryCapability.STORAGE_NODE, cell) { it != RelativeSide.FRONT }
	}

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

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

	protected val target = front.track(ForgeCapabilities.ITEM_HANDLER)

	abstract val filter: ItemFilter

	init {
		savetables.stateful(::filter, FILTER_KEY)
	}

	companion object {
		const val FILTER_KEY = "filter"
		const val MAX_FILTERS = 6 * 3
	}
}

class StorageImporterBlockEntity(
	blockPos: BlockPos, blockState: BlockState
) : AbstractStorageImportExport(MBlockEntities.STORAGE_IMPORTER, blockPos, blockState), IItemHandler {
	override val filter = ItemFilter(MAX_FILTERS) {
		markDirtyFast()
	}

	private var lastSlot = 0
	private var nextTick = INTERVAL

	init {
		front.Cap(ForgeCapabilities.ITEM_HANDLER, this)
	}

	override fun getSlots(): Int {
		return 1
	}

	override fun getStackInSlot(slot: Int): ItemStack {
		return ItemStack.EMPTY
	}

	private fun acceptItem(stack: ItemStack, simulate: Boolean): ItemStack {
		val wrapped = ItemStorageStack(stack)

		val view = cell.graph.getVirtualComponent(StorageStack.ITEMS)
		val leftover = view.insertStack(wrapped, true)
		if (leftover == wrapped) return stack

		val required = StorageStack.ITEMS.energyPerInsert(wrapped)

		if (energy.extractEnergy(required, true) == required) {
			if (!simulate) {
				val leftover2 = view.insertStack(wrapped, false)

				if (leftover == leftover2) {
					energy.extractEnergy(required, false)
					return leftover.toItemStack()
				} else {
					energy.extractEnergy(StorageStack.ITEMS.energyPerInsert(leftover2), false)
					return leftover2.toItemStack()
				}
			}

			return leftover.toItemStack()
		}

		return stack
	}

	override fun insertItem(slot: Int, stack: ItemStack, simulate: Boolean): ItemStack {
		if (redstoneControl.isBlockedByRedstone || !filter.match(stack))
			return stack

		return acceptItem(stack, simulate)
	}

	override fun extractItem(slot: Int, amount: Int, simulate: Boolean): ItemStack {
		return ItemStack.EMPTY
	}

	override fun getSlotLimit(slot: Int): Int {
		return Int.MAX_VALUE
	}

	override fun isItemValid(slot: Int, stack: ItemStack): Boolean {
		return filter.match(stack)
	}

	override fun tick() {
		super.tick()

		if (redstoneControl.isBlockedByRedstone)
			return

		cell.tickEnergyDemanding()

		nextTick--

		val target = target.get().orNull()

		if (nextTick <= 0 && target != null) {
			if (lastSlot >= target.slots) {
				lastSlot = 0
			}

			val extracted = target.extractItem(lastSlot, MAX_MOVE_PER_OPERATION, true)

			if (extracted.isEmpty || !filter.match(extracted)) {
				lastSlot++
			} else {
				val leftover = acceptItem(extracted, true)

				if (leftover.count != extracted.count) {
					val extracted2 = target.extractItem(lastSlot, extracted.count - leftover.count, false)
					acceptItem(extracted2, false)
				}
			}
		}

		if (nextTick <= 0) {
			nextTick = INTERVAL
		}
	}

	companion object {
		const val MAX_MOVE_PER_OPERATION = 4
		private const val INTERVAL = 5
	}
}

class StorageExporterBlockEntity(blockPos: BlockPos, blockState: BlockState) :
	AbstractStorageImportExport(MBlockEntities.STORAGE_EXPORTER, blockPos, blockState),
	IStorageEventConsumer<ItemStorageStack> {
	override val defaultDisplayName: Component
		get() = MACHINE_NAME

	private val relevantTuples = ObjectOpenHashSet<UUID>()

	override val storageType: StorageStack.Type<ItemStorageStack>
		get() = StorageStack.ITEMS

	init {
		cell.addStorageComponent(this)
	}

	override fun onStackAdded(stack: ItemStorageStack, id: UUID, provider: IStorageProvider<ItemStorageStack>) {
		if (!filter.match(stack.toItemStack())) {
			return
		}

		relevantTuples.add(id)
	}

	override fun onStackChanged(stack: ItemStorageStack, id: UUID) {
		// no-op
	}

	override fun onStackRemoved(id: UUID) {
		relevantTuples.remove(id)
	}

	override val filter = ItemFilter(MAX_FILTERS) {
		relevantTuples.clear()

		val component = cell.graph.getVirtualComponent(StorageStack.ITEMS)

		for (tuple in component.stacks) {
			onStackAdded(tuple, component)
		}

		lastSlot = 0

		markDirtyFast()
	}.also { it.isWhitelist = true }

	private var lastSlot = 0
	private var nextTick = INTERVAL

	override fun tick() {
		super.tick()

		if (redstoneControl.isBlockedByRedstone)
			return

		cell.tickEnergyDemanding()

		nextTick--

		val target = target.get().orNull()

		if (nextTick <= 0 && target != null) {
			val items = cell.graph.getVirtualComponent(StorageStack.ITEMS)

			if (lastSlot >= target.slots) {
				lastSlot = 0
			}

			val any = ArrayList(relevantTuples).any { id ->
				val stack = items[id]

				if (stack.isEmpty || !target.isItemValid(lastSlot, stack.toItemStack())) return@any false

				val extracted = items.extractStack(id, stack.count.coerceAtMost(MAX_MOVE_PER_OPERATION), true)
				if (extracted.isEmpty) return@any false

				val required = StorageStack.ITEMS.energyPerOperation(extracted)
				if (energy.extractEnergy(required, true) != required) return@any false

				val toInsert = extracted.toItemStack()
				val leftover = target.insertItem(lastSlot, toInsert, true)

				if (leftover.count != toInsert.count) {
					val extracted2 = items.extractStack(id, (toInsert.count - leftover.count).toBigInteger(), false)
					energy.extractEnergy(StorageStack.ITEMS.energyPerOperation(extracted2), false)
					val leftover2 = target.insertItem(lastSlot, extracted2.toItemStack(), false)

					if (leftover2.isNotEmpty) {
						items.insertStack(ItemStorageStack.unsafe(leftover2), false)
					}

					true
				} else {
					false
				}
			}

			if (!any) {
				lastSlot++
			}
		}

		if (nextTick <= 0) {
			nextTick = INTERVAL
		}
	}

	companion object {
		val MAX_MOVE_PER_OPERATION: BigInteger = BigInteger.valueOf(4L)
		private val MACHINE_NAME = TranslatableComponent("block.${OverdriveThatMatters.MOD_ID}.${MNames.STORAGE_EXPORTER}")
		private const val INTERVAL = 5
	}
}
