package ocelot.desktop.ui.widget
import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.ClickHandler
import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{ClickEvent, KeyEvent, MouseEvent}
import ocelot.desktop.util.DrawUtils
import ocelot.desktop.util.animation.ColorAnimation
import org.lwjgl.input.Keyboard

import scala.collection.mutable.ArrayBuffer

class TextInput(val initialText: String = "") extends Widget with ClickHandler {
  def onInput(text: String): Unit = {}

  def onConfirm(): Unit = {
    unfocus()
  }

  def validator(text: String): Boolean = true
  final def isInputValid: Boolean = validator(text)

  var isFocused = false

  def text: String = chars.mkString
  def text_=(value: String): Unit = chars = value.toCharArray

  private var cursorPos = 0
  private var cursorOffset = 0f
  private var scroll = 0f
  private val CursorBlinkTime = 2f
  private var blinkTimer = 0f
  private var chars = initialText.toCharArray
  private var textWidth = 0
  private var textChanged = false
  private val events = ArrayBuffer[TextEvent]()

  override def receiveMouseEvents = true
  override def receiveAllMouseEvents = true

  private var prevEnabled = enabled

  eventHandlers += {
    case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) =>
      if (isFocused && !clippedBounds.contains(UiHandler.mousePosition))
        unfocus()

    case ClickEvent(MouseEvent.Button.Left, pos) if enabled =>
      focus()
      events += new CursorMoveTo(pos.x)

    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
      events += new CursorMoveLeft
    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
      events += new CursorMoveRight
    case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
      events += new CursorMoveStart
    case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
      events += new CursorMoveEnd
    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
      events += new EraseCharBack
    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
      events += new EraseCharFront

    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused =>
      for (char <- UiHandler.clipboard.toCharArray)
        events += new WriteChar(char)

    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, ch) if isFocused =>
      if (KeyEvents.isDown(Keyboard.KEY_LCONTROL))
        for (char <- UiHandler.clipboard.toCharArray)
          events += new WriteChar(char)
      else
        events += new WriteChar(ch)

    case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused =>
      onConfirm()

    case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused =>
      if (!char.isControl)
        events += new WriteChar(char)
  }

  def setInput(text: String): Unit = {
    this.chars = text.toCharArray
    cursorPos = 0
    cursorOffset = 0
    textWidth = 0
    textChanged = true
  }

  override def minimumSize: Size2D = Size2D(200, 24)
  override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24)

  private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
  private val backgroundAnimation = new ColorAnimation(targetBackgroundColor, 7f)
  private val borderAnimation = new ColorAnimation(targetBorderColor, 7f)

  private def updateAnimationTargets(): Unit = {
    foregroundAnimation.goto(targetForegroundColor)
    backgroundAnimation.goto(targetBackgroundColor)
    borderAnimation.goto(targetBorderColor)
  }

  def focus(): Unit = {
    if (!isFocused) {
      if (enabled) {
        isFocused = true
      }

      updateAnimationTargets()
    }

    blinkTimer = 0
  }

  def unfocus(): Unit = {
    if (isFocused) {
      isFocused = false
      updateAnimationTargets()
    }
  }

  private def targetBorderColor: Color = ColorScheme(
    if (validator(chars.mkString)) {
      if (isFocused) "TextInputBorderFocused"
      else if (!enabled) "TextInputBorderDisabled"
      else "TextInputBorder"
    } else {
      if (isFocused) "TextInputBorderErrorFocused"
      else if (!enabled) "TextInputBorderErrorDisabled"
      else "TextInputBorderError"
    }
  )

  private def targetBackgroundColor: Color = ColorScheme(
    if (!enabled) "TextInputBackgroundDisabled"
    else "TextInputBackground"
  )

  private def targetForegroundColor: Color = ColorScheme(
    if (!enabled) "TextInputForegroundDisabled"
    else "TextInputForeground"
  )

  override def draw(g: Graphics): Unit = {
    if (textWidth == 0f && chars.nonEmpty)
      textWidth = chars.map(g.font.charWidth(_)).sum

    textChanged = false
    for (event <- events) event.handle(g)
    events.clear()

    if (textChanged) {
      val str = chars.mkString
      onInput(str)
      updateAnimationTargets()
    }

    g.rect(bounds, backgroundAnimation.color)
    DrawUtils.ring(g, position.x, position.y, size.width, size.height, thickness = 2, borderAnimation.color)

    g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)

    g.background = Color.Transparent
    g.foreground = foregroundAnimation.color

    var charOffset = 0
    for (char <- chars) {
      g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
      charOffset += g.font.charWidth(char)
    }

    if (isFocused && blinkTimer < CursorBlinkTime * 0.5f) {
      g.rect(position.x + 7 + cursorOffset - scroll, position.y + 4, 2, 16, borderAnimation.color)
    }
  }

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

    val nextEnabled = enabled

    if (nextEnabled != prevEnabled) {
      updateAnimationTargets()
      prevEnabled = nextEnabled
    }

    if (isFocused && !enabled) {
      unfocus()
    }

    foregroundAnimation.update()
    backgroundAnimation.update()
    borderAnimation.update()
    blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime
  }

  private def charWidth(g: Graphics, c: Char): Int = g.font.charWidth(c)

  //noinspection SameParameterValue
  private def charsWidth(g: Graphics, chars: Array[Char], first: Int, last: Int): Int = {
    var width = 0
    for (index <- first to last) {
      width += g.font.charWidth(chars(index))
    }
    width
  }

  private def adjustScroll(): Unit = {
    if (cursorOffset < scroll)
      scroll = cursorOffset
    if (cursorOffset - scroll > size.width - 16)
      scroll = cursorOffset - size.width + 16
  }

  private abstract class TextEvent {
    def handle(g: Graphics): Unit
  }

  private class CursorMoveLeft extends TextEvent {
    override def handle(g: Graphics): Unit = {
      if (cursorPos == 0) return

      cursorOffset -= charWidth(g, chars(cursorPos - 1))
      cursorPos -= 1
      blinkTimer = 0

      adjustScroll()
    }
  }

  private class CursorMoveRight extends TextEvent {
    override def handle(g: Graphics): Unit = {
      if (cursorPos >= chars.length) return

      cursorOffset += charWidth(g, chars(cursorPos))
      cursorPos += 1
      blinkTimer = 0

      adjustScroll()
    }
  }

  private class CursorMoveStart extends TextEvent {
    override def handle(g: Graphics): Unit = {
      cursorPos = 0
      cursorOffset = 0
      blinkTimer = 0
      scroll = 0
    }
  }

  private class CursorMoveEnd extends TextEvent {
    override def handle(g: Graphics): Unit = {
      cursorPos = chars.length
      cursorOffset = textWidth
      blinkTimer = 0
      scroll = (textWidth - size.width + 16).max(0)
    }
  }

  private class CursorMoveTo(mouseX: Float) extends TextEvent {
    override def handle(g: Graphics): Unit = {
      val absoluteX = mouseX - bounds.x + scroll - 4
      var width = 0
      var pos = 0
      while (width < absoluteX && pos < chars.length) {
        width += g.font.charWidth(chars(pos))
        if (width < absoluteX) pos += 1
      }

      cursorPos = chars.length.min(pos).max(0)
      cursorOffset = if (cursorPos > 0) charsWidth(g, chars, 0, (cursorPos - 1).max(0)) else 0
      blinkTimer = 0
      adjustScroll()
    }
  }

  private class EraseCharBack extends TextEvent {
    override def handle(g: Graphics): Unit = {
      val (lhs, rhs) = chars.splitAt(cursorPos)
      if (lhs.isEmpty) return

      val cw = charWidth(g, lhs.last)
      chars = lhs.take(lhs.length - 1) ++ rhs
      textChanged = true
      textWidth -= cw
      cursorOffset -= cw
      cursorPos -= 1
      blinkTimer = 0
      scroll = (scroll - cw).max(0)
    }
  }

  private class EraseCharFront extends TextEvent {
    override def handle(g: Graphics): Unit = {
      val (lhs, rhs) = chars.splitAt(cursorPos)
      if (rhs.isEmpty) return

      val cw = charWidth(g, rhs.head)
      chars = lhs ++ rhs.drop(1)
      textChanged = true
      textWidth -= cw
      blinkTimer = 0

      if (rhs.drop(1).map(charWidth(g, _)).sum < size.width - 16)
        scroll = (scroll - cw).max(0)
    }
  }

  private class WriteChar(char: Char) extends TextEvent {
    override def handle(g: Graphics): Unit = {
      val (lhs, rhs) = chars.splitAt(cursorPos)
      chars = lhs ++ Array(char) ++ rhs
      textChanged = true
      textWidth += charWidth(g, char)
      (new CursorMoveRight).handle(g)
    }
  }
}
