require_relative 'cluster_factory'
require_relative 'point'

module Geometry

=begin rdoc
A cluster of objects representing a Line of infinite length

Supports two-point, slope-intercept, and point-slope initializer forms

== Usage

=== Two-point constructors
    line = Geometry::Line[[0,0], [10,10]]
    line = Geometry::Line[Geometry::Point[0,0], Geometry::Point[10,10]]
    line = Geometry::Line[Vector[0,0], Vector[10,10]]

=== Slope-intercept constructors
    Geometry::Line[Rational(3,4), 5]	# Slope = 3/4, Intercept = 5
    Geometry::Line[0.75, 5]

=== Point-slope constructors
    Geometry::Line(Geometry::Point[0,0], 0.75)
    Geometry::Line(Vector[0,0], Rational(3,4))

===  Special constructors (2D only)
    Geometry::Line.horizontal(y=0)
    Geometry::Line.vertical(x=0)
=end

    class Line
	include ClusterFactory

	# @!attribute [r] horizontal?
	#   @return [Boolean]	true if the slope is zero

	# @!attribute [r] slope
	#   @return [Number]	the slope of the {Line}

	# @!attribute [r] vertical?
	#   @return [Boolean]	true if the slope is infinite

	# @overload [](Array, Array)
	#   @return [TwoPointLine]
	# @overload [](Point, Point)
	#   @return [TwoPointLine]
	# @overload [](Vector, Vector)
	#   @return [TwoPointLine]
	# @overload [](y-intercept, slope)
	#   @return [SlopeInterceptLine]
	# @overload [](point, slope)
	#   @return [PointSlopeLine]
	def self.[](*args)
	    if( 2 == args.size )
		args.map! {|x| x.is_a?(Array) ? Point[*x] : x}

		# If both args are Points, create a TwoPointLine
		return TwoPointLine.new(*args) if args.all? {|x| x.is_a?(Vector)}

		# If only the first arg is a Point, create a PointSlopeLine
		return PointSlopeLine.new(*args) if args.first.is_a?(Vector)

		# Otherise, create a SlopeInterceptLine
		return SlopeInterceptLine.new(*args)
	    else
		nil
	    end
	end

	# @overload new(from, to)
	# @option options [Point] :from	A starting {Point}
	# @option options [Point] :to	An end {Point}
	# @return [TwoPointLine]
	# @overload new(start, end)
	# @option options [Point] :start	A starting {Point}
	# @option options [Point] :end	An end {Point}
	# @return [TwoPointLine]
	def self.new(options={})
	    from = options[:from] || options[:start]
	    to = options[:end] || options[:to]

	    if from and to
		TwoPointLine.new(from, to)
	    else
		raise ArgumentError, "Start and end Points must be provided"
	    end
	end

	def self.horizontal(y_intercept=0)
	    SlopeInterceptLine.new(0, y_intercept)
	end
	def self.vertical(x_intercept=0)
	    SlopeInterceptLine.new(1/0.0, x_intercept)
	end
    end

    module SlopedLine
	# @!attribute slope
	#   @return [Number]  the slope of the {Line}
	attr_reader :slope

	# @!attribute horizontal?
	#   @return [Boolean]  true if the slope is zero
	def horizontal?
	    slope.zero?
	end

	# @!attribute vertical?
	#   @return [Boolean]  true if the slope is infinite
	def vertical?
	    slope.infinite? != nil
	rescue	# Non-Float's don't have an infinite? method
	    false
	end
    end

    # @private
    class PointSlopeLine < Line
	include SlopedLine

	# @!attribute point
	#   @return [Point]  the stating point
	attr_reader :point

	# @param point	[Point]	    a {Point} that lies on the {Line}
	# @param slope	[Number]    the slope of the {Line}
	def initialize(point, slope)
	    @point = Point[point]
	    @slope = slope
	end

	# Two {PointSlopeLine}s are equal if both have equal slope and origin
	def ==(other)
	    case other
		when SlopeInterceptLine
		    # Check that the slopes are equal and that the starting point will solve the slope-intercept equation
		    (slope == other.slope) && (point.y == other.slope * point.x + other.intercept)
		when TwoPointLine
		    # Plug both of other's endpoints into the line equation and check that they solve it
		    first_diff = other.first - point
		    last_diff = other.last - point
		    (first_diff.y == slope*first_diff.x) && (last_diff.y == slope*last_diff.x)
		else
		    self.eql? other
	    end
	end

	# Two {PointSlopeLine}s are equal if both have equal slopes and origins
	#   @note eql? does not check for equivalence between cluster subclases
	def eql?(other)
	    (point == other.point) && (slope == other.slope)
	end

	def to_s
	    'Line(' + @slope.to_s + ',' + @point.to_s + ')'
	end

	# Find the requested axis intercept
	# @param axis	[Symbol]    the axis to intercept (either :x or :y)
	# @return [Number]  the location of the intercept
	def intercept(axis=:y)
	    case axis
		when :x
		    vertical? ? point.x : (horizontal? ? nil : (slope * point.x - point.y))
		when :y
		    vertical? ? nil : (horizontal? ? point.y : (point.y - slope * point.x))
	    end
	end
    end

    # @private
    class SlopeInterceptLine < Line
	include SlopedLine

	# @param slope	    [Number]    the slope
	# @param intercept  [Number]	the location of the y-axis intercept
	def initialize(slope, intercept)
	    @slope = slope
	    @intercept = intercept
	end

	# Two {SlopeInterceptLine}s are equal if both have equal slope and intercept
	def ==(other)
	    case other
		when PointSlopeLine
		    # Check that the slopes are equal and that the starting point will solve the slope-intercept equation
		    (slope == other.slope) && (other.point.y == slope * other.point.x + intercept)
		when TwoPointLine
		    # Check that both endpoints solve the line equation
		    ((other.first.y == slope * other.first.x + intercept)) && (other.last.y == (slope * other.last.x + intercept))
		else
		    self.eql? other
	    end
	end

	# Two {SlopeInterceptLine}s are equal if both have equal slopes and intercepts
	#   @note eql? does not check for equivalence between cluster subclases
	def eql?(other)
	    (intercept == other.intercept) && (slope == other.slope)
	end

	# Find the requested axis intercept
	# @param axis	[Symbol]    the axis to intercept (either :x or :y)
	# @return [Number]  the location of the intercept
	def intercept(axis=:y)
	    case axis
		when :x
		    vertical? ? @intercept : (horizontal? ? nil : (-@intercept/@slope))
		when :y
		    vertical? ? nil : @intercept
	    end
	end

	def to_s
	    'Line(' + @slope.to_s + ',' + @intercept.to_s + ')'
	end
    end

    # @private
    class TwoPointLine < Line
	# @!attribute first
	#   @return [Point]  the {Line}'s starting point
	attr_reader :first

	# @!attribute last
	#   @return [Point]  the {Line}'s end point
	attr_reader :last

	# @param first	[Point]	the starting point
	# @param last	[Point]	the end point
	def initialize(first, last)
	    @first = Point[first]
	    @last = Point[last]
	end

	def inspect
	    'Line(' + @first.inspect + ', ' + @last.inspect + ')'
	end
	alias :to_s :inspect

	# Two {TwoPointLine}s are equal if both have equal {Point}s in the same order
	def ==(other)
	    case other
		when PointSlopeLine
		    # Plug both endpoints into the line equation and check that they solve it
		    first_diff = first - other.point
		    last_diff = last - other.point
		    (first_diff.y == other.slope*first_diff.x) && (last_diff.y == other.slope*last_diff.x)
		when SlopeInterceptLine
		    # Check that both endpoints solve the line equation
		    ((first.y == other.slope * first.x + other.intercept)) && (last.y == (other.slope * last.x + other.intercept))
		else
		    self.eql?(other) || ((first == other.last) && (last == other.first))
	    end
	end

	# Two {TwoPointLine}s are equal if both have equal endpoints
	#   @note eql? does not check for equivalence between cluster subclases
	def eql?(other)
	    (first == other.first) && (last == other.last)
	end

# @group Accessors
	# !@attribute [r[ slope
	#   @return [Number]	the slope of the {Line}
	def slope
	    (last.y - first.y)/(last.x - first.x)
	end

	def horizontal?
	    first.y == last.y
	end

	def vertical?
	    first.x == last.x
	end

	# Find the requested axis intercept
	# @param axis	[Symbol]    the axis to intercept (either :x or :y)
	# @return [Number]  the location of the intercept
	def intercept(axis=:y)
	    case axis
		when :x
		    vertical? ? first.x : (horizontal? ? nil : (first.x - first.y/slope))
		when :y
		    vertical? ? nil : (horizontal? ? first.y : (first.y - slope * first.x))
	    end
	end

# @endgroup
    end
end