package ru.dbotthepony.mc.otm.container

import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntComparators
import it.unimi.dsi.fastutil.ints.IntSpliterator
import it.unimi.dsi.fastutil.objects.ObjectSpliterators
import net.minecraft.world.item.ItemStack
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.Tag
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Player
import net.minecraft.world.entity.player.StackedContents
import net.minecraft.world.inventory.StackedContentsCompatible
import net.minecraft.world.item.Item
import net.minecraft.world.item.Items
import net.minecraftforge.common.util.INBTSerializable
import net.minecraftforge.registries.ForgeRegistries
import ru.dbotthepony.mc.otm.container.util.IContainerSlot
import ru.dbotthepony.mc.otm.core.addSorted
import ru.dbotthepony.mc.otm.core.collect.any
import ru.dbotthepony.mc.otm.core.collect.count
import ru.dbotthepony.mc.otm.core.collect.emptyIterator
import ru.dbotthepony.mc.otm.core.collect.filter
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.util.ItemValueCodec
import ru.dbotthepony.mc.otm.core.util.VarIntValueCodec
import ru.dbotthepony.mc.otm.network.synchronizer.FieldSynchronizer
import ru.dbotthepony.mc.otm.network.synchronizer.IField
import java.lang.ref.WeakReference
import java.util.*
import java.util.function.Consumer
import java.util.function.Predicate
import java.util.function.Supplier
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.collections.ArrayList

@Suppress("UNUSED")
open class MatteryContainer(var listener: ContainerListener, private val size: Int) : IMatteryContainer, INBTSerializable<Tag?>, StackedContentsCompatible {
	constructor(watcher: () -> Unit, size: Int) : this({ _, _, _ -> watcher.invoke() }, size)
	constructor(size: Int) : this(EmptyListener, size)

	fun interface ContainerListener {
		fun setChanged(slot: Int, new: ItemStack, old: ItemStack)
	}

	object EmptyListener : ContainerListener {
		override fun setChanged(slot: Int, new: ItemStack, old: ItemStack) {}
	}

	init {
		require(size >= 0) { "Invalid container size $size" }
	}

	private val slots = Array(size) { ItemStack.EMPTY }
	private val nonEmptyFlags = BitSet()
	private var nonEmptyIndices = IntArrayList()
	private var indicesReferenced = false
	private data class Update(val slot: Int, val new: ItemStack, val old: ItemStack)
	private val queuedUpdates = ArrayList<Update>()
	private var queueUpdates = false

	private fun cowIndices() {
		if (indicesReferenced) {
			nonEmptyIndices = IntArrayList(nonEmptyIndices)
			indicesReferenced = false
		}
	}

	private val trackedSlots: Array<ItemStack> = Array(size) { ItemStack.EMPTY }
	private val filters: Array<Item?> = arrayOfNulls(size)
	private var filterSynchronizer: WeakReference<IField<MutableMap<Int, Item>>>? = null

	var changeset = 0
		private set

	inner class Cache<T>(private val supplier: (MatteryContainer) -> T) : Supplier<T> {
		private var thisChangeset = -1
		private var value: T? = null
		private var isDisabled = false

		fun disable() {
			isDisabled = true
		}

		fun enable() {
			isDisabled = false
		}

		override fun get(): T {
			if (thisChangeset == changeset && !isDisabled) {
				return value as T
			}

			value = supplier.invoke(this@MatteryContainer)
			thisChangeset = changeset
			return value as T
		}
	}

	fun clearSlotFilters() {
		Arrays.fill(filters, null)
		filterSynchronizer?.get()?.value?.clear()
	}

	fun removeFilterSynchronizer() {
		filterSynchronizer?.get()?.remove()
		filterSynchronizer = null
	}

	fun addFilterSynchronizer(synchronizer: FieldSynchronizer): IField<MutableMap<Int, Item>> {
		check(filterSynchronizer?.get() == null) { "Already has filter synchronizer" }

		val field = synchronizer.Map(
			keyCodec = VarIntValueCodec,
			valueCodec = ItemValueCodec,
			backingMap = Int2ObjectOpenHashMap(),
			callback = {
				for (change in it) {
					change.map({ k, v -> filters[k] = v }, { k -> filters[k] = null }, { Arrays.fill(filters, null) })
				}
			}
		)

		for ((i, filter) in filters.withIndex()) {
			if (filter != null) {
				field.value[i] = filter
			}
		}

		filterSynchronizer = WeakReference(field)
		return field
	}

	final override fun setSlotFilter(slot: Int, filter: Item?): Boolean {
		if (filters[slot] !== filter) {
			filters[slot] = filter

			filterSynchronizer?.get()?.let {
				if (filter == null) {
					it.value.remove(slot)
				} else {
					it.value[slot] = filter
				}
			}
		}

		return true
	}

	final override fun getSlotFilter(slot: Int) = filters[slot]
	final override fun hasSlotFilter(slot: Int) = filters[slot] !== null
	final override fun isSlotForbiddenForAutomation(slot: Int) = filters[slot] === Items.AIR

	final override fun testSlotFilter(slot: Int, itemStack: ItemStack): Boolean {
		return testSlotFilter(slot, itemStack.item)
	}

	final override fun testSlotFilter(slot: Int, item: Item): Boolean {
		if (filters[slot] == null) {
			return true
		} else {
			return filters[slot] === item
		}
	}

	final override fun getContainerSize() = size
	protected open fun startedIgnoringUpdates() {}
	protected open fun stoppedIgnoringUpdates() {}

	private fun deserializeNBT(tag: CompoundTag) {
		// нам не интересен размер
		tag.map("items") { it: ListTag ->
			deserializeNBT(it)
		}

		tag.map("filters") { it: ListTag ->
			val map = filterSynchronizer?.get()

			for (i in 0 until it.size.coerceAtMost(size)) {
				val nbt = it[i] as CompoundTag
				val index = nbt.getInt("slotIndex")
				filters[index] = ForgeRegistries.ITEMS.getValue(ResourceLocation.tryParse(nbt.getString("filter")) ?: continue)

				if (filters[index] != null) {
					map?.value?.put(index, filters[index]!!)
				}
			}
		}

		setChanged()
	}

	private fun deserializeNBT(tag: ListTag) {
		if (tag.all { (it as CompoundTag).contains("slotIndex") }) {
			val freeSlots = IntAVLTreeSet()

			for (i in 0 until size)
				freeSlots.add(i)

			for (i in 0 until tag.size.coerceAtMost(size)) {
				val nbt = tag[i] as CompoundTag
				var slotID = nbt.getInt("slotIndex")

				if (freeSlots.remove(slotID)) {
					slots[slotID] = ItemStack.of(nbt)
				} else if (freeSlots.isEmpty()) {
					break
				} else {
					slotID = freeSlots.firstInt()
					freeSlots.remove(slotID)
					slots[slotID] = ItemStack.of(nbt)
				}
			}
		} else {
			for (i in 0 until tag.size.coerceAtMost(size)) {
				slots[i] = ItemStack.of(tag[i] as CompoundTag)
			}
		}
	}

	override fun deserializeNBT(tag: Tag?) {
		Arrays.fill(slots, ItemStack.EMPTY)
		Arrays.fill(filters, null)
		nonEmptyFlags.clear()
		nonEmptyIndices = IntArrayList()

		filterSynchronizer?.get()?.value?.clear()

		when (tag) {
			is CompoundTag -> deserializeNBT(tag)
			is ListTag -> {
				deserializeNBT(tag)
				setChanged()
			}

			else -> setChanged()
		}
	}

	private fun internalSetChanged(slot: Int, new: ItemStack, old: ItemStack) {
		if (queueUpdates) {
			queuedUpdates.add(Update(slot, new, old))
		} else {
			setChanged(slot, new, old)
		}
	}

	private fun runUpdates() {
		for ((slot, new, old) in queuedUpdates) {
			setChanged(slot, new, old)
		}

		queuedUpdates.clear()
	}

	protected open fun setChanged(slot: Int, new: ItemStack, old: ItemStack) {
		listener.setChanged(slot, new, old)
	}

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["items"] = ListTag().also {
				for ((i, item) in slots.withIndex()) {
					if (!item.isEmpty) {
						it.add(item.serializeNBT().also {
							it["slotIndex"] = i
						})
					}
				}
			}

			it["filters"] = ListTag().also {
				for ((i, filter) in filters.withIndex()) {
					if (filter != null) {
						it.add(CompoundTag().also {
							it["filter"] = filter.registryName!!.toString()
							it["slotIndex"] = i
						})
					}
				}
			}
		}
	}

	final override fun isEmpty(): Boolean {
		return nonEmptyIndices.isEmpty
	}

	@Suppress("nothing_to_inline")
	inline operator fun get(slot: Int) = getItem(slot)

	@Suppress("nothing_to_inline")
	inline operator fun set(slot: Int, stack: ItemStack) = setItem(slot, stack)

	operator fun contains(other: ItemStack): Boolean {
		for (i in 0 until size) {
			if (ItemStack.isSameItemSameTags(this[i], other)) {
				return true
			}
		}

		return false
	}

	final override fun getItem(slot: Int): ItemStack {
		val item = slots[slot]

		if (item.isEmpty) {
			if (nonEmptyFlags[slot]) {
				setChanged(slot)
			}

			return ItemStack.EMPTY
		} else {
			if (!nonEmptyFlags[slot]) {
				setChanged(slot)
			}

			return item
		}
	}

	override fun fillStackedContents(contents: StackedContents) {
		for (item in iterator()) {
			contents.accountStack(item)
		}
	}

	final override fun removeItem(slot: Int, amount: Int): ItemStack {
		if (amount <= 0 || slot < 0 || slot >= size || slots[slot].isEmpty)
			return ItemStack.EMPTY

		val old = slots[slot].copy()
		val split = slots[slot].split(amount)
		trackedSlots[slot] = slots[slot].copy()
		changeset++
		updateEmptyFlag(slot)
		internalSetChanged(slot, if (slots[slot].isEmpty) ItemStack.EMPTY else slots[slot], old)

		return split
	}

	final override fun removeItemNoUpdate(slot: Int): ItemStack {
		val old = slots[slot]
		slots[slot] = ItemStack.EMPTY
		trackedSlots[slot] = ItemStack.EMPTY

		if (old.isNotEmpty) {
			updateEmptyFlag(slot)
			changeset++
		}

		return old
	}

	final override fun setItem(slot: Int, itemStack: ItemStack) {
		if (slots[slot].isEmpty && itemStack.isEmpty || itemStack === slots[slot])
			return

		val old = slots[slot]
		slots[slot] = if (itemStack.isEmpty) ItemStack.EMPTY else itemStack
		trackedSlots[slot] = if (itemStack.isEmpty) ItemStack.EMPTY else itemStack.copy()

		updateEmptyFlag(slot)
		changeset++
		internalSetChanged(slot, itemStack, old)
	}

	final override fun setChanged() {
		queueUpdates = true

		try {
			for (slot in 0 until size) {
				setChanged(slot)
			}

			runUpdates()
		} finally {
			queuedUpdates.clear()
			queueUpdates = false
		}
	}

	final override fun setChanged(slot: Int) {
		if (!slots[slot].equals(trackedSlots[slot], false)) {
			trackedSlots[slot] = slots[slot].copy()
			updateEmptyFlag(slot)
			changeset++
			internalSetChanged(slot, slots[slot], trackedSlots[slot])
			// mojang соси))0)0))0)))))0)
		}
	}

	private fun updateEmptyFlag(slot: Int) {
		if (slots[slot].isEmpty) {
			if (nonEmptyFlags[slot]) {
				nonEmptyFlags[slot] = false
				cowIndices()
				nonEmptyIndices.rem(slot)
			}
		} else {
			if (!nonEmptyFlags[slot]) {
				nonEmptyFlags[slot] = true
				cowIndices()
				nonEmptyIndices.addSorted(slot, IntComparators.NATURAL_COMPARATOR)
			}
		}
	}

	override fun stillValid(player: Player): Boolean {
		return true
	}

	final override fun clearContent() {
		nonEmptyFlags.clear()
		nonEmptyIndices = IntArrayList()

		Arrays.fill(trackedSlots, ItemStack.EMPTY)

		for (slot in 0 until size) {
			if (!slots[slot].isEmpty) {
				val old = slots[slot]
				slots[slot] = ItemStack.EMPTY
				internalSetChanged(slot, ItemStack.EMPTY, old)
			}
		}
	}

	private inner class Iterator : kotlin.collections.Iterator<ItemStack> {
		init {
			indicesReferenced = true
		}

		private val parent = nonEmptyIndices.intIterator()
		private var lastIndex = -1

		override fun hasNext(): Boolean {
			return parent.hasNext()
		}

		override fun next(): ItemStack {
			lastIndex = parent.nextInt()
			return getItem(lastIndex)
		}
	}

	private inner class Spliterator(private val parent: IntSpliterator) : java.util.Spliterator<ItemStack> {
		override fun tryAdvance(action: Consumer<in ItemStack>): Boolean {
			return parent.tryAdvance {
				action.accept(getItem(it))
			}
		}

		override fun trySplit(): java.util.Spliterator<ItemStack>? {
			return parent.trySplit()?.let(::Spliterator)
		}

		override fun estimateSize(): Long {
			return parent.estimateSize()
		}

		override fun characteristics(): Int {
			return parent.characteristics()
		}
	}

	final override fun iterator(): kotlin.collections.Iterator<ItemStack> {
		if (isEmpty) {
			return emptyIterator()
		}

		return Iterator()
	}

	final override fun iterator(nonEmpty: Boolean): kotlin.collections.Iterator<ItemStack> {
		if (!nonEmpty) {
			return (0 until size).iterator().map { slots[it] }
		} else if (isEmpty) {
			return emptyIterator()
		} else {
			return Iterator()
		}
	}

	private inner class Slot(override val slot: Int) : IContainerSlot {
		override val container: Container
			get() = this@MatteryContainer

		override val isForbiddenForAutomation: Boolean
			get() = isSlotForbiddenForAutomation(slot)

		override fun getFilter(): Item? {
			return getSlotFilter(slot)
		}

		override fun setFilter(filter: Item?): Boolean {
			return setSlotFilter(slot, filter)
		}

		override fun getMaxStackSize(item: ItemStack): Int {
			return getMaxStackSize(slot, item)
		}

		override fun setChanged() {
			setChanged(slot)
		}
	}

	final override fun slotIterator(): kotlin.collections.Iterator<IContainerSlot> {
		indicesReferenced = true
		return nonEmptyIndices.iterator().map { Slot(it) }
	}

	final override fun slotIterator(nonEmpty: Boolean): kotlin.collections.Iterator<IContainerSlot> {
		if (!nonEmpty) {
			return (0 until size).iterator().map { Slot(it) }
		} else if (isEmpty) {
			return emptyIterator()
		} else {
			indicesReferenced = true
			return nonEmptyIndices.iterator().map { Slot(it) }
		}
	}

	final override fun containerSlot(slot: Int): IContainerSlot {
		return Slot(slot)
	}

	final override fun countItem(item: Item): Int {
		return iterator().filter { it.item == item }.count().toInt()
	}

	final override fun hasAnyOf(items: Set<Item>): Boolean {
		return iterator().any { it.item in items }
	}

	final override fun hasAnyMatching(predicate: Predicate<ItemStack>): Boolean {
		return iterator().any(predicate)
	}

	final override fun spliterator(): java.util.Spliterator<ItemStack> {
		if (isEmpty) {
			return ObjectSpliterators.emptySpliterator()
		}

		indicesReferenced = true
		return Spliterator(nonEmptyIndices.intSpliterator())
	}

	fun stream(): Stream<ItemStack> {
		return StreamSupport.stream(spliterator(), false)
	}
}
