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

import com.google.common.collect.ImmutableList
import com.mojang.blaze3d.platform.InputConstants
import com.mojang.blaze3d.systems.RenderSystem
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.objects.ReferenceArraySet
import net.minecraft.client.gui.ComponentPath
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.events.GuiEventListener
import net.minecraft.client.gui.navigation.FocusNavigationEvent
import net.minecraft.client.gui.navigation.ScreenRectangle
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.renderer.Rect2i
import net.minecraft.network.chat.Component
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.ItemStack
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.SystemTime
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.client.moveMousePosScaled
import ru.dbotthepony.mc.otm.client.render.currentScissorRect
import ru.dbotthepony.mc.otm.client.render.popScissorRect
import ru.dbotthepony.mc.otm.client.render.pushScissorRect
import ru.dbotthepony.mc.otm.client.screen.MatteryScreen
import ru.dbotthepony.mc.otm.client.screen.panels.input.QueryUserPanel
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.roundToInt

data class ScreenPos(val x: Float, val y: Float)

data class DockProperty(val left: Float = 0f, val top: Float = 0f, val right: Float = 0f, val bottom: Float = 0f) {
	val isEmpty get() = left == 0f && right == 0f && top == 0f && bottom == 0f

	companion object {
		val EMPTY = DockProperty()
	}
}

enum class Dock {
	NONE, LEFT, RIGHT, TOP, BOTTOM, FILL
}

enum class DockResizeMode(val changeWidth: Boolean, val changeHeight: Boolean) {
	ALL(true, true), NONE(false, false), WIDTH(true, false), HEIGHT(false, true)
}

interface ISlotPanel<out S : Slot> {
	val slot: S
}

interface IItemStackPanel {
	val itemStack: ItemStack
}

data class Rect2f(val x: Float, val y: Float, val width: Float, val height: Float) {
	fun toIntRect(): Rect2i {
		return Rect2i(x.roundToInt(), y.roundToInt(), width.roundToInt(), height.roundToInt())
	}
}

open class EditablePanel<out S : Screen> @JvmOverloads constructor(
	val screen: S,
	parent: EditablePanel<*>?,

	// относительно родителя
	x: Float = 0f,
	y: Float = 0f,

	width: Float = 10f,
	height: Float = 10f,
) : Comparable<EditablePanel<*>> {
	// layout engine does not support navigation using keyboard
	// fuck off
	val listener: GuiEventListener = object : GuiEventListener {
		override fun setFocused(p_265728_: Boolean) {
			if (p_265728_) {
				if (isVisible())
					requestFocus()
			} else {
				killFocus()
			}
		}

		override fun isFocused(): Boolean {
			return isEverFocused()
		}

		override fun mouseMoved(p_94758_: Double, p_94759_: Double) {
			this@EditablePanel.mouseMoved(p_94758_, p_94759_)
		}

		override fun mouseClicked(p_94737_: Double, p_94738_: Double, p_94739_: Int): Boolean {
			return this@EditablePanel.mouseClicked(p_94737_, p_94738_, p_94739_)
		}

		override fun mouseReleased(p_94753_: Double, p_94754_: Double, p_94755_: Int): Boolean {
			return this@EditablePanel.mouseReleased(p_94753_, p_94754_, p_94755_)
		}

		override fun mouseDragged(p_94740_: Double, p_94741_: Double, p_94742_: Int, p_94743_: Double, p_94744_: Double): Boolean {
			return this@EditablePanel.mouseDragged(p_94740_, p_94741_, p_94742_, p_94743_, p_94744_)
		}

		override fun mouseScrolled(p_94734_: Double, p_94735_: Double, p_94736_: Double): Boolean {
			return this@EditablePanel.mouseScrolled(p_94734_, p_94735_, p_94736_)
		}

		override fun keyPressed(p_94745_: Int, p_94746_: Int, p_94747_: Int): Boolean {
			return this@EditablePanel.keyPressed(p_94745_, p_94746_, p_94747_)
		}

		override fun keyReleased(p_94750_: Int, p_94751_: Int, p_94752_: Int): Boolean {
			return this@EditablePanel.keyReleased(p_94750_, p_94751_, p_94752_)
		}

		override fun charTyped(p_94732_: Char, p_94733_: Int): Boolean {
			return this@EditablePanel.charTyped(p_94732_, p_94733_)
		}

		override fun nextFocusPath(p_265234_: FocusNavigationEvent): ComponentPath? {
			return null
		}

		override fun isMouseOver(p_94748_: Double, p_94749_: Double): Boolean {
			return this@EditablePanel.isMouseOver(p_94748_, p_94749_)
		}

		override fun getCurrentFocusPath(): ComponentPath? {
			return null
		}

		override fun getRectangle(): ScreenRectangle {
			val rect = calculateAbsoluteRectangle()
			return ScreenRectangle(rect.x.toInt(), rect.y.toInt(), rect.width.toInt(), rect.height.toInt())
		}
	}

	var childrenOrder = 0
		set(value) {
			if (field != value) {
				field = value
				parent?.sortChildren()
			}
		}

	override fun compareTo(other: EditablePanel<*>): Int {
		return childrenOrder.compareTo(other.childrenOrder)
	}

	var parent: EditablePanel<*>? = null
		set(value) {
			if (field === value)
				return

			val set = ReferenceArraySet<EditablePanel<*>>()
			set.add(this)
			var parent = value

			while (parent != null) {
				if (set.add(parent)) {
					parent = parent.parent
				} else {
					throw IllegalStateException("Cyclic parent reference detected: ${set.joinToString(", ") { "$it<${it::class.qualifiedName}>" }}")
				}
			}

			field?.onUnParent(this)
			field = value
			layoutInvalidated = true
			value?.onParent(this)
		}

	val topMostParent: EditablePanel<*>?
		get() {
			var parent = parent
			var parent2 = parent

			while (parent2 != null) {
				parent = parent2
				parent2 = parent.parent
			}

			return parent
		}

	var x: Float = 0f
		set(value) {
			if (field == value) return

			val old = field
			field = value
			// layoutInvalidated = true
			xPosUpdated(value, old)
			parent?.boundsInvalidated = true
		}

	var y: Float = 0f
		set(value) {
			if (field == value) return

			val old = field
			field = value
			// layoutInvalidated = true
			yPosUpdated(value, old)
			parent?.boundsInvalidated = true
		}

	var width: Float = 10f
		set(value) {
			if (field == value) return

			assert(value >= 0f) {"Invalid width $value"}

			val old = field
			field = value
			layoutInvalidated = true
			widthUpdated(value, old)
			parent?.boundsInvalidated = true
		}

	var height: Float = 10f
		set(value) {
			if (field == value) return

			assert(value >= 0f) {"Invalid height $value"}

			val old = field
			field = value
			layoutInvalidated = true
			heightUpdated(value, old)
			parent?.boundsInvalidated = true
		}

	/**
	 * Width to be utilized in docking code.
	 *
	 * Can only differ from [width] if [dockResize] is not [DockResizeMode.ALL] and not [DockResizeMode.WIDTH]
	 */
	var dockedWidth: Float = 0f
		private set

	/**
	 * Height to be utilized in docking code.
	 *
	 * Can only differ from [height] if [dockResize] is not [DockResizeMode.ALL] and not [DockResizeMode.HEIGHT]
	 */
	var dockedHeight: Float = 0f
		private set

	private val childrenInternal = ArrayList<EditablePanel<*>>()
	private val visibleChildrenInternal = ArrayList<EditablePanel<*>>()
	val children: List<EditablePanel<*>> = Collections.unmodifiableList(childrenInternal)
	val visibleChildren: List<EditablePanel<*>> = Collections.unmodifiableList(visibleChildrenInternal)

	var layoutInvalidated = true
		private set
	var boundsInvalidated = true
		private set

	protected var visibleAsChildren: Boolean = true
		private set

	var visible: Boolean = true
		set(value) {
			if (field != value) {
				val old = isVisible()
				field = value
				val new = isVisible()

				if (old != new) {
					visibilityChanges(new, old)
					val visibleChildrenParent = parent?.visibleChildrenInternal

					if (new) {
						killFocus()

						if (visibleChildrenParent?.contains(this) == false) {
							visibleChildrenParent.add(this)
							parent?.layoutInvalidated = true
						}
					} else {
						if (visibleChildrenParent?.contains(this) == true) {
							visibleChildrenParent.remove(this)
							parent?.layoutInvalidated = true
						}
					}

					updateVisible()
				}
			}
		}

	// Позволяет смещать потомков на эту координату
	var xOffset = 0f
	var yOffset = 0f

	// Обрезать отрисовку потомков?
	var scissor = false
	var scissorPadding = DockProperty.EMPTY
	var dock = Dock.NONE
		set(value) {
			if (field != value) {
				field = value
				parent?.layoutInvalidated = true
			}
		}
	var dockResize = DockResizeMode.ALL
		set(value) {
			if (field != value) {
				field = value
				parent?.layoutInvalidated = true
			}
		}
	var dockMargin = DockProperty.EMPTY
		set(value) {
			if (field != value) {
				field = value
				parent?.layoutInvalidated = true
			}
		}

	var dockLeft: Float
		get() = dockMargin.left
		set(value) {
			if (value != dockMargin.left) {
				dockMargin = dockMargin.copy(left = value)
			}
		}

	var dockRight: Float
		get() = dockMargin.right
		set(value) {
			if (value != dockMargin.right) {
				dockMargin = dockMargin.copy(right = value)
			}
		}

	var dockTop: Float
		get() = dockMargin.top
		set(value) {
			if (value != dockMargin.top) {
				dockMargin = dockMargin.copy(top = value)
			}
		}

	var dockBottom: Float
		get() = dockMargin.bottom
		set(value) {
			if (value != dockMargin.bottom) {
				dockMargin = dockMargin.copy(bottom = value)
			}
		}

	var dockPadding = DockProperty.EMPTY
		set(value) {
			if (field != value) {
				field = value
				parent?.layoutInvalidated = true
			}
		}

	var acceptMouseInput = true
	var acceptKeyboardInput = true
	var grabMouseInput = false

	open val needsPostRender: Boolean get() = false

	fun tryToGrabMouseInput(): Boolean {
		if (grabMouseInput) {
			return true
		}

		if (screen !is MatteryScreen<*>) {
			grabMouseInput = true
			return true
		}

		if (screen.isMouseGrabbed()) {
			return false
		}

		grabMouseInput = true
		return true
	}

	val tooltips = ArrayList<Component>()

	var blockingWindow: EditablePanel<*>? = null
		get() {
			if (field?.isRemoved != true) {
				return field
			}

			field = null
			return null
		}

	var flashingSince: SystemTime? = null
		private set

	var isFlashing: Boolean
		get() {
			val flashingSince = flashingSince ?: return false

			if (flashingSince.millis >= 1200L) {
				this.flashingSince = null
				return false
			}

			return true
		}
		set(value) {
			if (value) {
				if (screen is MatteryScreen<*>) {
					screen.popup(findAbsoluteRoot() as EditablePanel<MatteryScreen<*>>)
				}

				if (!flashAnyBlocker(true)) {
					flashingSince = SystemTime()
					findAbsoluteRoot().requestFocus()
				}
			} else {
				flashingSince = null
			}
		}

	val isFlashFrame: Boolean
		get() = isFlashing && flashingSince!!.millis % 400L <= 200L

	val isFlashFrameRecursive: Boolean
		get() {
			if (isFlashFrame) {
				return true
			}

			var parent = parent

			while (parent != null) {
				if (parent.isFlashFrame) {
					return true
				}

				parent = parent.parent
			}

			return false
		}

	private fun flashAnyBlockerInner(doFlash: Boolean): Boolean {
		val blockingWindow = blockingWindow

		if (blockingWindow != null) {
			if (doFlash) {
				blockingWindow.isFlashing = true
			}

			return true
		}

		for (children in childrenInternal) {
			if (children.flashAnyBlockerInner(doFlash)) {
				return true
			}
		}

		return false
	}

	protected fun flashAnyBlocker(doFlash: Boolean = true): Boolean {
		return findAbsoluteRoot().flashAnyBlockerInner(doFlash)
	}

	@JvmOverloads
	fun setDockMargin(left: Float = 0f, top: Float = 0f, right: Float = 0f, bottom: Float = 0f) {
		dockMargin = DockProperty(left, top, right, bottom)
	}

	@JvmOverloads
	fun setDockPadding(left: Float = 0f, top: Float = 0f, right: Float = 0f, bottom: Float = 0f) {
		dockPadding = DockProperty(left, top, right, bottom)
	}

	/**
	 * Rectangle which hold all children in panel-local (to this panel) coordinates
	 */
	var childrenRectX = 0f
		private set

	/**
	 * Rectangle which hold all children in panel-local (to this panel) coordinates
	 */
	var childrenRectY = 0f
		private set

	/**
	 * Rectangle which hold all children in panel-local (to this panel) coordinates
	 */
	var childrenRectWidth = 0f
		private set

	/**
	 * Rectangle which hold all children in panel-local (to this panel) coordinates
	 */
	var childrenRectHeight = 0f
		private set

	val isOutsideOfParent: Boolean get() {
		val parent = parent ?: return false
		return x < 0 || y < 0 || x + width > parent.width || y + height > parent.height
	}

	private var once = false
		set(value) {
			if (value && field != value) {
				if (updateVisibility) {
					updateVisible()
				}

				field = value
			}
		}

	private var updateVisibility = false

	fun calculateAbsoluteRectangle(): Rect2f {
		if (scissor) {
			return Rect2f(absoluteX, absoluteY, width, height)
		}

		val x = if (childrenRectX < 0) childrenRectX + absoluteX else absoluteX
		val y = if (childrenRectY < 0) childrenRectY + absoluteY else absoluteY
		val width = if (childrenRectWidth > width) childrenRectWidth else width
		val height = if (childrenRectHeight > height) childrenRectHeight else height

		return Rect2f(x, y, width, height)
	}

	fun calculateAbsoluteRectangles(): List<Rect2f> {
		if (scissor) {
			return listOf(Rect2f(absoluteX, absoluteY, width, height))
		}

		if (childrenRectX >= 0 && childrenRectY >= 0 && childrenRectWidth <= width && childrenRectHeight <= height) {
			return listOf(Rect2f(absoluteX, absoluteY, width, height))
		}

		val result = ArrayList<Rect2f>()
		result.add(Rect2f(absoluteX, absoluteY, width, height))

		for (children in childrenInternal) {
			if (children.isOutsideOfParent && children.isVisible()) {
				result.addAll(children.calculateAbsoluteRectangles())
			}
		}

		return result
	}

	/**
	 * Whenever [calculateAbsoluteObstructingRectangle] and [calculateAbsoluteObstructingRectangles] treat
	 * *this* panel visible. Does not affect children.
	 */
	var isObstructing = true
		protected set

	fun calculateAbsoluteObstructingRectangle(): Rect2f {
		if (!isObstructing) {
			return Rect2f(childrenRectX + absoluteX, childrenRectY + absoluteY, childrenRectWidth, childrenRectHeight)
		}

		return calculateAbsoluteRectangle()
	}

	fun calculateAbsoluteObstructingRectangles(): List<Rect2f> {
		if ((scissor || childrenRectX >= 0 && childrenRectY >= 0 && childrenRectWidth <= width && childrenRectHeight <= height) && isObstructing) {
			return listOf(Rect2f(absoluteX, absoluteY, width, height))
		}

		if (scissor && !isObstructing) {
			// TODO: NYI
			return listOf(Rect2f(absoluteX, absoluteY, width, height))
		}

		val result = ArrayList<Rect2f>()

		if (isObstructing) {
			result.add(Rect2f(absoluteX, absoluteY, width, height))
		}

		for (children in visibleChildrenInternal) {
			if (children.isOutsideOfParent || !isObstructing) {
				result.addAll(children.calculateAbsoluteObstructingRectangles())
			}
		}

		return result
	}

	// Абсолютная координата, обновляется на отрисовке
	var absoluteX = 0f
		private set
	var absoluteY = 0f
		private set

	var isHovered = false
		private set(value) {
			if (value == field) {
				return
			}

			val old = field
			field = value

			onHoverUpdate(old, value)
		}

	fun unsetHovered() {
		isHovered = false

		for (child in childrenInternal) {
			child.unsetHovered()
		}
	}

	protected open fun onHoverUpdate(oldHover: Boolean, newHover: Boolean) {
		if (newHover) {
			onHovered()
		} else {
			onUnHovered()
		}
	}

	protected open fun onHovered() {}
	protected open fun onUnHovered() {}

	var hasFocusedChildren = false
		private set(value) {
			if (field != value) {
				field = value
				onHierarchicalFocusChanged()
			}
		}

	var isFocusedThis = false
		private set(value) {
			if (field != value) {
				field = value
				onFocusChanged()
			}
		}

	var autoRequestFocus = true

	val font: Font get() = if (screen is MatteryScreen<*>) screen.font else minecraft.font

	fun invalidateLayout() {
		layoutInvalidated = true
	}

	init {
		this.x = x
		this.y = y
		this.width = width
		this.height = height
		this.parent = parent
	}

	var childrenSortingInvalidated = false
		private set

	fun invalidateChildrenSorting() {
		childrenSortingInvalidated = true
	}

	private fun onParent(child: EditablePanel<*>) {
		if (childrenInternal.contains(child)) throw IllegalStateException("Already containing $child")
		childrenInternal.add(child)
		invalidateChildrenSorting()

		if (child.visible) {
			visibleChildrenInternal.add(child)
			layoutInvalidated = true
		}

		if (child.isVisible() != isVisible()) {
			if (once)
				updateVisible()
			else
				updateVisibility = true
		}
	}

	private fun onUnParent(child: EditablePanel<*>) {
		val indexOf = childrenInternal.indexOf(child)
		if (indexOf == -1) throw IllegalStateException("Already not containing $child")
		childrenInternal.removeAt(indexOf)
		invalidateChildrenSorting()

		if (child.visible) {
			visibleChildrenInternal.remove(child)
			layoutInvalidated = true
		}

		if (child.isVisible() != isVisible()) {
			updateVisible()
		}
	}

	private fun sortChildren() {
		childrenSortingInvalidated = false
		childrenInternal.sort()

		val old = ArrayList(visibleChildrenInternal)
		visibleChildrenInternal.sort()

		if (old != visibleChildrenInternal) {
			invalidateLayout()
		}
	}

	protected open fun innerRender(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {}
	protected open fun innerRenderPost(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {}
	protected open fun innerRenderTooltips(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float): Boolean {
		return false
	}

	protected open fun shouldRenderTooltips(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float): Boolean {
		return isHovered || isGrabbingMouseInput()
	}

	fun isVisible(): Boolean {
		return visible && visibleAsChildren && !isRemoved
	}

	fun isScissored(): Boolean {
		var parent = parent

		while (parent != null) {
			if (parent.scissor) {
				return true
			}

			parent = parent.parent
		}

		return false
	}

	fun performLayoutIfNeeded() {
		if (childrenSortingInvalidated)
			sortChildren()

		if (layoutInvalidated) {
			performLayout()
		} else if (boundsInvalidated) {
			updateBounds()
		}
	}

	fun updateAbsoluteCoordinates() {
		if (parent == null) {
			absoluteX = x
			absoluteY = y
		}

		for (child in visibleChildrenInternal) {
			child.absoluteX = absoluteX + child.x + xOffset
			child.absoluteY = absoluteY + child.y + yOffset

			child.updateAbsoluteCoordinates()
		}
	}

	fun render(graphics: GuiGraphics, post: Boolean, mouseX: Float, mouseY: Float, partialTick: Float) {
		once = true

		if (!isVisible()) {
			return
		}

		val poseStack = graphics.pose()

		if (!post) {
			performLayoutIfNeeded()

			val parent = this.parent

			if (parent == null) {
				absoluteX = x
				absoluteY = y
			} else {
				if ((x < 0f || y < 0f || x + width > parent.width || y + height > parent.height) && parent.parent == null) {
					// no op - we updated ourselves in tickHover
				} else {
					isHovered = parent.isHovered &&
							mouseX >= absoluteX &&
							mouseX < absoluteX + width &&
							mouseY >= absoluteY &&
							mouseY < absoluteY + height
				}
			}
		}

		val scissor = this.scissor

		if (scissor) {
			val scale = minecraft.window.guiScale

			val (left, top, right, bottom) = scissorPadding

			pushScissorRect(
				((absoluteX + left) * scale).toInt(),
				((absoluteY + top) * scale).toInt(),
				((width - right - left) * scale).toInt() + 1,
				((height - bottom - top) * scale).toInt() + 1,
			)
		}

		val currentScissorRect = currentScissorRect

		if ((!post || needsPostRender) && (currentScissorRect == null || currentScissorRect.crossScaled(absoluteX, absoluteY, width, height))) {
			// do not render if we are getting cut off by screen scissor
			clearDepth(graphics, absoluteX, absoluteY, width, height)
			poseStack.pushPose()
			poseStack.translate(absoluteX.toDouble(), absoluteY.toDouble(), 10.0)
			RenderSystem.setShaderColor(1f, 1f, 1f, if (isFlashFrameRecursive) 0.5f else 1f)

			if (post)
				innerRenderPost(graphics, mouseX, mouseY, partialTick)
			else
				innerRender(graphics, mouseX, mouseY, partialTick)

			RenderSystem.setShaderColor(1f, 1f, 1f, 1f)
			poseStack.popPose()
		}

		for (child in visibleChildrenInternal) {
			child.absoluteX = absoluteX + child.x + xOffset
			child.absoluteY = absoluteY + child.y + yOffset

			child.render(graphics, post, mouseX, mouseY, partialTick)
		}

		if (scissor) {
			popScissorRect()
		}
	}

	open fun tickHover(mouseX: Float, mouseY: Float): Boolean {
		if (isRemoved) {
			return false
		}

		if ((childrenRectHeight > height || childrenRectWidth > width || childrenRectX < 0 || childrenRectY < 0) && parent == null) {
			var hit = false

			for (child in childrenInternal) {
				if (child.tickHover(mouseX, mouseY)) {
					hit = true
				}
			}

			isHovered = mouseX >= absoluteX &&
					mouseX <= absoluteX + width &&
					mouseY >= absoluteY &&
					mouseY <= absoluteY + height

			return hit || isHovered
		}

		isHovered =
				mouseX >= absoluteX &&
				mouseX <= absoluteX + width &&
				mouseY >= absoluteY &&
				mouseY <= absoluteY + height

		return isHovered
	}

	fun findSlot(mouseX: Float, mouseY: Float): Pair<Boolean, Slot?> {
		if (!isVisible()) {
			return false to null
		}

		if (!acceptMouseInput) {
			return (mouseX >= absoluteX &&
					mouseX <= absoluteX + width &&
					mouseY >= absoluteY &&
					mouseY <= absoluteY + height) to null
		}

		if (grabMouseInput && this is ISlotPanel<*>) {
			return true to this.slot
		}

		for (child in visibleChildrenInternal) {
			val (status, slot) = child.findSlot(mouseX, mouseY)

			if (status) {
				return true to slot
			}
		}

		if (
			mouseX >= absoluteX &&
			mouseX <= absoluteX + width &&
			mouseY >= absoluteY &&
			mouseY <= absoluteY + height
		) {
			if (this is ISlotPanel<*>) {
				return true to this.slot
			}

			return true to null
		}

		return false to null
	}

	fun findItemStack(mouseX: Float, mouseY: Float, ignoreMouseInputLock: Boolean = false): Pair<EditablePanel<*>?, ItemStack> {
		if (!isVisible()) {
			return null to ItemStack.EMPTY
		}

		if (!acceptMouseInput && !ignoreMouseInputLock) {
			if (mouseX >= absoluteX &&
				mouseX <= absoluteX + width &&
				mouseY >= absoluteY &&
				mouseY <= absoluteY + height) {
				return this to ItemStack.EMPTY
			} else {
				return null to ItemStack.EMPTY
			}
		}

		if (grabMouseInput && this is IItemStackPanel) {
			return this to this.itemStack
		}

		for (child in visibleChildrenInternal) {
			val status = child.findItemStack(mouseX, mouseY, ignoreMouseInputLock)

			if (status.first != null) {
				return status
			}
		}

		if (
			mouseX >= absoluteX &&
			mouseX <= absoluteX + width &&
			mouseY >= absoluteY &&
			mouseY <= absoluteY + height
		) {
			if (this is IItemStackPanel) {
				return this to this.itemStack
			}

			return this to ItemStack.EMPTY
		}

		return null to ItemStack.EMPTY
	}

	fun renderTooltips(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float): Boolean {
		if (!isVisible()) {
			return false
		}

		val currentScissorRect = currentScissorRect

		if (currentScissorRect != null && !currentScissorRect.crossScaled(absoluteX, absoluteY, width, height)) {
			return false
		}

		for (child in visibleChildrenInternal) {
			if (child.renderTooltips(graphics, mouseX, mouseY, partialTick)) {
				return true
			}
		}

		if (shouldRenderTooltips(graphics, mouseX, mouseY, partialTick)) {
			if (innerRenderTooltips(graphics, mouseX, mouseY, partialTick)) {
				return true
			}

			if (tooltips.isNotEmpty()) {
				graphics.renderComponentTooltip(
					font,
					tooltips,
					mouseX.toInt(),
					mouseY.toInt()
				)

				return true
			}
		}

		return false
	}

	@JvmOverloads
	fun localToScreen(_x: Float = 0f, _y: Float = 0f): ScreenPos {
		var parent = this.parent

		var x = this.x + _x
		var y = this.y + _y

		while (parent != null) {
			x += parent.x + parent.xOffset
			y += parent.y + parent.yOffset
			parent = parent.parent
		}

		return ScreenPos(x, y)
	}

	fun screenToLocal(_x: Float = 0f, _y: Float = 0f): ScreenPos {
		val pos = localToScreen()
		return ScreenPos(_x - pos.x, _y - pos.y)
	}

	fun screenToLocal(_x: Double = 0.0, _y: Double = 0.0): ScreenPos {
		val pos = localToScreen()
		return ScreenPos((_x - pos.x).toFloat(), (_y - pos.y).toFloat())
	}

	fun findRoot(): EditablePanel<*>? {
		var parent: EditablePanel<*>? = this.parent ?: return null

		while (true) {
			val parent2 = parent!!.parent

			if (parent2 != null) {
				parent = parent2
			} else {
				return parent
			}
		}
	}

	fun findAbsoluteRoot(): EditablePanel<*> {
		return findRoot() ?: this
	}

	/**
	 * Performs layout instantly instead of doing so on next frame.
	 *
	 * In almost all cases you want to call [invalidateLayout], and not this.
	 */
	open fun performLayout() {
		layoutInvalidated = false

		var left = dockPadding.left
		var right = dockPadding.right
		var top = dockPadding.top
		var bottom = dockPadding.bottom

		for (child in visibleChildrenInternal) {
			when (child.dock) {
				Dock.NONE -> {}
				Dock.FILL -> {}

				Dock.LEFT -> {
					child.x = left + child.dockMargin.left
					child.y = top + child.dockMargin.top

					left += child.width + child.dockMargin.left + child.dockMargin.right

					child.dockedHeight = 1f.coerceAtLeast(height - top - bottom - child.dockMargin.top - child.dockMargin.bottom)

					if (child.dockResize.changeHeight)
						child.height = child.dockedHeight
					else if (child.height != child.dockedHeight)
						child.y += child.dockedHeight / 2f - child.height / 2f
				}

				Dock.RIGHT -> {
					child.x = width - right - child.width - child.dockMargin.right
					child.y = top + child.dockMargin.top

					right += child.width + child.dockMargin.left + child.dockMargin.right

					child.dockedHeight = 1f.coerceAtLeast(height - top - bottom - child.dockMargin.top - child.dockMargin.bottom)

					if (child.dockResize.changeHeight)
						child.height = child.dockedHeight
					else if (child.height != child.dockedHeight)
						child.y += child.dockedHeight / 2f - child.height / 2f
				}

				Dock.TOP -> {
					child.x = left + child.dockMargin.left
					child.y = top + child.dockMargin.top

					top += child.height + child.dockMargin.top + child.dockMargin.bottom

					child.dockedWidth = 1f.coerceAtLeast(width - left - right - child.dockMargin.left - child.dockMargin.right)

					if (child.dockResize.changeWidth)
						child.width = child.dockedWidth
					else if (child.width != child.dockedWidth)
						child.x += child.dockedWidth / 2f - child.width / 2f
				}

				Dock.BOTTOM -> {
					child.x = left + child.dockMargin.left
					child.y = height - bottom - child.height - child.dockMargin.bottom

					bottom += child.height + child.dockMargin.top + child.dockMargin.bottom

					child.dockedWidth = 1f.coerceAtLeast(width - left - right - child.dockMargin.left - child.dockMargin.right)

					if (child.dockResize.changeWidth)
						child.width = child.dockedWidth
					else if (child.width != child.dockedWidth)
						child.x += child.dockedWidth / 2f - child.width / 2f
				}
			}
		}

		for (child in visibleChildrenInternal) {
			if (child.dock == Dock.FILL) {
				val w = width - child.dockMargin.left - child.dockMargin.right - right - left
				val h = height - child.dockMargin.bottom - child.dockMargin.top - bottom - top

				if (w < 1f || h < 1f) {
					LOGGER.error("Unable to fit $child inside $this (FILL returned dimensions of $w $h)")
				}

				child.x = left + child.dockMargin.left
				child.y = top + child.dockMargin.top

				child.dockedWidth = w.coerceAtLeast(1f)
				child.dockedHeight = h.coerceAtLeast(1f)

				if (child.dockResize.changeHeight)
					child.height = child.dockedHeight
				else if (child.height != child.dockedHeight)
					child.y += child.dockedHeight / 2f - child.height / 2f

				if (child.dockResize.changeWidth)
					child.width = child.dockedWidth
				else if (child.width != child.dockedWidth)
					child.x += child.dockedWidth / 2f - child.width / 2f
			}
		}

		updateBounds()

		for (child in visibleChildrenInternal) {
			child.parentLayoutPerformed()
		}
	}

	private var repositioningCallback: Runnable? = null

	fun customDock(callback: Runnable?) {
		if (callback != null) dock = Dock.NONE
		repositioningCallback = callback
	}

	open fun parentLayoutPerformed() {
		repositioningCallback?.run()
	}

	fun updateBounds() {
		boundsInvalidated = false

		childrenRectX = width
		childrenRectY = height
		childrenRectWidth = 0f
		childrenRectHeight = 0f

		for (child in visibleChildrenInternal) {
			childrenRectX = childrenRectX.coerceAtMost(child.x)
			childrenRectY = childrenRectY.coerceAtMost(child.y)
			childrenRectWidth = childrenRectWidth.coerceAtLeast(child.x + child.width)
			childrenRectHeight = childrenRectHeight.coerceAtLeast(child.y + child.height)
		}

		childrenRectX = childrenRectX.coerceAtMost(childrenRectWidth)
		childrenRectY = childrenRectY.coerceAtMost(childrenRectHeight)

		parent?.updateBounds()
	}

	/**
	 * Attempts to tightly fit dimensions to all children
	 *
	 * Performs layout if required
	 */
	open fun sizeToContents() {
		if (layoutInvalidated) {
			performLayout()
		}

		var width = 1f
		var height = 1f

		for (child in visibleChildrenInternal) {
			width = width.coerceAtLeast(child.x + child.width)
			height = height.coerceAtLeast(child.y + child.height)
		}

		this.width = width
		this.height = height
	}

	protected open fun visibilityChanges(new: Boolean, old: Boolean) {

	}

	private fun updateVisible() {
		val isVisible = isVisible()

		for (child in childrenInternal) {
			val old = child.isVisible()
			child.visibleAsChildren = isVisible
			val new = child.isVisible()

			if (old != new) {
				child.visibilityChanges(new, old)
			}

			child.updateVisible()
		}
	}

	private fun killGrabMouseInput() {
		for (child in childrenInternal) {
			child.killGrabMouseInput()
		}

		grabMouseInput = false
	}

	private fun killFocusThis(children: Boolean): Boolean {
		var status = false

		if (isFocusedThis) {
			isFocusedThis = false
			status = true
		}

		if (children && hasFocusedChildren) {
			hasFocusedChildren = false
			status = true
		}

		if (grabMouseInput) {
			grabMouseInput = false
			status = true
		}

		return status
	}

	fun killFocus(): Boolean {
		var status = false
		status = killFocusThis(true) || status

		for (child in childrenInternal) {
			status = child.killFocus() || status
		}

		return status
	}

	fun findHierarchicalFocus(): EditablePanel<*>? {
		if (isFocusedThis) {
			return this
		}

		for (child in childrenInternal) {
			val focus = child.findHierarchicalFocus()

			if (focus != null) {
				return focus
			}
		}

		return null
	}

	fun hasHierarchicalFocus(): Boolean {
		return findHierarchicalFocus() != null
	}

	/**
	 * Called when [hasFocusedChildren] changes
	 */
	protected open fun onHierarchicalFocusChanged() {}

	/**
	 * Called when [isFocusedThis] changes
	 */
	protected open fun onFocusChanged() {}

	fun isEverFocused(): Boolean {
		return isFocusedThis || hasFocusedChildren
	}

	fun requestFocus() {
		if (isFocusedThis || !isVisible()) {
			return
		}

		var filter: EditablePanel<*>? = null
		var parent: EditablePanel<*>? = this

		while (parent != null) {
			if (parent.parent == null) {
				parent.popup(false)
			}

			for (child in parent.childrenInternal) {
				if (child !== filter) {
					child.killFocus()
				}
			}

			filter = parent
			parent = parent.parent

			if (parent != null) {
				parent.killFocusThis(false)
				parent.hasFocusedChildren = true
			}
		}

		isFocusedThis = true
	}

	protected open fun xPosUpdated(new: Float, old: Float) {}
	protected open fun yPosUpdated(new: Float, old: Float) {}
	protected open fun widthUpdated(new: Float, old: Float) {}
	protected open fun heightUpdated(new: Float, old: Float) {}

	fun setPos(x: Float, y: Float) {
		this.x = x
		this.y = y
	}

	fun setPos(x: Double, y: Double) {
		this.x = x.toFloat()
		this.y = y.toFloat()
	}

	fun setSize(w: Float, h: Float) {
		this.width = w
		this.height = h
	}

	fun setSize(w: Double, h: Double) {
		this.width = w.toFloat()
		this.height = h.toFloat()
	}

	fun setDimensions(x: Float, y: Float, w: Float, h: Float) {
		setPos(x, y)
		setSize(w, h)
	}

	fun setDimensions(x: Double, y: Double, w: Double, h: Double) {
		setPos(x, y)
		setSize(w, h)
	}

	fun fetchChildren(): List<EditablePanel<*>> {
		return ImmutableList.copyOf(childrenInternal)
	}

	fun getUndockedChildren(): List<EditablePanel<*>> {
		val list = LinkedList<EditablePanel<*>>()

		for (child in childrenInternal) {
			if (child.dock == Dock.NONE) {
				list.add(child)
			}
		}

		return list
	}

	fun getUndockedVisibleChildren(): List<EditablePanel<*>> {
		val list = LinkedList<EditablePanel<*>>()

		for (child in visibleChildrenInternal) {
			if (child.dock == Dock.NONE) {
				list.add(child)
			}
		}

		return list
	}

	fun getChildren(index: Int): EditablePanel<*>? {
		if (index < 0 || index >= childrenInternal.size) return null
		return childrenInternal[index]
	}

	fun isGrabbingMouseInput(): Boolean {
		if (grabMouseInput) {
			return true
		}

		for (child in childrenInternal) {
			if (child.isGrabbingMouseInput()) {
				return true
			}
		}

		return false
	}

	open fun mouseMoved(x: Double, y: Double) {

	}

	protected open fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
		return true
	}

	fun withinBounds(x: Double, y: Double): Boolean {
		val pos: ScreenPos = localToScreen()
		return pos.x <= x && pos.x + width > x && pos.y <= y && pos.y + height > y
	}

	fun withinExtendedBounds(x: Double, y: Double): Boolean {
		val pos: ScreenPos = localToScreen(childrenRectX.coerceAtMost(0f), childrenRectY.coerceAtMost(0f))
		val pos2: ScreenPos = localToScreen(childrenRectWidth.coerceAtLeast(width), childrenRectHeight.coerceAtLeast(height))

		return x in pos.x .. pos2.x &&
				y in pos.y .. pos2.y
	}

	fun mouseClicked(x: Double, y: Double, button: Int): Boolean {
		if (!isVisible() || !acceptMouseInput) return false
		if (flashAnyBlocker()) return true

		if (grabMouseInput) {
			if (mouseClickedInner(x, y, button)) {
				if (autoRequestFocus) requestFocus()
				return true
			}

			return false
		}

		for (child in visibleChildrenInternal) {
			if (child.isGrabbingMouseInput() && child.mouseClickedChecked(x, y, button)) {
				return true
			}
		}

		for (child in visibleChildrenInternal) {
			if (child.mouseClickedChecked(x, y, button)) {
				return true
			}
		}

		if (autoRequestFocus) requestFocus()
		return mouseClickedInner(x, y, button)
	}

	fun mouseClickedChecked(x: Double, y: Double, button: Int): Boolean {
		if (!isVisible() || !acceptMouseInput) return false

		if (isGrabbingMouseInput() || withinBounds(x, y)) {
			return mouseClicked(x, y, button)
		} else if (withinExtendedBounds(x, y)) {
			popup(false)

			for (child in visibleChildrenInternal) {
				if (child.mouseClickedChecked(x, y, button)) {
					return true
				}
			}
		}

		return false
	}

	protected open fun mouseReleasedInner(x: Double, y: Double, button: Int): Boolean {
		return true
	}

	fun mouseReleased(x: Double, y: Double, button: Int): Boolean {
		if (!isVisible() || !acceptMouseInput) return false
		if (flashAnyBlocker(false)) return true
		if (grabMouseInput) return mouseReleasedInner(x, y, button)

		for (child in visibleChildrenInternal) {
			if (child.isGrabbingMouseInput() && child.mouseReleasedChecked(x, y, button)) {
				return true
			}
		}

		for (child in visibleChildrenInternal) {
			if (child.mouseReleasedChecked(x, y, button)) {
				return true
			}
		}

		return mouseReleasedInner(x, y, button)
	}

	fun mouseReleasedChecked(x: Double, y: Double, button: Int): Boolean {
		if (!isVisible() || !acceptMouseInput) return false

		if (isGrabbingMouseInput() || withinBounds(x, y)) {
			return mouseReleased(x, y, button)
		} else if (withinExtendedBounds(x, y)) {
			for (child in visibleChildrenInternal) {
				if (child.mouseReleasedChecked(x, y, button)) {
					return true
				}
			}
		}

		return false
	}

	protected open fun mouseDraggedInner(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double): Boolean {
		return true
	}

	fun mouseDragged(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double): Boolean {
		if (!isVisible() || !acceptMouseInput) return false
		if (flashAnyBlocker(false)) return true
		if (grabMouseInput) return mouseDraggedInner(x, y, button, xDelta, yDelta)

		for (child in visibleChildrenInternal) {
			if (child.isGrabbingMouseInput() && child.mouseDraggedChecked(x, y, button, xDelta, yDelta)) {
				return true
			}
		}

		for (child in visibleChildrenInternal) {
			if (child.mouseDraggedChecked(x, y, button, xDelta, yDelta)) {
				return true
			}
		}

		return mouseDraggedInner(x, y, button, xDelta, yDelta)
	}

	fun mouseDraggedChecked(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double): Boolean {
		if (!isVisible() || !acceptMouseInput) return false

		if (isGrabbingMouseInput() || withinBounds(x, y)) {
			return mouseDragged(x, y, button, xDelta, yDelta)
		} else if (withinExtendedBounds(x, y)) {
			for (child in visibleChildrenInternal) {
				if (child.mouseDraggedChecked(x, y, button, xDelta, yDelta)) {
					return true
				}
			}
		}

		return false
	}

	protected open fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean {
		return true
	}

	fun mouseScrolled(x: Double, y: Double, scroll: Double): Boolean {
		if (!isVisible() || !acceptMouseInput) return false

		if (flashAnyBlocker(true)) {
			return true
		}

		if (grabMouseInput) return mouseScrolledInner(x, y, scroll)

		for (child in visibleChildrenInternal) {
			if (child.isGrabbingMouseInput() && child.mouseScrolledChecked(x, y, scroll)) {
				return true
			}
		}

		for (child in visibleChildrenInternal) {
			if (child.mouseScrolledChecked(x, y, scroll)) {
				return true
			}
		}

		return mouseScrolledInner(x, y, scroll)
	}

	fun mouseScrolledChecked(x: Double, y: Double, scroll: Double): Boolean {
		if (!isVisible() || !acceptMouseInput) return false

		if (isGrabbingMouseInput() || withinBounds(x, y)) {
			return mouseScrolled(x, y, scroll)
		} else if (withinExtendedBounds(x, y)) {
			for (child in visibleChildrenInternal) {
				if (child.mouseScrolledChecked(x, y, scroll)) {
					return true
				}
			}
		}

		return false
	}

	protected open fun keyPressedInternal(key: Int, scancode: Int, mods: Int): Boolean {
		return false
	}

	fun keyPressed(key: Int, scancode: Int, mods: Int): Boolean {
		if (!isVisible() || !acceptKeyboardInput) return false
		if (!isEverFocused()) return false
		if (flashAnyBlocker(key !in ignoreFlashKeys)) return true
		if (isFocusedThis) return keyPressedInternal(key, scancode, mods)

		for (child in visibleChildrenInternal) {
			if (child.keyPressed(key, scancode, mods)) {
				return true
			}
		}

		if (hasFocusedChildren) return keyPressedInternal(key, scancode, mods)
		return false
	}

	protected open fun keyReleasedInternal(key: Int, scancode: Int, mods: Int): Boolean {
		return false
	}

	fun keyReleased(key: Int, scancode: Int, mods: Int): Boolean {
		if (!isVisible() || !acceptKeyboardInput) return false
		if (!isEverFocused()) return false
		if (flashAnyBlocker(false)) return true
		if (isFocusedThis) return keyReleasedInternal(key, scancode, mods)

		for (child in visibleChildrenInternal) {
			if (child.keyReleased(key, scancode, mods)) {
				return true
			}
		}

		if (hasFocusedChildren) return keyReleasedInternal(key, scancode, mods)
		return false
	}

	protected open fun charTypedInternal(codepoint: Char, mods: Int): Boolean {
		return false
	}

	fun charTyped(codepoint: Char, mods: Int): Boolean {
		if (!isVisible() || !acceptKeyboardInput) return false
		if (!isEverFocused()) return false
		if (flashAnyBlocker(false)) return true
		if (isFocusedThis) return charTypedInternal(codepoint, mods)

		for (child in visibleChildrenInternal) {
			if (child.charTyped(codepoint, mods)) {
				return true
			}
		}

		if (hasFocusedChildren) return charTypedInternal(codepoint, mods)
		return true
	}

	fun isMouseOver(x: Double, y: Double): Boolean { // called to check whenever we are hovering at this
		if (!isVisible() || !acceptMouseInput) return false
		if (isGrabbingMouseInput()) return true

		val pos = localToScreen()
		return x >= pos.x && x <= pos.x + width && y >= pos.y && y + height <= pos.y
	}

	var tickCount = 0
		private set

	protected open fun tickInner() {}

	fun tick() {
		once = true
		tickCount++

		tickInner()

		for (child in Array(visibleChildrenInternal.size) { visibleChildrenInternal[it] }) {
			child.tick()
		}
	}

	var isRemoved = false
		private set

	protected open fun beforeRemoved() {}
	protected open fun onRemoved() {}

	fun remove() {
		if (isRemoved) {
			return
		}

		killFocus()
		beforeRemoved()

		if (screen is MatteryScreen<*>) {
			screen.removePanel(this as EditablePanel<MatteryScreen<*>>)
		}

		for (child in Array(childrenInternal.size) { childrenInternal[it] }) {
			child.remove()
		}

		parent = null

		onRemoved()
		isRemoved = true
	}

	fun popup(focus: Boolean = true, doFlash: Boolean = true) {
		if (isRemoved) {
			return
		}

		if (screen is MatteryScreen<*>) {
			screen.popup(findAbsoluteRoot() as EditablePanel<MatteryScreen<*>>)
		}

		if (focus && !flashAnyBlocker(doFlash)) {
			findAbsoluteRoot().requestFocus()
		}
	}

	fun asGrid(): FlexGridPanel<*> {
		val grid = FlexGridPanel(screen, parent, x, y, width, height)
		parent = grid
		grid.dock = dock
		dock = Dock.NONE
		grid.dockPadding = dockPadding
		grid.dockMargin = dockMargin
		setDockPadding(0f, 0f, 0f, 0f)
		setDockMargin(0f, 0f, 0f, 0f)
		return grid
	}

	fun mouseToCenter() {
		moveMousePosScaled(absoluteX + width / 2f, absoluteY + height / 2f)
	}

	/**
	 * See [ru.dbotthepony.mc.otm.client.render.clearDepth]
	 */
	fun clearDepth(graphics: GuiGraphics, x: Float = 0f, y: Float = 0f, width: Float = this.width, height: Float = this.height) = ru.dbotthepony.mc.otm.client.render.clearDepth(graphics, x, y, width, height)

	fun queryUser(title: Component, text: Component, onConfirm: Runnable, onCancel: Runnable? = null): QueryUserPanel<S> {
		return QueryUserPanel(screen, title, listOf(text), onConfirm, onCancel).also { blockingWindow = it }
	}

	fun queryUser(title: Component, text: Collection<Component>, onConfirm: Runnable, onCancel: Runnable? = null): QueryUserPanel<S> {
		return QueryUserPanel(screen, title, text, onConfirm, onCancel).also { blockingWindow = it }
	}

	companion object {
		private val LOGGER = LogManager.getLogger()!!

		private val ignoreFlashKeys = IntAVLTreeSet()

		init {
			ignoreFlashKeys.add(InputConstants.KEY_LEFT)
			ignoreFlashKeys.add(InputConstants.KEY_RIGHT)
			ignoreFlashKeys.add(InputConstants.KEY_UP)
			ignoreFlashKeys.add(InputConstants.KEY_DOWN)
			ignoreFlashKeys.add(InputConstants.KEY_RCONTROL)
			ignoreFlashKeys.add(InputConstants.KEY_LCONTROL)
			ignoreFlashKeys.add(InputConstants.KEY_RSHIFT)
			ignoreFlashKeys.add(InputConstants.KEY_LSHIFT)
		}
	}
}
