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

import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
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.Capability
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.MatteryCapability
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.extractEnergyExact
import ru.dbotthepony.mc.otm.config.ConciseBalanceValues
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.math.RelativeSide
import ru.dbotthepony.mc.otm.core.math.toIntSafe
import ru.dbotthepony.mc.otm.core.orNull
import ru.dbotthepony.mc.otm.graph.storage.StorageNode
import ru.dbotthepony.mc.otm.menu.storage.StorageExporterMenu
import ru.dbotthepony.mc.otm.menu.storage.StorageImporterMenu
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.ITEM_STORAGE
import ru.dbotthepony.mc.otm.storage.ItemStackWrapper
import ru.dbotthepony.mc.otm.storage.StorageStackType
import ru.dbotthepony.mc.otm.storage.addStack
import java.math.BigInteger
import java.util.*
import java.util.stream.Stream

abstract class AbstractStorageImportExport<T>(
	blockType: BlockEntityType<*>,
	blockPos: BlockPos,
	blockState: BlockState,
	energyValues: ConciseBalanceValues = MachinesConfig.STORAGE_INTERFACES
) : MatteryPoweredBlockEntity(blockType, blockPos, blockState) {
	val energy = WorkerEnergyStorage(::setChangedLight, energyValues)

	init {
		exposeEnergyGlobally(energy)
		savetable(::energy, ENERGY_KEY)
	}

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

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

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

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

	init {
		exposeSideless(MatteryCapability.STORAGE_NODE, cell)
		exposeAllSidesExcept(RelativeSide.FRONT, MatteryCapability.STORAGE_NODE, cell)
	}

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

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

	protected abstract val targetCapability: Capability<T>

	protected val target by lazy {
		front.track(targetCapability)
	}

	abstract val filter: ItemFilter

	init {
		savetable(::filter, FILTER_KEY)
	}

	companion object {
		const val FILTER_KEY = "filter"
	}
}

class StorageImporterBlockEntity(blockPos: BlockPos, blockState: BlockState) : AbstractStorageImportExport<IItemHandler>(MBlockEntities.STORAGE_IMPORTER, blockPos, blockState), IItemHandler {
	override val filter = ItemFilter(MAX_FILTERS) { _, _, _ ->
		setChangedLight()
	}

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

	private var lastSlot = 0
	private var nextTick = INTERVAL

	private val enoughEnergy get() = energy.batteryLevel >= ITEM_STORAGE.energyPerOperation

	override val targetCapability: Capability<IItemHandler>
		get() = ForgeCapabilities.ITEM_HANDLER

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

	override fun getSlots(): Int {
		return 1
	}

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

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

		val view = cell.graph.getVirtualComponent(ITEM_STORAGE)
		val maxMove = energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, stack.count.toBigInteger(), true)

		if (maxMove == BigInteger.ZERO)
			return stack

		val leftover = view.insertStack(ItemStackWrapper(stack).also { it.count = maxMove }, simulate)

		if (simulate)
			return leftover.stack

		energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, maxMove - leftover.count, false)
		return leftover.stack
	}

	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 && enoughEnergy) {
			val items = cell.graph.getVirtualComponent(ITEM_STORAGE)

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

			val maxMove = energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, MAX_MOVE_PER_OPERATION, true)
			var extracted = target.extractItem(lastSlot, maxMove, true)

			if (extracted.isEmpty || !filter.match(extracted)) {
				lastSlot++
			} else {
				val leftOver = items.insertStack(ItemStackWrapper(extracted), true)

				if (leftOver.count.toInt() != extracted.count) {
					extracted = target.extractItem(lastSlot, extracted.count - leftOver.count.toInt(), false)
					energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, extracted.count, false)
					items.insertStack(ItemStackWrapper(extracted), false)
				} else {
					nextTick += INTERVAL * 4
				}
			}
		}

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

	companion object {
		const val MAX_MOVE_PER_OPERATION = 4
		private const val INTERVAL = 5
		const val MAX_FILTERS = 6 * 3
	}
}

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

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

	private val relevantTuples = HashSet<UUID>()

	override val storageType: StorageStackType<ItemStackWrapper>
		get() = ITEM_STORAGE

	init {
		cell.addStorageComponent(this)
	}

	override fun addStack(stack: ItemStackWrapper, id: UUID, provider: IStorageProvider<ItemStackWrapper>) {
		if (!filter.match(stack.item)) {
			return
		}

		relevantTuples.add(id)
	}

	override fun changeStack(stack: ItemStackWrapper, id: UUID, oldCount: BigInteger) {
		// no-op
	}

	override fun removeStack(stack: ItemStackWrapper, id: UUID) {
		relevantTuples.remove(id)
	}

	override val filter = ItemFilter(MAX_FILTERS) { _, _, _ ->
		relevantTuples.clear()

		val component = cell.graph.getVirtualComponent(ITEM_STORAGE)

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

		lastSlot = 0

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

	private var lastSlot = 0
	private var nextTick = INTERVAL
	private val enoughEnergy get() = energy.batteryLevel >= ITEM_STORAGE.energyPerOperation

	override val targetCapability: Capability<IItemHandler>
		get() = ForgeCapabilities.ITEM_HANDLER

	private val exportStacks: Stream<Pair<UUID, ItemStackWrapper>>
		get() {
			val view = cell.graph.getVirtualComponent(ITEM_STORAGE)
			return relevantTuples.stream().map { it to view[it] }
		}

	override fun tick() {
		super.tick()

		if (redstoneControl.isBlockedByRedstone)
			return

		cell.tickEnergyDemanding()

		nextTick--

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

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

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

			var hit = false

			for (stack in exportStacks) {
				if (!target.isItemValid(lastSlot, stack.second.item)) {
					continue
				}

				val exportAmountA = items.extractStack(
					stack.first, stack.second.count.coerceAtMost(
						MAX_MOVE_PER_OPERATION
					), true
				).count

				if (exportAmountA == BigInteger.ZERO) {
					continue
				}

				var exportAmount =
					energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, exportAmountA, true).toIntSafe()

				if (exportAmount == 0) {
					break
				}

				val leftover = target.insertItem(lastSlot, stack.second.stack.also { it.count = exportAmount }, true)

				if (leftover.count != exportAmount) {
					hit = true
					exportAmount = items.extractStack(
						stack.first,
						(exportAmount - leftover.count).toBigInteger(),
						false
					).count.toInt()
					target.insertItem(lastSlot, stack.second.stack.also { it.count = exportAmount }, false)
					energy.extractEnergyExact(ITEM_STORAGE.energyPerOperation, exportAmount, false)
					break
				}
			}

			if (!hit) {
				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
		const val MAX_FILTERS = 6 * 3
	}
}
