import { Vec3 } from '../math/Vec3'

/**
 * A Quaternion describes a rotation in 3D space. The Quaternion is mathematically defined as Q = x*i + y*j + z*k + w, where (i,j,k) are imaginary basis vectors. (x,y,z) can be seen as a vector related to the axis of rotation, while the real multiplier, w, is related to the amount of rotation.
 * @param x Multiplier of the imaginary basis vector i.
 * @param y Multiplier of the imaginary basis vector j.
 * @param z Multiplier of the imaginary basis vector k.
 * @param w Multiplier of the real part.
 * @see http://en.wikipedia.org/wiki/Quaternion
 */
export class Quaternion {
  x: number
  y: number
  z: number
  w: number

  constructor(x = 0, y = 0, z = 0, w = 1) {
    this.x = x
    this.y = y
    this.z = z
    this.w = w
  }

  /**
   * Set the value of the quaternion.
   */
  set(x: number, y: number, z: number, w: number): Quaternion {
    this.x = x
    this.y = y
    this.z = z
    this.w = w
    return this
  }

  /**
   * Convert to a readable format
   * @return "x,y,z,w"
   */
  toString(): string {
    return `${this.x},${this.y},${this.z},${this.w}`
  }

  /**
   * Convert to an Array
   * @return [x, y, z, w]
   */
  toArray(): [number, number, number, number] {
    return [this.x, this.y, this.z, this.w]
  }

  /**
   * Set the quaternion components given an axis and an angle in radians.
   */
  setFromAxisAngle(vector: Vec3, angle: number): Quaternion {
    const s = Math.sin(angle * 0.5)
    this.x = vector.x * s
    this.y = vector.y * s
    this.z = vector.z * s
    this.w = Math.cos(angle * 0.5)
    return this
  }

  /**
   * Converts the quaternion to [ axis, angle ] representation.
   * @param targetAxis A vector object to reuse for storing the axis.
   * @return An array, first element is the axis and the second is the angle in radians.
   */
  toAxisAngle(targetAxis = new Vec3()): [Vec3, number] {
    this.normalize() // if w>1 acos and sqrt will produce errors, this cant happen if quaternion is normalised
    const angle = 2 * Math.acos(this.w)
    const s = Math.sqrt(1 - this.w * this.w) // assuming quaternion normalised then w is less than 1, so term always positive.
    if (s < 0.001) {
      // test to avoid divide by zero, s is always positive due to sqrt
      // if s close to zero then direction of axis not important
      targetAxis.x = this.x // if it is important that axis is normalised then replace with x=1; y=z=0;
      targetAxis.y = this.y
      targetAxis.z = this.z
    } else {
      targetAxis.x = this.x / s // normalise axis
      targetAxis.y = this.y / s
      targetAxis.z = this.z / s
    }
    return [targetAxis, angle]
  }

  /**
   * Set the quaternion value given two vectors. The resulting rotation will be the needed rotation to rotate u to v.
   */
  setFromVectors(u: Vec3, v: Vec3): Quaternion {
    if (u.isAntiparallelTo(v)) {
      const t1 = sfv_t1
      const t2 = sfv_t2

      u.tangents(t1, t2)
      this.setFromAxisAngle(t1, Math.PI)
    } else {
      const a = u.cross(v)
      this.x = a.x
      this.y = a.y
      this.z = a.z
      this.w = Math.sqrt(u.length() ** 2 * v.length() ** 2) + u.dot(v)
      this.normalize()
    }
    return this
  }

  /**
   * Multiply the quaternion with an other quaternion.
   */
  mult(quat: Quaternion, target = new Quaternion()): Quaternion {
    const ax = this.x
    const ay = this.y
    const az = this.z
    const aw = this.w
    const bx = quat.x
    const by = quat.y
    const bz = quat.z
    const bw = quat.w

    target.x = ax * bw + aw * bx + ay * bz - az * by
    target.y = ay * bw + aw * by + az * bx - ax * bz
    target.z = az * bw + aw * bz + ax * by - ay * bx
    target.w = aw * bw - ax * bx - ay * by - az * bz

    return target
  }

  /**
   * Get the inverse quaternion rotation.
   */
  inverse(target = new Quaternion()): Quaternion {
    const x = this.x
    const y = this.y
    const z = this.z
    const w = this.w

    this.conjugate(target)
    const inorm2 = 1 / (x * x + y * y + z * z + w * w)
    target.x *= inorm2
    target.y *= inorm2
    target.z *= inorm2
    target.w *= inorm2

    return target
  }

  /**
   * Get the quaternion conjugate
   */
  conjugate(target = new Quaternion()): Quaternion {
    target.x = -this.x
    target.y = -this.y
    target.z = -this.z
    target.w = this.w

    return target
  }

  /**
   * Normalize the quaternion. Note that this changes the values of the quaternion.
   */
  normalize(): Quaternion {
    let l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w)
    if (l === 0) {
      this.x = 0
      this.y = 0
      this.z = 0
      this.w = 0
    } else {
      l = 1 / l
      this.x *= l
      this.y *= l
      this.z *= l
      this.w *= l
    }
    return this
  }

  /**
   * Approximation of quaternion normalization. Works best when quat is already almost-normalized.
   * @author unphased, https://github.com/unphased
   */
  normalizeFast(): Quaternion {
    const f = (3.0 - (this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w)) / 2.0
    if (f === 0) {
      this.x = 0
      this.y = 0
      this.z = 0
      this.w = 0
    } else {
      this.x *= f
      this.y *= f
      this.z *= f
      this.w *= f
    }
    return this
  }

  /**
   * Multiply the quaternion by a vector
   */
  vmult(v: Vec3, target = new Vec3()): Vec3 {
    const x = v.x
    const y = v.y
    const z = v.z
    const qx = this.x
    const qy = this.y
    const qz = this.z
    const qw = this.w

    // q*v
    const ix = qw * x + qy * z - qz * y

    const iy = qw * y + qz * x - qx * z
    const iz = qw * z + qx * y - qy * x
    const iw = -qx * x - qy * y - qz * z

    target.x = ix * qw + iw * -qx + iy * -qz - iz * -qy
    target.y = iy * qw + iw * -qy + iz * -qx - ix * -qz
    target.z = iz * qw + iw * -qz + ix * -qy - iy * -qx

    return target
  }

  /**
   * Copies value of source to this quaternion.
   * @return this
   */
  copy(quat: Quaternion): Quaternion {
    this.x = quat.x
    this.y = quat.y
    this.z = quat.z
    this.w = quat.w
    return this
  }

  /**
   * Convert the quaternion to euler angle representation. Order: YZX, as this page describes: https://www.euclideanspace.com/maths/standards/index.htm
   * @param order Three-character string, defaults to "YZX"
   */
  toEuler(target: Vec3, order = 'YZX'): void {
    let heading
    let attitude
    let bank
    const x = this.x
    const y = this.y
    const z = this.z
    const w = this.w

    switch (order) {
      case 'YZX':
        const test = x * y + z * w
        if (test > 0.499) {
          // singularity at north pole
          heading = 2 * Math.atan2(x, w)
          attitude = Math.PI / 2
          bank = 0
        }
        if (test < -0.499) {
          // singularity at south pole
          heading = -2 * Math.atan2(x, w)
          attitude = -Math.PI / 2
          bank = 0
        }
        if (heading === undefined) {
          const sqx = x * x
          const sqy = y * y
          const sqz = z * z
          heading = Math.atan2(2 * y * w - 2 * x * z, 1 - 2 * sqy - 2 * sqz) // Heading
          attitude = Math.asin(2 * test) // attitude
          bank = Math.atan2(2 * x * w - 2 * y * z, 1 - 2 * sqx - 2 * sqz) // bank
        }
        break
      default:
        throw new Error(`Euler order ${order} not supported yet.`)
    }

    target.y = heading
    target.z = attitude as number
    target.x = bank as number
  }

  /**
   * @param order The order to apply angles: 'XYZ' or 'YXZ' or any other combination.
   *
   * See {@link https://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors MathWorks} reference
   */
  setFromEuler(x: number, y: number, z: number, order = 'XYZ'): Quaternion {
    const c1 = Math.cos(x / 2)
    const c2 = Math.cos(y / 2)
    const c3 = Math.cos(z / 2)
    const s1 = Math.sin(x / 2)
    const s2 = Math.sin(y / 2)
    const s3 = Math.sin(z / 2)

    if (order === 'XYZ') {
      this.x = s1 * c2 * c3 + c1 * s2 * s3
      this.y = c1 * s2 * c3 - s1 * c2 * s3
      this.z = c1 * c2 * s3 + s1 * s2 * c3
      this.w = c1 * c2 * c3 - s1 * s2 * s3
    } else if (order === 'YXZ') {
      this.x = s1 * c2 * c3 + c1 * s2 * s3
      this.y = c1 * s2 * c3 - s1 * c2 * s3
      this.z = c1 * c2 * s3 - s1 * s2 * c3
      this.w = c1 * c2 * c3 + s1 * s2 * s3
    } else if (order === 'ZXY') {
      this.x = s1 * c2 * c3 - c1 * s2 * s3
      this.y = c1 * s2 * c3 + s1 * c2 * s3
      this.z = c1 * c2 * s3 + s1 * s2 * c3
      this.w = c1 * c2 * c3 - s1 * s2 * s3
    } else if (order === 'ZYX') {
      this.x = s1 * c2 * c3 - c1 * s2 * s3
      this.y = c1 * s2 * c3 + s1 * c2 * s3
      this.z = c1 * c2 * s3 - s1 * s2 * c3
      this.w = c1 * c2 * c3 + s1 * s2 * s3
    } else if (order === 'YZX') {
      this.x = s1 * c2 * c3 + c1 * s2 * s3
      this.y = c1 * s2 * c3 + s1 * c2 * s3
      this.z = c1 * c2 * s3 - s1 * s2 * c3
      this.w = c1 * c2 * c3 - s1 * s2 * s3
    } else if (order === 'XZY') {
      this.x = s1 * c2 * c3 - c1 * s2 * s3
      this.y = c1 * s2 * c3 - s1 * c2 * s3
      this.z = c1 * c2 * s3 + s1 * s2 * c3
      this.w = c1 * c2 * c3 + s1 * s2 * s3
    }

    return this
  }

  clone(): Quaternion {
    return new Quaternion(this.x, this.y, this.z, this.w)
  }

  /**
   * Performs a spherical linear interpolation between two quat
   *
   * @param toQuat second operand
   * @param t interpolation amount between the self quaternion and toQuat
   * @param target A quaternion to store the result in. If not provided, a new one will be created.
   * @returns {Quaternion} The "target" object
   */
  slerp(toQuat: Quaternion, t: number, target = new Quaternion()): Quaternion {
    const ax = this.x
    const ay = this.y
    const az = this.z
    const aw = this.w
    let bx = toQuat.x
    let by = toQuat.y
    let bz = toQuat.z
    let bw = toQuat.w
    let omega
    let cosom
    let sinom
    let scale0
    let scale1

    // calc cosine
    cosom = ax * bx + ay * by + az * bz + aw * bw

    // adjust signs (if necessary)
    if (cosom < 0.0) {
      cosom = -cosom
      bx = -bx
      by = -by
      bz = -bz
      bw = -bw
    }

    // calculate coefficients
    if (1.0 - cosom > 0.000001) {
      // standard case (slerp)
      omega = Math.acos(cosom)
      sinom = Math.sin(omega)
      scale0 = Math.sin((1.0 - t) * omega) / sinom
      scale1 = Math.sin(t * omega) / sinom
    } else {
      // "from" and "to" quaternions are very close
      //  ... so we can do a linear interpolation
      scale0 = 1.0 - t
      scale1 = t
    }

    // calculate final values
    target.x = scale0 * ax + scale1 * bx
    target.y = scale0 * ay + scale1 * by
    target.z = scale0 * az + scale1 * bz
    target.w = scale0 * aw + scale1 * bw

    return target
  }

  /**
   * Rotate an absolute orientation quaternion given an angular velocity and a time step.
   */
  integrate(angularVelocity: Vec3, dt: number, angularFactor: Vec3, target = new Quaternion()): Quaternion {
    const ax = angularVelocity.x * angularFactor.x,
      ay = angularVelocity.y * angularFactor.y,
      az = angularVelocity.z * angularFactor.z,
      bx = this.x,
      by = this.y,
      bz = this.z,
      bw = this.w

    const half_dt = dt * 0.5

    target.x += half_dt * (ax * bw + ay * bz - az * by)
    target.y += half_dt * (ay * bw + az * bx - ax * bz)
    target.z += half_dt * (az * bw + ax * by - ay * bx)
    target.w += half_dt * (-ax * bx - ay * by - az * bz)

    return target
  }
}

const sfv_t1 = new Vec3()
const sfv_t2 = new Vec3()
