package ru.dbotthepony.mc.otm.client.screen

import com.mojang.blaze3d.systems.RenderSystem
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import net.minecraft.ChatFormatting
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.network.chat.Component
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraftforge.client.event.ContainerScreenEvent.Render.Background
import net.minecraftforge.client.event.ContainerScreenEvent.Render.Foreground
import net.minecraftforge.common.MinecraftForge
import org.lwjgl.opengl.GL11
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.moveMousePosScaled
import ru.dbotthepony.mc.otm.client.render.translation
import ru.dbotthepony.mc.otm.client.screen.panels.*
import ru.dbotthepony.mc.otm.client.screen.panels.slot.AbstractSlotPanel
import ru.dbotthepony.mc.otm.client.screen.panels.slot.InventorySlotPanel
import ru.dbotthepony.mc.otm.client.screen.panels.util.DiscreteScrollBarPanel
import ru.dbotthepony.mc.otm.client.screen.panels.util.HeightControls
import ru.dbotthepony.mc.otm.client.screen.panels.util.ScrollBarConstants
import ru.dbotthepony.mc.otm.config.ClientConfig
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.core.math.component1
import ru.dbotthepony.mc.otm.core.math.component2
import ru.dbotthepony.mc.otm.core.math.integerDivisionDown
import ru.dbotthepony.mc.otm.menu.MatteryMenu
import java.util.*
import kotlin.collections.ArrayDeque
import kotlin.collections.List
import kotlin.collections.MutableSet
import kotlin.collections.indices
import kotlin.collections.isNotEmpty
import kotlin.collections.withIndex

/**
 * This class encapsulate most of logic for handling EditablePanel and it's children.
 *
 * Widgets such as slots are not drawn/interacted with using MatteryScreen,
 * but are handled by appropriate panels
 *
 * @param <T> type of menu, which extends MatteryMenu
 */
@Suppress("LeakingThis")
abstract class MatteryScreen<T : MatteryMenu>(menu: T, inventory: Inventory, title: Component) : AbstractContainerScreen<T>(menu, inventory, title) {
	constructor(menu: T, title: Component) : this(menu, menu.inventory, title)

	protected val panels = ArrayDeque<EditablePanel<*>>()
	val panelsView: List<EditablePanel<*>> = Collections.unmodifiableList(panels)

	var inventoryFrame: FramePanel<MatteryScreen<*>>? = null
	var mainFrame: FramePanel<MatteryScreen<*>>? = null
	private var madeMainFrame = false

	var font: Font
		get() = font
		set(font) { super.font = font }
	var hoveredSlot: Slot?
		get() = hoveredSlot
		set(value) { super.hoveredSlot = value }
	val quickCraftSlots: MutableSet<Slot> get() = quickCraftSlots
	val quickCraftingType get() = quickCraftingType
	val isQuickCrafting get() = isQuickCrafting

	fun renderItemStack(graphics: GuiGraphics, itemstack: ItemStack, countOverride: String? = null) {
		if (!itemstack.isEmpty) {
			RenderSystem.enableDepthTest()

			val (x, y) = graphics.pose().translation()

			graphics.renderItem(
				requireNotNull(ru.dbotthepony.mc.otm.client.minecraft.player) { "yo, dude, what the fuck" },
				itemstack,
				1,
				1,
				(x + y * ru.dbotthepony.mc.otm.client.minecraft.window.guiScaledWidth).toInt()
			)

			RenderSystem.depthFunc(GL11.GL_ALWAYS)
			graphics.renderItemDecorations(
				font,
				itemstack,
				1,
				1,
				countOverride
			)
		}
	}

	init {
		for (slot in menu.slots) {
			slot.x = Int.MAX_VALUE
			slot.y = Int.MAX_VALUE
		}
	}

	private val inventorySlotsRows = Int2ObjectOpenHashMap<EditablePanel<MatteryScreen<*>>>()
	private lateinit var slotListCanvas: EditablePanel<MatteryScreen<*>>
	private lateinit var inventoryScrollbar: DiscreteScrollBarPanel<MatteryScreen<*>>

	var inventoryRows = lastRows
		set(value) {
			val newValue = value.coerceAtLeast(MIN_ROWS).coerceAtMost(MAX_ROWS)
			val old = field

			if (field != newValue) {
				field = newValue
				updateInventoryRows(old)
				lastRows = newValue
			}
		}

	protected open fun updateInventoryRows(old: Int) {
		if (inventoryFrame == null) {
			return
		}

		slotListCanvas.height = AbstractSlotPanel.SIZE * inventoryRows
		inventoryFrame!!.height = BASE_INVENTORY_FRAME_HEIGHT + slotListCanvas.height

		for (i in inventoryScrollbar.scroll + inventoryRows until inventoryScrollbar.scroll + old) {
			getInventorySlotsRow(i).visible = false
		}

		for (i in inventoryScrollbar.scroll until inventoryScrollbar.scroll + inventoryRows) {
			getInventorySlotsRow(i).also {
				it.visible = true
				it.parent = slotListCanvas
				it.y = (i - inventoryScrollbar.scroll) * AbstractSlotPanel.SIZE
			}
		}

		inventoryScrollbar.scroll = inventoryScrollbar.scroll
	}

	protected fun makeInventoryRowsControls(parent: EditablePanel<*>, x: Float, y: Float, callback: (Float) -> Unit): HeightControls<*> {
		return HeightControls(this, parent, x, y) {
			inventoryRows += if (it) 1 else -1
			callback.invoke(if (it) -AbstractSlotPanel.SIZE / 2f else AbstractSlotPanel.SIZE / 2f)
			HeightControls.Status.of(inventoryRows < 6, inventoryRows > 3)
		}.also {
			it.controlStatus = HeightControls.Status.of(inventoryRows < 6, inventoryRows > 3)
		}
	}

	init {
		if (menu.playerInventorySlots.isNotEmpty() && menu.autoCreateInventoryFrame) {
			if (menu.playerExoSuitSlots.isEmpty()) {
				inventoryFrame = FramePanel<MatteryScreen<*>>(this, null, 0f, 0f, INVENTORY_FRAME_WIDTH, INVENTORY_FRAME_HEIGHT, inventory.displayName).also(this::addPanel)
				inventoryFrame!!.makeHelpButton().addSlotFiltersHelp().also {
					if (menu.ply.matteryPlayer?.hasExoPack == true)
						it.tooltips.add(TranslatableComponent("otm.gui.help.slot_charging"))
				}

				val hotbarStrip = EditablePanel(this, inventoryFrame, height = AbstractSlotPanel.SIZE)
				hotbarStrip.dock = Dock.BOTTOM

				for (slot in menu.playerHotbarSlots) {
					InventorySlotPanel(this, hotbarStrip, slot).also {
						it.dock = Dock.LEFT
					}
				}

				val canvas = EditablePanel(this, inventoryFrame)

				canvas.dock = Dock.FILL

				for (i in 0 .. 2) {
					getInventorySlotsRow(i).also {
						it.parent = inventoryFrame
						it.y = i * AbstractSlotPanel.SIZE
						it.parent = canvas
					}
				}
			} else {
				inventoryFrame = FramePanel<MatteryScreen<*>>(this, null, 0f, 0f, INVENTORY_FRAME_WIDTH_EXTENDED, BASE_INVENTORY_FRAME_HEIGHT + AbstractSlotPanel.SIZE * inventoryRows, inventory.displayName).also(this::addPanel)
				inventoryFrame!!.makeHelpButton().addSlotFiltersHelp().also {
					if (menu.ply.matteryPlayer?.hasExoPack == true)
						it.tooltips.add(TranslatableComponent("otm.gui.help.slot_charging"))
				}

				inventoryScrollbar = DiscreteScrollBarPanel(this, inventoryFrame, { integerDivisionDown(menu.playerCombinedInventorySlots.size, 9) }, {
						_, old, new ->

					for (i in old until old + inventoryRows) {
						getInventorySlotsRow(i).visible = false
					}

					for (i in new until new + inventoryRows) {
						getInventorySlotsRow(i).also {
							it.visible = true
							it.parent = slotListCanvas
							it.y = (i - new) * AbstractSlotPanel.SIZE
						}
					}

					lastScroll = new
				})

				slotListCanvas = object : EditablePanel<MatteryScreen<*>>(this@MatteryScreen, inventoryFrame, height = AbstractSlotPanel.SIZE * inventoryRows) {
					override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean {
						inventoryScrollbar.mouseScrolledInner(x, y, scroll)
						return true
					}
				}

				slotListCanvas.dock = Dock.TOP

				val hotbarStrip = EditablePanel(this, inventoryFrame, height = AbstractSlotPanel.SIZE)
				hotbarStrip.dock = Dock.BOTTOM

				for (slot in menu.playerHotbarSlots) {
					InventorySlotPanel(this, hotbarStrip, slot).also {
						it.dock = Dock.LEFT
					}
				}

				inventoryScrollbar.parent = slotListCanvas
				inventoryScrollbar.dock = Dock.RIGHT

				inventoryScrollbar.scroll = lastScroll

				for (i in inventoryScrollbar.scroll until inventoryScrollbar.scroll + inventoryRows) {
					getInventorySlotsRow(i).also {
						it.parent = slotListCanvas
						it.y = (i - inventoryScrollbar.scroll) * AbstractSlotPanel.SIZE
					}
				}

				makeInventoryRowsControls(inventoryFrame!!, inventoryFrame!!.width + 2f, 0f) { movePixels ->
					mainFrame?.let { it.y += movePixels }
					inventoryFrame?.let { it.y += movePixels }
					moveMousePosScaled(y = movePixels)
				}
			}
		}
	}

	protected fun getInventorySlotsRow(index: Int): EditablePanel<MatteryScreen<*>> {
		return inventorySlotsRows.computeIfAbsent(index, Int2ObjectFunction {
			val canvas = object : EditablePanel<MatteryScreen<*>>(this@MatteryScreen, null, width = AbstractSlotPanel.SIZE * 9f, height = AbstractSlotPanel.SIZE) {
				override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean {
					return false
				}
			}

			val offset = it * 9

			if (menu.playerCombinedInventorySlots.size <= offset) {
				return@Int2ObjectFunction canvas
			}

			for (i in 0 .. (8).coerceAtMost(menu.playerCombinedInventorySlots.size - offset - 1)) {
				val slot = menu.playerCombinedInventorySlots[offset + i]

				object : InventorySlotPanel<MatteryScreen<*>, MatteryMenu.InventorySlot>(this@MatteryScreen, canvas, slot) {
					init {
						dock = Dock.LEFT
					}

					override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean {
						return false
					}

					override var slotFilter: Item? by slot.filter!!
				}
			}

			return@Int2ObjectFunction canvas
		})
	}

	override fun init() {
		super.init()

		if (!madeMainFrame) {
			madeMainFrame = true
			mainFrame = makeMainFrame()?.also(this::addPanel)
			movePanels()
		}

		movePanels()
	}

	fun addPanel(panel: EditablePanel<MatteryScreen<*>>): Boolean {
		if (!panels.contains(panel)) {
			panels.addFirst(panel)
			return true
		}

		return false
	}

	fun removePanel(panel: EditablePanel<MatteryScreen<*>>): Boolean {
		val indexOf = panels.indexOf(panel)

		if (indexOf != -1) {
			panels.removeAt(indexOf)
			return true
		}

		return false
	}

	/**
	 * Moves panel at top of all other panels
	 *
	 * @param panel panel to be pushed up
	 */
	fun popup(panel: EditablePanel<MatteryScreen<*>>): Boolean {
		val indexOf = panels.indexOf(panel)
		if (indexOf <= 0) return false
		panels.removeAt(indexOf)
		panels.addFirst(panel)
		return true
	}

	/**
	 * Safely to be overriden. By default, creates well-known dimensions window
	 *
	 * can be freely overridden by child classes
	 *
	 * @return FramePanel created, or null
	 */
	protected open fun makeMainFrame(): FramePanel<MatteryScreen<*>>? {
		return FramePanel(this, null, 0f, 0f, DEFAULT_FRAME_WIDTH, DEFAULT_FRAME_HEIGHT, getTitle()).also {
			it.onClose { onClose() }
			it.makeCloseButton()
		}
	}

	public override fun recalculateQuickCraftRemaining() {
		super.recalculateQuickCraftRemaining()
	}

	fun movePanels() {
		var top = height / 2
		val mainFrame = mainFrame
		val inventoryFrame = inventoryFrame

		if (mainFrame != null) {
			top -= (mainFrame.height / 2).toInt()
		}

		if (inventoryFrame != null) {
			top -= (inventoryFrame.height / 2).toInt()
		}

		if (mainFrame != null) {
			mainFrame.setPos(width / 2 - mainFrame.width / 2, top.toFloat())
			top += mainFrame.height.toInt()
		}

		if (inventoryFrame != null) {
			inventoryFrame.setPos(width / 2 - inventoryFrame.width / 2, top.toFloat())
			top += inventoryFrame.height.toInt()
		}
	}

	override fun mouseClicked(x: Double, y: Double, button: Int): Boolean {
		var click = false
		var focusKilled = false

		for (panel in panels) {
			if (click || !panel.mouseClickedChecked(x, y, button)) {
				focusKilled = panel.killFocus() || focusKilled
			} else {
				if (returnSlot != null) {
					super.mouseClicked(x, y, button)
					returnSlot = null
				}

				click = true
			}
		}

		return click || focusKilled
	}

	private var lastDragSlot: Slot? = null

	override fun mouseDragged(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double): Boolean {
		for (panel in panels) {
			if (panel.mouseDraggedChecked(x, y, button, xDelta, yDelta)) {
				if (returnSlot != null) {
					super.mouseDragged(x, y, button, xDelta, yDelta)
					lastDragSlot = returnSlot
					returnSlot = null
				}

				return true
			}
		}

		return false
	}

	override fun mouseReleased(p_97812_: Double, p_97813_: Double, p_97814_: Int): Boolean {
		if (lastDragSlot != null) {
			if (isQuickCrafting) {
				returnSlot = lastDragSlot
				val ret = super.mouseReleased(p_97812_, p_97813_, p_97814_)
				returnSlot = null
				lastDragSlot = null
				return ret
			} else {
				lastDragSlot = null
			}
		}

		for (panel in panels) {
			if (panel.mouseReleasedChecked(p_97812_, p_97813_, p_97814_)) {
				if (returnSlot != null) {
					super.mouseReleased(p_97812_, p_97813_, p_97814_)
					returnSlot = null
					lastDragSlot = null
				}

				return true
			}
		}

		return super.mouseReleased(p_97812_, p_97813_, p_97814_)
	}

	override fun mouseScrolled(p_94686_: Double, p_94687_: Double, p_94688_: Double): Boolean {
		for (panel in panels) {
			if (panel.mouseScrolledChecked(p_94686_, p_94687_, p_94688_)) {
				return true
			}
		}

		return false
	}

	override fun keyReleased(p_94715_: Int, p_94716_: Int, p_94717_: Int): Boolean {
		for (panel in panels) {
			if (panel.keyReleased(p_94715_, p_94716_, p_94717_)) {
				return true
			}
		}

		return false
	}

	override fun charTyped(p_94683_: Char, p_94684_: Int): Boolean {
		for (panel in panels) {
			if (panel.charTyped(p_94683_, p_94684_)) {
				return true
			}
		}

		return false
	}

	override fun keyPressed(key: Int, scancode: Int, mods: Int): Boolean {
		for (panel in panels) {
			if (panel.keyPressed(key, scancode, mods)) {
				if (returnSlot != null) {
					super.keyPressed(key, scancode, mods)
					returnSlot = null
				}

				return true
			}
		}

		return super.keyPressed(key, scancode, mods)
	}

	override fun renderBg(p_283065_: GuiGraphics, p_97788_: Float, p_97789_: Int, p_97790_: Int) {}

	var returnSlot: Slot? = null

	override fun findSlot(x: Double, y: Double): Slot? {
		if (returnSlot != null) {
			return returnSlot
		}

		val xFloat = x.toFloat()
		val yFloat = y.toFloat()

		for (panel in panels) {
			val (status, slot) = panel.findSlot(xFloat, yFloat)

			if (status) {
				return slot
			}
		}

		return null
	}

	fun isMouseGrabbed(): Boolean {
		for (panel in panels) {
			if (panel.isGrabbingMouseInput()) {
				return true
			}
		}

		return false
	}

	override fun render(graphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
		val mouseXf = mouseX.toFloat()
		val mouseYf = mouseY.toFloat()

		// dark background
		this.renderBackground(graphics)

		hoveredSlot = null
		var hovered = false

		for (panel in panels) {
			if (hovered) {
				panel.unsetHovered()
			} else if (panel.tickHover(mouseXf, mouseYf)) {
				hovered = true
			}
		}

		RenderSystem.defaultBlendFunc()
		RenderSystem.enableBlend()
		RenderSystem.enableDepthTest()

		for (i in panels.indices.reversed()) {
			val panel = panels[i]
			RenderSystem.depthFunc(GL11.GL_ALWAYS)
			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
			panel.render(graphics, false, mouseXf, mouseYf, partialTick)
		}

		for (i in panels.indices.reversed()) {
			val panel = panels[i]
			RenderSystem.depthFunc(GL11.GL_ALWAYS)
			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
			panel.render(graphics, true, mouseXf, mouseYf, partialTick)
		}

		RenderSystem.depthFunc(GL11.GL_LESS)
		MinecraftForge.EVENT_BUS.post(Background(this, graphics, mouseX, mouseY))
		RenderSystem.disableDepthTest()

		// Screen.super.render
		for (widget in renderables) {
			widget.render(graphics, mouseX, mouseY, partialTick)
		}
		// /Screen.super.render

		RenderSystem.disableDepthTest()
		MinecraftForge.EVENT_BUS.post(Foreground(this, graphics, mouseX, mouseY))

		var itemstack = if (draggingItem.isEmpty) menu.carried else draggingItem

		if (!itemstack.isEmpty) {
			val i2 = if (draggingItem.isEmpty) 8 else 16
			var overrideCount: String? = null

			if (isQuickCrafting && quickCraftSlots.size > 1) {
				itemstack = itemstack.copy()
				itemstack.count = quickCraftingRemainder

				if (itemstack.isEmpty) {
					overrideCount = ChatFormatting.YELLOW.toString() + "0"
				}
			}

			@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
			renderFloatingItem(graphics, itemstack, mouseX - 8, mouseY - i2, overrideCount)
		}

		if (menu.carried.isEmpty) {
			val hoveredSlot = hoveredSlot

			if (hoveredSlot != null && hoveredSlot.hasItem()) {
				this.renderTooltip(graphics, mouseX, mouseY)
			} else {
				for (panel in panels) {
					RenderSystem.disableDepthTest()

					if (panel.renderTooltips(graphics, mouseXf, mouseYf, partialTick)) {
						break
					}
				}

				RenderSystem.enableDepthTest()
			}
		}
	}

	override fun containerTick() {
		val copy = arrayOfNulls<EditablePanel<*>>(panels.size)
		for ((i, panel) in panels.withIndex()) copy[i] = panel
		for (panel in copy) panel!!.tick()
	}

	companion object {
		const val DEFAULT_FRAME_WIDTH = AbstractSlotPanel.SIZE * 9f + 16f
		const val DEFAULT_FRAME_HEIGHT = 100f

		const val INVENTORY_FRAME_WIDTH = DEFAULT_FRAME_WIDTH
		const val INVENTORY_FRAME_WIDTH_EXTENDED = DEFAULT_FRAME_WIDTH + ScrollBarConstants.WIDTH + 2f
		const val INVENTORY_FRAME_HEIGHT = 3f * AbstractSlotPanel.SIZE + AbstractSlotPanel.SIZE + 24f
		const val BASE_INVENTORY_FRAME_HEIGHT = AbstractSlotPanel.SIZE + 24f

		const val GAUGE_TOP_WITH_SLOT = 17f
		const val GAUGE_TOP_WITHOUT_SLOT = 26f
		const val SLOT_TOP_UNDER_GAUGE = 70f
		const val PROGRESS_ARROW_TOP = 42f
		const val PROGRESS_SLOT_TOP = 41f
		const val LEFT_MARGIN = 8f

		const val MIN_ROWS = 3
		const val MAX_ROWS = 6

		var lastScroll = 0
		var lastRows by ClientConfig::EXOPACK_INVENTORY_ROWS
	}
}
