package ru.dbotthepony.mc.otm.container

import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.server.level.ServerPlayer
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraftforge.common.util.INBTSerializable
import net.minecraftforge.network.NetworkEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.menu.MatteryMenu
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.core.nbt.set
import java.util.Arrays
import java.util.LinkedList
import java.util.function.Supplier

data class ItemFilterSlotPacket(val slotID: Int, val newValue: ItemStack) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(slotID)
		buff.writeItemStack(newValue, true)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true

		if (context.get().sender != null) {
			context.get().enqueueWork {
				playServer(context.get().sender!!)
			}
		} else {
			context.get().enqueueWork {
				playClient()
			}
		}
	}

	fun playClient() {
		val menu = minecraft.player?.containerMenu as? MatteryMenu ?: throw IllegalStateException("No MatteryMenu is open right now, can't handle ItemFilterSlotReplicaPacket")
		menu.filterSlots[slotID].set(newValue)
	}

	fun playServer(player: ServerPlayer) {
		if (player.isSpectator) {
			return
		}

		val menu = player.containerMenu as? MatteryMenu ?: return LOGGER.error("No MatteryMenu is open right now, can't handle ItemFilterSlotReplicaPacket from $player")
		val slot = menu.filterSlots.getOrNull(slotID) ?: return LOGGER.error("ItemFilterSlotReplicaPacket: unknown slot $slotID from $player in $menu!")

		if (slot.filter?.isLocked == true)
			return

		slot.set(newValue)
	}

	companion object {
		fun read(buff: FriendlyByteBuf): ItemFilterSlotPacket {
			return ItemFilterSlotPacket(buff.readInt(), buff.readItem())
		}

		private val LOGGER = LogManager.getLogger()
	}
}

data class ItemFilterNetworkSlot(val slotID: Int, val networkID: Int, var filter: ItemFilter?) {
	fun get() = filter?.get(slotID) ?: remote
	fun set(value: ItemStack) {
		if (filter != null)
			filter!![slotID] = value
		else
			remote = value
	}

	private var remote: ItemStack = ItemStack.EMPTY

	fun sendChanges(full: Boolean = false): ItemFilterSlotPacket? {
		requireNotNull(filter) { "Invalid side" }

		if (full || !ItemStack.isSameItemSameTags(remote, get())) {
			remote = get()
			return ItemFilterSlotPacket(networkID, get())
		}

		return null
	}
}

fun interface ItemFilterCallback {
	fun invoke(slot: Int?, oldValue: ItemStack?, newValue: ItemStack?)
}

fun interface ItemFilterFullCallback {
	fun invoke(self: ItemFilter, slot: Int?, oldValue: ItemStack?, newValue: ItemStack?)
}

class ItemFilter(
	val size: Int,
	private val modified: ItemFilterFullCallback? = null
) : INBTSerializable<CompoundTag?> {
	constructor(size: Int, modified: ItemFilterCallback?) :
			this(size,
				modified?.let {
					fn ->
						return@let ItemFilterFullCallback { _: ItemFilter, slot: Int?, oldValue: ItemStack?, newValue: ItemStack? -> fn.invoke(slot, oldValue, newValue) }
					})

	private val filter = Array<ItemStack>(size) { ItemStack.EMPTY }
	private val linkedFilter = LinkedList<ItemStack>()

	var isLocked = false

	var isWhitelist = false
		set(value) {
			if (value != field) {
				field = value
				modified?.invoke(this, null, null, null)
			}
		}

	var matchTag = false
		set(value) {
			if (value != field) {
				field = value
				modified?.invoke(this, null, null, null)
			}
		}

	var matchNBT = false
		set(value) {
			if (value != field) {
				field = value
				modified?.invoke(this, null, null, null)
			}
		}

	fun clear() {
		isWhitelist = false
		matchTag = false
		matchNBT = false

		Arrays.fill(filter, ItemStack.EMPTY)
		linkedFilter.clear()

		modified?.invoke(this, null, null, null)
	}

	fun copyFrom(other: ItemFilter) {
		require(other.size == size) { "Size differs (this filter has size of $size, other has size of ${other.size})" }

		linkedFilter.clear()

		for (i in 0 until size) {
			filter[i] = other.filter[i]

			if (!filter[i].isEmpty) {
				linkedFilter.add(filter[i])
			}
		}

		isWhitelist = other.isWhitelist
		matchTag = other.matchTag
		matchNBT = other.matchNBT

		modified?.invoke(this, null, null, null)
	}

	operator fun set(index: Int, value: ItemStack) {
		if (value.isEmpty && filter[index].isEmpty) {
			return
		}

		val old = filter[index]
		filter[index] = value.let { if (!it.isEmpty) it.copy().also { it.count = 1 } else it }

		if (!filter[index].isEmpty && filter[index].tag != null && filter[index].tag!!.isEmpty) {
			filter[index].tag = null
		}

		if (!old.isEmpty)
			linkedFilter.remove(old)

		if (!filter[index].isEmpty)
			linkedFilter.add(filter[index])

		modified?.invoke(this, index, old, filter[index])
	}

	operator fun get(index: Int): ItemStack = filter[index].copy()

	fun match(value: ItemStack): Boolean {
		if (value.isEmpty) {
			return false
		}

		if (linkedFilter.isEmpty()) {
			return !isWhitelist
		}

		for (item in linkedFilter) {
			var matched = item.`is`(value.item)

			if (matched && matchTag) {
				matched = false

				val thisTags = item.tags
				val stackTags = HashSet<TagKey<Item>>()

				for (tag in value.tags) {
					stackTags.add(tag)
				}

				for (tag1 in thisTags) {
					if (stackTags.contains(tag1)) {
						matched = true
						break
					}
				}
			}

			if (matched && matchNBT) {
				val a = item.tag
				val b = value.tag

				if (a == null && b == null) {
					// nothing
				} else if (a != null && b != null) {
					matched = a == b
				} else if (a != null) {
					matched = a.isEmpty
				} else {
					matched = false
				}
			}

			if (matched) {
				return isWhitelist
			}
		}

		return !isWhitelist
	}

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["items"] = ListTag().also {
				for (value in filter) {
					it.add(value.serializeNBT())
				}
			}

			it["is_whitelist"] = isWhitelist
			it["match_tag"] = matchTag
			it["match_nbt"] = matchNBT
		}
	}

	override fun deserializeNBT(nbt: CompoundTag?) {
		for (i in filter.indices)
			filter[i] = ItemStack.EMPTY

		if (nbt == null)
			return

		nbt.map("items") { it: ListTag ->
			for ((i, value) in it.withIndex()) {
				if (value is CompoundTag) {
					filter[i] = ItemStack.of(value)

					if (!filter[i].isEmpty) {
						linkedFilter.add(filter[i])
					}
				}
			}
		}

		isWhitelist = nbt.getBoolean("is_whitelist")
		matchTag = nbt.getBoolean("match_tag")
		matchNBT = nbt.getBoolean("match_nbt")
	}
}
