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

import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
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.chat.Component
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.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 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.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.render.IGUIRenderable
import ru.dbotthepony.mc.otm.client.render.UVWindingOrder
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.graph.storage.StorageNode
import ru.dbotthepony.mc.otm.graph.storage.StorageGraph
import ru.dbotthepony.mc.otm.registry.MBlockEntities
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 ru.dbotthepony.mc.otm.storage.powered.PoweredVirtualComponent
import java.math.BigInteger
import java.util.*
import kotlin.collections.HashMap
import ru.dbotthepony.mc.otm.client.render.Widgets8
import ru.dbotthepony.mc.otm.container.CombinedContainer
import ru.dbotthepony.mc.otm.container.MatteryCraftingContainer
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.container.set
import ru.dbotthepony.mc.otm.container.util.slotIterator
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.collect.toList
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.util.ItemStorageStackSorter
import ru.dbotthepony.mc.otm.core.value

interface IItemMonitorPlayerSettings {
	var ingredientPriority: ItemMonitorPlayerSettings.IngredientPriority
	var resultTarget: ItemMonitorPlayerSettings.ResultTarget
	var craftingAmount: ItemMonitorPlayerSettings.Amount
	var sorting: ItemStorageStackSorter
	var ascendingSort: Boolean
}

private fun takeOne(inventory: Container?, item: ItemStack): Boolean {
	val iterator = inventory?.slotIterator() ?: return false

	for (slot in iterator) {
		val stack = slot.item

		if (stack.equals(item, false)) {
			stack.shrink(1)
			slot.setChanged()
			return true
		}
	}

	return false
}

private fun takeOne(id: UUID?, view: IStorageProvider<ItemStorageStack>?): Boolean {
	return view?.extractStack(id ?: return false, BigInteger.ONE, false)?.isNotEmpty == true
}

class ItemMonitorPlayerSettings : INBTSerializable<CompoundTag>, IItemMonitorPlayerSettings {
	interface Setting {
		val component: Component
		val icon: IGUIRenderable
		val winding: UVWindingOrder
	}

	enum class IngredientPriority(override val component: Component, icon: Lazy<IGUIRenderable>, override val winding: UVWindingOrder = UVWindingOrder.NORMAL) : Setting {
		// Refill everything from system
		SYSTEM(TranslatableComponent("otm.gui.item_monitor.refill_source.system"), lazy { Widgets8.WHITE_ARROW_DOWN }, UVWindingOrder.FLIP) {
			override fun takeOne(item: ItemStack, view: IStorageProvider<ItemStorageStack>?, inventory: CombinedContainer?, id: UUID?): Boolean {
				return takeOne(id, view)
			}
		},

		// Refill everything from player's inventory
		INVENTORY(TranslatableComponent("otm.gui.item_monitor.refill_source.inventory"), lazy { Widgets8.WHITE_ARROW_DOWN }) {
			override fun takeOne(item: ItemStack, view: IStorageProvider<ItemStorageStack>?, inventory: CombinedContainer?, id: UUID?): Boolean {
				return takeOne(inventory, item)
			}
		},

		// Refill everything from system, if can't refill from player's inventory
		SYSTEM_FIRST(TranslatableComponent("otm.gui.item_monitor.refill_source.system_first"), lazy { Widgets8.ARROW_SIDEWAYS }, UVWindingOrder.FLIP) {
			override fun takeOne(item: ItemStack, view: IStorageProvider<ItemStorageStack>?, inventory: CombinedContainer?, id: UUID?): Boolean {
				return takeOne(id, view) || takeOne(inventory, item)
			}
		},

		// Refill everything from player's inventory, if can't refill from system
		INVENTORY_FIRST(TranslatableComponent("otm.gui.item_monitor.refill_source.inventory_first"), lazy { Widgets8.ARROW_SIDEWAYS }) {
			override fun takeOne(item: ItemStack, view: IStorageProvider<ItemStorageStack>?, inventory: CombinedContainer?, id: UUID?): Boolean {
				return takeOne(inventory, item) || takeOne(id, view)
			}
		},

		// Do not refill (?)
		DO_NOT(TranslatableComponent("otm.gui.item_monitor.refill_source.do_not"), lazy { Widgets8.MINUS }) {
			override fun takeOne(item: ItemStack, view: IStorageProvider<ItemStorageStack>?, inventory: CombinedContainer?, id: UUID?): Boolean {
				return false
			}
		};

		override val icon: IGUIRenderable by icon

		abstract fun takeOne(item: ItemStack, view: IStorageProvider<ItemStorageStack>?, inventory: CombinedContainer?, id: UUID?): Boolean
	}

	enum class ResultTarget(override val component: Component, icon: Lazy<IGUIRenderable>, override val winding: UVWindingOrder = UVWindingOrder.NORMAL) : Setting {
		// Everything goes into storage system
		ALL_SYSTEM(TranslatableComponent("otm.gui.item_monitor.result_target.all_system"), lazy { Widgets8.ARROW_PAINTED_UP }),

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

		// Everything goes into player inventory
		ALL_INVENTORY(TranslatableComponent("otm.gui.item_monitor.result_target.all_inventory"), lazy { Widgets8.ARROW_PAINTED_UP }, UVWindingOrder.FLIP);

		override val icon: IGUIRenderable by icon
	}

	enum class Amount(override val component: Component, icon: Lazy<IGUIRenderable>, override val winding: UVWindingOrder = UVWindingOrder.NORMAL) : Setting {
		ONE(TranslatableComponent("otm.gui.item_monitor.amount.one"), lazy { Widgets8.ONE }),
		STACK(TranslatableComponent("otm.gui.item_monitor.amount.stack"), lazy { Widgets8.S }),
		FULL(TranslatableComponent("otm.gui.item_monitor.amount.full"), lazy { Widgets8.F });

		override val icon: IGUIRenderable by icon
	}

	override var ingredientPriority = IngredientPriority.SYSTEM
	override var resultTarget = ResultTarget.MIXED
	override var craftingAmount = Amount.STACK
	override var sorting: ItemStorageStackSorter = ItemStorageStackSorter.DEFAULT
	override var ascendingSort: Boolean = false

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["ingredientPriority"] = ingredientPriority.name
			it["resultTarget"] = resultTarget.name
			it["quickCraftAmount"] = craftingAmount.name
			it["sorting"] = sorting.name
			it["ascendingSort"] = ascendingSort
		}
	}

	override fun deserializeNBT(nbt: CompoundTag) {
		ingredientPriority = nbt.mapString("ingredientPriority", IngredientPriority::valueOf, IngredientPriority.SYSTEM)
		resultTarget = nbt.mapString("resultTarget", ResultTarget::valueOf, ResultTarget.MIXED)
		craftingAmount = nbt.mapString("quickCraftAmount", Amount::valueOf, Amount.STACK)
		sorting = nbt.mapString("sorting", ItemStorageStackSorter::valueOf, ItemStorageStackSorter.DEFAULT)
		ascendingSort = nbt.getBoolean("ascendingSort")
	}
}

class ItemMonitorBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryPoweredBlockEntity(MBlockEntities.ITEM_MONITOR, blockPos, blockState), IStorageEventConsumer<ItemStorageStack> {
	override val energy = ProfiledEnergyStorage(WorkerEnergyStorage(this::markDirtyFast, MachinesConfig.ITEM_MONITOR))
	val energyConfig = ConfigurableEnergy(energy)

	init {
		savetables.stateful(::energy, ENERGY_KEY)
	}

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

	val cell = object : StorageNode(energy) {
		override fun attachComponents(to: StorageGraph) {
			super.attachComponents(to)
			poweredView = PoweredVirtualComponent(to.getVirtualComponent(StorageStack.ITEMS), ::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 = Object2IntOpenHashMap<Player>()
	private val lastCraftingRecipe = Object2ObjectArrayMap<Player, CraftingRecipe>()
	private var inProcessOfCraft = false
	private val craftingGridTuples = arrayOfNulls<UUID>(3 * 3)

	fun howMuchPlayerCrafted(ply: Player): Int = craftingAmount.getInt(ply)
	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 : MatteryCraftingContainer(::markDirtyFast, 3, 3) {
		override fun setChanged(slot: Int, new: ItemStack, old: ItemStack) {
			markDirtyFast()

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

	private fun scanCraftingGrid(justCheckForRecipeChange: Boolean): Boolean {
		val level = level ?: return false
		val server = level.server ?: return false

		var craftingRecipe = craftingRecipe
		if (craftingRecipe != null && craftingRecipe.matches(craftingGrid, level)) return true
		if (justCheckForRecipeChange) return false

		craftingRecipe = server.recipeManager.getRecipeFor(RecipeType.CRAFTING, craftingGrid, level).orElse(null)?.value
		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[ItemStorageStack.unsafe(item)]
				}
			}
		}

		this.craftingRecipe = craftingRecipe
		return false
	}

	override val storageType: StorageStack.Type<ItemStorageStack>
		get() = StorageStack.ITEMS

	override fun onStackAdded(stack: ItemStorageStack, id: UUID, provider: IStorageProvider<ItemStorageStack>) {
		if (craftingRecipe != null) {
			for (i in craftingGridTuples.indices) {
				val item = craftingGrid[i]

				if (!item.isEmpty && stack.equalsWithoutCount(ItemStorageStack.unsafe(item))) {
					craftingGridTuples[i] = id
				}
			}
		}
	}

	override fun onStackChanged(stack: ItemStorageStack, id: UUID) {}
	override fun onStackRemoved(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(player: Player) = true

		override fun getItem(slot: Int): ItemStack {
			require(slot == 0) { "Invalid slot: $slot" }
			return craftingRecipe?.resultItem?.copy() ?: ItemStack.EMPTY
		}

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

			val level = level ?: return ItemStack.EMPTY
			val craftingRecipe = craftingRecipe ?: return ItemStack.EMPTY
			val craftingPlayer = craftingPlayer ?: return ItemStack.EMPTY

			try {
				ForgeHooks.setCraftingPlayer(craftingPlayer)

				if (craftingRecipe.resultItem.count != amount) {
					return ItemStack.EMPTY
				}
			} finally {
				ForgeHooks.setCraftingPlayer(null)
			}

			craftingAmount[craftingPlayer] = craftingAmount.getInt(craftingPlayer) + 1
			lastCraftingRecipe[craftingPlayer] = craftingRecipe

			tickList.namedTimer(craftingPlayer, 4) {
				craftingAmount.removeInt(craftingPlayer)
				lastCraftingRecipe.remove(craftingPlayer)
			}

			val settings = getSettings(craftingPlayer)

			inProcessOfCraft = true

			try {
				ForgeHooks.setCraftingPlayer(craftingPlayer)
				val residue: NonNullList<ItemStack>
				val result: ItemStack

				try {
					residue = craftingRecipe.getRemainingItems(craftingGrid)
					result = craftingRecipe.resultItem
				} finally {
					ForgeHooks.setCraftingPlayer(null)
				}

				check(residue.size == craftingGrid.containerSize) { "Container and residue list sizes mismatch: ${residue.size} != ${craftingGrid.containerSize}" }

				val combinedInventory = craftingPlayer.matteryPlayer?.inventoryAndExopack
				val copy = craftingGrid.iterator(false).map { it.copy() }.toList()

				// удаляем по одному предмету из сетки крафта
				for (slot in 0 until craftingGrid.containerSize) {
					craftingGrid.removeItem(slot, 1)

					if (residue[slot].isNotEmpty) {
						craftingGrid[slot] = residue[slot]
					}
				}

				if (!scanCraftingGrid(true)) {
					// заполняем опустевшие слоты
					for (slot in 0 until craftingGrid.containerSize) {
						if (
							craftingGrid[slot].isEmpty &&
							copy[slot].isNotEmpty &&
							settings.ingredientPriority.takeOne(copy[slot], poweredView, combinedInventory, craftingGridTuples[slot])
						) {
							craftingGrid[slot] = copy[slot].copyWithCount(1)
						}
					}
				}

				if (!scanCraftingGrid(true)) {
					// избавляемся от остатков, если они мешают
					for (slot in 0 until craftingGrid.containerSize) {
						if (residue[slot].isNotEmpty) {
							var remaining = residue[slot]

							if (settings.resultTarget != ItemMonitorPlayerSettings.ResultTarget.ALL_INVENTORY) {
								remaining = poweredView?.insertStack(ItemStorageStack(remaining), false)?.toItemStack() ?: remaining
							}

							remaining = combinedInventory?.addItem(remaining, false) ?: remaining

							if (remaining.isNotEmpty) {
								craftingPlayer.spawnAtLocation(remaining)
							}

							if (copy[slot].isNotEmpty && settings.ingredientPriority.takeOne(copy[slot], poweredView, combinedInventory, craftingGridTuples[slot])) {
								craftingGrid[slot] = copy[slot].copyWithCount(1)
							}
						}
					}
				}

				return result.copy()
			} finally {
				inProcessOfCraft = false
				scanCraftingGrid(false)
			}
		}

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

		override fun setItem(p_18944_: Int, p_18945_: ItemStack) {
			if ((craftingRecipe != null && !craftingRecipe!!.resultItem.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) {
			check(this.settings.put(UUID.fromString(key), ItemMonitorPlayerSettings().also { it.deserializeNBT(settings.getCompound(key)) }) == null)
		}

		craftingGrid.deserializeNBT(nbt["crafting_grid"] as? CompoundTag)
	}

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

	override fun tick() {
		super.tick()
		cell.tickEnergyDemanding()
	}

	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)
		scanCraftingGrid(false)
	}

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