package ocelot.desktop.ui.widget

import ocelot.desktop.color.{Color, RGBAColor, RGBAColorNorm}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.node.Node.Size
import ocelot.desktop.node.nodes.{ComputerNode, ScreenNode}
import ocelot.desktop.node.{EntityNode, Node, NodePort}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event._
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents}
import ocelot.desktop.ui.layout.{CopyLayout, Layout}
import ocelot.desktop.ui.particle.ParticleSystem
import ocelot.desktop.ui.widget.WorkspaceView.NodeLoadException
import ocelot.desktop.ui.widget.window.{NodeSelector, ProfilerWindow, WindowPool}
import ocelot.desktop.util.Keybind.{Center, PauseEmulation, Profiler}
import ocelot.desktop.util.Keymap.Press
import ocelot.desktop.util.ReflectionUtils.findUnaryConstructor
import ocelot.desktop.util.animation.ValueAnimation
import ocelot.desktop.util.{DrawUtils, Logging, Persistable}
import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings}
import totoro.ocelot.brain
import totoro.ocelot.brain.entity.traits.{Environment, SidedEnvironment}
import totoro.ocelot.brain.entity.{Case, Screen}
import totoro.ocelot.brain.event.{InventoryEvent, NodeEvent}
import totoro.ocelot.brain.nbt.ExtendedNBT._
import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound}
import totoro.ocelot.brain.util.{Direction, Tier}

import scala.collection.immutable.ArraySeq
import scala.collection.{immutable, mutable}
import scala.jdk.CollectionConverters._


class WorkspaceView extends Widget with Persistable with MouseHandler with HoverHandler with Logging {
  @volatile
  var nodes: immutable.Seq[Node] = immutable.ArraySeq[Node]()

  var particleSystem = new ParticleSystem
  var windowPool = new WindowPool
  var nodeSelector = new NodeSelector
  var profilerWindow = new ProfilerWindow

  var cameraOffset: Vector2D = Vector2D(0, 0)
  var newConnection: Option[(Node, NodePort, Vector2D)] = None

  private var newNodePos = Vector2D(0, 0)
  private val gridAlpha = new ValueAnimation(0, 1f)
  private val portsAlpha = new ValueAnimation(0, 7f)

  override protected val layout: Layout = new CopyLayout(this)
  children +:= windowPool

  children +:= new NoLayoutBox {
    override def hierarchy: ArraySeq[Widget] = ArraySeq.from(nodes)
  }

  def dispatchBrainEvent(event: brain.event.Event): Unit = event match {
    case event: NodeEvent =>
      nodes
        .filter(_.shouldReceiveEventsFor(event.address))
        .foreach(_.handleEvent(BrainEvent(event)))

    case event: InventoryEvent =>
      nodes.foreach(_.handleEvent(BrainEvent(event)))
  }

  def reset(): Unit = {
    nodes.foreach(_.dispose())
    nodes = nodes.empty
    particleSystem.clear()
    windowPool.deleteAllWindows()
    nodeSelector = new NodeSelector
    profilerWindow = new ProfilerWindow
    cameraOffset = Vector2D(0, 0)
  }

  override def dispose(): Unit = {
    super.dispose()

    nodes.foreach(_.dispose())
  }

  @throws[NodeLoadException]
  private def loadEntityNode(clazz: Class[_], nbt: NBTTagCompound): EntityNode = {
    val address = nbt.getString("address")
    val entity = OcelotDesktop.workspace.entityByAddress(address).getOrElse {
      throw NodeLoadException(s"could not find an entity $address")
    }
    val constructor = findUnaryConstructor(clazz, entity.getClass).getOrElse {
      throw NodeLoadException(
        s"the entity node cannot be instantiated with an argument of class ${entity.getClass.getName}"
      )
    }

    constructor.newInstance(entity).asInstanceOf[EntityNode]
  }

  @throws[NodeLoadException]
  private def loadPlainNode(clazz: Class[_]): Node = {
    try {
      clazz.getConstructor().newInstance().asInstanceOf[Node]
    } catch {
      case _: NoSuchMethodException =>
        throw NodeLoadException("the plain node does not have an available default constructor")
    }
  }

  override def load(nbt: NBTTagCompound): Unit = {
    reset()
    cameraOffset = new Vector2D(nbt.getCompoundTag("cameraOffset"))

    nbt.getTagList("nodes", NBT.TAG_COMPOUND).foreach((nbt: NBTTagCompound) => {
      val clazzName = nbt.getString("class")

      try {
        val clazz = Class.forName(clazzName)

        val node = {
          if (classOf[EntityNode].isAssignableFrom(clazz))
            loadEntityNode(clazz, nbt)
          else
            loadPlainNode(clazz)
        }

        node.workspaceView = this
        node.root = _root
        node.load(nbt)
        nodes :+= node
      } catch {
        case NodeLoadException(message) =>
          logger.error(s"Could not load a node (class $clazzName): $message")
      }
    })

    nbt.getTagList("connections", NBT.TAG_COMPOUND).foreach((nbt: NBTTagCompound) => {
      val portA = NodePort.fromByte(nbt.getByte("ap"))
      val addrA = nbt.getString("a")
      val nodeA = findNodeByAddress(nodes, addrA).get // TODO: slow
      val portB = NodePort.fromByte(nbt.getByte("bp"))
      val addrB = nbt.getString("b")
      val nodeB = findNodeByAddress(nodes, addrB).get // TODO: slow
      nodeA.connect(portA, nodeB, portB)
    })
  }

  private def findNodeByAddress(nodes: Iterable[Node], address: String): Option[EntityNode] = {
    nodes
      .collect { case node: EntityNode => node }
      .find(_.entity match {
        case sided: SidedEnvironment =>
          Direction.values.exists(dir => sided.canConnect(dir) && sided.sidedNode(dir).address == address)
        case env => env.node.address == address
      })
  }

  override def save(nbt: NBTTagCompound): Unit = {
    val cameraOffsetTag = new NBTTagCompound
    cameraOffset.save(cameraOffsetTag)
    nbt.setTag("cameraOffset", cameraOffsetTag)

    nbt.setTagList("nodes",
      nodes.toList.map(node => {
        val nbt = new NBTTagCompound
        nbt.setString("class", node.getClass.getName)
        node.save(nbt)
        nbt: NBTBase
      }).asJava)

    val pairs = new mutable.HashSet[(Byte, String, Byte, String)]

    for (nodeA <- nodes) {
      for ((portA, nodeB, portB) <- nodeA.connections) {
        val byteA = portA.toByte
        val addrA = getNode(nodeA.asInstanceOf[EntityNode].entity, portA.direction).address
        val byteB = portB.toByte
        val addrB = getNode(nodeB.asInstanceOf[EntityNode].entity, portB.direction).address
        val pair = (byteA, addrA, byteB, addrB)
        val pairRev = (byteB, addrB, byteA, addrA)
        if (!pairs.contains(pair) && !pairs.contains(pairRev)) pairs += pair
      }
    }

    nbt.setTagList(
      "connections",
      pairs.toList.map { case (byteA, addrA, byteB, addrB) =>
        val nbt = new NBTTagCompound
        nbt.setByte("ap", byteA)
        nbt.setString("a", addrA)
        nbt.setByte("bp", byteB)
        nbt.setString("b", addrB)
        nbt: NBTBase
      }.asJava,
    )
  }

  private def getNode(environment: Environment, direction: Option[Direction.Value]) = {
    environment match {
      case sided: SidedEnvironment => sided.sidedNode(direction.get)
      case env => env.node
    }
  }

  def newWorkspace(): Unit = {
    reset()
    createDefaultWorkspace()
  }

  def addNode(node: Node, pos: Vector2D = newNodePos): Unit = {
    node.position = pos + cameraOffset + Vector2D(34 - node.width / 2, 34 - node.height / 2)
    resolveCollision(node)
    node.workspaceView = this
    node.root = _root
    node.parent = Some(this)
    nodes = nodes.appended(node)
  }

  def buildNewConnection(): Unit = {
    for ((node, port, _) <- newConnection) {
      newConnectionTarget match {
        case Some((target, targetPort)) =>
          if (node.isConnected(port, target, targetPort))
            node.disconnect(port, target, targetPort)
          else
            node.connect(port, target, targetPort)
        case None =>
      }
    }

    newConnection = None
  }

  def createDefaultWorkspace(): Unit = {
    addNode(
      {
        val node = new ComputerNode(new Case(Tier.Three))
        node.fillSlotsWithDefaultItems()
        node
      },
      Vector2D(Size, Size),
    )

    addNode(
      {
        val node = new ScreenNode(new Screen(Tier.Three))
        node.attachKeyboard()
        node
      },
      Vector2D(204, 136),
    )

    nodes.head.connect(NodePort(), nodes(1), NodePort())
  }

  override protected def receiveClickEvents: Boolean = true

  override protected def receiveDragEvents: Boolean = true

  eventHandlers += {
    case ev @ DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left | MouseEvent.Button.Middle, _) =>
      moveCameraOffset(ev.delta)

    case ClickEvent(MouseEvent.Button.Left | MouseEvent.Button.Right, pos) =>
      if (nodeSelector.isClosed) {
        openSelector(pos)
      } else {
        nodeSelector.close()
      }

    case Press(Profiler) =>
      profilerWindow.open()

    case Press(Center) =>
      moveCameraOffset(-cameraOffset)

    case Press(PauseEmulation) =>
      OcelotDesktop.emulationPaused = !OcelotDesktop.emulationPaused
  }

  private def moveCameraOffset(delta: Vector2D): Unit = {
    cameraOffset += delta
    for (node <- nodes)
      node.position += delta
    windowPool.movePinnedWindows(delta)
  }

  private def openSelector(p: Vector2D): Unit = {
    val pos = if (KeyEvents.isControlDown)
      (p - cameraOffset).snap(Size) + cameraOffset
    else
      p - Vector2D(32, 32)

    nodeSelector.recalculateBounds()
    nodeSelector.open()
    newNodePos = pos - cameraOffset

    val size = Size2D(nodeSelector.maximumSize.width, nodeSelector.finalHeight)

    val offsets = Array(
      Vector2D(-size.width / 2 + 40, 100),
      Vector2D(-size.width / 2 + 40, -size.height - 100),
      Vector2D(-size.width - 100, -size.height / 2 + 40),
      Vector2D(100, -size.height / 2 + 40),
    )

    val newNodeRect = Rect2D(pos.x, pos.y, 64, 64)
    var bestDeviation = Float.PositiveInfinity

    for (offset <- offsets) {
      val idealPos = pos + Vector2D(32, 32) + offset
      val actualPos = idealPos.max(Vector2D(20, 20)).min(Vector2D(width - size.width - 20, height - size.height - 20))
      val rect = Rect2D(actualPos, size)
      if (!newNodeRect.collides(rect)) {
        val deviation = (actualPos - idealPos).lengthSquared
        if (deviation < bestDeviation) {
          bestDeviation = deviation
          nodeSelector.position = actualPos
        }
      }
    }
  }

  def collides(node: Node): Boolean = {
    nodes.filter(_ != node).exists(_.bounds.collides(node.bounds))
  }

  def resolveCollision(node: Node): Unit = {
    val order = nodes.sortBy(n => (node.bounds.center - n.bounds.center).lengthSquared)
    for (obstacle <- order) {
      if (obstacle != node) {
        val a = node.bounds
        val b = obstacle.bounds

        if (a.collides(b)) {
          val penetrationVector = a.minkowskiDifference(b).closestPointOnBounds(Vector2D(0, 0))
          node.position -= penetrationVector
        }
      }
    }
  }

  private def checkConnectorCollision(side: Vector2D, center: Vector2D, length: Float): Boolean = {
    val dir = (side - center).normalizeAxisAligned
    val ort = dir.perpendicularCCW
    val thickness = 8f
    val size = if (dir.x == 0)
      Size2D(-thickness * ort.x.sign, 1.5 * length * dir.y.sign)
    else
      Size2D(1.5 * length * dir.x.sign, -thickness * ort.y.sign)
    val clearance = Rect2D(side + ort * thickness * 0.5f, size).fixNegative
    !nodes.exists(_.bounds.collides(clearance))
  }

  private def findSuitableConnections(a: Array[(Vector2D, Vector2D)],
                                      b: Array[(Vector2D, Vector2D)],
                                      checkCollision: Boolean,
                                      forceParallel: Boolean,
                                      clampMin: Boolean): Array[Array[Vector2D]] = {
    val product = for (x <- a; y <- b) yield (x, y)
    var iter = product.map { case ((aSide, aCenter), (bSide, bCenter)) =>
      val (aLen, bLen) = connectorLen(aSide, aCenter, bSide, bCenter, clampMin)
      ((aSide, aCenter, aLen), (bSide, bCenter, bLen))
    }

    if (checkCollision) {
      iter = iter.filter { case ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) =>
        checkConnectorCollision(aSide, aCenter, aLen) &&
        checkConnectorCollision(bSide, bCenter, bLen)
      }
    }

    var paths = iter.map { case ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) =>
      val aEnd = aSide + (aSide - aCenter).normalizeAxisAligned * aLen
      val bEnd = bSide + (bSide - bCenter).normalizeAxisAligned * bLen
      Array(aSide, aEnd, bEnd, bSide)
    }.filter(DrawUtils.isValidPolyline)

    if (forceParallel) {
      paths = paths.filter { case Array(aStart, aEnd, bEnd, bStart) =>
        (aEnd - aStart).dot(bEnd - bStart) != 0f
      }
    }

    paths
  }

  private def connectorLen(aSide: Vector2D, aCenter: Vector2D,
                           bSide: Vector2D, bCenter: Vector2D,
                           clampMin: Boolean): (Float, Float) = {
    val aDir = aSide - aCenter
    val bDir = bSide - bCenter
    val xDist = (aSide - bSide).x.abs
    val yDist = (aSide - bSide).y.abs
    val aLen = if (aDir.x.abs > 0) xDist / 4f else yDist / 4f
    val bLen = if (bDir.x.abs > 0) xDist / 4f else yDist / 4f
    if (clampMin)
      (aLen.min(40f).max(10f), bLen.min(40f).max(10f))
    else
      (aLen.min(40f), bLen.min(40f))
  }

  private def portDirections(node: Node, port: NodePort): Array[(Vector2D, Vector2D)] = {
    val rect = node.bounds
    val sides = node.portsBounds.find(_._1 == port).getOrElse(return Array.empty)._2
      .map(rects => rects.edgeCenters.maxBy(c => (c - rect.center).lengthSquared))

    sides.zipWithIndex.map { case (side, i) =>
      val center = if (i % 2 == 0) rect.center.copy(x = side.x) else rect.center.copy(y = side.y)
      (side, center)
    }
  }

  private def drawConnection(g: Graphics, aNode: Node, aPort: NodePort, bNode: Node, bPort: NodePort, color: Color)
    : Unit = {
    val (aRect, bRect) = (aNode.bounds, bNode.bounds)
    if (aRect.collides(bRect)) return
    if (aRect.x < bRect.x) {
      drawConnection(g, bNode, bPort, aNode, aPort, color)
      return
    }

    val paths = findSuitableConnections(portDirections(aNode, aPort),
      portDirections(bNode, bPort), checkCollision = true, forceParallel = false, clampMin = true)

    if (paths.isEmpty) {
      // give up
      g.line(aRect.center, bRect.center, 8, ColorScheme("ConnectionBorder"))
      g.line(aRect.center, bRect.center, 4, color)
    } else {
      val path = paths.minBy { case Array(_, aEnd, bEnd, _) => (aEnd - bEnd).lengthSquared }
      DrawUtils.polyline(g, path, 8, ColorScheme("ConnectionBorder"))
      DrawUtils.polyline(g, path, 4, color)

      val aDir = (path(1) - path(0)).normalizeAxisAligned * 8
      g.line(path(0), path(0) + aDir, 4, aPort.getColor)
      val bDir = (path(2) - path(3)).normalizeAxisAligned * 8
      g.line(path(3), path(3) + bDir, 4, bPort.getColor)
    }
  }

  private def drawNewConnectionNoTarget(g: Graphics, node: Node, port: NodePort, endpoint: Vector2D): Unit = {
    val color = ColorScheme("ConnectionNew")
    val rect = node.bounds

    val filtered = portDirections(node, port)
      .map { case (side, center) =>
        val (len, _) = connectorLen(side, side, endpoint, endpoint, clampMin = true)
        (center, side, len)
      }
      .filter { case (center, side, len) => checkConnectorCollision(side, center, len) }
      .map { case (center, side, len) =>
        val end = side + (side - center).normalizeAxisAligned * len
        (side, end, (end - endpoint).lengthSquared)
      }
      .filter { case (side, end, _) => DrawUtils.isValidPolyline(Array(side, end, endpoint)) }

    if (filtered.isEmpty)
      g.line(rect.center, endpoint, 4, color)
    else {
      val (side, end, _) = filtered.minBy(_._3)
      DrawUtils.polyline(g, Array(side, end, endpoint), 4, color)
      val dir = (end - side).normalizeAxisAligned * 8
      g.line(side, side + dir, 4, port.getColor)
    }
  }

  private def newConnectionTarget: Option[(Node, NodePort)] = newConnection flatMap { case (node, _, endpoint) =>
    val validTargets = {
      nodes.iterator
        .filter(_ != node)
        .filter(_.bounds.inflated(20).contains(endpoint))
        .filter(_.ports.nonEmpty)
    }

    if (validTargets.nonEmpty) {
      val target = validTargets.minBy(n => (n.bounds.center - endpoint).lengthSquared)
      val targetPort = target.portsBounds.flatMap(p => p._2.map(a => (p._1, a)))
        .minBy(p => (p._2.center - endpoint).lengthSquared)._1
      Some((target, targetPort))
    } else
      None
  }

  private def drawNewConnection(g: Graphics): Unit = {
    val (node, port, endpoint) = newConnection.get
    if (node.bounds.contains(endpoint)) return

    newConnectionTarget match {
      case Some((target, targetPort)) =>
        val color = ColorScheme(if (node.isConnected(port, target, targetPort)) "ConnectionDel" else "ConnectionNew")
        drawConnection(g, node, port, target, targetPort, color)
      case None =>
        drawNewConnectionNoTarget(g, node, port, endpoint)
    }
  }

  private def drawNewConnectionHints(g: Graphics): Unit = {
    val (node, port, _) = newConnection.get

    g.setSmallFont()

    for ((node, port) <- Some((node, port)) ++ newConnectionTarget) {
      val iconBoundsInflate = -(node.bounds.w - 22) / 2
      g.rect(node.bounds.inflated(-10), RGBAColor(0, 0, 0, 150))
      g.sprite(port.getIcon, node.bounds.inflated(iconBoundsInflate), port.getColor)
      drawCenteredConnectionHintLabel(
        g,
        node.bounds,
        port.direction.map(node.directionLabel(_)).getOrElse("Any"),
        port.getColor
      )
    }

    g.setNormalFont()
  }

  private def drawCenteredConnectionHintLabel(g: Graphics, bounds: Rect2D, text: String, color: Color): Unit = {
    // the area should be reasonably big to fit icon + text
    if (bounds.w >= 48) {
      val x = bounds.x + bounds.w / 2.0f - (text.length * 7.0f) / 2.0f
      val y = bounds.y + bounds.h - 18
      g.foreground = color
      g.text(x, y, text, shrink = 1)
    }
  }

  private def drawSelectorConnection(
    g: Graphics, aRect: Rect2D, bRect: Rect2D,
    thickness: Float = 4,
    color: Color = RGBAColor(150, 150, 150)
  ): Unit = {
    if (aRect.collides(bRect)) return
    val (a, b) = if (aRect.x > bRect.x) (aRect, bRect) else (bRect, aRect)

    val paths = findSuitableConnections(a.edgeCenters.map((_, a.center)),
      b.edgeCenters.map((_, b.center)),
      checkCollision = false, forceParallel = true, clampMin = false)

    if (paths.nonEmpty) {
      val path = paths.minBy { case Array(_, aEnd, bEnd, _) => (aEnd - bEnd).lengthSquared }
      DrawUtils.polyline(g, path, thickness, color)
    }
  }

  private def drawGrid(g: Graphics): Unit = {
    val color = RGBAColorNorm(1, 1, 1, gridAlpha.value)

    g.translate(position.x, position.y)

    var y = (cameraOffset.y - position.y) % Size
    while (y < size.height) {
      g.line(Vector2D(0, y), Vector2D(size.width, y), 1, color)
      y += Size
    }

    var x = (cameraOffset.x - position.x) % Size
    while (x < size.width) {
      g.line(Vector2D(x, 0), Vector2D(x, size.height), 1, color)
      x += Size
    }

    g.translate(-position.x, -position.y)
  }

  override def shouldClip: Boolean = true

  override def draw(g: Graphics): Unit = {
    g.save()

    val backgroundOffsetX = if (cameraOffset.x > 0) 304 - cameraOffset.x.toInt % 304 else -cameraOffset.x.toInt % 304
    val backgroundOffsetY = if (cameraOffset.y > 0) 304 - cameraOffset.y.toInt % 304 else -cameraOffset.y.toInt % 304
    val numRepeatsX = math.ceil((size.width + backgroundOffsetX) / 304f).asInstanceOf[Int]
    val numRepeatsY = math.ceil((size.height + backgroundOffsetY) / 304f).asInstanceOf[Int]

    g.translate(-backgroundOffsetX.toFloat, -backgroundOffsetY.toFloat)

    for (x <- 0 to numRepeatsX) {
      for (y <- 0 to numRepeatsY) {
        g.sprite(IconSource.BackgroundPattern, x.toFloat * 304, y.toFloat * 304, 304, 304)
      }
    }

    g.restore()

    gridAlpha.update()
    if (KeyEvents.isControlDown && (mouseOver || nodes.exists(_.mouseOver))) {
      gridAlpha.goto(0.1f)
      drawGrid(g)
    } else {
      if (gridAlpha.value > 0) drawGrid(g)
      gridAlpha.goto(0f)
    }

    val drawn = mutable.HashSet[(Node, NodePort, Node, NodePort)]()

    for (nodeA <- nodes) {
      nodeA.connections.foreach { case (portA, nodeB, portB) =>
        if (!drawn.contains((nodeA, portA, nodeB, portB)) && !drawn.contains((nodeB, portB, nodeA, portA))) {
          val color = ColorScheme("Connection").toHSVA
          val tint =
            (nodeA.getNodeByPort(portA).address + ":" + nodeB.getNodeByPort(portB).address).hashCode % 1000 / 1000f
          val tinted = color.copy(v = color.v + tint * 0.1f)
          drawn += ((nodeA, portA, nodeB, portB))
          drawConnection(g, nodeA, portA, nodeB, portB, color = tinted)
        }
      }
    }

    if (newConnection.isDefined) {
      drawNewConnection(g)
    }

    nodes.foreach(_.draw(g))
    nodes.foreach(_.drawLight(g))
    nodes.foreach(_.drawLabel(g))

    portsAlpha.update()
    portsAlpha.goto(if (newConnection.isDefined) 1 else 0)

    g.save()
    g.alphaMultiplier *= portsAlpha.value
    nodes.foreach(_.drawPorts(g))
    g.restore()

    val color = nodeSelector.ringColor
    if (!nodeSelector.isClosed) {
      g.sprite(
        IconSource.Nodes.NewNode,
        newNodePos.x + cameraOffset.x,
        newNodePos.y + cameraOffset.y,
        64,
        64,
        color,
      )
    }

    if (newConnection.isDefined) {
      drawNewConnectionHints(g)
    }

    if (!nodeSelector.isClosed)
      drawSelectorConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64),
        nodeSelector.bounds, color = color)

    particleSystem.draw(g)

    drawChildren(g)

    if (OcelotDesktop.emulationPaused) {
      g.fontSizeMultiplier = 2.0f
      g.foreground = Color.White
      g.alphaMultiplier = (math.sin(System.currentTimeMillis() * math.Pi / 180 / 3).toFloat + 1f) / 2.0f
      g.text(size.width - 130, 60, "|| PAUSE")
      g.fontSizeMultiplier = 1.0f
      g.alphaMultiplier = 1.0f
    }
  }

  override def update(): Unit = {
    super.update()

    nodes.foreach(_.update())
    particleSystem.update(UiHandler.dt)

    if (mouseOver) {
      root.get.statusBar.addKeyEntry(Settings.get.keymap.name(Center), "Reset camera")
    }

    if (mouseOver || nodes.exists(_.mouseOver)) {
      root.get.statusBar.addKeyEntry("CTRL", "Show grid")
    }

    if (mouseOver && newConnection.isEmpty) {
      root.get.statusBar.addMouseEntry(
        IconSource.Icons.LMB,
        if (nodeSelector.isClosed) "Add node" else "Close selector",
      )
      root.get.statusBar.addMouseEntry(IconSource.Icons.DragLMB, "Move camera")
    }
  }
}

object WorkspaceView {
  private case class NodeLoadException(message: String) extends Exception(message)
}
