package ocelot.desktop.windows

import ocelot.desktop.audio.SoundSource
import ocelot.desktop.color.RGBAColor
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.graphics.Texture.MinFilteringMode
import ocelot.desktop.node.nodes.ScreenNode
import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents}
import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent, ScrollEvent}
import ocelot.desktop.ui.widget.window.BasicWindow
import ocelot.desktop.util.{DrawUtils, Logging}
import ocelot.desktop.windows.ScreenWindow._
import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings}
import org.apache.commons.lang3.StringUtils
import org.lwjgl.input.Keyboard
import totoro.ocelot.brain.entity.Screen
import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.Tier

class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
  private var lastMousePos = Vector2D(0, 0)
  private var sentTouchEvent = false
  private var startingWidth = 0f
  private var scaleDragPoint: Option[Vector2D] = None

  private var _scale = 1f
  private var scaleX: Float = 1f
  private var scaleY: Float = 1f

  private def scale: Float = _scale

  private def scale_=(value: Float): Any = {
    _scale = value

    scaleX = (FontWidth * scale).floor / FontWidth
    scaleY = (FontHeight * scale).floor / FontHeight
  }

  private def screen: Screen = screenNode.screen

  private def screenWidth: Int = screenNode.screenWidth

  private def screenHeight: Int = screenNode.screenHeight

  override def minimumSize: Size2D = Size2D(
    screenWidth * FontWidth * scaleX + BorderHorizontal,
    screenHeight * scaleY * FontHeight + BorderVertical,
  )

  override def receiveScrollEvents: Boolean = true

  eventHandlers += {
    case event: KeyEvent if shouldHandleKeys =>
      event.state match {
        case KeyEvent.State.Press | KeyEvent.State.Repeat =>
          screen.keyDown(event.char, event.code, OcelotDesktop.player)

          // note: in OpenComputers, key_down signal is fired __before__ clipboard signal
          if (event.code == Keyboard.KEY_INSERT)
            screen.clipboard(UiHandler.clipboard, OcelotDesktop.player)

        case KeyEvent.State.Release =>
          screen.keyUp(event.char, event.code, OcelotDesktop.player)
      }

      event.consume()

    case event: MouseEvent if shouldHandleKeys =>
      val pos = convertMousePos(UiHandler.mousePosition)
      val inside = checkBounds(pos)

      if (inside)
        lastMousePos = pos

      event.state match {
        case MouseEvent.State.Press =>
          if (inside && screen.tier > Tier.One) {
            screen.mouseDown(pos.x, pos.y, event.button.id, OcelotDesktop.player)
            sentTouchEvent = true
            event.consume()
          }

          if (
            pinButtonBounds.contains(UiHandler.mousePosition) || closeButtonBounds.contains(UiHandler.mousePosition)
          ) {
            SoundSource.InterfaceClick.press.play()
          }

        case MouseEvent.State.Release =>
          if (event.button == MouseEvent.Button.Middle && inside) {
            screen.clipboard(UiHandler.clipboard, OcelotDesktop.player)
            event.consume()
          }

          if (sentTouchEvent) {
            screen.mouseUp(lastMousePos.x, lastMousePos.y, event.button.id, OcelotDesktop.player)
            event.consume()
            sentTouchEvent = false
          } else if (pinButtonBounds.contains(UiHandler.mousePosition)) {
            if (isPinned) unpin() else pin()
            SoundSource.InterfaceClick.release.play()
          } else if (closeButtonBounds.contains(UiHandler.mousePosition)) {
            close()
            SoundSource.InterfaceClick.release.play()
          }

        case _ =>
      }

    case event: ScrollEvent if shouldHandleKeys && screen.tier > Tier.One =>
      screen.mouseScroll(lastMousePos.x, lastMousePos.y, event.offset, OcelotDesktop.player)
      event.consume()

    case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, _) =>
      if (scaleDragRegion.contains(event.start)) {
        scaleDragPoint = Some(event.start)
        startingWidth = screenWidth * FontWidth * scaleX
      }

    case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, mousePos) =>
      for (point <- scaleDragPoint) {
        val sx = point.x - mousePos.x
        val sy = point.y - mousePos.y

        val uiScale = UiHandler.scalingFactor
        var newScale = scale

        // TODO: refactor this mess, make it consider both sizes and not have two nearby slightly off "snap points"

        if (sx.abs > sy.abs) {
          val newWidth = startingWidth - sx
          val maxWidth = screenWidth * FontWidth
          var midScale = (newWidth / maxWidth).max(0f)

          if (!KeyEvents.isShiftDown && scale <= 1.001)
            midScale = midScale.min(1f)

          val lowScale = (FontWidth * midScale * uiScale).floor / FontWidth / uiScale
          val highScale = (FontWidth * midScale * uiScale).ceil / FontWidth / uiScale

          newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
        } else {
          val newHeight = startingWidth * (screenHeight * FontHeight / screenWidth / FontWidth) - sy
          val maxHeight = screenHeight * FontHeight
          var midScale = (newHeight / maxHeight).max(0f)

          if (!KeyEvents.isShiftDown && scale <= 1.001)
            midScale = midScale.min(1f)

          val lowScale = (FontHeight * midScale * uiScale).floor / FontHeight / uiScale
          val highScale = (FontHeight * midScale * uiScale).ceil / FontHeight / uiScale

          newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
        }

        if (newScale != scale)
          scale = newScale

        // enforce minimal screen size
        if (scale <= 0.249f) {
          scale = 0.25f
        }
      }

    case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) =>
      scaleDragPoint = None
  }

  private def shouldHandleKeys: Boolean = isFocused && !root.get.modalDialogPool.isVisible

  override def save(nbt: NBTTagCompound): Unit = {
    nbt.setFloat("scale", scale)
    super.save(nbt)
  }

  override def fitToCenter(): Unit = {
    scale = math.min(
      ((UiHandler.root.width * 0.9f) / (screenWidth * FontWidth + BorderHorizontal)).min(1f).max(0f),
      ((UiHandler.root.height * 0.9f) / (screenHeight * FontHeight + BorderVertical)).min(1f).max(0f),
    )

    super.fitToCenter()
  }

  override def load(nbt: NBTTagCompound): Unit = {
    scale = nbt.getFloat("scale")

    super.load(nbt)
  }

  private def checkBounds(p: Vector2D): Boolean = p.x >= 0 && p.y >= 0 && p.x < screenWidth && p.y < screenHeight

  private def convertMousePos(p: Vector2D): Vector2D = {
    // no synchronization here (see the note in ScreenNode): the method to change this property is indirect.
    if (screen.getPrecisionMode) {
      Vector2D(
        (p.x - BorderLeft - position.x) / FontWidth / scaleX,
        (p.y - BorderTop - position.y) / FontHeight / scaleY,
      )
    } else {
      Vector2D(
        math.floor((p.x - BorderLeft - position.x) / FontWidth / scaleX),
        math.floor((p.y - BorderTop - position.y) / FontHeight / scaleY),
      )
    }
  }

  override protected def dragRegions: Iterator[Rect2D] = Iterator(
    Rect2D(position.x, position.y, size.width, BorderTop.toFloat),
    Rect2D(position.x, position.y, BorderLeft.toFloat, size.height),

    // these two must not include `scaleDragRegion`
    Rect2D(position.x, position.y + size.height - BorderBottom, size.width - BorderRight, BorderBottom.toFloat),
    Rect2D(position.x + size.width - BorderRight, position.y, BorderRight.toFloat, size.height - BorderBottom),
  )

  private def scaleDragRegion: Rect2D = Rect2D(
    position.x + size.width - BorderRight,
    position.y + size.height - BorderBottom,
    BorderRight.toFloat,
    BorderBottom.toFloat,
  )

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

    if (scaleDragPoint.isDefined || scaleDragRegion.contains(UiHandler.mousePosition)) {
      root.get.statusBar.addMouseEntry("icons/DragLMB", "Scale screen")
      root.get.statusBar.addKeyMouseEntry("icons/DragLMB", "SHIFT", "Scale screen (magnify)")
    }

    val currentMousePos = convertMousePos(UiHandler.mousePosition)
    if (!checkBounds(currentMousePos) || currentMousePos == lastMousePos) return

    lastMousePos = currentMousePos

    if (isFocused && screen.tier > Tier.One) {
      for (button <- MouseEvents.pressedButtons) {
        screen.mouseDrag(lastMousePos.x, lastMousePos.y, button.id, OcelotDesktop.player)
      }
    }
  }

  private def pinButtonBounds: Rect2D = Rect2D(
    position.x + screenWidth * FontWidth * scaleX - 13,
    position.y + 3,
    14,
    14,
  )

  private def closeButtonBounds: Rect2D = Rect2D(
    position.x + screenWidth * FontWidth * scaleX + 2,
    position.y + 3,
    15,
    14,
  )

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

    val startX = position.x + BorderLeft
    val startY = position.y + BorderTop
    val windowWidth = screenWidth * FontWidth * scaleX
    val windowHeight = screenHeight * FontHeight * scaleY

    DrawUtils.shadow(g, startX - 22, startY - 22, windowWidth + 44, windowHeight + 52, 0.5f)
    DrawUtils.screenBorder(g, startX, startY, windowWidth, windowHeight)

    // no synchronization here (see the note in ScreenNode): the methods to turn the screen on/off are indirect.
    if (screen.getPowerState) {
      screenNode.drawScreenData(
        g,
        startX,
        startY,
        scaleX,
        scaleY,
        if (Settings.get.screenWindowMipmap) {
          MinFilteringMode.LinearMipmapLinear
        } else {
          MinFilteringMode.Nearest
        },
      )

    } else {
      g.rect(startX, startY, windowWidth, windowHeight, ColorScheme("ScreenOff"))
    }

    g.setSmallFont()
    g.background = RGBAColor(0, 0, 0, 0)
    g.foreground = RGBAColor(110, 110, 110)

    val freeSpace = ((windowWidth - 15) / 8).toInt
    val label = screenNode.label.get
    val text = if (label.length <= freeSpace)
      label
    else
      StringUtils.substring(label, 0, (freeSpace - 1).max(0).min(label.length)) + "…"

    g.text(startX - 4, startY - 14, text)
    g.setNormalFont()

    g.sprite(if (isPinned) "icons/Unpin" else "icons/Pin", pinButtonBounds)
    g.sprite("icons/Close", closeButtonBounds)

    endDraw(g)
  }
}

object ScreenWindow {
  private val BorderTop = 20
  private val BorderLeft = 16
  private val BorderRight = 16
  private val BorderBottom = 16

  private val BorderVertical = BorderTop + BorderBottom
  private val BorderHorizontal = BorderLeft + BorderRight
}
