package ocelot.desktop.ui.widget

import ocelot.desktop.color.Color
import ocelot.desktop.util.NumberUtils.ExtendedFloat
import ocelot.desktop.geometry._
import ocelot.desktop.graphics.scene.{Camera3D, Scene3D}
import ocelot.desktop.graphics.{Graphics, IconSource, Viewport3D}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{DragEvent, MouseEvent, ScrollEvent}
import ocelot.desktop.ui.layout.{CopyLayout, Layout, LinearLayout}
import ocelot.desktop.util.{DrawUtils, Orientation}

abstract class Viewport3DWidget extends Widget with MouseHandler with HoverHandler {
  private final val ScrollSpeed: Float = 1.125f
  private final val RotateSpeed: Float = 0.01f
  private final val PanSpeed: Float = 0.005f
  private final val MinDistance: Float = 0.5f
  private final val MaxDistance: Float = 50.0f
  private final val Smoothing: Float = 1e-9f

  private final val HomeTarget: Vector3D = Vector3D(0.5, 0.5, 0.5)
  private final val HomeRotation: Quaternion = Quaternion.euler(Vector3D(-45.toRadians, 45.toRadians, 0.toRadians))
  private final val HomeDistance: Float = 3f

  protected def createScene(): Scene3D

  // XXX: the viewport's dimensions must be kept outside the viewport object itself: see #121
  private val viewportWidth = 600
  private val viewportHeight = 400

  private lazy val viewport = new Viewport3D(viewportWidth, viewportHeight)
  private var viewportDisposed = false // extra safety

  protected val camera: Camera3D = new Camera3D()

  private var cameraCurrentTarget, cameraFinalTarget = HomeTarget
  private var cameraCurrentRotation, cameraFinalRotation = HomeRotation
  private var cameraCurrentDistance, cameraFinalDistance = HomeDistance

  private def cameraTransform(distance: Float, rotation: Quaternion, target: Vector3D): Transform3D = {
    val dir = rotation * Vector3D.Forward
    val up = rotation * Vector3D.Up
    val origin = target - dir * distance
    Transform3D(Basis3D.lookingAt(-dir, up), origin)
  }

  private def cameraCurrentTransform: Transform3D = {
    cameraTransform(cameraCurrentDistance, cameraCurrentRotation, cameraCurrentTarget)
  }

  private def cameraFinalTransform: Transform3D = {
    cameraTransform(cameraFinalDistance, cameraFinalRotation, cameraCurrentTarget)
  }

  private var scene: Scene3D = createScene()

  override protected val layout: Layout = new CopyLayout(this)

  override def minimumSize: Size2D = Size2D(viewportWidth + 4, viewportHeight + 4)

  override def maximumSize: Size2D = Size2D(viewportWidth + 4, viewportHeight + 4)

  children :+= new PaddingBox(
    new Widget {
      override def minimumSize: Size2D = Size2D(viewportWidth, viewportHeight)

      override def maximumSize: Size2D = Size2D(viewportWidth, viewportHeight)

      override def draw(g: Graphics): Unit = {
        DrawUtils.panel(g, bounds.x - 2, bounds.y - 2, bounds.w + 4, bounds.h + 4)

        if (!viewportDisposed) {
          viewport.draw(scene)
          g.blitViewport3D(viewport, bounds)
        }
      }
    },
    Padding2D.equal(2),
  )

  children :+= {
    val toolbar: Widget = new Widget {
      override val layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 2)
    }

    setupToolbar(toolbar)

    new PaddingBox(toolbar, Padding2D(left = 8, top = 8))
  }

  protected def setupToolbar(toolbar: Widget): Unit = {
    toolbar.children :+= new IconButton(
      IconSource.Icons.Home,
      IconSource.Icons.Home,
      pressedColor = Color.White.withAlpha(0.5f),
      releasedColor = Color.White.withAlpha(0.2f),
      darkenActiveColorFactor = 0.5f,
    ) {
      override def onClicked(): Unit = {
        cameraFinalTarget = HomeTarget
        cameraFinalRotation = HomeRotation
        cameraFinalDistance = HomeDistance
      }
    }
  }

  override protected def receiveDragEvents: Boolean = true

  override def receiveScrollEvents: Boolean = true

  eventHandlers += {
    case ScrollEvent(offset) =>
      cameraFinalDistance = (if (offset > 0) cameraFinalDistance / ScrollSpeed else cameraFinalDistance * ScrollSpeed)
        .clamped(MinDistance, MaxDistance)

    case event @ DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, _) if KeyEvents.isShiftDown =>
      val basis = cameraFinalTransform.basis
      val movement = (basis.left * event.delta.x + basis.up * event.delta.y) * cameraFinalDistance * PanSpeed
      cameraCurrentTarget += movement
      cameraFinalTarget += movement

    case event @ DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, _) =>
      val basis = cameraFinalTransform.basis
      val xRotation = Quaternion.axisAngle(Vector3D.Up, -event.delta.x * RotateSpeed)
      val yRotation = Quaternion.axisAngle(basis.left, event.delta.y * RotateSpeed)
      var deltaRotation = xRotation * yRotation

      val newRotation = deltaRotation * cameraCurrentRotation
      if ((newRotation * Vector3D.Up).dot(Vector3D.Up) < 0.0) {
        // rotation would make camera upside down, so don't let it
        deltaRotation = xRotation
      }

      cameraCurrentRotation = deltaRotation * cameraCurrentRotation
      cameraFinalRotation = deltaRotation * cameraFinalRotation
  }

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

    val alpha = 1 - math.pow(Smoothing, UiHandler.dt).toFloat
    cameraCurrentTarget = cameraCurrentTarget.lerp(cameraFinalTarget, alpha)
    cameraCurrentRotation = cameraCurrentRotation.slerp(cameraFinalRotation, alpha)
    cameraCurrentDistance = cameraCurrentDistance.lerp(cameraFinalDistance, alpha)

    camera.transform = cameraCurrentTransform

    scene = createScene()

    if (mouseOver) {
      root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, "Rotate view")
      root.get.statusBar.addKeyMouseEntry(IconSource.Icons.LMB, "SHIFT", "Pan view")
    }
  }

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

    viewport.freeResource()
    viewportDisposed = true
  }
}
