import { Mat3 } from '../math/Mat3'

/**
 * 3-dimensional vector
 * @example
 *     const v = new Vec3(1, 2, 3)
 *     console.log('x=' + v.x) // x=1
 */
export class Vec3 {
  x: number
  y: number
  z: number

  static ZERO: Vec3
  static UNIT_X: Vec3
  static UNIT_Y: Vec3
  static UNIT_Z: Vec3

  constructor(x = 0.0, y = 0.0, z = 0.0) {
    this.x = x
    this.y = y
    this.z = z
  }

  /**
   * Vector cross product
   * @param target Optional target to save in.
   */
  cross(vector: Vec3, target = new Vec3()): Vec3 {
    const vx = vector.x
    const vy = vector.y
    const vz = vector.z
    const x = this.x
    const y = this.y
    const z = this.z

    target.x = y * vz - z * vy
    target.y = z * vx - x * vz
    target.z = x * vy - y * vx

    return target
  }

  /**
   * Set the vectors' 3 elements
   */
  set(x: number, y: number, z: number): Vec3 {
    this.x = x
    this.y = y
    this.z = z
    return this
  }

  /**
   * Set all components of the vector to zero.
   */
  setZero(): void {
    this.x = this.y = this.z = 0
  }

  /**
   * Vector addition
   */
  vadd(vector: Vec3): Vec3
  vadd(vector: Vec3, target: Vec3): void
  vadd(vector: Vec3, target?: Vec3): Vec3 | void {
    if (target) {
      target.x = vector.x + this.x
      target.y = vector.y + this.y
      target.z = vector.z + this.z
    } else {
      return new Vec3(this.x + vector.x, this.y + vector.y, this.z + vector.z)
    }
  }

  /**
   * Vector subtraction
   * @param target Optional target to save in.
   */
  vsub(vector: Vec3): Vec3
  vsub(vector: Vec3, target: Vec3): void
  vsub(vector: Vec3, target?: Vec3): Vec3 | void {
    if (target) {
      target.x = this.x - vector.x
      target.y = this.y - vector.y
      target.z = this.z - vector.z
    } else {
      return new Vec3(this.x - vector.x, this.y - vector.y, this.z - vector.z)
    }
  }

  /**
   * Get the cross product matrix a_cross from a vector, such that a x b = a_cross * b = c
   *
   * See {@link https://www8.cs.umu.se/kurser/TDBD24/VT06/lectures/Lecture6.pdf Umeå University Lecture}
   */
  crossmat(): Mat3 {
    return new Mat3([0, -this.z, this.y, this.z, 0, -this.x, -this.y, this.x, 0])
  }

  /**
   * Normalize the vector. Note that this changes the values in the vector.

   * @return Returns the norm of the vector
   */
  normalize(): number {
    const x = this.x
    const y = this.y
    const z = this.z
    const n = Math.sqrt(x * x + y * y + z * z)
    if (n > 0.0) {
      const invN = 1 / n
      this.x *= invN
      this.y *= invN
      this.z *= invN
    } else {
      // Make something up
      this.x = 0
      this.y = 0
      this.z = 0
    }
    return n
  }

  /**
   * Get the version of this vector that is of length 1.
   * @param target Optional target to save in
   * @return Returns the unit vector
   */
  unit(target = new Vec3()): Vec3 {
    const x = this.x
    const y = this.y
    const z = this.z
    let ninv = Math.sqrt(x * x + y * y + z * z)
    if (ninv > 0.0) {
      ninv = 1.0 / ninv
      target.x = x * ninv
      target.y = y * ninv
      target.z = z * ninv
    } else {
      target.x = 1
      target.y = 0
      target.z = 0
    }
    return target
  }

  /**
   * Get the length of the vector
   */
  length(): number {
    const x = this.x
    const y = this.y
    const z = this.z
    return Math.sqrt(x * x + y * y + z * z)
  }

  /**
   * Get the squared length of the vector.
   */
  lengthSquared(): number {
    return this.dot(this)
  }

  /**
   * Get distance from this point to another point
   */
  distanceTo(p: Vec3): number {
    const x = this.x
    const y = this.y
    const z = this.z
    const px = p.x
    const py = p.y
    const pz = p.z
    return Math.sqrt((px - x) * (px - x) + (py - y) * (py - y) + (pz - z) * (pz - z))
  }

  /**
   * Get squared distance from this point to another point
   */
  distanceSquared(p: Vec3): number {
    const x = this.x
    const y = this.y
    const z = this.z
    const px = p.x
    const py = p.y
    const pz = p.z
    return (px - x) * (px - x) + (py - y) * (py - y) + (pz - z) * (pz - z)
  }

  /**
   * Multiply all the components of the vector with a scalar.
   * @param target The vector to save the result in.
   */
  scale(scalar: number, target = new Vec3()): Vec3 {
    const x = this.x
    const y = this.y
    const z = this.z
    target.x = scalar * x
    target.y = scalar * y
    target.z = scalar * z
    return target
  }

  /**
   * Multiply the vector with an other vector, component-wise.
   * @param target The vector to save the result in.
   */
  vmul(vector: Vec3, target = new Vec3()): Vec3 {
    target.x = vector.x * this.x
    target.y = vector.y * this.y
    target.z = vector.z * this.z
    return target
  }

  /**
   * Scale a vector and add it to this vector. Save the result in "target". (target = this + vector * scalar)
   * @param target The vector to save the result in.
   */
  addScaledVector(scalar: number, vector: Vec3, target = new Vec3()): Vec3 {
    target.x = this.x + scalar * vector.x
    target.y = this.y + scalar * vector.y
    target.z = this.z + scalar * vector.z
    return target
  }

  /**
   * Calculate dot product
   * @param vector
   */
  dot(vector: Vec3): number {
    return this.x * vector.x + this.y * vector.y + this.z * vector.z
  }

  isZero(): boolean {
    return this.x === 0 && this.y === 0 && this.z === 0
  }

  /**
   * Make the vector point in the opposite direction.
   * @param target Optional target to save in
   */
  negate(target = new Vec3()): Vec3 {
    target.x = -this.x
    target.y = -this.y
    target.z = -this.z
    return target
  }

  /**
   * Compute two artificial tangents to the vector
   * @param t1 Vector object to save the first tangent in
   * @param t2 Vector object to save the second tangent in
   */
  tangents(t1: Vec3, t2: Vec3): void {
    const norm = this.length()
    if (norm > 0.0) {
      const n = Vec3_tangents_n
      const inorm = 1 / norm
      n.set(this.x * inorm, this.y * inorm, this.z * inorm)
      const randVec = Vec3_tangents_randVec
      if (Math.abs(n.x) < 0.9) {
        randVec.set(1, 0, 0)
        n.cross(randVec, t1)
      } else {
        randVec.set(0, 1, 0)
        n.cross(randVec, t1)
      }
      n.cross(t1, t2)
    } else {
      // The normal length is zero, make something up
      t1.set(1, 0, 0)
      t2.set(0, 1, 0)
    }
  }

  /**
   * Converts to a more readable format
   */
  toString(): string {
    return `${this.x},${this.y},${this.z}`
  }

  /**
   * Converts to an array
   */
  toArray(): [number, number, number] {
    return [this.x, this.y, this.z]
  }

  /**
   * Copies value of source to this vector.
   */
  copy(vector: Vec3): Vec3 {
    this.x = vector.x
    this.y = vector.y
    this.z = vector.z
    return this
  }

  /**
   * Do a linear interpolation between two vectors
   * @param t A number between 0 and 1. 0 will make this function return u, and 1 will make it return v. Numbers in between will generate a vector in between them.
   */
  lerp(vector: Vec3, t: number, target: Vec3): void {
    const x = this.x
    const y = this.y
    const z = this.z
    target.x = x + (vector.x - x) * t
    target.y = y + (vector.y - y) * t
    target.z = z + (vector.z - z) * t
  }

  /**
   * Check if a vector equals is almost equal to another one.
   */
  almostEquals(vector: Vec3, precision = 1e-6): boolean {
    if (
      Math.abs(this.x - vector.x) > precision ||
      Math.abs(this.y - vector.y) > precision ||
      Math.abs(this.z - vector.z) > precision
    ) {
      return false
    }
    return true
  }

  /**
   * Check if a vector is almost zero
   */
  almostZero(precision = 1e-6): boolean {
    if (Math.abs(this.x) > precision || Math.abs(this.y) > precision || Math.abs(this.z) > precision) {
      return false
    }
    return true
  }

  /**
   * Check if the vector is anti-parallel to another vector.
   * @param precision Set to zero for exact comparisons
   */
  isAntiparallelTo(vector: Vec3, precision?: number): boolean {
    this.negate(antip_neg)
    return antip_neg.almostEquals(vector, precision)
  }

  /**
   * Clone the vector
   */
  clone(): Vec3 {
    return new Vec3(this.x, this.y, this.z)
  }
}

Vec3.ZERO = new Vec3(0, 0, 0)
Vec3.UNIT_X = new Vec3(1, 0, 0)
Vec3.UNIT_Y = new Vec3(0, 1, 0)
Vec3.UNIT_Z = new Vec3(0, 0, 1)

const Vec3_tangents_n = new Vec3()
const Vec3_tangents_randVec = new Vec3()
const antip_neg = new Vec3()
