require 'mittsu/math/vector'

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

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

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

    def x; @elements[0]; end
    def y; @elements[1]; end
    def z; @elements[2]; 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 apply_euler(euler)
      quaternion = Mittsu::Quaternion.new
      self.apply_quaternion(quaternion.set_from_euler(euler))
      self
    end

    def apply_axis_angle(axis, angle)
      quaternion = Mittsu::Quaternion.new
      self.apply_quaternion(quaternion.set_from_axis_angle(axis, angle))
      self
    end

    def apply_matrix3(m)
      _x, _y, _z = *@elements

      e = m.elements

      @elements[0] = e[0] * _x + e[3] * _y + e[6] * _z
      @elements[1] = e[1] * _x + e[4] * _y + e[7] * _z
      @elements[2] = e[2] * _x + e[5] * _y + e[8] * _z

      self
    end

    def apply_matrix4(m)
      # input: THREE.Matrix4 affine matrix
      _x, _y, _z = *@elements

      e = m.elements

      @elements[0] = e[0] * _x + e[4] * _y + e[8]  * _z + e[12]
      @elements[1] = e[1] * _x + e[5] * _y + e[9]  * _z + e[13]
      @elements[2] = e[2] * _x + e[6] * _y + e[10] * _z + e[14]

      self
    end

    def apply_projection(m)
      # input: THREE.Matrix4 projection matrix
      _x, _y, _z = *@elements

      e = m.elements
      d = 1.0 / (e[3] * _x + e[7] * _y + e[11] * _z + e[15]) # perspective divide

      @elements[0] = (e[0] * _x + e[4] * _y + e[8]  * _z + e[12]) * d
      @elements[1] = (e[1] * _x + e[5] * _y + e[9]  * _z + e[13]) * d
      @elements[2] = (e[2] * _x + e[6] * _y + e[10] * _z + e[14]) * d

      self
    end

    def apply_quaternion(q)
      _x, _y, _z = *@elements

      qx = q.x
      qy = q.y
      qz = q.z
      qw = q.w

      # calculate quat * vector
      ix =  qw * _x + qy * _z - qz * _y
      iy =  qw * _y + qz * _x - qx * _z
      iz =  qw * _z + qx * _y - qy * _x
      iw = -qx * _x - qy * _y - qz * _z

      # calculate result * inverse quat
      @elements[0] = ix * qw + iw * - qx + iy * - qz - iz * - qy
      @elements[1] = iy * qw + iw * - qy + iz * - qx - ix * - qz
      @elements[2] = iz * qw + iw * - qz + ix * - qy - iy * - qx

      self
    end

    def project(camera)
      matrix = Mittsu::Matrix4.new
      matrix.multiply_matrices(camera.projection_matrix, matrix.get_inverse(camera.matrix_world))
      self.apply_projection(matrix)
    end

    def unproject(camera)
      matrix = Mittsu::Matrix4.new
      matrix.multiply_matrices(camera.matrix_world, matrix.inverse(camera.projection_matrix))
      self.apply_projection(matrix)
    end

    def transform_direction(m)
      # input: THREE.Matrix4 affine matrix
      # vector interpreted as a direction
      _x, _y, _z = *@elements

      e = m.elements

      @elements[0] = e[0] * _x + e[4] * _y + e[8]  * _z
      @elements[1] = e[1] * _x + e[5] * _y + e[9]  * _z
      @elements[2] = e[2] * _x + e[6] * _y + e[10] * _z

      self.normalize

      self
    end

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

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

    def cross(v)
      _x, _y, _z = *@elements
      @elements[0] = _y * v.z - _z * v.y
      @elements[1] = _z * v.x - _x * v.z
      @elements[2] = _x * v.y - _y * v.x
      self
    end

    def cross_vectors(a, b)
      ax, ay, az = a.x, a.y, a.z
      bx, by, bz = b.x, b.y, b.z

      @elements[0] = ay * bz - az * by
      @elements[1] = az * bx - ax * bz
      @elements[2] = ax * by - ay * bx

      self
    end

    def distance_to_squared(v)
      dx = x - v.x
      dy = y - v.y
      dz = z - v.z
      dx * dx + dy * dy + dz * dz
    end

    def set_from_matrix_position(m)
      @elements[0] = m.elements[12]
      @elements[1] = m.elements[13]
      @elements[2] = m.elements[14]
      self
    end

    def set_from_matrix_scale(m)
      sx = self.set(m.elements[0], m.elements[1], m.elements[ 2]).length
      sy = self.set(m.elements[4], m.elements[5], m.elements[ 6]).length
      sz = self.set(m.elements[8], m.elements[9], m.elements[10]).length

      @elements[0] = sx
      @elements[1] = sy
      @elements[2] = sz

      self
    end

    def set_from_matrix_column(index, matrix)
      offset = index * 4

      me = matrix.elements

      @elements[0] = me[offset]
      @elements[1] = me[offset + 1]
      @elements[2] = me[offset + 2]

      self
    end

    def set_scalar(scalar)
      set(scalar, scalar, scalar)

      self
    end

    def set_component(index, value)
      _x, _y, _z = *@elements

      case index
      when 0
        set(value, _y, _z)
      when 1
        set(_x, value, _z)
      when 2
        set(_x, _y, value)
      else
        raise ArgumentError, "index is out of range: #{index}"
      end

      self
    end

    def get_component(index)
      _x, _y, _z = *@elements

      case index
      when 0
        return _x
      when 1
        return _y
      when 2
        return _z
      else
        raise ArgumentError, "index is out of range: #{index}"
      end
    end

    def add_scaled_vector(vector, scalar)
      set(vector.x * scalar, vector.y * scalar, vector.z * scalar)

      self
    end

    def set_from_spherical(sphere)
      set_from_spherical_coords(sphere.radius, sphere.phi, sphere.theta)
    end

    def set_from_spherical_coords(radius, phi, theta)
      sin_phi_radius = ::Math.sin(phi) * radius
      set(sin_phi_radius * ::Math.sin(theta), ::Math.cos(phi) * radius, sin_phi_radius * ::Math.cos(theta))

      self
    end

    def set_from_cylindrical(cylinder)
      set_from_cylindrical_coords(cylinder.radius, cylinder.theta, cylinder.y)
    end

    def set_from_cylindrical_coords(radius, theta, y)
      set(radius * ::Math.sin(theta), y, radius * ::Math.sin(theta))

      self
    end

    def set_from_matrix3_column(matrix, index)
      from_array(matrix.elements, index * 3)
    end

    def equals(vector)
      _x, _y, _z = *@elements

      ((vector.x == _x) && (vector.y == _y) && (vector.z == _z ))
    end

    def from_buffer_attribute(attribute, index)
      set(attribute.get_x(index), attribute.get_y(index), attribute.get_z(index))

      self
    end

    def clamp_length(min, max)
      divide_scalar(length || 1).multiply_scalar([min, [max, length].min].max)
    end

    def manhattan_distance_to(vector)
      _x, _y, _z = *@elements

      (_x - vector.x).abs + (_y - vector.y).abs + (_z - vector.z).abs
    end

    # TODO: maybe add range 3 values as arguments (range_x, range_y, range_z) to this method
    def random()
      set(Random.new.rand, Random.new.rand, Random.new.rand)

      self
    end

    def random_direction()
      u = (Random.new.rand - 0.5) *2
      t = Random.new.rand * ::Math::PI * 2
      f = ::Math.sqrt(1 - u ** 2)

      set(f * ::Math.cos(t), f * ::Math.sin(t), u)

      self
    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]

      self
    end
  end
end
