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

import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import net.minecraft.core.BlockPos
import net.minecraft.core.NonNullList
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.inventory.CraftingContainer
import net.minecraft.world.inventory.TransientCraftingContainer
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.CraftingRecipe
import net.minecraft.world.item.crafting.RecipeType
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.common.ForgeHooks
import net.minecraftforge.common.util.INBTSerializable
import net.minecraftforge.network.NetworkEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.core.TranslatableComponent
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.client.minecraft
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.graph.storage.StorageNode
import ru.dbotthepony.mc.otm.graph.storage.StorageGraph
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.container.set
import ru.dbotthepony.mc.otm.core.nbt.mapString
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.menu.storage.ItemMonitorMenu
import ru.dbotthepony.mc.otm.storage.*
import java.math.BigInteger
import java.util.*
import java.util.function.Supplier
import kotlin.collections.HashMap

class ItemMonitorPlayerSettings : INBTSerializable<CompoundTag>, MatteryPacket {
	enum class IngredientPriority(val component: Component) {
		// Refill everything from system
		SYSTEM(TranslatableComponent("otm.gui.item_monitor.refill_source.system")),

		// Refill everything from player's inventory
		INVENTORY(TranslatableComponent("otm.gui.item_monitor.refill_source.inventory")),

		// Refill everything from system, if can't refill from player's inventory
		SYSTEM_FIRST(TranslatableComponent("otm.gui.item_monitor.refill_source.system_first")),

		// Refill everything from player's inventory, if can't refill from system
		INVENTORY_FIRST(TranslatableComponent("otm.gui.item_monitor.refill_source.inventory_first")),

		// Do not refill (?)
		DO_NOT(TranslatableComponent("otm.gui.item_monitor.refill_source.do_not")),
	}

	enum class ResultTarget(val component: Component) {
		// Everything goes into storage system
		ALL_SYSTEM(TranslatableComponent("otm.gui.item_monitor.result_target.all_system")),

		// Result goes to player inventory
		// Crafting remainder goes to storage system
		MIXED(TranslatableComponent("otm.gui.item_monitor.result_target.mixed")),

		// Everything goes into player inventory
		ALL_INVENTORY(TranslatableComponent("otm.gui.item_monitor.result_target.all_inventory")),
	}

	enum class Amount(val component: Component) {
		ONE(TranslatableComponent("otm.gui.item_monitor.amount.one")),
		STACK(TranslatableComponent("otm.gui.item_monitor.amount.stack")),
		FULL(TranslatableComponent("otm.gui.item_monitor.amount.full"))
	}

	var ingredientPriority = IngredientPriority.SYSTEM
	var resultTarget = ResultTarget.MIXED
	var craftingAmount = Amount.STACK

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it[INGREDIENT_PRIORITY_KEY] = ingredientPriority.name
			it[RESULT_TARGET_KEY] = resultTarget.name
			it[QUICK_CRAFT_AMOUNT_KEY] = craftingAmount.name
		}
	}

	override fun deserializeNBT(nbt: CompoundTag) {
		ingredientPriority = nbt.mapString(INGREDIENT_PRIORITY_KEY, IngredientPriority::valueOf, IngredientPriority.SYSTEM)
		resultTarget = nbt.mapString(RESULT_TARGET_KEY, ResultTarget::valueOf, ResultTarget.MIXED)
		craftingAmount = nbt.mapString(QUICK_CRAFT_AMOUNT_KEY, Amount::valueOf, Amount.STACK)
	}

	fun read(buff: FriendlyByteBuf) {
		ingredientPriority = buff.readEnum(IngredientPriority::class.java)
		resultTarget = buff.readEnum(ResultTarget::class.java)
		craftingAmount = buff.readEnum(Amount::class.java)
	}

	fun read(other: ItemMonitorPlayerSettings) {
		ingredientPriority = other.ingredientPriority
		resultTarget = other.resultTarget
		craftingAmount = other.craftingAmount
	}

	override fun write(buff: FriendlyByteBuf) {
		buff.writeEnum(ingredientPriority)
		buff.writeEnum(resultTarget)
		buff.writeEnum(craftingAmount)
	}

	private fun playClient() {
		val ply = minecraft.player ?: throw IllegalStateException("Player is missing")
		val container = ply.containerMenu as? ItemMonitorMenu ?: return LOGGER.error("Received ItemMonitorPlayerSettings but container is missing or not of right type ({})!", ply.containerMenu)

		container.settings.read(this)
	}

	// spectators are allowed because they change their own setting which is invisible to everyone else
	private fun playServer(ply: ServerPlayer) {
		val container = ply.containerMenu as? ItemMonitorMenu ?: return LOGGER.error("Received ItemMonitorPlayerSettings from {} but container is missing or not of right type ({})!", ply, ply.containerMenu)
		container.settings.read(this)
		(container.tile as ItemMonitorBlockEntity).setChangedLight()
	}

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

		if (ply != null) {
			playServer(ply)
		} else {
			playClient()
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): ItemMonitorPlayerSettings {
			return ItemMonitorPlayerSettings().also { it.read(buff) }
		}

		private val LOGGER = LogManager.getLogger()
		const val INGREDIENT_PRIORITY_KEY = "ingredientPriority"
		const val RESULT_TARGET_KEY = "resultTarget"
		const val QUICK_CRAFT_AMOUNT_KEY = "quickCraftAmount"
	}
}

private fun takeOne(inventory: Inventory, item: ItemStack): Boolean {
	for (i in 0 until inventory.containerSize) {
		if (!inventory[i].isEmpty && ItemStack.isSameItemSameTags(inventory[i], item)) {
			inventory.removeItem(i, 1)
			return true
		}
	}

	return false
}

private fun takeOne(id: UUID?, view: IStorageProvider<ItemStackWrapper>): Boolean {
	val extracted = view.extractStack(id ?: return false, BigInteger.ONE, false)

	if (!extracted.isEmpty) {
		return true
	}

	return false
}

class ItemMonitorBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) :
	MatteryPoweredBlockEntity(MBlockEntities.ITEM_MONITOR, p_155229_, p_155230_),
	IStorageEventConsumer<ItemStackWrapper> {

	val energy = WorkerEnergyStorage(this, MachinesConfig.ITEM_MONITOR)

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

	var poweredView: PoweredVirtualComponent<ItemStackWrapper>? = null
		private set

	val cell = object : StorageNode(energy) {
		override fun attachComponents(to: StorageGraph) {
			super.attachComponents(to)
			poweredView = PoweredVirtualComponent(to.getVirtualComponent(ITEM_STORAGE), energy)
		}

		override fun removeComponents(from: StorageGraph) {
			super.removeComponents(from)
			poweredView?.removeListeners()
			poweredView = null
		}
	}

	init {
		exposeGlobally(MatteryCapability.STORAGE_NODE, cell)
	}

	private val settings = HashMap<UUID, ItemMonitorPlayerSettings>()
	private val craftingAmount = Object2ObjectArrayMap<Player, Int>()
	private val lastCraftingRecipe = Object2ObjectArrayMap<Player, CraftingRecipe>()

	fun howMuchPlayerCrafted(ply: Player): Int = craftingAmount[ply] ?: 0
	fun lastCraftingRecipe(ply: Player) = lastCraftingRecipe[ply]

	// a lot of code is hardcoded to take CraftingContainer as it's input
	// hence we are forced to work around this by providing proxy container
	val craftingGrid = object : MatteryContainer(::setChangedLight, 3 * 3) {
		override fun setChanged(slot: Int, new: ItemStack, old: ItemStack) {
			super.setChanged(slot, new, old)
			craftingGridDummy[slot] = new

			if (!inProcessOfCraft)
				scanCraftingGrid()
		}
	}.also(::addDroppableContainer)

	private var inProcessOfCraft = false

	private fun scanCraftingGrid() {
		val server = level?.server ?: return
		val oldRecipe = craftingRecipe

		if (oldRecipe != null && oldRecipe.matches(craftingGridDummy, level!!)) {
			return
		}

		craftingRecipe = server.recipeManager.getRecipeFor(RecipeType.CRAFTING, craftingGridDummy, level!!).orElse(null)
		val craftingRecipe = craftingRecipe

		if (oldRecipe != craftingRecipe) {
			Arrays.fill(craftingGridTuples, null)

			val poweredView = poweredView

			if (craftingRecipe != null && poweredView != null) {
				for (i in craftingGridTuples.indices) {
					val item = craftingGrid[i]

					if (!item.isEmpty) {
						craftingGridTuples[i] = poweredView[ItemStackWrapper(item).key()]
					}
				}
			}
		}
	}

	private val craftingGridTuples = arrayOfNulls<UUID>(3 * 3)

	private val craftingMenu = object : AbstractContainerMenu(null, Int.MIN_VALUE) {
		override fun stillValid(p_38874_: Player): Boolean {
			return true
		}

		override fun quickMoveStack(p_38941_: Player, p_38942_: Int): ItemStack {
			return ItemStack.EMPTY
		}
	}

	private val craftingGridDummy = TransientCraftingContainer(craftingMenu, 3, 3)

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

	override fun addStack(stack: ItemStackWrapper, id: UUID, provider: IStorageProvider<ItemStackWrapper>) {
		// no op
	}

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

	override fun removeStack(stack: ItemStackWrapper, id: UUID) {
		for (i in craftingGridTuples.indices) {
			if (craftingGridTuples[i] == id) {
				craftingGridTuples[i] = null
			}
		}
	}

	init {
		cell.addStorageComponent(this)
	}

	var craftingRecipe: CraftingRecipe? = null
		private set

	inner class CraftingResultContainer : Container {
		var craftingPlayer: ServerPlayer? = null

		override fun clearContent() {
			throw UnsupportedOperationException()
		}

		override fun getContainerSize() = 1
		override fun isEmpty() = craftingRecipe == null
		override fun stillValid(p_18946_: Player) = true

		override fun getItem(p_18941_: Int): ItemStack {
			require(p_18941_ == 0) { "Invalid slot ID: $p_18941_" }
			return craftingRecipe?.getResultItem(level?.registryAccess() ?: return ItemStack.EMPTY)?.copy() ?: ItemStack.EMPTY
		}

		override fun removeItem(index: Int, amount: Int): ItemStack {
			require(index == 0) { "Invalid index $index" }

			val craftingRecipe = craftingRecipe
			val craftingPlayer = craftingPlayer

			if (craftingRecipe == null || craftingPlayer == null || craftingRecipe.getResultItem(level?.registryAccess() ?: return ItemStack.EMPTY).count != amount) {
				return ItemStack.EMPTY
			}

			var crafts = craftingAmount[craftingPlayer] ?: 0
			crafts++
			craftingAmount[craftingPlayer] = crafts
			lastCraftingRecipe[craftingPlayer] = craftingRecipe

			val settings = getSettings(craftingPlayer)

			inProcessOfCraft = true

			try {
				ForgeHooks.setCraftingPlayer(craftingPlayer)
				val remainingItems: NonNullList<ItemStack> = craftingRecipe.getRemainingItems(craftingGridDummy)
				ForgeHooks.setCraftingPlayer(null)

				check(remainingItems.size == craftingGrid.containerSize) { "${remainingItems.size} != ${craftingGrid.containerSize} !!!" }

				for (slot in 0 until craftingGrid.containerSize) {
					val oldItem = craftingGrid[slot].let { if (!it.isEmpty || it.count < 2) it.copy() else it }

					if (!craftingGrid[slot].isEmpty) {
						craftingGrid.removeItem(slot, 1)
					}

					var newItem = craftingGrid[slot]

					if (newItem.isEmpty) {
						when (settings.ingredientPriority) {
							ItemMonitorPlayerSettings.IngredientPriority.SYSTEM -> {
								if (poweredView != null && takeOne(craftingGridTuples[slot], poweredView!!)) {
									newItem = oldItem
									craftingGrid[slot] = newItem
								}
							}

							ItemMonitorPlayerSettings.IngredientPriority.INVENTORY -> {
								if (takeOne(craftingPlayer.inventory, oldItem)) {
									newItem = oldItem
									craftingGrid[slot] = newItem
								}
							}

							ItemMonitorPlayerSettings.IngredientPriority.SYSTEM_FIRST -> {
								if (poweredView != null && takeOne(craftingGridTuples[slot], poweredView!!)) {
									newItem = oldItem
									craftingGrid[slot] = newItem
								}

								if (newItem.isEmpty && takeOne(craftingPlayer.inventory, oldItem)) {
									newItem = oldItem
									craftingGrid[slot] = newItem
								}
							}

							ItemMonitorPlayerSettings.IngredientPriority.INVENTORY_FIRST -> {
								if (takeOne(craftingPlayer.inventory, oldItem)) {
									newItem = oldItem
									craftingGrid[slot] = newItem
								} else if (poweredView != null && takeOne(craftingGridTuples[slot], poweredView!!)) {
									newItem = oldItem
									craftingGrid[slot] = newItem
								}
							}

							ItemMonitorPlayerSettings.IngredientPriority.DO_NOT -> { /* no op */ }
						}
					}

					val remainder = remainingItems[slot]

					if (!remainder.isEmpty) {
						when (settings.resultTarget) {
							ItemMonitorPlayerSettings.ResultTarget.ALL_SYSTEM, ItemMonitorPlayerSettings.ResultTarget.MIXED -> {
								val remaining = poweredView?.insertStack(ItemStackWrapper(remainder), false)?.stack ?: remainder

								if (!remaining.isEmpty) {
									if (newItem.isEmpty) {
										craftingGrid[slot] = remaining
									} else if (ItemStack.isSameItemSameTags(newItem, remaining)) {
										newItem.grow(remaining.count)
										craftingGrid.setChanged(slot)
									} else if (!craftingPlayer.inventory.add(remaining)) {
										craftingPlayer.drop(remaining, false)
									}
								}
							}

							ItemMonitorPlayerSettings.ResultTarget.ALL_INVENTORY -> {
								if (!craftingPlayer.inventory.add(remainder)) {
									val remaining = poweredView?.insertStack(ItemStackWrapper(remainder), false)?.stack ?: remainder

									if (!remaining.isEmpty) {
										if (newItem.isEmpty) {
											craftingGrid[slot] = remaining
										} else if (ItemStack.isSameItemSameTags(newItem, remaining)) {
											newItem.grow(remaining.count)
											craftingGrid.setChanged(slot)
										} else {
											craftingPlayer.drop(remaining, false)
										}
									}
								}
							}
						}
					}
				}
			} finally {
				inProcessOfCraft = false
				scanCraftingGrid()
			}

			return craftingRecipe.getResultItem(level?.registryAccess() ?: return ItemStack.EMPTY).copy()
		}

		override fun removeItemNoUpdate(p_18951_: Int): ItemStack {
			throw UnsupportedOperationException()
		}

		override fun setItem(p_18944_: Int, p_18945_: ItemStack) {
			if ((craftingRecipe != null && !craftingRecipe!!.let { it.getResultItem(level?.registryAccess() ?: return@let ItemStack.EMPTY) }.isEmpty) || !p_18945_.isEmpty) {
				throw RuntimeException("BUG-DETECT: Tried to set crafting result slot item to something, and either we have crafting recipe which have valid result, or we are trying to set slot to non empty item: $p_18945_")
			}
		}

		override fun setChanged() {}
	}

	val craftingResultContainer = CraftingResultContainer()

	override fun saveLevel(nbt: CompoundTag) {
		super.saveLevel(nbt)

		nbt.put("player_settings", CompoundTag().also {
			for ((key, value) in this.settings) {
				it[key.toString()] = value.serializeNBT()
			}
		})

		nbt["crafting_grid"] = craftingGrid.serializeNBT()
	}

	override fun load(nbt: CompoundTag) {
		super.load(nbt)
		this.settings.clear()

		val settings = nbt.getCompound("player_settings")

		for (key in settings.allKeys) {
			val uuid = UUID.fromString(key)
			val deserialized = ItemMonitorPlayerSettings()
			deserialized.deserializeNBT(settings.getCompound(key))

			check(this.settings.put(uuid, deserialized) == null) { "Duplicate UUID??? $uuid" }
		}

		craftingGrid.deserializeNBT(nbt["crafting_grid"])
	}

	fun getSettings(ply: ServerPlayer): ItemMonitorPlayerSettings {
		return settings.computeIfAbsent(ply.uuid) { ItemMonitorPlayerSettings() }
	}

	override fun tick() {
		super.tick()

		cell.tickEnergyDemanding()

		if (craftingAmount.size != 0)
			craftingAmount.clear()

		if (lastCraftingRecipe.size != 0)
			lastCraftingRecipe.clear()
	}

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

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

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