package ocelot.desktop.ui.event.handlers

import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler.{ClickInfo, withinTolerance}
import ocelot.desktop.ui.event.{ClickEvent, DoubleClickEvent, DragEvent, MouseEvent, TripleClickEvent}
import ocelot.desktop.ui.widget.Widget

import scala.collection.mutable

trait MouseHandler extends Widget {
  private val startPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]()
  private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]()
  private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]()

  private var lastClickInfo = Option.empty[ClickInfo]

  override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents

  protected def receiveClickEvents: Boolean = false

  protected def receiveDragEvents: Boolean = false

  /**
    * If `true`, drag events, once they start, will be sent on every update cycle even if the mouse does not move.
    */
  protected def spamDragEvents: Boolean = true

  /**
    * If `true`, a [[ClickEvent]] will be registered even if the mouse button is released
    * outside the tolerance threshold, as long as it stays within the widget's bounds.
    */
  protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents

  eventHandlers += {
    case MouseEvent(MouseEvent.State.Pressed, button) =>
      startPositions += (button -> UiHandler.mousePosition)

    case event @ MouseEvent(MouseEvent.State.Released, button) =>
      val mousePos = UiHandler.mousePosition

      val dragStopped = receiveDragEvents && dragButtons.remove(button)
      val clicked = (
        receiveClickEvents
          && !dragStopped
          && startPositions.get(button).exists(p => {
            if (allowClickReleaseOutsideThreshold) {
              clippedBounds.contains(mousePos)
            } else {
              withinTolerance(p, mousePos)
            }
          })
      )

      if (dragStopped) {
        handleEvent(DragEvent(DragEvent.State.Stop, button, mousePos))
      }

      if (clicked) {
        handleEvent(ClickEvent(button, mousePos))

        val info = ClickInfo(lastClickInfo, mousePos, event)
        lastClickInfo = Some(info)

        info.count match {
          case 2 => handleEvent(DoubleClickEvent(button, mousePos))
          case 3 => handleEvent(TripleClickEvent(button, mousePos))
          case _ =>
        }
      }

      startPositions.remove(button)
  }

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

    if (!receiveDragEvents) {
      return
    }

    val mousePos = UiHandler.mousePosition

    for ((button, startPos) <- startPositions) {
      if (!dragButtons.contains(button) && !withinTolerance(startPos, mousePos)) {
        handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0)))
        dragButtons += button
        prevPositions += (button -> mousePos)
      }
    }

    for (button <- dragButtons if spamDragEvents || prevPositions(button) != mousePos) {
      handleEvent(
        DragEvent(
          DragEvent.State.Drag,
          button,
          mousePos,
          startPositions(button),
          mousePos - prevPositions(button),
        )
      )

      prevPositions(button) = mousePos
    }
  }
}

object MouseHandler {
  private val Tolerance = 8
  private val ConsecutiveClickTimeMillis = 1e3f / 2

  private def withinTolerance(a: Vector2D, b: Vector2D): Boolean = (b - a).lengthSquared < Tolerance * Tolerance

  case class ClickInfo(position: Vector2D, time: Long, button: MouseEvent.Button.Value, var count: Int) {
    def isConsecutive(prev: ClickInfo): Boolean = {
      val quickEnough = time - prev.time < ConsecutiveClickTimeMillis
      val sameButton = button == prev.button
      val closeEnough = withinTolerance(position, prev.position)

      quickEnough && sameButton && closeEnough
    }
  }

  object ClickInfo {
    def apply(prev: Option[ClickInfo], position: Vector2D, event: MouseEvent): ClickInfo = {
      val info = ClickInfo(position, System.currentTimeMillis(), event.button, 1)

      for (prev <- prev if prev.isConsecutive(info)) {
        if (prev.count < 3) {
          info.count = prev.count + 1
        }
      }
      
      info
    }
  }
}
