package ocelot.desktop.geometry

import ocelot.desktop.util.NumberUtils.ExtendedFloat

object Vector3D {
  val Zero: Vector3D = Vector3D(0, 0, 0)

  val AxisX: Vector3D = Vector3D(1, 0, 0)
  val AxisY: Vector3D = Vector3D(0, 1, 0)
  val AxisZ: Vector3D = Vector3D(0, 0, 1)

  val Up: Vector3D = AxisY
  val Down: Vector3D = -AxisY

  val Left: Vector3D = -AxisX
  val Right: Vector3D = AxisX

  val Forward: Vector3D = -AxisZ
  val Back: Vector3D = AxisZ

  def apply(x: Double, y: Double, z: Double): Vector3D = Vector3D(x.toFloat, y.toFloat, z.toFloat)
}

case class Vector3D(x: Float, y: Float, z: Float) {
  def +(that: Vector3D): Vector3D = Vector3D(x + that.x, y + that.y, z + that.z)

  def -(that: Vector3D): Vector3D = Vector3D(x - that.x, y - that.y, z - that.z)

  def *(that: Vector3D): Vector3D = Vector3D(x * that.x, y * that.y, z * that.z)

  def /(that: Vector3D): Vector3D = Vector3D(x / that.x, y / that.y, z / that.z)

  def *(scalar: Float): Vector3D = Vector3D(x * scalar, y * scalar, z * scalar)

  def /(scalar: Float): Vector3D = Vector3D(x / scalar, y / scalar, z / scalar)

  def unary_-(): Vector3D = Vector3D(-x, -y, -z)

  def dot(that: Vector3D): Float = x * that.x + y * that.y + z * that.z

  def cross(that: Vector3D): Vector3D = Vector3D(
    y * that.z - z * that.y,
    z * that.x - x * that.z,
    x * that.y - y * that.x,
  )

  def angle(that: Vector3D): Float = {
    math.acos((dot(that) / length / that.length).clamped(-1, 1)).toFloat
  }

  def lerp(that: Vector3D, alpha: Float): Vector3D = {
    this * (1 - alpha) + that * alpha
  }

  def length: Float = math.sqrt(lengthSquared).toFloat

  def lengthSquared: Float = x * x + y * y + z * z

  def normalize: Vector3D = {
    val l = this.length
    if (l > 0) this / l else this
  }

  def abs: Vector3D = Vector3D(x.abs, y.abs, z.abs)

  def min(that: Vector3D): Vector3D = Vector3D(x.min(that.x), y.min(that.y), z.min(that.z))

  def max(that: Vector3D): Vector3D = Vector3D(x.max(that.x), y.max(that.y), z.max(that.z))

  def map(f: (Float, Float, Float) => (Float, Float, Float)): Vector3D = {
    val (nx, ny, nz) = f(x, y, z)
    Vector3D(nx, ny, nz)
  }

  def mapX(f: Float => Float): Vector3D = Vector3D(f(x), y, z)

  def mapY(f: Float => Float): Vector3D = Vector3D(x, f(y), z)

  def mapZ(f: Float => Float): Vector3D = Vector3D(x, y, f(z))

  def setX(nx: Float): Vector3D = Vector3D(nx, y, z)

  def setY(ny: Float): Vector3D = Vector3D(x, ny, z)

  def setZ(nz: Float): Vector3D = Vector3D(x, y, nz)

  override def toString: String = s"Vector3D [$x, $y, $z]"
}
