require 'mittsu/math/vector'

module Mittsu
  class Vector4 < Vector
    ELEMENTS = { x: 0, y: 1, z: 2, w: 3 }
    DIMENSIONS = ELEMENTS.count

    def initialize(x = 0, y = 0, z = 0, w = 1)
      super [x.to_f, y.to_f, z.to_f, w.to_f]
    end

    def set(x, y, z, w)
      super [x.to_f, y.to_f, z.to_f, w.to_f]
    end

    def x; @elements[0]; end
    def y; @elements[1]; end
    def z; @elements[2]; end
    def w; @elements[3]; end

    def x=(value); @elements[0] = value.to_f; end
    def y=(value); @elements[1] = value.to_f; end
    def z=(value); @elements[2] = value.to_f; end
    def w=(value); @elements[3] = value.to_f; end

    def apply_matrix4(m)
      _x, _y, _z, _w = *@elements
      e = m.elements
      @elements[0] = e[0] * _x + e[4] * _y + e[8]  * _z + e[12] * _w
      @elements[1] = e[1] * _x + e[5] * _y + e[9]  * _z + e[13] * _w
      @elements[2] = e[2] * _x + e[6] * _y + e[10] * _z + e[14] * _w
      @elements[3] = e[3] * _x + e[7] * _y + e[11] * _z + e[15] * _w
      self
    end

    def divide_scalar(scalar)
      if scalar != 0.0
        inv_scalar = 1.0 / scalar
        @elements[0] *= inv_scalar
        @elements[1] *= inv_scalar
        @elements[2] *= inv_scalar
        @elements[3] *= inv_scalar
      else
        @elements = [0.0, 0.0, 0.0, 1.0]
      end
      self
    end

    def set_axis_angle_from_quaternion(q)
      # http:#www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/index.htm
      # q is assumed to be normalized
      @elements[3] = 2.0 * ::Math.acos(q.w)
      s = ::Math.sqrt(1.0 - q.w * q.w)
      if s < 0.0001
         @elements[0] = 1.0
         @elements[1] = 0.0
         @elements[2] = 0.0
      else
         @elements[0] = q.x / s
         @elements[1] = q.y / s
         @elements[2] = q.z / s
      end
      self
    end

    def set_axis_angle_from_rotation_matrix(m)
      # http:#www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToAngle/index.htm
      # assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
      angle, x1, y1, z1 = nil    # variables for result
      epsilon = 0.01    # margin to allow for rounding errors
      epsilon2 = 0.1    # margin to distinguish between 0 and 180 degrees
      te = m.elements
      m11, m12, m13 = te[0], te[4], te[8]
      m21, m22, m23 = te[1], te[5], te[9]
      m31, m32, m33 = te[2], te[6], te[10]
      if (((m12 - m21).abs < epsilon) &&
          ((m13 - m31).abs < epsilon) &&
          ((m23 - m32).abs < epsilon))
        # singularity found
        # first check for identity matrix which must have +1 for all terms
        # in leading diagonal and zero in other terms
        if (((m12 + m21).abs < epsilon2) &&
            ((m13 + m31).abs < epsilon2) &&
            ((m23 + m32).abs < epsilon2) &&
            ((m11 + m22 + m33 - 3).abs < epsilon2))
          # self singularity is identity matrix so angle = 0
          @elements = [1.0, 0.0, 0.0, 0.0]
          return self # zero angle, arbitrary axis
        end
        # otherwise self singularity is angle = 180
        angle = ::Math::PI
        xx = (m11 + 1.0) / 2.0
        yy = (m22 + 1.0) / 2.0
        zz = (m33 + 1.0) / 2.0
        xy = (m12 + m21) / 4.0
        xz = (m13 + m31) / 4.0
        yz = (m23 + m32) / 4.0
        if (xx > yy) && (xx > zz) # m11 is the largest diagonal term
          if xx < epsilon
            x1 = 0.0
            y1 = 0.707106781
            z1 = 0.707106781
          else
            x1 = ::Math.sqrt(xx)
            y1 = xy / x1
            z1 = xz / x1
          end
        elsif yy > zz # m22 is the largest diagonal term
          if yy < epsilon
            x1 = 0.707106781
            y1 = 0.0
            z1 = 0.707106781
          else
            y1 = ::Math.sqrt(yy)
            x1 = xy / y1
            z1 = yz / y1
          end
        else # m33 is the largest diagonal term so base result on self
          if zz < epsilon
            x1 = 0.707106781
            y1 = 0.707106781
            z1 = 0.0
          else
            z1 = ::Math.sqrt(zz)
            x1 = xz / z1
            y1 = yz / z1
          end
        end
        @elements = [x1, y1, z1, angle]
        return self # return 180 deg rotation
      end
      # as we have reached here there are no singularities so we can handle normally
      s = ::Math.sqrt((m32 - m23) * (m32 - m23) +
        (m13 - m31) * (m13 - m31) +
        (m21 - m12) * (m21 - m12)) # used to normalize
      s = 1.0 if (s.abs < 0.001)
      # prevent divide by zero, should not happen if matrix is orthogonal and should be
      # caught by singularity test above, but I've left it in just in case
      @elements[0] = (m32 - m23) / s
      @elements[1] = (m13 - m31) / s
      @elements[2] = (m21 - m12) / s
      @elements[3] = ::Math.acos((m11 + m22 + m33 - 1.0) / 2.0)
      self
    end

    def dot(v)
      x * v.x + y * v.y + z * v.z + w * v.w
    end

    def length_manhattan
      x.abs + y.abs + z.abs + w.abs
    end

    def from_attribute(attribute, index, offset = 0)
      index = index * attribute.itemSize + offset
      @elements[0] = attribute.array[index]
      @elements[1] = attribute.array[index + 1]
      @elements[2] = attribute.array[index + 2]
      @elements[3] = attribute.array[index + 3]
      self
    end
  end
end
