require 'gmath3D'

module GMath3D
  #
  # Line represents a infinite line on 3D space.
  #
  class Line < Geom
    public
    attr_accessor :base_point
    attr_accessor :direction

    def to_s
      "Line[point#{@base_point.to_element_s}, vector#{@direction.to_element_s}"
    end

    # [Input]
    #  _point_ and _direction_ should be Vector3.
    # [Output]
    #  return new instance of Line.
    def initialize(point = Vector3.new(0.0,0.0,0.0), direction = Vector3.new(1.0,0.0,0.0))
      Util3D.check_arg_type(Vector3, point)
      Util3D.check_arg_type(Vector3, direction)
      super()
      @base_point = point
      @direction  = direction
    end

    def initialize_copy( original_obj )
      @base_point = original_obj.base_point.dup
      @direction = original_obj.direction.dup
    end

    # [Input]
    #  _rhs_ is Line.
    # [Output]
    #  return true if rhs equals myself.
    def ==(rhs)
      return false if rhs == nil
      return false if( !rhs.kind_of?(Line) )
      return false if( self.base_point != rhs.base_point)
      return false if( self.direction != rhs.direction)
      return true
    end

    # [Input]
    #  _parameter_ should be Numeric.
    # [Output]
    #  return a point on line at input parameter position as Vector3
    def point(parameter)
      Util3D.check_arg_type(::Numeric, parameter)
      @base_point + @direction*parameter
    end

    # This function returns closest distance between Line and anothor element.
    # [Input]
    #  _target_ should be Vector3 or Line.
    #
    # [Output]
    #  [in case _target_ is Vector3]
    #   return "distance, closest point on myself, parameter on myself" as [Numeric, Vector3, Numeric]
    #  [in case _target_ is Line]
    #   return "distance, point on myself, point on target, parameter on myself, parameter on tatget" 
    #   as [Numeric, Vector3, Vector3, Numeric, Numeric]
    def distance(target)
      # with Point
      if(target.kind_of?(Vector3))
        return distance_to_point(target)
        #with Line
      elsif(target.kind_of?(Line))
        return distance_to_line(target)
      end
      Util3D.raise_argurment_error(target)
    end

    private
    def distance_to_point(target_point)
      point_on_line1 = self.base_point
      point_on_line2 = self.base_point + self.direction

      vecAB = point_on_line2 - point_on_line1
      vecAP = target_point - point_on_line1
      vecAQ, parameter = vecAP.project_to(vecAB)
      cross_point = point_on_line1 + vecAQ
      vecPQ = vecAQ - vecAP
      return vecPQ.length, cross_point, parameter
    end

    def distance_to_line(target_line)
      if(self.direction.parallel?(target_line.direction)) then
        distanceInfo = self.distance(target_line.base_point)
        return distanceInfo[0], nil, nil, nil, nil
      else
        line1_point1 = self.base_point
        line1_point2 = self.base_point + self.direction
        line2_point1 = target_line.base_point
        line2_point2 = target_line.base_point + target_line.direction

        vec_da = line1_point2 - line1_point1
        vec_db = line2_point2 - line2_point1
        vec_ab = line2_point1 - line1_point1

        abs_vec_db = vec_db.length*vec_db.length
        abs_vec_da = vec_da.length*vec_da.length

        delta = (abs_vec_da*abs_vec_db - vec_da.dot( vec_db )*vec_da.dot( vec_db ))

        if( delta < self.tolerance )
          # TODO ASSERT(false)
          return nil
        end
        parameter1 = (abs_vec_db*vec_ab.dot(vec_da) - vec_da.dot( vec_db )*vec_ab.dot( vec_db ) ) / delta
        parameter2 = (vec_da.dot( vec_db )*vec_ab.dot( vec_da ) - abs_vec_da*vec_ab.dot( vec_db ))/ delta

        line1_closest_point = line1_point1 + vec_da*parameter1
        line2_closest_point = line2_point1 + vec_db*parameter2
        distance = line1_closest_point.distance( line2_closest_point )
        return distance, line1_closest_point, line2_closest_point, parameter1, parameter2
      end
    end
  end
end

