package ru.dbotthepony.mc.otm.compat.mekanism

import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import mekanism.common.content.qio.QIOFrequency
import mekanism.common.content.qio.QIOFrequency.QIOItemTypeData
import mekanism.common.lib.frequency.Frequency
import mekanism.common.lib.inventory.HashedItem
import mekanism.common.tile.qio.TileEntityQIODriveArray
import net.minecraft.core.Direction
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.common.capabilities.ICapabilityProvider
import net.minecraftforge.common.util.LazyOptional
import net.minecraftforge.event.AttachCapabilitiesEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import ru.dbotthepony.mc.otm.core.util.ITickable
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.onceServer
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.isMekanismLoaded
import ru.dbotthepony.mc.otm.core.math.isPositive
import ru.dbotthepony.mc.otm.core.util.LOHolder
import ru.dbotthepony.mc.otm.graph.storage.StorageNode
import ru.dbotthepony.mc.otm.graph.storage.StorageGraph
import ru.dbotthepony.mc.otm.storage.*
import java.math.BigInteger
import java.util.UUID
import java.util.stream.Stream

private val QIO_LOCATION = ResourceLocation(OverdriveThatMatters.MOD_ID, "item_storage")

private class QIOTuple(
	val mekanismItem: HashedItem,
	override val stack: ItemStackWrapper,
	override val id: UUID,
	var mark: Long
) : IStorageTuple<ItemStackWrapper>

private class QIOFrequencyAccess(val parent: QIOFrequency) : IStorageComponent<ItemStackWrapper> {
	private var mark = 0L

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

	private val index = HashMap<UUID, QIOTuple>()
	private val tracked = HashMap<HashedItem, QIOTuple>()
	private val listeners = ArrayList<IStorageEventConsumer<ItemStackWrapper>>()

	override fun get(id: UUID): ItemStackWrapper {
		return index[id]?.stack ?: ItemStackWrapper.EMPTY
	}

	override val stacks: Stream<IStorageTuple<ItemStackWrapper>> get() {
		return ArrayList<IStorageTuple<ItemStackWrapper>>(index.size).also { it.addAll(index.values) }.stream()
	}

	override fun insertStack(stack: ItemStackWrapper, simulate: Boolean): ItemStackWrapper {
		// Because there is no simulate method on QIO array, we have to simulate it by ourselves.
		val hash = HashedItem.create(stack.item)

		if (!simulate) {
			//val used = TransporterManager.getToUse(stack.stack, parent.addItem(stack.stack))
			val used = parent.addItem(stack.stack)

			if (used.count == stack.count.toInt()) {
				return stack
			}

			scan(hash)
			return ItemStackWrapper(used)
		}

		if (parent.totalItemCount >= parent.totalItemCountCapacity) {
			return stack
		}

		if (!parent.itemDataMap.containsKey(hash) && parent.totalItemTypeCapacity <= parent.itemDataMap.size) {
			return stack
		}

		val inserted = stack.copy()
		inserted.count = (parent.totalItemCountCapacity - parent.totalItemCount).toBigInteger().coerceAtMost(stack.count)
		return inserted
	}

	override fun extractStack(id: UUID, amount: BigInteger, simulate: Boolean): ItemStackWrapper {
		// Because there is no simulate method on QIO array, we have to simulate it by ourselves.
		// ASSUMPTION: We can ALWAYS remove items from QIO grid.

		if (!amount.isPositive)
			return ItemStackWrapper.EMPTY

		var local = index[id] ?: return ItemStackWrapper.EMPTY
		scan(local.mekanismItem)
		local = index[id] ?: return ItemStackWrapper.EMPTY // unexpected...

		if (simulate) {
			return local.stack.copy().also { it.count = it.count.coerceAtMost(amount) }
		}

		val removed = parent.removeByType(local.mekanismItem, amount.toInt())

		if (removed.isEmpty) {
			return ItemStackWrapper.EMPTY
		}

		val copy = ItemStackWrapper(removed)

		if (local.stack.count > copy.count) {
			// expecting stack to be still present in QIO storage grid
			scan(local.mekanismItem)
		} else {
			// expecting stack to be removed from QIO storage grid
			check(parent.itemDataMap[local.mekanismItem] == null) {
				"${local.mekanismItem} is expected to be empty in $parent, got ${parent.itemDataMap[local.mekanismItem]}"
			}

			for (listener in listeners) {
				listener.removeStack(local)
			}

			index.remove(local.id)
			tracked.remove(local.mekanismItem)
		}

		return copy
	}

	override fun addListener(listener: IStorageEventConsumer<ItemStackWrapper>): Boolean {
		if (!listeners.contains(listener)) {
			listeners.add(listener)
			return true
		}

		return false
	}

	override fun removeListener(listener: IStorageEventConsumer<ItemStackWrapper>): Boolean {
		return listeners.remove(listener)
	}

	private fun scan(at: HashedItem, value: QIOItemTypeData = parent.itemDataMap[at] ?: throw IllegalArgumentException("$parent does not have item $at (${at.stack})")) {
		val local = tracked[at]

		if (local != null) {
			local.mark = mark

			if (local.stack.count.toLong() != value.count) {
				val oldCount = local.stack.count
				local.stack.count = value.count.toBigInteger()

				for (listener in listeners) {
					listener.changeStack(local, oldCount)
				}
			}
		} else {
			val tuple = QIOTuple(at, ItemStackWrapper(at.stack).also { it.count = value.count.toBigInteger() }, UUID.randomUUID(), mark)
			index[tuple.id] = tuple

			for (listener in listeners) {
				listener.addStack(tuple, this)
			}

			tracked[at] = tuple
		}
	}

	fun scan() {
		mark++

		for ((key, value) in parent.itemDataMap) {
			scan(key, value)
		}

		var changed: ArrayList<HashedItem>? = null

		for ((key, value) in tracked) {
			if (value.mark != mark) {
				if (changed == null) {
					changed = ArrayList()
				}

				changed.add(key)
			}
		}

		// sweep
		if (changed != null) {
			for (key in changed) {
				val tuple = tracked.remove(key)!!

				for (listener in listeners) {
					listener.removeStack(tuple)
				}

				index.remove(tuple.id)
				tracked.remove(tuple.mekanismItem)
			}
		}
	}
}

private class QIOStorage(private val tile: TileEntityQIODriveArray) : ICapabilityProvider {
	private var lastFrequency: QIOFrequency? = null
	private var frequencyAccess: QIOFrequencyAccess? = null
	private var wasAttached = false

	val cell: StorageNode = object : StorageNode(), ITickable {
		init {
			manualAttaching = true
		}

		override fun tick() {
			if (tile.isRemoved) {
				isValid = false
				return
			}

			val frequency = tile.qioFrequency

			if (frequency != lastFrequency) {
				if (wasAttached) {
					wasAttached = false

					val lastFrequency = lastFrequency ?: throw IllegalStateException("lastFrequency is null")

					graph.userData.remove(key(lastFrequency))
				}

				frequencyAccess?.let(this::removeStorageComponent)
				lastFrequency = frequency

				if (frequency != null) {
					frequencyAccess = QIOFrequencyAccess(frequency).also(this::addStorageComponent)
				}
			}

			val frequencyAccess = frequencyAccess ?: return
			frequencyAccess.scan()
			val key = key(frequencyAccess.parent)

			if (!graph.userData.containsKey(key)) {
				graph.userData[key] = true
				wasAttached = true
				graph.add(frequencyAccess)
			}
		}

		override fun removeComponents(from: StorageGraph) {
			super.removeComponents(from)

			if (wasAttached) {
				val lastFrequency = lastFrequency ?: throw IllegalStateException("lastFrequency is null")
				from.userData.remove(key(lastFrequency))
				wasAttached = false
			}
		}

		override fun invalidate() {
			super.invalidate()
			holder.invalidate()
		}

		override fun revive() {
			super.revive()
			holder.revive()
		}
	}

	private val holder = LOHolder(cell)

	override fun <T : Any> getCapability(cap: Capability<T>, side: Direction?): LazyOptional<T> {
		if (cap === MatteryCapability.STORAGE_NODE)
			return holder.get()

		return LazyOptional.empty()
	}

	companion object {
		private fun key(frequency: Frequency) = keyMap.computeIfAbsent(frequency.name to frequency.owner, Object2ObjectFunction { UUID.randomUUID() })
		private val keyMap = Object2ObjectArrayMap<Pair<String, UUID?>, UUID>()
	}
}

@SubscribeEvent
@Suppress("unused")
fun attachCapabilities(event: AttachCapabilitiesEvent<BlockEntity>) {
	if (!isMekanismLoaded) {
		throw IllegalStateException("This event should never be called if Mekanism is not loaded.")
	}

	if (event.`object` is TileEntityQIODriveArray) {
		val capability = QIOStorage(event.`object` as TileEntityQIODriveArray)
		event.addCapability(QIO_LOCATION, capability)

		onceServer {
			if (!event.`object`.isRemoved) {
				capability.cell.discover(event.`object`, MatteryCapability.STORAGE_NODE)
			}
		}
	}
}
