package ocelot.desktop.ui.widget

import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource}
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.{KeyEvent, MouseEvent, ScrollEvent}
import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.ScreenView.ScaleTag
import ocelot.desktop.util.{Keybind, Persistable, Register}
import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings}
import org.lwjgl.input.Keyboard
import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.{ClipboardSplitter, Tier}

// TODO: use an interface instead of ScreenNode.
abstract class ScreenView(screenNode: ScreenNode) extends Widget with Persistable {
  override protected val layout: Layout = new Layout(this)

  private var lastMousePos = Vector2D.Zero

  val scale: Register.Writeable[Float] = Register(1f)
  def scaleX: Float = (FontWidth * scale.value).floor / FontWidth
  def scaleY: Float = (FontHeight * scale.value).floor / FontHeight

  private def screenWidth: Int = screenNode.screenWidth
  private def screenHeight: Int = screenNode.screenHeight

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

  override def maximumSize: Size2D = minimumSize

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

  private def bufferCoordsInBounds(p: Vector2D): Boolean =
    new Rect2D(0, 0, screenWidth, screenHeight).contains(p)

  private def mouseCoordsInBounds: Boolean =
    bufferCoordsInBounds(toBufferCoords(UiHandler.mousePosition))

  override def receiveMouseEvents: Boolean = true

  override def receiveScrollEvents: Boolean = true

  protected def isFocused: Boolean

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

  // OC doesn't trigger several touch events in a row; the same holds for drop events.
  // For the following inputs:
  //   LMB down, RMB down, RMB up, LMB up
  // ...OC only registers LMB down and RMB up, dropping the other two events.
  private var pressedButton: Option[MouseEvent.Button.Value] = None

  // NOTE: events are handled before update().
  // if the brain initiates a viewport size change, mouse events could be sent for coordinates outside the new bounds.
  // TODO: look into how OpenComputers deals with that, if it does at all.
  eventHandlers += {
    case event: KeyEvent if shouldHandleInput && event.code != Keyboard.KEY_ESCAPE =>
      event.state match {
        case KeyEvent.State.Press | KeyEvent.State.Repeat =>
          screenNode.screen.keyDown(event.char, event.code, OcelotDesktop.player)

          // note: in OpenComputers, key_down signal is fired __before__ clipboard signal
          if (event.code == Settings.get.keymap(Keybind.Insert)) {
            sendClipboard()
          }

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

      event.consume()

    case event: MouseEvent if shouldHandleInput =>
      val pos = toBufferCoords(UiHandler.mousePosition)
      val inBounds = bufferCoordsInBounds(pos)

      if (inBounds) {
        lastMousePos = pos
      }

      event.state match {
        case MouseEvent.State.Pressed if inBounds && screenNode.screen.tier > Tier.One =>
          if (pressedButton.isEmpty) {
            screenNode.screen.mouseDown(pos.x, pos.y, event.button.id, OcelotDesktop.player)
          }

          pressedButton = Some(event.button)
          event.consume()

        case MouseEvent.State.Released =>
          if (inBounds && event.button == MouseEvent.Button.Middle) {
            sendClipboard()
            event.consume()
          }

          if (pressedButton.nonEmpty) {
            screenNode.screen.mouseUp(lastMousePos.x, lastMousePos.y, event.button.id, OcelotDesktop.player)
            pressedButton = None

            if (inBounds) {
              event.consume()
            }
          }

        case _ =>
      }

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

  private val clipboardSplitter = new ClipboardSplitter

  private def sendClipboard(): Unit = {
    clipboardSplitter.split(UiHandler.clipboard) match {
      case Some(value) =>
        for (part <- value) {
          screenNode.screen.clipboard(part, OcelotDesktop.player)
        }

      case None =>
        SoundSource.fromBuffer(SoundBuffers.NoteBlock("harp"), SoundCategory.Records).play()
    }
  }

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

  override def load(nbt: NBTTagCompound): Unit = {
    super.load(nbt)
    scale.nextValue = nbt.getFloat(ScaleTag)
  }

  private val screenSize = Register.sampling(Size2D(screenWidth, screenHeight))

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

    // NOTE: the single bar is intentional! both operands have to be evaluated.
    if (scale.update() | screenSize.update()) {
      recalculateBoundsAndRelayout()
    }

    val mousePos = toBufferCoords(UiHandler.mousePosition)

    if (bufferCoordsInBounds(mousePos) && mousePos != lastMousePos) {
      lastMousePos = mousePos

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

  override def draw(g: Graphics): Unit = {
    // no synchronization here (see the note in ScreenNode): the methods to turn the screen on/off are indirect.
    if (screenNode.screen.getPowerState) {
      screenNode.drawScreenData(
        g,
        position.x,
        position.y,
        scaleX,
        scaleY,
        if (Settings.get.screenWindowMipmap) {
          MinFilteringMode.LinearMipmapLinear
        } else {
          MinFilteringMode.Nearest
        },
      )
    } else {
      g.rect(bounds, ColorScheme("ScreenOff"))
    }
  }
}

object ScreenView {
  private val ScaleTag: String = "scale"
}
