package ocelot.desktop.node.nodes

import ocelot.desktop.color.{Color, IntColor}
import ocelot.desktop.geometry.{Rect2D, Size2D}
import ocelot.desktop.graphics.Texture.MinFilteringMode
import ocelot.desktop.graphics.{Graphics, IconSource, ScreenViewport}
import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize, Size}
import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth, Margin}
import ocelot.desktop.node.{EntityNode, LabeledEntityNode, Node, WindowedNode}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.ClickEvent
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.{ScreenAspectRatioDialog, TickUpdatable}
import ocelot.desktop.util.TierColor
import ocelot.desktop.windows.ScreenWindow
import ocelot.desktop.{OcelotDesktop, Settings}
import totoro.ocelot.brain.entity.{Keyboard, Screen}
import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.PackedColor

class ScreenNode(val screen: Screen)
    extends EntityNode(screen) with LabeledEntityNode with WindowedNode[ScreenWindow] with TickUpdatable {

  // NOTE: whenever you access screen's TextBuffer methods, make sure to synchronize on `screen`:
  //       computers may update the screen data concurrently via direct methods!
  //       conversely, if a property is only modified via indirect methods, no additional synchronization is necessary.
  override def minimumSize: Size2D = Size2D(
    Size * screen.aspectRatio._1,
    Size * screen.aspectRatio._2,
  )

  override def maximumSize: Size2D = minimumSize

  // -------------------------------

  private var viewport: Option[ScreenViewport] = Some(new ScreenViewport(UiHandler.graphics, 160 * 8, 160 * 8))

  // the cached contents of the screen, updated every tick to synchronize with the TPS rate
  private var (colorBuffer, textBuffer) = screen.synchronized {
    (
      Array.fill[Short](screen.getHeight, screen.getWidth)(0),
      Array.fill[Int](screen.getHeight, screen.getWidth)(0x20),
    )
  }

  def screenWidth: Int = textBuffer.headOption.map(_.length).getOrElse(0)
  def screenHeight: Int = textBuffer.length

  private var bufferRendered = false
  copyBuffer()

  private var keyboard: Option[Keyboard] = None
  private val keyboardNBTKey: String = "keyboard"

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

    if (nbt.hasKey(keyboardNBTKey)) {
      keyboard = OcelotDesktop.workspace.entityByAddress(nbt.getString(keyboardNBTKey)).map(_.asInstanceOf[Keyboard])
    }
  }

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

    if (keyboard.isDefined) {
      nbt.setString(keyboardNBTKey, keyboard.get.node.address)
    }
  }

  override def dispose(): Unit = {
    super.dispose()
    keyboard.foreach(OcelotDesktop.workspace.remove(_))
    viewport.foreach(_.freeResource())
    viewport = None
  }

  def attachKeyboard(): Unit = {
    detachKeyboard()
    val kbd = new Keyboard
    OcelotDesktop.workspace.add(kbd)
    screen.connect(kbd)
    keyboard = Some(kbd)
  }

  def detachKeyboard(): Unit = {
    if (keyboard.isDefined) {
      screen.disconnect(keyboard.get)
      OcelotDesktop.workspace.remove(keyboard.get)
      keyboard = None
    }
  }

  override def icon: IconSource = IconSource.Nodes.Screen.Standalone

  override def iconColor: Color = TierColor.get(screen.tier)

  override def rotatable: Boolean = true

  override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
    // no synchronization here: the methods to turn the screen on/off are indirect.
    if (screen.getPowerState) {
      menu.addEntry(ContextMenuEntry("Turn off", IconSource.Icons.Power) {
        screen.setPowerState(false)
      })
    } else {
      menu.addEntry(ContextMenuEntry("Turn on", IconSource.Icons.Power) {
        screen.setPowerState(true)
      })
    }

    menu.addEntry(ContextMenuEntry("Set aspect ratio", IconSource.Icons.AspectRatio) {
      new ScreenAspectRatioDialog(this).show()
    })

    if (keyboard.isDefined) {
      menu.addEntry(ContextMenuEntry("Remove keyboard", IconSource.Icons.KeyboardOff) {
        detachKeyboard()
      })
    } else {
      menu.addEntry(ContextMenuEntry("Add keyboard", IconSource.Icons.Keyboard) {
        attachKeyboard()
      })
    }

    menu.addSeparator()

    super.setupContextMenu(menu, event)
  }

  private def drawScreenTexture(needsMipmap: Boolean): Unit = {
    if (!bufferRendered) {
      for (viewport <- viewport) {
        viewport.renderWith {
          val width = (screenWidth * FontWidth).toInt
          val height = (screenHeight * FontHeight).toInt

          viewport.resize(width, height)

          var color: Short = 0

          for (y <- 0 until screenHeight) {
            for (x <- 0 until screenWidth) {
              if (x == 0 || viewport.font.charWidth(textBuffer(y)(x - 1)) != 16) {
                color = colorBuffer(y)(x)

                // no synchronization here: the color format cannot be changed via direct methods.
                viewport.background = IntColor(PackedColor.unpackBackground(color, screen.data.format))
                viewport.foreground = IntColor(PackedColor.unpackForeground(color, screen.data.format))

                viewport.char(x * FontWidth, y * FontHeight, textBuffer(y)(x))
              }
            }
          }
        }
      }

      bufferRendered = true
    }

    for (viewport <- viewport if needsMipmap) {
      viewport.generateMipmap()
    }
  }

  def drawScreenData(
    g: Graphics,
    startX: Float,
    startY: Float,
    scaleX: Float,
    scaleY: Float,
    filteringMode: MinFilteringMode,
  ): Unit = {
    for (viewport <- viewport) {
      g.save()
      g.scale(scaleX, scaleY)

      val startXScaled = startX / scaleX
      val startYScaled = startY / scaleY

      drawScreenTexture(filteringMode.needsMipmap)
      val bounds = Rect2D(startXScaled, startYScaled, viewport.width, viewport.height)
      g.blitScreenViewport(viewport, bounds, filteringMode = filteringMode)

      g.restore()
    }
  }

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

    // Drawing multiblock screen sprite
    def drawScreenPart(icon: IconSource, x: Float, y: Float, width: Float, height: Float): Unit = {
      g.sprite(
        icon,
        x,
        y,
        width,
        height,
        iconColor,
      )
    }

    val aspectRatioWidth = screen.aspectRatio._1.toInt
    val aspectRatioHeight = screen.aspectRatio._2.toInt

    // 1 x n
    if (aspectRatioWidth == 1) {
      // 1 x 1
      if (aspectRatioHeight == 1) {
        drawScreenPart(
          IconSource.Nodes.Screen.Standalone,
          position.x + HighlightThickness,
          position.y + HighlightThickness,
          NoHighlightSize,
          NoHighlightSize,
        )
      }
      // 1 x n
      else {
        // Top
        drawScreenPart(
          IconSource.Nodes.Screen.ColumnTop,
          position.x + HighlightThickness,
          position.y + HighlightThickness,
          NoHighlightSize,
          Size - HighlightThickness,
        )

        // Middle
        for (y <- 1 until aspectRatioHeight - 1) {
          drawScreenPart(
            IconSource.Nodes.Screen.ColumnMiddle,
            position.x + HighlightThickness,
            position.y + y * Size,
            NoHighlightSize,
            Size,
          )
        }

        // Bottom
        drawScreenPart(
          IconSource.Nodes.Screen.ColumnBottom,
          position.x + HighlightThickness,
          position.y + (aspectRatioHeight - 1) * Size,
          NoHighlightSize,
          NoHighlightSize,
        )
      }
    }
    // n x n
    else {
      // n x 1
      if (aspectRatioHeight == 1) {
        // Left
        drawScreenPart(
          IconSource.Nodes.Screen.RowLeft,
          position.x + HighlightThickness,
          position.y + HighlightThickness,
          Size - HighlightThickness,
          NoHighlightSize,
        )

        // Middle
        for (x <- 1 until aspectRatioWidth - 1) {
          drawScreenPart(
            IconSource.Nodes.Screen.RowMiddle,
            position.x + x * Size,
            position.y + HighlightThickness,
            Size,
            NoHighlightSize,
          )
        }

        // Right
        drawScreenPart(
          IconSource.Nodes.Screen.RowRight,
          position.x + (aspectRatioWidth - 1) * Size,
          position.y + HighlightThickness,
          Size - HighlightThickness,
          NoHighlightSize,
        )
      }
      // n x n
      else {
        def drawLine(
          y: Float,
          height: Float,
          leftIcon: IconSource,
          middleIcon: IconSource,
          rightIcon: IconSource,
        ): Unit = {
          // Left
          drawScreenPart(
            leftIcon,
            position.x + HighlightThickness,
            y,
            Size,
            height,
          )

          // Middle
          for (x <- 1 until aspectRatioWidth - 1) {
            drawScreenPart(
              middleIcon,
              position.x + x * Size,
              y,
              Size,
              height,
            )
          }

          // Right
          drawScreenPart(
            rightIcon,
            position.x + (aspectRatioWidth - 1) * Size - HighlightThickness,
            y,
            Size,
            height,
          )
        }

        // Top
        drawLine(
          position.y + HighlightThickness,
          Size - HighlightThickness,
          IconSource.Nodes.Screen.TopLeft,
          IconSource.Nodes.Screen.TopMiddle,
          IconSource.Nodes.Screen.TopRight,
        )

        // Middle
        for (y <- 1 until aspectRatioHeight - 1) {
          drawLine(
            position.y + (y * Size),
            Size,
            IconSource.Nodes.Screen.MiddleLeft,
            IconSource.Nodes.Screen.Middle,
            IconSource.Nodes.Screen.MiddleRight,
          )
        }

        // Bottom
        drawLine(
          position.y + (aspectRatioHeight - 1) * Size,
          Size - HighlightThickness,
          IconSource.Nodes.Screen.BottomLeft,
          IconSource.Nodes.Screen.BottomMiddle,
          IconSource.Nodes.Screen.BottomRight,
        )
      }
    }

    // If screen is on
    // no synchronization here: the methods to turn the screen on/off are indirect.
    if (screen.getPowerState) {
      // If realtime rendering of screen data is allowed
      if (Settings.get.renderScreenDataOnNodes) {
        val virtualScreenBounds = Rect2D(
          position.x + Margin,
          position.y + Margin,
          size.width - Margin * 2,
          size.height - Margin * 2,
        )

        // Black background rect
        g.rect(
          virtualScreenBounds.x,
          virtualScreenBounds.y,
          virtualScreenBounds.w,
          virtualScreenBounds.h,
          Color.Black,
        )

        // Calculating pixel data bounds, so that they fit perfectly into the virtual screen
        val pixelDataSize = Size2D(
          screenWidth * FontWidth,
          screenHeight * FontHeight,
        )

        val scale = Math.min(
          virtualScreenBounds.w / pixelDataSize.width,
          virtualScreenBounds.h / pixelDataSize.height,
        )

        // Drawing pixel data
        drawScreenData(
          g,
          virtualScreenBounds.x + virtualScreenBounds.w / 2 - (pixelDataSize.width * scale) / 2,
          virtualScreenBounds.y + virtualScreenBounds.h / 2 - (pixelDataSize.height * scale) / 2,
          scale,
          scale,
          MinFilteringMode.NearestMipmapNearest,
        )
      }
      // Drawing simple overlay otherwise
      else {
        g.sprite(
          IconSource.Nodes.Screen.PowerOnOverlay,
          position.x + HighlightThickness,
          position.y + HighlightThickness,
          NoHighlightSize,
          NoHighlightSize,
        )
      }
    }
  }

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

    copyBuffer()
  }

  private def copyBuffer(): Unit = screen.synchronized {
    val newWidth = screen.getWidth
    val newHeight = screen.getHeight

    if (textBuffer.length != newHeight || textBuffer(0).length != newWidth) {
      // create new arrays
      colorBuffer = Array.tabulate(newHeight, newWidth)((y, x) => screen.data.color(y)(x))
      textBuffer = Array.tabulate(newHeight, newWidth)((y, x) => screen.data.buffer(y)(x))
    } else {
      // reuse existing arrays
      for (y <- 0 until newHeight) {
        for (x <- 0 until newWidth) {
          colorBuffer(y)(x) = screen.data.color(y)(x)
          textBuffer(y)(x) = screen.data.buffer(y)(x)
        }
      }
    }

    bufferRendered = false
  }

  override def createWindow(): ScreenWindow = new ScreenWindow(this)
}

object ScreenNode {
  val FontWidth = 8f
  val FontHeight = 16f
  val BorderSize = 8f

  // the contents of a screen are offset by a quarter of a texel in OpenComputers
  val Margin: Float = HighlightThickness + BorderSize + Node.Scale * 0.25f
}
