package ocelot.desktop.node

import ocelot.desktop.audio.SoundSource
import ocelot.desktop.color.{Color, RGBAColor}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.node.Node._
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.{Widget, WorkspaceView}
import ocelot.desktop.util.Persistable
import ocelot.desktop.util.animation.ColorAnimation
import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.network
import totoro.ocelot.brain.util.Direction.Direction

import scala.collection.mutable.ArrayBuffer

abstract class Node extends Widget with MouseHandler with HoverHandler with Persistable {
  var workspaceView: WorkspaceView = _

  protected val highlight = new ColorAnimation(RGBAColor(0, 0, 0, 0))

  protected var isMoving = false
  private var grabPoint: Vector2D = Vector2D(0, 0)

  protected val _connections: ArrayBuffer[(NodePort, Node, NodePort)] = ArrayBuffer[(NodePort, Node, NodePort)]()

  size = minimumSize

  override def load(nbt: NBTTagCompound): Unit = {
    super.load(nbt)

    position = new Vector2D(nbt.getCompoundTag("pos"))
  }

  override def save(nbt: NBTTagCompound): Unit = {
    super.save(nbt)

    val tag = new NBTTagCompound
    position.save(tag)
    nbt.setTag("pos", tag)
  }

  override protected def receiveClickEvents: Boolean = true

  override protected def receiveDragEvents: Boolean = true

  eventHandlers += {
    case event: ClickEvent =>
      onClick(event)

    case DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, pos) =>
      startMoving(pos)

    case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, pos) =>
      move(pos)

    case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) =>
      stopMoving()

    case DragEvent(DragEvent.State.Start, MouseEvent.Button.Right, pos) =>
      for (
        port <- portsBounds
          .flatMap(p => p._2.map(a => (p._1, a)))
          .minByOption(p => (p._2.center - pos).lengthSquared)
          .map(_._1)
      ) {
        workspaceView.newConnection = Some((this, port, pos))
      }

    case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Right, pos) =>
      for (newConnection <- workspaceView.newConnection) {
        workspaceView.newConnection = Some((this, newConnection._2, pos))
      }

    case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Right, _) =>
      workspaceView.buildNewConnection()

    case HoverEvent(HoverEvent.State.Enter) =>
      if (!isMoving)
        highlight.goto(HoveredHighlight)

    case HoverEvent(HoverEvent.State.Leave) =>
      if (!isMoving)
        highlight.goto(NoHighlight)
  }

  def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
    if (ports.nonEmpty) {
      menu.addEntry(ContextMenuEntry("Disconnect", IconSource.Icons.LinkSlash, SoundSource.InterfaceClickLow) {
        disconnectFromAll()
      })
    }

    menu.addEntry(ContextMenuEntry("Remove", IconSource.Icons.Delete, SoundSource.InterfaceClickLow) {
      destroy()
    })
  }

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

    if (mouseOver || isMoving) {
      root.get.statusBar.addMouseEntry(IconSource.Icons.RMB, "Menu")
      root.get.statusBar.addMouseEntry(IconSource.Icons.DragLMB, "Move node")

      if (ports.nonEmpty) {
        root.get.statusBar.addMouseEntry(IconSource.Icons.DragRMB, "Connect/Disconnect")
      }
    }
  }

  override def dispose(): Unit = {
    disconnectFromAll()

    super.dispose()
  }

  def icon: IconSource = IconSource.Icons.NA

  def iconColor: Color = RGBAColor(255, 255, 255)

  def ports: Array[NodePort] = Array()

  def getNodeByPort(port: NodePort): network.Node = throw new IllegalArgumentException("this node has no ports")

  def rotatable: Boolean

  def connections: Iterator[(NodePort, Node, NodePort)] = _connections.iterator

  def connect(portA: NodePort, node: Node, portB: NodePort): Unit = {
    this._connections.append((portA, node, portB))
    node._connections.append((portB, this, portA))
    this.onConnectionAdded(portA, node, portB)
    node.onConnectionAdded(portB, this, portA)
  }

  def disconnect(portA: NodePort, node: Node, portB: NodePort): Unit = {
    this._connections -= ((portA, node, portB))
    node._connections -= ((portB, this, portA))
    this.onConnectionRemoved(portA, node, portB)
    node.onConnectionRemoved(portB, this, portA)
  }

  def destroy(): Unit = {
    dispose()
    workspaceView.nodes = workspaceView.nodes.filter(_ != this)
  }

  def disconnectFromAll(): Unit = {
    for ((a, node, b) <- connections.toArray) {
      disconnect(a, node, b)
    }
  }

  def isConnected(portA: NodePort, node: Node, portB: NodePort): Boolean = {
    _connections.contains((portA, node, portB))
  }

  def onConnectionAdded(portA: NodePort, node: Node, portB: NodePort): Unit = {
    getNodeByPort(portA).connect(node.getNodeByPort(portB))
  }

  def onConnectionRemoved(portA: NodePort, node: Node, portB: NodePort): Unit = {
    getNodeByPort(portA).disconnect(node.getNodeByPort(portB))
  }

  def onClick(event: ClickEvent): Unit = {
    event match {
      case ClickEvent(MouseEvent.Button.Right, _) =>
        val menu = new ContextMenu
        setupContextMenu(menu, event)
        root.get.contextMenus.open(menu)

      case _ =>
    }
  }

  def portsBounds: Iterator[(NodePort, Array[Rect2D])] = {
    val length = -4
    val thickness = 4
    val stride = thickness + 4
    val hsize = Size2D(length, thickness)
    val vsize = Size2D(thickness, length)

    val centers = bounds.edgeCenters
    val ports = this.ports
    val numPorts = ports.length

    ports.sorted.iterator.zipWithIndex.map { case (port, portIdx) =>
      val top = Rect2D(centers(0) + Vector2D(-thickness / 2, -length), vsize)
      val right = Rect2D(centers(1) + Vector2D(0, -thickness / 2), hsize)
      val bottom = Rect2D(centers(2) + Vector2D(-thickness / 2, 0), vsize)
      val left = Rect2D(centers(3) + Vector2D(-length, -thickness / 2), hsize)
      val centersBounds = Array[Rect2D](top, right, bottom, left)

      val portBounds = (0 until 4).map(side => {
        val offset = thickness - numPorts * stride / 2 + portIdx * stride
        val rect = centersBounds(side)
        side match {
          case 0 | 2 => rect.mapX(_ + offset)
          case 1 | 3 => rect.mapY(_ + offset)
        }
      })

      (port, portBounds.toArray)
    }
  }

  override def minimumSize: Size2D = Size2D(Size, Size)

  override def maximumSize: Size2D = minimumSize

  private def startMoving(pos: Vector2D): Unit = {
    highlight.goto(MovingHighlight)
    isMoving = true
    grabPoint = pos - position
  }

  private def move(pos: Vector2D): Unit = {
    val oldPos = position

    val desiredPos = {
      if (KeyEvents.isControlDown) {
        (pos - workspaceView.cameraOffset).snap(Size) + // snap the position to the grid relative to the origin
          workspaceView.cameraOffset - // restore the camera offset
          grabPoint.snap(Size) + // accounts for multi-block screens
          Vector2D(
            ((Size - width) % Size) / 2,
            ((Size - height) % Size) / 2,
          ) // if a node is not full-size, moves it to the center of the grid cell
      } else
        pos - grabPoint
    }

    position = desiredPos
    workspaceView.resolveCollision(this)

    if (workspaceView.collides(this) || (position - desiredPos).lengthSquared > 50 * 50)
      position = oldPos
  }

  private def stopMoving(): Unit = {
    isMoving = false
    if (mouseOver)
      highlight.goto(HoveredHighlight)
    else
      highlight.goto(NoHighlight)

    workspaceView.resolveCollision(this)
  }

  protected def drawHighlight(g: Graphics): Unit = {
    highlight.update()

    g.rect(position.x, position.y, size.width, size.height, highlight.color)
  }

  override def draw(g: Graphics): Unit = {
    drawHighlight(g)

    g.sprite(
      icon,
      position.x + HighlightThickness,
      position.y + HighlightThickness,
      size.width - HighlightThickness * 2,
      size.height - HighlightThickness * 2,
      iconColor,
    )
  }

  private def drawPortLegend(g: Graphics): Unit = {
    if (highlight.color.a < 0.001) return

    g.setSmallFont()
    g.background = PortLegendBg.mapA(_ * highlight.color.a)

    val sidedPorts = ports.iterator.filter(_.direction.isDefined).toSeq.sorted
    val legendHeight = sidedPorts.size * 8
    val startY = position.y + (height - legendHeight) / 2

    for ((port, line) <- sidedPorts.iterator.zipWithIndex) {
      g.foreground = port.getColor.toRGBANorm.mapA(_ * highlight.color.a)
      g.text(bounds.max.x + HoverInfoMargin, startY + line * 8, directionLabel(port.direction.get), shrink = 1)
    }

    g.setNormalFont()
  }

  def drawLight(g: Graphics): Unit = {}

  def drawLabel(g: Graphics): Unit = {
    drawPortLegend(g)
  }

  def drawPorts(g: Graphics): Unit = {
    for ((port, rects) <- portsBounds) {
      val color = port.getColor
      for (rect <- rects) {
        g.rect(rect, color)
      }
    }
  }

  def directionLabel(direction: Direction): String = {
    (if (rotatable) direction.side else direction.cardinal).capitalize
  }
}

object Node {
  protected val MovingHighlight: RGBAColor = RGBAColor(240, 250, 240)
  protected val HoveredHighlight: RGBAColor = RGBAColor(160, 160, 160)
  protected val NoHighlight: RGBAColor = RGBAColor(160, 160, 160, 0)

  private val PortLegendBg = Color.Black.withAlpha(0.8f)

  val TexelCount = 16f
  val Scale = 4f
  val HighlightThickness = 2f
  private val HoverInfoMargin = 9f

  val NoHighlightSize: Float = TexelCount * Scale
  val Size: Float = NoHighlightSize + HighlightThickness * 2f
}
