package ocelot.desktop.ui.widget

import ocelot.desktop.ColorScheme
import ocelot.desktop.util.NumberUtils.ExtendedFloat
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.HoverHandler
import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{Capturing, MouseEvent, ScrollEvent}
import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.ScrollView.{DecayFactor, DragState, MaxScrollVelocity, MinThumbSize, ScrollVelocity}
import ocelot.desktop.util.Logging

class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandler {
  override protected val layout: Layout = new Layout(this) {
    override def recalculateBounds(): Unit = {
      super.recalculateBounds()
      maximumSize = inner.maximumSize
    }

    override def relayout(): Unit = {
      inner.rawSetPosition(position - Vector2D(xOffset, yOffset))
      inner.relayout()
    }
  }

  inner.size = Size2D.Zero
  children +:= inner

  var scrollToEnd = false

  private var xOffset = 0f
  private var yOffset = 0f
  private var xVelocity = 0f
  private var yVelocity = 0f
  private var dragState: DragState = DragState.None

  private var vAnim = 0f
  private var hAnim = 0f

  def offset: Vector2D = Vector2D(xOffset, yOffset)

  override def receiveScrollEvents = true

  override def receiveMouseEvents = true

  eventHandlers += {
    case ScrollEvent(offset) if bounds.contains(UiHandler.mousePosition) =>
      if (KeyEvents.isShiftDown) {
        xVelocity -= offset * ScrollVelocity
      } else {
        yVelocity -= offset * ScrollVelocity
        scrollToEnd = false
      }

    case Capturing(event @ MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left)) =>
      val pos = UiHandler.mousePosition

      if (vThumbBounds.contains(pos) || hThumbBounds.contains(pos)) {
        event.consume()
      }
      
      dragState = if (vThumbBounds.contains(pos)) {
        scrollToEnd = false
        DragState.Vertical(yOffset, pos)
      } else if (hThumbBounds.contains(pos)) {
        DragState.Horizontal(xOffset, pos)
      } else {
        DragState.None
      }

    case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) =>
      dragState = DragState.None
  }

  override def minimumSize: Size2D = Size2D.Zero

  override def size_=(value: Size2D): Unit = {
    super.size_=(value)
    clampOffsets(resetVelocities = true)
  }

  override def shouldClip = true

  override def draw(g: Graphics): Unit = {
    inner.draw(g)

    if (vThumbVisible) {
      drawVThumb(g)
    }

    if (hThumbVisible) {
      drawHThumb(g)
    }
  }

  private def nextOffsetVelocity(x: Float, v: Float, dt: Float): (Float, Float) = {
    var x_ = x
    var v_ = v
    v_ = v_.clamped(-MaxScrollVelocity, MaxScrollVelocity)
    x_ += v_ * dt
    // exponential decay.
    v_ *= math.exp(-DecayFactor * UiHandler.dt).toFloat

    (x_, v_)
  }

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

    val prevXOffset = xOffset
    val prevYOffset = yOffset

    // in plain text, the logic is:
    // - if we're not holding down a thumb, apply the accumulated scroll velocity
    // - otherwise reset the velocity to zero and set the offset depending on where the cursor is

    if (dragState.isHorizontal) {
      xVelocity = 0
    } else {
      val next = nextOffsetVelocity(xOffset, xVelocity, UiHandler.dt)
      xOffset = next._1
      xVelocity = next._2
    }

    if (scrollToEnd) {
      yOffset = maxYOffset
      yVelocity = 0

      if (dragState.isVertical) {
        dragState = DragState.None
      }
    } else if (dragState.isVertical) {
      yVelocity = 0
    } else {
      val next = nextOffsetVelocity(yOffset, yVelocity, UiHandler.dt)
      yOffset = next._1
      yVelocity = next._2
    }

    clampOffsets(resetVelocities = true)

    val mousePos = UiHandler.mousePosition

    val vAnimDir = if (mouseOver && vThumbHoverArea.contains(mousePos) || dragState.isVertical) 1 else -1
    val hAnimDir = if (mouseOver && hThumbHoverArea.contains(mousePos) || dragState.isHorizontal) 1 else -1

    vAnim = (vAnim + UiHandler.dt / 0.2f * vAnimDir).clamped()
    hAnim = (hAnim + UiHandler.dt / 0.2f * hAnimDir).clamped()

    dragState match {
      case DragState.Vertical(startOffset, startPos) =>
        yOffset = startOffset + (mousePos - startPos).y / vThumbCoeff
        clampOffsets(resetVelocities = false)

      case DragState.Horizontal(startOffset, startPos) =>
        xOffset = startOffset + (mousePos - startPos).x / hThumbCoeff
        clampOffsets(resetVelocities = false)

      case DragState.None =>
    }

    if (prevXOffset != xOffset || prevYOffset != yOffset) {
      relayout()
    }
  }

  private def drawVThumb(g: Graphics): Unit = {
    val b = vThumbBounds
    g.rect(position.x + size.width - 11, position.y, 10, size.height, ColorScheme("Scrollbar").mapA(_ * vAnim))
    g.rect(b.x + 3, b.y, b.w - 6, b.h, ColorScheme("ScrollbarThumb").withAlpha(vAnim * 0.5f + 0.4f))
  }

  private def drawHThumb(g: Graphics): Unit = {
    val b = hThumbBounds
    g.rect(position.x, position.y + size.height - 11, size.width - 12, 10, ColorScheme("Scrollbar").mapA(_ * hAnim))
    g.rect(b.x, b.y + 3, b.w, b.h - 6, ColorScheme("ScrollbarThumb").withAlpha(hAnim * 0.5f + 0.4f))
  }

  private def maxXOffset: Float = inner.size.width - size.width

  private def maxYOffset: Float = inner.size.height - size.height

  private def clampOffsets(resetVelocities: Boolean): Unit = {
    val newXOffset = xOffset.clamped(max = maxXOffset)
    val newYOffset = yOffset.clamped(max = maxYOffset)

    if (resetVelocities) {
      if (xOffset != newXOffset) {
        xVelocity = 0
      }

      if (yOffset != newYOffset) {
        yVelocity = 0
      }
    }

    xOffset = newXOffset
    yOffset = newYOffset
  }

  private def vThumbHoverArea: Rect2D = {
    Rect2D(position.x + size.width - 12, position.y, 12, size.height)
  }

  private def computeThumbSize(preferredSize: Float, scrollBarSize: Float): Float = {
    preferredSize.max(MinThumbSize).min(scrollBarSize)
  }

  private def vThumbBounds: Rect2D = {
    val x = position.x + size.width - 12
    val y = position.y + yOffset * vThumbCoeff

    Rect2D(x, y + 2, 12, computeThumbSize(vThumbCoeff * size.height, vScrollBarSize))
  }

  private def hThumbHoverArea: Rect2D = {
    Rect2D(position.x, position.y + size.height - 12, size.width, 12)
  }

  private def hThumbBounds: Rect2D = {
    val x = position.x + xOffset * hThumbCoeff
    val y = position.y + size.height - 12

    Rect2D(x + 2, y, computeThumbSize(hThumbCoeff * size.width, hScrollBarSize), 12)
  }

  protected def vThumbVisible: Boolean = inner.size.height > size.height

  protected def hThumbVisible: Boolean = inner.size.width > size.width

  private def vScrollBarSize: Float = size.height - 4

  private def hScrollBarSize: Float = size.width - 14

  private def vThumbCoeff: Float = vScrollBarSize / inner.size.height

  private def hThumbCoeff: Float = hScrollBarSize / inner.size.width
}

object ScrollView {
  private val ScrollVelocity = 385f
  private val MaxScrollVelocity = ScrollVelocity * 4
  private val DecayFactor = 5f
  private val MinThumbSize = 15f

  /**
    * Which of the thumbs are being dragged.
    */
  sealed trait DragState {
    def isVertical: Boolean = false
    def isHorizontal: Boolean = false
  }

  object DragState {
    case class Vertical(startOffset: Float, startPos: Vector2D) extends DragState {
      override def isVertical: Boolean = true
    }

    case class Horizontal(startOffset: Float, startPos: Vector2D) extends DragState {
      override def isHorizontal: Boolean = true
    }

    case object None extends DragState
  }
}
