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

import com.mojang.blaze3d.platform.InputConstants
import it.unimi.dsi.fastutil.ints.Int2FloatAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import net.minecraft.ChatFormatting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.item.ItemStack
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.android.AndroidResearch
import ru.dbotthepony.mc.otm.android.AndroidResearchManager
import ru.dbotthepony.mc.otm.android.AndroidResearchType
import ru.dbotthepony.mc.otm.block.entity.tech.AndroidStationBlockEntity
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.client.playGuiClickSound
import ru.dbotthepony.mc.otm.client.render.RenderGravity
import ru.dbotthepony.mc.otm.client.render.Widgets18
import ru.dbotthepony.mc.otm.client.render.drawLine
import ru.dbotthepony.mc.otm.client.render.renderRect
import ru.dbotthepony.mc.otm.client.screen.MatteryScreen
import ru.dbotthepony.mc.otm.client.screen.panels.*
import ru.dbotthepony.mc.otm.client.screen.panels.button.ButtonPanel
import ru.dbotthepony.mc.otm.client.screen.panels.button.makeDeviceControls
import ru.dbotthepony.mc.otm.client.screen.panels.slot.AbstractSlotPanel
import ru.dbotthepony.mc.otm.client.screen.panels.slot.BatterySlotPanel
import ru.dbotthepony.mc.otm.client.screen.panels.slot.EquipmentBatterySlotPanel
import ru.dbotthepony.mc.otm.client.screen.panels.util.DraggableCanvasPanel
import ru.dbotthepony.mc.otm.client.screen.widget.WideProfiledPowerGaugePanel
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.core.ifPresentK
import ru.dbotthepony.mc.otm.menu.tech.AndroidStationMenu
import ru.dbotthepony.mc.otm.network.MatteryPlayerNetworkChannel
import ru.dbotthepony.mc.otm.network.AndroidResearchRequestPacket
import java.util.*
import kotlin.collections.ArrayList
import kotlin.properties.Delegates

private fun exploreTree(research: AndroidResearchType, seen: MutableSet<AndroidResearchType>, result: MutableList<AndroidResearchType>) {
	if (!seen.add(research)) {
		return
	}

	result.add(research)

	for (children in research.flatUnlocks) {
		exploreTree(children, seen, result)
	}
}

private fun findGraphs(): List<Pair<AndroidResearchType, List<AndroidResearchType>>> {
	val seen = HashSet<AndroidResearchType>()
	val list = ArrayList<Pair<AndroidResearchType, ArrayList<AndroidResearchType>>>()

	for (research in AndroidResearchManager) {
		if (research.flatPrerequisites.isEmpty()) {
			val tree = ArrayList<AndroidResearchType>()

			exploreTree(research, seen, tree)

			if (tree.isNotEmpty()) {
				list.add(research to tree)
			}
		}
	}

	return list
}

private fun isTree(root: AndroidResearchType): Boolean {
	if (root.flatPrerequisites.size > 1) {
		return false
	}

	for (child in root.flatUnlocks) {
		if (!isTree(child)) {
			return false
		}
	}

	return true
}

private typealias LinePos = Pair<Pair<Float, Float>, Pair<Float, Float>>

private class Tree(val node: AndroidResearchType) : Iterable<Tree> {
	val subtrees = ArrayList<Tree>()

	val height: Int
	val width: Int

	init {
		for (child in node.flatUnlocks) {
			val tree = Tree(child)
			subtrees.add(tree)
		}

		if (subtrees.isEmpty()) {
			height = 0
			width = 1
		} else {
			var height = 0
			var width = 0

			for (child in subtrees) {
				height = height.coerceAtLeast(child.height)
				width += child.width
			}

			this.height = height + 1
			this.width = width
		}

		if (subtrees.isNotEmpty()) {
			subtrees.sortWith { a, b ->
				return@sortWith b.height.compareTo(a.height)
			}
		}
	}

	override fun iterator(): Iterator<Tree> {
		if (subtrees.size < 2) {
			return subtrees.iterator()
		} else {
			return object : Iterator<Tree> {
				private var index = subtrees.size - 1
				private var backward = true

				override fun hasNext(): Boolean {
					return backward || index < subtrees.size - 1
				}

				override fun next(): Tree {
					val index = index

					if (backward) {
						this.index -= 2

						if (this.index == -1) {
							backward = false
							this.index = 0
						} else if (this.index == 0) {
							backward = false
						}
					} else {
						if (this.index == 0) {
							this.index = if (subtrees.size % 2 == 0) 2 else 1
						} else {
							this.index += 2
						}
					}

					return subtrees[index]
				}
			}
		}
	}

	fun put(
		rows: Int2ObjectFunction<EditablePanel<AndroidStationScreen>>,
		left: Float,
		capability: MatteryPlayerCapability
	): Pair<AndroidResearchButton, Float> {
		val totalWidth = width * 24f

		val lines = ArrayList<LinePos>()
		val linesToResearch = IdentityHashMap<AndroidResearchType, ArrayList<LinePos>>()

		val button = AndroidResearchButton(rows[node.researchTreeDepth], capability.getResearch(node), lines, linesToResearch)
		button.x = left + totalWidth / 2f - button.width / 2f

		@Suppress("name_shadowing")
		var left = left

		var busLine: LinePos? = null

		if (subtrees.size > 1) {
			val x = button.width / 2f
			val y = button.y + button.height

			busLine = (x to y) to
					(x to y + 3f)

			lines.add(busLine)
		}

		var minX = Float.MAX_VALUE
		var maxX = -Float.MAX_VALUE

		for (subtree in this) {
			val (btn, move) = subtree.put(rows, left, capability)
			left += move

			val line: LinePos

			if (subtrees.size == 1) {
				val x = (button.width / 2f)
				val y = button.y + button.height

				line = (x to y) to
						(x to y + 6f)
			} else {
				val x = btn.x + btn.width / 2f - button.x
				val y = button.y + button.height + 3f

				line = (x to y) to
						(x to y + 3f)

				minX = minX.coerceAtMost(x)
				maxX = maxX.coerceAtLeast(x)

				val highlightLine =
					((button.width / 2f) to y) to
					(x to y)

				linesToResearch.computeIfAbsent(subtree.node) { ArrayList() }.add(highlightLine)

				for (children in subtree.node.allUnlocks) {
					linesToResearch.computeIfAbsent(children) { ArrayList() }.add(highlightLine)
				}
			}

			lines.add(line)

			linesToResearch.computeIfAbsent(subtree.node) { ArrayList() }.add(line)

			for (children in subtree.node.allUnlocks) {
				linesToResearch.computeIfAbsent(children) { ArrayList() }.add(line)
			}

			if (busLine != null) {
				linesToResearch.computeIfAbsent(subtree.node) { ArrayList() }.add(busLine)

				for (children in subtree.node.allUnlocks) {
					linesToResearch.computeIfAbsent(children) { ArrayList() }.add(busLine)
				}
			}
		}

		if (subtrees.size > 1) {
			val y = button.y + button.height + 3f

			lines.add(
				(minX - 0.5f to y) to
				(maxX to y)
			)
		}

		return button to totalWidth
	}
}

private class AndroidResearchButton(
	parent: EditablePanel<AndroidStationScreen>,
	private val node: AndroidResearch,
	private val lines: List<LinePos>,
	private val highlightLines: Map<AndroidResearchType, List<LinePos>>
): EditablePanel<AndroidStationScreen>(
	parent.screen,
	parent,
	0f,
	0f,
	AndroidStationScreen.BUTTON_SIZE,
	AndroidStationScreen.BUTTON_SIZE
) {
	init {
		setDockMargin(2f, 2f, 2f, 2f)
	}

	override fun onHovered() {
		super.onHovered()

		screen.hoveredResearch = node
	}

	override fun onUnHovered() {
		super.onUnHovered()

		if (screen.hoveredResearch == node) {
			screen.hoveredResearch = null
		}
	}

	override fun innerRender(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
		val hovered = screen.hoveredResearch

		val isBlockedByHovered = hovered != null && node.type in hovered.type.allBlocking

		if (isBlockedByHovered) {
			graphics.renderRect(0f, 0f, width, height, color = AndroidStationScreen.CAN_NOT_BE_RESEARCHED)
		}

		val nodeColor = if (node.isResearched) {
			AndroidStationScreen.RESEARCHED
		} else if (node.isAnyBlockerResearchedIndirect) {
			AndroidStationScreen.ALREADY_BLOCKED
		} else if (node.canResearch) {
			AndroidStationScreen.CAN_BE_RESEARCHED
		} else {
			AndroidStationScreen.CAN_NOT_BE_RESEARCHED
		}

		val icon = node.type.resolvedSkinIcon
		val itemIcon = node.type.itemIcon

		if (icon != null) {
			icon.render(graphics, 0f, 0f, width, height, color = nodeColor)
		} else if (itemIcon != null) {
			val itemstack = ItemStack(itemIcon, 1)

			val stack = graphics.pose()
			stack.pushPose()
			stack.translate(1f, 1f, 0f)
			screen.renderItemStack(graphics, itemstack)
			stack.popPose()

			clearDepth(graphics)
		} else {
			graphics.renderRect(0f, 0f, width, height, color = nodeColor)
		}

		if (isBlockedByHovered) {
			Widgets18.CROSS.render(graphics, color = RGBAColor.RED)
		} else if (node.isAnyBlockerResearched) {
			Widgets18.FORWARD_SLASH.render(graphics, color = RGBAColor.RED)
		}

		val text = node.type.iconText

		if (text != null) {
			graphics.drawString(font, text.visualOrderText, width - font.width(text), height - font.lineHeight, -0x1, true)
		}

		for (line in lines) {
			val (pos1, pos2) = line
			val (x1, y1) = pos1
			val (x2, y2) = pos2

			graphics.drawLine(x1, y1, x2, y2, 0.5f, color = RGBAColor.WHITE)
		}

		hovered ?: return
		val pathLines = highlightLines[hovered.type]

		if (pathLines != null) {
			for (line in pathLines) {
				val (pos1, pos2) = line
				val (x1, y1) = pos1
				val (x2, y2) = pos2

				graphics.drawLine(x1, y1, x2, y2, 0.5f, color = RGBAColor.LIGHT_GREEN)
			}
		}

		// very lazy solution
		val mark = Any()
		val drawn = IdentityHashMap<LinePos, Any>()

		for (blocker in hovered.type.allBlocking) {
			val blockLines = highlightLines[blocker]

			if (blockLines != null) {
				for (line in blockLines) {
					if (drawn.containsKey(line)) {
						continue
					}

					drawn[line] = mark

					val (pos1, pos2) = line
					val (x1, y1) = pos1
					val (x2, y2) = pos2

					graphics.drawLine(x1, y1, x2, y2, 0.5f, color = RGBAColor.RED)
				}
			}
		}
	}

	override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
		if (button == InputConstants.MOUSE_BUTTON_LEFT && minecraft.player?.isSpectator != true) {
			if (node.canResearch && !node.isResearched && (parent?.screen as AndroidStationScreen).menu.energyWidget.level >= AndroidStationBlockEntity.ENERGY_PER_RESEARCH) {
				if (node.type.flatBlocking.isNotEmpty()) {
					queryUser(
						TranslatableComponent("otm.android_station.research.confirm", node.type.displayName),
						listOf(
							TranslatableComponent("otm.android_station.research.research_will_be_blocked"),
							*node.type.flatBlocking.map { it.displayName }.toTypedArray()
						),
						onConfirm = {
							MatteryPlayerNetworkChannel.sendToServer(AndroidResearchRequestPacket(node.type))
						}
					)
				} else {
					MatteryPlayerNetworkChannel.sendToServer(AndroidResearchRequestPacket(node.type))
				}
			}

			playGuiClickSound()
		}

		return true
	}

	override fun innerRenderTooltips(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float): Boolean {
		if (isHovered) {
			val list = ArrayList<Component>().also { it.addAll(node.screenTooltipLines) }

			val enoughPower = (parent?.screen as AndroidStationScreen).menu.energyWidget.level >= AndroidStationBlockEntity.ENERGY_PER_RESEARCH

			if (node.isResearched) {
				list.add(TranslatableComponent("otm.android_station.research.researched").withStyle(ChatFormatting.DARK_AQUA))
			} else if (node.canResearch) {
				list.add(TranslatableComponent("otm.android_station.research.can_be_researched").withStyle(ChatFormatting.DARK_GREEN))

				if (!enoughPower) {
					list.add(TranslatableComponent("otm.android_station.research.low_power").withStyle(ChatFormatting.DARK_RED))
				}
			} else {
				list.add(TranslatableComponent("otm.android_station.research.can_not_be_researched").withStyle(ChatFormatting.DARK_RED))

				if (!enoughPower) {
					list.add(TranslatableComponent("otm.android_station.research.low_power").withStyle(ChatFormatting.DARK_RED))
				}
			}

			graphics.renderComponentTooltip(font, list, mouseX.toInt(), mouseY.toInt())
		}

		return isHovered
	}
}

private enum class PreviewScrollers(
	val init: (EditablePanel<*>, Random) -> Unit,
	val scroll: (EditablePanel<*>, Random) -> Boolean
) {
	LEFT_TO_RIGHT(
		init = { it, random ->
			it.xOffset = it.width
			it.yOffset = random.nextFloat(-it.childrenRectHeight + 18f, 0f)
		},

		scroll = { it, _ ->
			it.xOffset -= 1f
			it.xOffset >= -it.childrenRectWidth
		}
	),

	RIGHT_TO_LEFT(
		init = { it, random ->
			it.xOffset = -it.childrenRectWidth
			it.yOffset = random.nextFloat(-it.childrenRectHeight + 18f, 0f)
		},

		scroll = { it, _ ->
			it.xOffset += 1f
			it.xOffset <= it.width
		}
	),

	BOTTOM_TO_TOP(
		init = { it, random ->
			it.yOffset = -it.childrenRectHeight
			it.xOffset = random.nextFloat(-it.childrenRectWidth + 18f, 0f)
		},

		scroll = { it, _ ->
			it.yOffset += 1f
			it.yOffset <= it.height
		}
	),

	TOP_TO_BOTTOM(
		init = { it, random ->
			it.yOffset = it.height
			it.xOffset = random.nextFloat(-it.childrenRectWidth + 18f, 0f)
		},

		scroll = { it, _ ->
			it.yOffset -= 1f
			it.yOffset >= it.childrenRectHeight
		}
	)
}

class AndroidStationScreen constructor(p_97741_: AndroidStationMenu, p_97742_: Inventory, p_97743_: Component) :
	MatteryScreen<AndroidStationMenu>(p_97741_, p_97742_, p_97743_) {

	private fun makeCanvas(isPreview: Boolean): DraggableCanvasPanel<AndroidStationScreen> {
		val rows = Int2ObjectOpenHashMap<EditablePanel<AndroidStationScreen>>()

		val canvas = object : DraggableCanvasPanel<AndroidStationScreen>(this@AndroidStationScreen, null) {
			private val random = Random()
			private var scroller: PreviewScrollers = PreviewScrollers.values().let { it[random.nextInt(it.size)] }
			private var firstTick = false

			override fun innerRender(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
				graphics.renderRect(0f, 0f, width, height, color = RGBAColor.BLACK)
			}

			override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
				if (isPreview) {
					playGuiClickSound()
					openResearchTree()
					return true
				}

				return super.mouseClickedInner(x, y, button)
			}

			override fun tickInner() {
				super.tickInner()

				if (isPreview && !layoutInvalidated) {
					if (firstTick) {
						scroller.init.invoke(this, random)
					}

					val status = scroller.scroll.invoke(this, random)

					if (!status) {
						scroller = PreviewScrollers.values().let { it[random.nextInt(it.size)] }
						scroller.init.invoke(this, random)
					}
				}
			}
		}

		minecraft?.player?.getCapability(MatteryCapability.MATTERY_PLAYER)?.ifPresentK {
			var totalWidth = 0f

			for (graph in findGraphs()) {
				if (isTree(graph.first)) {
					val tree = Tree(graph.first)

					for (i in 0 .. tree.height) {
						rows.computeIfAbsent(i, Int2ObjectFunction {
							object : EditablePanel<AndroidStationScreen>(this@AndroidStationScreen, canvas, 0f, it * 24f, 10000f, 22f) {
								override fun mouseClickedInner(x: Double, y: Double, button: Int) = false
								override fun mouseReleasedInner(x: Double, y: Double, button: Int) = false
								override fun mouseDraggedInner(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double) = false
							}
						})
					}

					totalWidth += tree.put(rows, totalWidth, it).second
				} else {
					val rowWidths = Int2FloatAVLTreeMap()

					for (research in graph.second) {
						val row = rows.computeIfAbsent(research.researchTreeDepth, Int2ObjectFunction {
							object : EditablePanel<AndroidStationScreen>(this@AndroidStationScreen, canvas, 0f, it * 24f, 10000f, 22f) {
								override fun mouseClickedInner(x: Double, y: Double, button: Int) = false
								override fun mouseReleasedInner(x: Double, y: Double, button: Int) = false
								override fun mouseDraggedInner(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double) = false
							}
						})

						val button = AndroidResearchButton(row, it.getResearch(research), emptyList(), emptyMap())
						val width = rowWidths[research.researchTreeDepth]
						button.setPos(totalWidth + width, 0f)
						rowWidths[research.researchTreeDepth] += 22f
					}

					var maximal = 0f

					for (value in rowWidths.values) {
						maximal = maximal.coerceAtLeast(value)
					}

					totalWidth += maximal
				}
			}
		}

		for (row in rows.values) {
			row.sizeToContents()
			row.acceptMouseInput = !isPreview
		}

		return canvas
	}

	private var research: FramePanel<AndroidStationScreen>? = null
	private var playerStrip: EditablePanel<AndroidStationScreen> by Delegates.notNull()
	var hoveredResearch: AndroidResearch? = null

	private fun openResearchTree() {
		if (research?.isRemoved == false) {
			research!!.toScreenCenter()
			popup(research!!)
			research!!.requestFocus()
			return
		}

		val window = minecraft!!.window

		research = FramePanel(this, null, 0f, 0f, window.guiScaledWidth * 0.8f, window.guiScaledHeight * 0.8f, TranslatableComponent("otm.gui.android_research"))

		val researchCanvas = makeCanvas(false)
		researchCanvas.dock = Dock.FILL
		researchCanvas.parent = research

		val bottom = EditablePanel(this, research, 0f, 0f, 0f, 20f)
		val close = ButtonPanel(this, bottom, 0f, 0f, 90f, 20f, TranslatableComponent("otm.gui.matter_panel.close"), onPress = Runnable { research!!.remove() })

		bottom.dock = Dock.BOTTOM
		close.dock = Dock.RIGHT

		bottom.setDockMargin(0f, 0f, 4f, 4f)
		researchCanvas.setDockMargin(4f, 4f, 4f, 4f)

		research!!.toScreenCenter()
		research!!.behaveAsWindow()
		addPanel(research!!)
		research!!.requestFocus()
	}

	override fun resize(minecraft: Minecraft, p_96576_: Int, p_96577_: Int) {
		super.resize(minecraft, p_96576_, p_96577_)

		val research = research

		if (research != null) {
			val window = minecraft.window
			research.setSize(window.guiScaledWidth * 0.8f, window.guiScaledHeight * 0.8f)
			research.toScreenCenter()
		}
	}

	override fun makeMainFrame(): FramePanel<AndroidStationScreen> {
		val frame = FramePanel(this, 200f, 108f, title)

		frame.makeCloseButton()
		frame.onClose { onClose() }

		object : Label<AndroidStationScreen>(this@AndroidStationScreen, frame, height = 11f, shadow = true) {
			init {
				dock = Dock.BOTTOM
				dockTop = 2f
				align = RenderGravity.TOP_RIGHT
			}

			override fun innerRender(graphics: GuiGraphics, mouseX: Float, mouseY: Float, partialTick: Float) {
				if (menu.energyWidget.level >= AndroidStationBlockEntity.ENERGY_PER_RESEARCH) {
					text = POWER_OK
					color = RGBAColor.LIGHT_GREEN
				} else if (menu.energyWidget.level >= AndroidStationBlockEntity.ENERGY_PER_OPERATION) {
					text = LOW_POWER_1
					color = RGBAColor.DARK_RED
				} else {
					text = LOW_POWER_0
					color = RGBAColor.RED
				}

				super.innerRender(graphics, mouseX, mouseY, partialTick)
			}
		}

		val stripLeft = EditablePanel(this, frame, width = AbstractSlotPanel.SIZE)

		stripLeft.dock = Dock.LEFT
		stripLeft.dockRight = 3f

		WideProfiledPowerGaugePanel(this, stripLeft, menu.profiledEnergy).also {
			it.dock = Dock.TOP
		}

		BatterySlotPanel(this, stripLeft, menu.batterySlot).also {
			it.dock = Dock.TOP
			it.dockTop = 6f
		}

		makeCuriosPanel(this, frame, menu.equipment.curiosSlots, autoAlign = true)

		val playerStrip = PlayerEquipmentPanel(this, frame, armorSlots = menu.equipment.armorSlots)
		playerStrip.dock = Dock.TOP

		val androidStrip = EditablePanel(this, playerStrip, width = AbstractSlotPanel.SIZE)
		androidStrip.dock = Dock.LEFT

		EquipmentBatterySlotPanel(this, androidStrip, menu.androidBattery).also { it.dock = Dock.TOP }

		val preview = makeCanvas(true)

		preview.parent = playerStrip
		preview.height = 72f
		preview.dock = Dock.TOP

		this.playerStrip = playerStrip

		makeDeviceControls(this, frame, redstoneConfig = menu.redstoneConfig, energyConfig = menu.energyConfig)

		return frame
	}

	companion object {
		const val GRID_WIDTH = 5
		const val BUTTON_SIZE = 18f
		val RESEARCHED = RGBAColor(150, 150, 200)
		val CAN_BE_RESEARCHED = RGBAColor(150, 200, 150)
		val CAN_NOT_BE_RESEARCHED = RGBAColor(200, 150, 150)
		val ALREADY_BLOCKED = RGBAColor(105, 79, 79)

		private val LOW_POWER_0 = TranslatableComponent("otm.android_station.low_power_0")
		private val LOW_POWER_1 = TranslatableComponent("otm.android_station.low_power_1")
		private val POWER_OK = TranslatableComponent("otm.android_station.power_ok")
	}
}
