package ru.dbotthepony.mc.otm.container

import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
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 kotlin.jvm.JvmOverloads
import net.minecraft.world.entity.player.Player
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.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.*

@Suppress("UNUSED")
open class MatteryContainer(protected val watcher: Runnable, private val size: Int) : Container, Iterable<ItemStack>, INBTSerializable<Tag?> {
	constructor(size: Int) : this({}, size)

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

	protected val slots: Array<ItemStack> = Array(size) { ItemStack.EMPTY }
	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

	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
	}

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

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

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

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

	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)
		filterSynchronizer?.get()?.value?.clear()

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

			else -> setChanged()
		}
	}

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

	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
						})
					}
				}
			}
		}
	}

	fun hasEmptySlot(): Boolean {
		for (i in 0 until size) {
			if (this[i].isEmpty) {
				return true
			}
		}

		return false
	}

	fun handler(
		insert_validator: (slot: Int, stack: ItemStack) -> Boolean,
		extract_validator: (slot: Int, amount: Int, stack: ItemStack) -> Boolean
	): ContainerHandler {
		return ContainerHandler(this, object : HandlerFilter {
			override fun canInsert(slot: Int, stack: ItemStack) = insert_validator(slot, stack)
			override fun canExtract(slot: Int, amount: Int, stack: ItemStack) = extract_validator(slot, amount, stack)
		})
	}

	fun handler(
		filter: HandlerFilter
	): ContainerHandler {
		return ContainerHandler(this, filter)
	}

	fun handler(insert_validator: (Int, ItemStack) -> Boolean): ContainerHandler {
		return ContainerHandler(this, object : HandlerFilter {
			override fun canInsert(slot: Int, stack: ItemStack) = insert_validator(slot, stack)
			override fun canExtract(slot: Int, amount: Int, stack: ItemStack) = false
		})
	}

	fun handler(): ContainerHandler {
		return ContainerHandler(this)
	}

	open fun getMaxStackSize(slot: Int) = maxStackSize

	open fun getMaxStackSizeWithItem(slot: Int): Int {
		val item = this[slot]

		if (!item.isEmpty) {
			return getMaxStackSize(slot).coerceAtMost(item.maxStackSize)
		}

		return getMaxStackSize(slot)
	}

	/**
	 * @return Leftover [ItemStack]
	 */
	@JvmOverloads
	fun addItem(stack: ItemStack, range: IntRange, simulate: Boolean = false, onlyIntoExisting: Boolean = false, popTime: Int? = null): ItemStack {
		if (range.last >= size || range.first < 0)
			throw IllegalArgumentException("Invalid range: $range")

		if (stack.isEmpty)
			return stack

		val copy = stack.copy()

		// двигаем в одинаковые слоты
		for (slot in range) {
			if (ItemStack.isSameItemSameTags(slots[slot], copy)) {
				val slotStack = slots[slot]
				val slotLimit = getMaxStackSize(slot).coerceAtMost(slotStack.maxStackSize)

				if (slotStack.count < slotLimit) {
					val newCount = (slotStack.count + copy.count).coerceAtMost(slotLimit)
					val diff = newCount - slotStack.count

					if (!simulate) {
						val old = slotStack.copy()
						slotStack.count = newCount
						trackedSlots[slot] = slotStack.copy()
						setChanged(slot, slotStack, old)

						if (popTime != null) {
							slotStack.popTime = popTime
						}
					}

					copy.shrink(diff)

					if (copy.isEmpty) {
						return copy
					}
				}
			}
		}

		if (!onlyIntoExisting) {
			// двигаем в пустые слоты
			for (slot in range) {
				if (slots[slot].isEmpty) {
					val diff = copy.count.coerceAtMost(getMaxStackSize(slot).coerceAtMost(copy.maxStackSize))

					if (!simulate) {
						val copyToPut = copy.copy()
						copyToPut.count = diff
						this[slot] = copyToPut

						if (popTime != null) {
							copyToPut.popTime = popTime
						}
					}

					copy.shrink(diff)

					if (copy.isEmpty) {
						return copy
					}
				}
			}
		}

		return copy
	}

	fun addItem(stack: ItemStack, simulate: Boolean, onlyIntoExisting: Boolean = false, popTime: Int? = null): ItemStack {
		return addItem(stack, 0 until size, simulate, onlyIntoExisting = onlyIntoExisting, popTime = popTime)
	}

	/**
	 * Unlike [addItem], modifies original [stack]
	 *
	 * @return Whenever [stack] was modified
	 */
	fun consumeItem(stack: ItemStack, simulate: Boolean, onlyIntoExisting: Boolean = false, popTime: Int? = null): Boolean {
		if (stack.isEmpty)
			return false

		val result = addItem(stack, 0 until size, simulate, onlyIntoExisting = onlyIntoExisting, popTime = popTime)

		if (result.count != stack.count) {
			if (!simulate) {
				stack.count = result.count
			}

			return true
		}

		return false
	}

	@JvmOverloads
	fun fullyAddItem(stack: ItemStack, start: Int = 0, end: Int = size - 1): Boolean {
		return fullyAddItem(stack, start .. end)
	}

	fun fullyAddItem(stack: ItemStack, range: IntRange): Boolean {
		if (!addItem(stack, range, true).isEmpty)
			return false

		return addItem(stack, range, false).isEmpty
	}

	override fun isEmpty(): Boolean {
		return slots.all { it.isEmpty }
	}

	operator fun get(slot: Int) = getItem(slot)
	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)
			return ItemStack.EMPTY
		else
			return 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()
		setChanged(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
		return old
	}

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

		val old = slots[slot]
		slots[slot] = stack
		trackedSlots[slot] = if (stack.isEmpty) ItemStack.EMPTY else stack.copy()
		setChanged(slot, stack, old)
	}

	final override fun setChanged() {
		for (slot in 0 until size) {
			setChanged(slot)
		}
	}

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

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

	final override fun clearContent() {
		for (slot in 0 until size) {
			if (!slots[slot].isEmpty) {
				val old = slots[slot]
				slots[slot] = ItemStack.EMPTY
				setChanged(slot, ItemStack.EMPTY, old)
			}
		}

		Arrays.fill(trackedSlots, ItemStack.EMPTY)
	}

	final override fun iterator(): MutableIterator<ItemStack> {
		return ContainerIterator(this)
	}
}
