// animate self-implements its matrix decompose/recompose; these are just the numeric-array shapes.
type mat4 = Float32Array | number[]
type quat = Float32Array | number[]
type vec2 = Float32Array | number[]
type vec3 = Float32Array | number[]
import type { Easing } from "./easings"

type AnimateProps = number | string | vec2 | vec3 | quat

export type AnimateOptions<T> = {
  from: T,
  to: T,
  duration: number,
  onUpdate(val: T): void,
  onComplete?(): void,
  easing?: Easing,
}

export const animate = <T extends AnimateProps>(options: AnimateOptions<T>): number => {
  
  let onRawUpdate
  if (typeof options.to === "number") {
    const { onUpdate, from, to } = options as AnimateOptions<number>
    onRawUpdate = (t: number) => onUpdate(from + (to - from) * t)
  } else if (options.to === "string") {
    // TODO: Make color animate here
    const { onUpdate, from, to } = options as AnimateOptions<string>
    onRawUpdate = (t: number) => onUpdate(from)
    
    // onRawUpdate = (val: number) => onUpdate(from + (to - from) * val)
  } else if (options.to.length === 2) {
    const { onUpdate, from, to } = options as AnimateOptions<vec2>
    const tempVec = new Float32Array(3)
    onRawUpdate = (t: number) => onUpdate(lerp2(tempVec, from, to, t))

  } else if (options.to.length === 3) {
    const { onUpdate, from, to } = options as AnimateOptions<vec3>
    const tempVec = new Float32Array(3)

    onRawUpdate = (t: number) => onUpdate(lerp(tempVec, from, to, t))
  } else if (options.to.length === 4) { 
    const { onUpdate, from, to } = options as AnimateOptions<quat>

    const tempQuat = new Float32Array(4)
    onRawUpdate = (t: number) => onUpdate(slerp(tempQuat, from, to, t))
  } else {
    onRawUpdate = () => {}
  }

  const animationId = _creatorUtils.animate(val => {
    const _val = options.easing? options.easing(val): val
    onRawUpdate(_val)
    if (val === 1 && options.onComplete) {
      options.onComplete()
    }
  }, options.duration)

  return animationId
}

export const stopAnimation = (id: number): void =>  _creatorUtils.stopAnimation(id)
export const pauseAnimation = (id: number): void => _creatorUtils.pauseAnimation(id)
export const resumeAnimation = (id: number): void => _creatorUtils.resumeAnimation(id)

export const animateMat4 = (options: AnimateOptions<mat4>): number => {
  const fromPosition = new Float32Array(3)
  const fromQuaternion = new Float32Array(4)
  const fromScale = new Float32Array(3)
  const toPosition = new Float32Array(3)
  const toQuaternion = new Float32Array(4)
  const toScale = new Float32Array(3)
  decompose(fromQuaternion, fromPosition, fromScale, (options as AnimateOptions<mat4>).from)
  decompose(toQuaternion, toPosition, toScale, (options as AnimateOptions<mat4>).to)

  const tempPosition = new Float32Array(3)
  const tempQuat = new Float32Array(4)
  const tempScale = new Float32Array(3)
  const tempMat = new Float32Array(16)

  const onUpdate = (options as AnimateOptions<mat4>).onUpdate
  const onRawUpdate = (t: number) => {
    lerp(tempPosition, fromPosition, toPosition, t)
    slerp(tempQuat, fromQuaternion, toQuaternion, t)
    lerp(tempScale, fromScale, toScale, t)

    onUpdate(fromRotationTranslationScale(tempMat, tempQuat, tempPosition, tempScale))
  }

  const animationId = _creatorUtils.animate(val => {
    const _val = options.easing? options.easing(val): val
    onRawUpdate(_val)
    if (val === 1 && options.onComplete) {
      options.onComplete()
    }
  }, options.duration)

  return animationId
}

const EPSILON = 1e-6;
function slerp(out: quat, a: quat, b: quat, t: number) {
  const ax = a[0], ay = a[1], az = a[2], aw = a[3];
  let bx = b[0], by = b[1], bz = b[2], bw = b[3];
  let omega, cosom, sinom, scale0, scale1;
  cosom = ax * bx + ay * by + az * bz + aw * bw;
  if (cosom < 0) {
    cosom = -cosom;
    bx = -bx;
    by = -by;
    bz = -bz;
    bw = -bw;
  }
  if (1 - cosom > EPSILON) {
    omega = Math.acos(cosom);
    sinom = Math.sin(omega);
    scale0 = Math.sin((1 - t) * omega) / sinom;
    scale1 = Math.sin(t * omega) / sinom;
  } else {
    scale0 = 1 - t;
    scale1 = t;
  }
  out[0] = scale0 * ax + scale1 * bx;
  out[1] = scale0 * ay + scale1 * by;
  out[2] = scale0 * az + scale1 * bz;
  out[3] = scale0 * aw + scale1 * bw;
  return out;
}
function lerp(out: vec3, a: vec3, b: vec3, t: number) {
  const ax = a[0];
  const ay = a[1];
  const az = a[2];
  out[0] = ax + t * (b[0] - ax);
  out[1] = ay + t * (b[1] - ay);
  out[2] = az + t * (b[2] - az);
  return out;
}

function lerp2(out: vec2, a: vec2, b: vec2, t: number) {
  const ax = a[0];
  const ay = a[1];
  const az = a[2];
  out[0] = ax + t * (b[0] - ax);
  out[1] = ay + t * (b[1] - ay);
  return out;
}
function fromRotationTranslationScale(out: mat4, q: quat, v: vec3, s: vec3) {
  const x = q[0], y = q[1], z = q[2], w = q[3];
  const x2 = x + x;
  const y2 = y + y;
  const z2 = z + z;
  const xx = x * x2;
  const xy = x * y2;
  const xz = x * z2;
  const yy = y * y2;
  const yz = y * z2;
  const zz = z * z2;
  const wx = w * x2;
  const wy = w * y2;
  const wz = w * z2;
  const sx = s[0];
  const sy = s[1];
  const sz = s[2];
  out[0] = (1 - (yy + zz)) * sx;
  out[1] = (xy + wz) * sx;
  out[2] = (xz - wy) * sx;
  out[3] = 0;
  out[4] = (xy - wz) * sy;
  out[5] = (1 - (xx + zz)) * sy;
  out[6] = (yz + wx) * sy;
  out[7] = 0;
  out[8] = (xz + wy) * sz;
  out[9] = (yz - wx) * sz;
  out[10] = (1 - (xx + yy)) * sz;
  out[11] = 0;
  out[12] = v[0];
  out[13] = v[1];
  out[14] = v[2];
  out[15] = 1;
  return out;
}

function decompose(out_r: mat4, out_t: vec3, out_s: vec3, mat: mat4) {
  out_t[0] = mat[12];
  out_t[1] = mat[13];
  out_t[2] = mat[14];
  const m11 = mat[0];
  const m12 = mat[1];
  const m13 = mat[2];
  const m21 = mat[4];
  const m22 = mat[5];
  const m23 = mat[6];
  const m31 = mat[8];
  const m32 = mat[9];
  const m33 = mat[10];
  out_s[0] = Math.hypot(m11, m12, m13);
  out_s[1] = Math.hypot(m21, m22, m23);
  out_s[2] = Math.hypot(m31, m32, m33);
  const is1 = 1 / out_s[0];
  const is2 = 1 / out_s[1];
  const is3 = 1 / out_s[2];
  const sm11 = m11 * is1;
  const sm12 = m12 * is2;
  const sm13 = m13 * is3;
  const sm21 = m21 * is1;
  const sm22 = m22 * is2;
  const sm23 = m23 * is3;
  const sm31 = m31 * is1;
  const sm32 = m32 * is2;
  const sm33 = m33 * is3;
  const trace = sm11 + sm22 + sm33;
  let S = 0;
  if (trace > 0) {
    S = Math.sqrt(trace + 1) * 2;
    out_r[3] = 0.25 * S;
    out_r[0] = (sm23 - sm32) / S;
    out_r[1] = (sm31 - sm13) / S;
    out_r[2] = (sm12 - sm21) / S;
  } else if (sm11 > sm22 && sm11 > sm33) {
    S = Math.sqrt(1 + sm11 - sm22 - sm33) * 2;
    out_r[3] = (sm23 - sm32) / S;
    out_r[0] = 0.25 * S;
    out_r[1] = (sm12 + sm21) / S;
    out_r[2] = (sm31 + sm13) / S;
  } else if (sm22 > sm33) {
    S = Math.sqrt(1 + sm22 - sm11 - sm33) * 2;
    out_r[3] = (sm31 - sm13) / S;
    out_r[0] = (sm12 + sm21) / S;
    out_r[1] = 0.25 * S;
    out_r[2] = (sm23 + sm32) / S;
  } else {
    S = Math.sqrt(1 + sm33 - sm11 - sm22) * 2;
    out_r[3] = (sm12 - sm21) / S;
    out_r[0] = (sm31 + sm13) / S;
    out_r[1] = (sm23 + sm32) / S;
    out_r[2] = 0.25 * S;
  }
  return out_r;
}