module Mittsu
  class Quaternion
    EPS = 0.000001

    attr_reader :x, :y, :z, :w

    def initialize(x = 0.0, y = 0.0, z = 0.0, w = 1.0)
      @x, @y, @z, @w = x, y, z, w
      @on_change_callback = nil
    end

    def set(x, y, z, w)
      @x = x.to_f
      @y = y.to_f
      @z = z.to_f
      @w = w.to_f
      self.on_change_callback
      self
    end

    def x=(x)
      @x = x.to_f
      self.on_change_callback
    end

    def y=(y)
      @y = y.to_f
      self.on_change_callback
    end

    def z=(z)
      @z = z.to_f
      self.on_change_callback
    end

    def w=(w)
      @w = w.to_f
      self.on_change_callback
    end

    def copy(quaternion)
      @x = quaternion.x
      @y = quaternion.y
      @z = quaternion.z
      @w = quaternion.w
      self.on_change_callback
      self
    end

    def set_from_euler(euler, update = true)
      # http:#www.mathworks.com/matlabcentral/fileexchange/
      #   20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/
      #  content/SpinCalc.m
      c1 = ::Math.cos(euler.x / 2.0)
      c2 = ::Math.cos(euler.y / 2.0)
      c3 = ::Math.cos(euler.z / 2.0)
      s1 = ::Math.sin(euler.x / 2.0)
      s2 = ::Math.sin(euler.y / 2.0)
      s3 = ::Math.sin(euler.z / 2.0)
      if euler.order == 'XYZ'
        @x = s1 * c2 * c3 + c1 * s2 * s3
        @y = c1 * s2 * c3 - s1 * c2 * s3
        @z = c1 * c2 * s3 + s1 * s2 * c3
        @w = c1 * c2 * c3 - s1 * s2 * s3
      elsif euler.order == 'YXZ'
        @x = s1 * c2 * c3 + c1 * s2 * s3
        @y = c1 * s2 * c3 - s1 * c2 * s3
        @z = c1 * c2 * s3 - s1 * s2 * c3
        @w = c1 * c2 * c3 + s1 * s2 * s3
      elsif euler.order == 'ZXY'
        @x = s1 * c2 * c3 - c1 * s2 * s3
        @y = c1 * s2 * c3 + s1 * c2 * s3
        @z = c1 * c2 * s3 + s1 * s2 * c3
        @w = c1 * c2 * c3 - s1 * s2 * s3
      elsif euler.order == 'ZYX'
        @x = s1 * c2 * c3 - c1 * s2 * s3
        @y = c1 * s2 * c3 + s1 * c2 * s3
        @z = c1 * c2 * s3 - s1 * s2 * c3
        @w = c1 * c2 * c3 + s1 * s2 * s3
      elsif euler.order == 'YZX'
        @x = s1 * c2 * c3 + c1 * s2 * s3
        @y = c1 * s2 * c3 + s1 * c2 * s3
        @z = c1 * c2 * s3 - s1 * s2 * c3
        @w = c1 * c2 * c3 - s1 * s2 * s3
      elsif euler.order == 'XZY'
        @x = s1 * c2 * c3 - c1 * s2 * s3
        @y = c1 * s2 * c3 - s1 * c2 * s3
        @z = c1 * c2 * s3 + s1 * s2 * c3
        @w = c1 * c2 * c3 + s1 * s2 * s3
      end
      self.on_change_callback if update
      self
    end

    def set_from_axis_angle(axis, angle)
      # http:#www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
      # assumes axis is normalized
      half_angle = angle / 2.0
      s = ::Math.sin(half_angle)
      @x = axis.x * s
      @y = axis.y * s
      @z = axis.z * s
      @w = ::Math.cos(half_angle)
      self.on_change_callback
      self
    end

    def set_from_rotation_matrix(m)
      # http:#www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm
      # assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
      te = m.elements
      m11 = te[0]; m12 = te[4]; m13 = te[8]
      m21 = te[1]; m22 = te[5]; m23 = te[9]
      m31 = te[2]; m32 = te[6]; m33 = te[10]
      trace = m11 + m22 + m33
      if trace > 0
        s = 0.5 / ::Math.sqrt(trace + 1.0)
        @w = 0.25 / s
        @x = (m32 - m23) * s
        @y = (m13 - m31) * s
        @z = (m21 - m12) * s
      elsif m11 > m22 && m11 > m33
        s = 2.0 * ::Math.sqrt(1.0 + m11 - m22 - m33)
        @w = (m32 - m23) / s
        @x = 0.25 * s
        @y = (m12 + m21) / s
        @z = (m13 + m31) / s
      elsif m22 > m33
        s = 2.0 * ::Math.sqrt(1.0 + m22 - m11 - m33)
        @w = (m13 - m31) / s
        @x = (m12 + m21) / s
        @y = 0.25 * s
        @z = (m23 + m32) / s
      else
        s = 2.0 * ::Math.sqrt(1.0 + m33 - m11 - m22)
        @w = (m21 - m12) / s
        @x = (m13 + m31) / s
        @y = (m23 + m32) / s
        @z = 0.25 * s
      end
      self.on_change_callback
      self
    end

    def set_from_unit_vectors(v_from, v_to)
      # http:#lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final
      # assumes direction vectors v_from and v_to are normalized
      v1 = Mittsu::Vector3.new
      r = v_from.dot(v_to) + 1.0
      if r < EPS
        r = 0.0
        if v_from.x.abs > v_from.z.abs
          v1.set(-v_from.y, v_from.x, 0.0)
        else
          v1.set(0.0, -v_from.z, v_from.y)
        end
      else
        v1.cross_vectors(v_from, v_to)
      end
      @x = v1.x
      @y = v1.y
      @z = v1.z
      @w = r
      self.normalize
      self
    end

    def inverse
      self.conjugate.normalize
      self
    end

    def conjugate
      @x *= -1.0
      @y *= -1.0
      @z *= -1.0
      self.on_change_callback
      self
    end

    def dot(v)
      @x * v._x + @y * v._y + @z * v._z + @w * v._w
    end

    def length_sq
      @x * @x + @y * @y + @z * @z + @w * @w
    end

    def length
      ::Math.sqrt(@x * @x + @y * @y + @z * @z + @w * @w)
    end

    def normalize
      l = self.length
      if l == 0.0
        @x = 0.0
        @y = 0.0
        @z = 0.0
        @w = 1.0
      else
        l = 1.0 / l
        @x = @x * l
        @y = @y * l
        @z = @z * l
        @w = @w * l
      end
      self.on_change_callback
      self
    end

    def multiply(q)
      self.multiply_quaternions(self, q)
    end

    def multiply_quaternions(a, b)
      # from http:#www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
      qax = a.x; qay = a.y; qaz = a.z; qaw = a.w
      qbx = b.x; qby = b.y; qbz = b.z; qbw = b.w
      @x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby
      @y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz
      @z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx
      @w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz
      self.on_change_callback
      self
    end

    def slerp(qb, t)
      return self if t.zero?
      return self.copy(qb) if t == 1.0
      _x, _y, _z, _w = @x, @y, @z, @w
      # http:#www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
      cos_half_theta = _w * qb.w + _x * qb.x + _y * qb.y + _z * qb.z
      if cos_half_theta < 0.0
        @w = -qb.w
        @x = -qb.x
        @y = -qb.y
        @z = -qb.z
        cos_half_theta = - cos_half_theta
      else
        self.copy(qb)
      end
      if cos_half_theta >= 1.0
        @w = _w
        @x = _x
        @y = _y
        @z = _z
        return self
      end
      half_theta = ::Math.acos(cos_half_theta)
      sin_half_theta = ::Math.sqrt(1.0 - cos_half_theta * cos_half_theta)
      if sin_half_theta.abs < 0.001
        @w = 0.5 * (_w + @w)
        @x = 0.5 * (_x + @x)
        @y = 0.5 * (_y + @y)
        @z = 0.5 * (_z + @z)
        return self
      end
      ratio_a = ::Math.sin((1.0. - t) * half_theta) / sin_half_theta,
      ratio_b = ::Math.sin(t * half_theta) / sin_half_theta
      @w = (_w * ratio_a + @w * ratio_b)
      @x = (_x * ratio_a + @x * ratio_b)
      @y = (_y * ratio_a + @y * ratio_b)
      @z = (_z * ratio_a + @z * ratio_b)
      self.on_change_callback
      self
    end

    def ==(quaternion)
      (quaternion.x == @x) && (quaternion.y == @y) && (quaternion.z == @z) && (quaternion.w == @w)
    end

    def from_array(array, offset = 0)
      @x = array[offset]
      @y = array[offset + 1]
      @z = array[offset + 2]
      @w = array[offset + 3]
      self.on_change_callback
      self
    end

    def to_array(array = [], offset = 0)
      array[offset] = @x
      array[offset + 1] = @y
      array[offset + 2] = @z
      array[offset + 3] = @w
      array
    end

    def on_change(&callback)
      @on_change_callback = callback
      self
    end

    def on_change_callback
      return unless @on_change_callback
      @on_change_callback.call
    end

    def clone
      Mittsu::Quaternion.new(@x, @y, @z, @w)
    end

    def self.slerp(qa, qb, qm, t)
      qm.copy(qa).slerp(qb, t)
    end
  end
end
