import { mat4, quat, vec3 } from 'gl-matrix'

/**
 * Returns an euler angle representation of a quaternion
 * @param  {vec3} out Euler angles, pitch-yaw-roll
 * @param  {quat} mat Quaternion
 * @return {vec3} out
 */
quat.getEuler = function (out, quat) {
  let x = quat[0]
  let y = quat[1]
  let z = quat[2]
  let w = quat[3]
  let x2 = x * x
  let y2 = y * y
  let z2 = z * z
  let w2 = w * w
  let unit = x2 + y2 + z2 + w2
  let test = x * w - y * z
  if (test > 0.499995 * unit) {
    // singularity at the north pole
    out[0] = Math.PI / 2
    out[1] = 2 * Math.atan2(y, x)
    out[2] = 0
  } else if (test < -0.499995 * unit) {
    // singularity at the south pole
    out[0] = -Math.PI / 2
    out[1] = 2 * Math.atan2(y, x)
    out[2] = 0
  } else {
    out[0] = Math.asin(2 * (x * z - w * y))
    out[1] = Math.atan2(2 * (x * w + y * z), 1 - 2 * (z2 + w2))
    out[2] = Math.atan2(2 * (x * y + z * w), 1 - 2 * (y2 + z2))
  }
  return out
}

class Node {
  constructor (pos, scale, rotation) {
    // Translation
    this.pos = pos || [0.0, 0.0, 0.0]

    // Scaling
    this.scale = scale || [1.0, 1.0, 1.0]

    // Rotation in Euler angles (pitch, yaw, roll) in degrees
    this.rotation = rotation || [0.0, 0.0, 0.0]

    this.transform = mat4.create()

    this.updateTransform()

    this.parent = null
    this.children = []
  }

  updateTransform () {
    let matrix = mat4.create()
    let rot = quat.create()

    // Set rotation
    quat.fromEuler(rot, this.rotation[0], this.rotation[1], this.rotation[2])
    mat4.fromRotationTranslationScale(matrix, rot, this.pos, this.scale)

    // Add local transform to the global transform, if present
    // Will be present in loaded models
    if (this.worldTransform) {
      mat4.mul(matrix, matrix, this.worldTransform)
    }

    // Add parent's transform to this
    if (this.parent) {
      mat4.mul(matrix, this.parent.transform, matrix)
    }

    // Set the matrix
    this.transform = matrix

    // Update children's transforms
    for (let i in this.children) {
      let child = this.children[i]
      if (!(child instanceof Node)) continue
      child.updateTransform()
    }
  }

  fromLocalTransform (transform) {
    let quaternion = quat.create()
    let translation = vec3.create()
    let rotation = vec3.create()
    let scale = vec3.create()

    mat4.transpose(transform, transform)

    mat4.getScaling(scale, transform)
    mat4.getRotation(quaternion, transform)
    mat4.getTranslation(translation, transform)

    quat.getEuler(rotation, quaternion)

    this.rotation = rotation
    this.pos = translation
    this.scale = scale

    this.updateTransform()
  }

  // Setters
  setPosition (newPos) {
    this.pos = newPos
    this.updateTransform()
  }

  setTranslation (newTrans) {
    this.setPosition(newTrans)
  }

  setScale (newScale) {
    this.scale = newScale
    this.updateTransform()
  }

  setRotation (newRotation) {
    this.rotation = newRotation
    this.updateTransform()
  }

  // Transforms
  translate (pos) {
    mat4.translate(this.transform, this.transform, pos)
  }

  scale (scale) {
    mat4.scale(this.transform, this.transform, scale)
  }

  rotate (rot) {
    if (rot[0] !== 0) {
      mat4.rotateX(this.transform, this.transform, rot[0])
    }

    if (rot[1] !== 0) {
      mat4.rotateY(this.transform, this.transform, rot[1])
    }

    if (rot[2] !== 0) {
      mat4.rotateZ(this.transform, this.transform, rot[2])
    }
  }

  // Getters
  get position () {
    return this.pos
  }

  get translation () {
    return this.pos
  }

  get absolutePosition () {
    let p
    if (this.parent) {
      p = this.parent.absolutePosition
      p = [p[0] + this.pos[0], p[1] + this.pos[1], p[2] + this.pos[2]]
    } else {
      p = this.pos
    }
    return p
  }

  // Draw base
  draw (gl, shader) {
    // Nothing to draw here, so just draw children
    for (let i in this.children) {
      let child = this.children[i]
      if (!(child instanceof Node)) continue
      child.draw(gl, shader)
    }
  }

  update (dt) {
    // Update children
    for (let i in this.children) {
      let child = this.children[i]
      if (!(child instanceof Node)) continue
      child.update(dt)
    }
  }

  addChild (ch) {
    // Recursive add in case of table
    if (ch && typeof ch === 'object' && ch.length) {
      for (let i in ch) {
        this.addChild(ch[i])
      }
    }

    if (!(ch instanceof Node)) return
    ch.setParent(this)
    this.children.push(ch)
    this.updateTransform()
  }

  setParent (p) {
    if (!(p instanceof Node)) return
    this.parent = p
    this.updateTransform()
  }
}

// A node defining a single mesh
class MeshInstance extends Node {
  constructor (mesh, pos, scale, rot) {
    super(pos, scale, rot)
    this.mesh = mesh
  }

  draw (gl, shader) {
    if (!this.mesh) return super.draw(gl, shader)
    // Set model transform matrix uniform
    const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix')
    gl.uniformMatrix4fv(transformLocation, false, this.transform)

    // Draw the mesh
    this.mesh.prepare(gl, shader)
    this.mesh.draw(gl, shader)
    this.mesh.postdraw(gl, shader)

    // Draw children
    super.draw(gl, shader)
  }
}

export { Node, MeshInstance }
