#--
#	Rubygame -- Ruby bindings to SDL to facilitate game creation
#	Copyright (C) 2004  John 'jacius' Croisant
#
#	This library is free software; you can redistribute it and/or
#	modify it under the terms of the GNU Lesser General Public
#	License as published by the Free Software Foundation; either
#	version 2.1 of the License, or (at your option) any later version.
#
#	This library is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#	Lesser General Public License for more details.
#
#	You should have received a copy of the GNU Lesser General Public
#	License along with this library; if not, write to the Free Software
#	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#++

#--
# Table of Contents:
#
# class Rect
# 	GENERAL:
# 		initialize
# 		new_from_object
# 		to_s
# 		to_a, to_ary
# 		[]
# 	ATTRIBUTES:
# 		x, y, w, h [<- accessors]
# 		width, height, size
# 		left, top, right, bottom
# 		center, centerx, centery
# 		topleft, topright
# 		bottomleft, bottomright
# 		midleft, midtop, midright, midbottom
# 	UTILITY METHODS:
# 		clamp, clamp!
# 		clip, clip!
# 		collide_hash, collide_hash_all
# 		collide_array, collide_array_all
# 		collide_point?
# 		collide_rect?
# 		contain?
# 		inflate, inflate!
# 		move, move!
# 		normalize, normalize!
# 		union, union!
# 		union_all, union_all!
#
#	class Surface
#		make_rect
# 
#++

module Rubygame

# A Rect is a representation of a rectangle, with four core attributes
# (x offset, y offset, width, and height) and a variety of functions
# for manipulating and accessing these attributes.
#
# Like all coordinates in Rubygame (and its base library, SDL), x and y
# offsets are measured from the top-left corner of the screen, with greater
# y offsets being lower. Thus, specifying the x and y offsets of the Rect
# is equivalent to setting the location of its top-left corner.
# 
# In Rubygame, Rects are used for collision detection and describing 
# the area of a Surface to operate on.
class Rect < Array

	#--
	# GENERAL
	#++

	# Create a new Rect, attempting to extract its own information from 
	# the given arguments. The arguments must fall into one of these cases:
	# 
	#   - 4 integers +(x, y, w, h)+.
	#   - 1 Rect or Array containing 4 integers +([x, y, w, h])+.
	#   - 2 Arrays containing 2 integers each +([x,y], [w,h])+.
	#   - 1 object with a +rect+ attribute which is a valid Rect object.
	# 
	# All rect core attributes (x,y,w,h) must be integers.
	# 
	def initialize(*argv)
		case argv.length
		when 1
			if argv[0].kind_of? Array; super(argv[0])
			elsif argv[0].respond_to? :rect; super(argv[0])
			end
		when 2
			super(argv[0].concat(argv[1]))
		when 4
			super(argv)
		end
		return self
	end

	# Extract or generate a Rect from the given object, if possible, using the
	# following process:
	# 
	#  1. If it's a Rect already, return a duplicate Rect.
	#  2. Elsif it's an Array with at least 4 values, make a Rect from it.
	#  3. Elsif it has a +rect+ attribute., perform (1) and (2) on that.
	#  4. Otherwise, raise TypeError.
	# 
	# See also Surface#make_rect()
	def Rect.new_from_object(object)
		case(object)
		when Rect
			return object.dup
		when Array 
			if object.length >= 4
				return Rect.new(object)
			else
				raise(ArgumentError,"Array does not have enough indices to be made into a Rect (%d for 4)."%object.length )
			end
		else
			begin
				case(object.rect)
				when Rect
					return object.rect.dup
				when Array
					if object.rect.length >= 4
						return Rect.new(object.rect)
					else
						raise(ArgumentError,"Array does not have enough indices to be made into a Rect (%d for 4)."%object.rect.length )
					end
				end				# case object.rect
			rescue NoMethodError # if no rect.rect
				raise(TypeError,"Object must be a Rect or Array [x,y,w,h], or have an attribute called 'rect'. (Got %s instance.)"%object.class)
			end
		end # case object
	end


	# Print the Rect in the form "+#<Rect [x,y,w,h]>+"
	def to_s; "#<Rect [%s,%s,%s,%s]>"%self; end

	# Print the Rect in the form "+#<Rect:id [x,y,w,h]>+"
	def inspect; "#<Rect:#{self.object_id} [%s,%s,%s,%s]>"%self; end

	#--
	# ATTRIBUTES
	#++

	# Returns self[0]
	def x; return self[0]; end
	# Sets self[0] to +val+
	def x=(val); self[0] = val; end

	alias left x
	alias left= x=;
	alias l x
	alias l= x=;

	# Returns self[1]
	def y; return self[1]; end
	# Sets self[1] to +val+
	def y=(val); self[1] = val; end

	alias top y
	alias top= y=;
	alias t y
	alias t= y=;

	# Returns self[2]
	def w; return self[2]; end
	# Sets self[2] to +val+
	def w=(val); self[2] = val; end

	alias width w
	alias width= w=;

	# Returns self[3]
	def h; return self[3]; end
	# Sets self[3] to +val+
	def h=(val); self[3] = val; end

	alias height h
	alias height= h=;

	# Return the width and height of the Rect.
	def size; return self[2..3]; end

	# Set the width and height of the Rect.
	def size=(size); self[2..3] = size; return size; end

	# Return the x coordinate of the right side of the Rect.
	def right; return self[0]+self[2]; end

	# Set the x coordinate of the right side of the Rect by translating the
	# Rect (adjusting the x offset).
	def right=(r); self[0] = r - self[2]; return r; end

	alias r right
	alias r= right=;

	# Return the y coordinate of the bottom side of the Rect.
	def bottom; return self[1]+self[3]; end

	# Set the y coordinate of the bottom side of the Rect by translating the
	# Rect (adjusting the y offset).
	def bottom=(b); self[1] = b - self[3]; return b; end

	alias b bottom
	alias b= bottom=;

	# Return the x and y coordinates of the center of the Rect.
	def center; return self.centerx, self.centery; end

	# Set the x and y coordinates of the center of the Rect by translating the
	# Rect (adjusting the x and y offsets).
	def center=(center)
		self.centerx, self.centery = center
		return center
	end
	alias c center
	alias c= center=;

	# Return the x coordinate of the center of the Rect
	def centerx; return self[0]+(self[2]/2); end

	# Set the x coordinate of the center of the Rect by translating the
	# Rect (adjusting the x offset).
	def centerx=(x); self[0] = x - (self[2]/2); return x; end

	alias cx centerx
	alias cx= centerx=;

	# Return the y coordinate of the center of the Rect
	def centery; return self[1]+(self[3]/2); end

	# Set the y coordinate of the center of the Rect by translating the
	# Rect (adjusting the y offset).
	def centery=(y); self[1] = y- (self[3]/2); return y; end

	alias cy centery
	alias cy= centery=;

	# Return the x and y coordinates of the top-left corner of the Rect
	def topleft; return self[0..1]; end

	# Set the x and y coordinates of the top-left corner of the Rect by 
	# translating the Rect (adjusting the x and y offsets).
	def topleft=(topleft)
		self[0..1] = topleft
		return topleft
	end

	alias tl topleft
	alias tl= topleft=;

	# Return the x and y coordinates of the top-right corner of the Rect
	def topright; return self.right, self[1]; end

	# Set the x and y coordinates of the top-right corner of the Rect by 
	# translating the Rect (adjusting the x and y offsets).
	def topright=(topright)
		self.right, self[1] = topright
		return topright
	end

	alias tr topright
	alias tr= topright=;

	# Return the x and y coordinates of the bottom-left corner of the Rect
	def bottomleft; return self[0], self.bottom; end

	# Set the x and y coordinates of the bottom-left corner of the Rect by 
	# translating the Rect (adjusting the x and y offsets).
	def bottomleft=(bottomleft)
		self[0], self.bottom = bottomleft
		return bottomleft
	end

	alias bl bottomleft
	alias bl= bottomleft=;

	# Return the x and y coordinates of the bottom-right corner of the Rect
	def bottomright; return self.right, self.bottom; end

	# Set the x and y coordinates of the bottom-right corner of the Rect by 
	# translating the Rect (adjusting the x and y offsets).
	def bottomright=(bottomright)
		self.right, self.bottom = bottomright
		return bottomright
	end

	alias br bottomright
	alias br= bottomright=;

	# Return the x and y coordinates of the midpoint on the left side of the
	# Rect.
	def midleft; return self[0], self.centery; end	

	# Set the x and y coordinates of the midpoint on the left side of the Rect
	# by translating the Rect (adjusting the x and y offsets).
	def midleft=(midleft)
		self[0], self.centery = midleft
		return midleft
	end

	alias ml midleft
	alias ml= midleft=;

	# Return the x and y coordinates of the midpoint on the left side of the
	# Rect.
	def midtop; return self.centerx, self[1]; end	

	# Set the x and y coordinates of the midpoint on the top side of the Rect
	# by translating the Rect (adjusting the x and y offsets).
	def midtop=(midtop)
		self.centerx, self[1] = midtop
		return midtop
	end

	alias mt midtop
	alias mt= midtop=;

	# Return the x and y coordinates of the midpoint on the left side of the
	# Rect.
	def midright; return self.right, self.centery; end	

	# Set the x and y coordinates of the midpoint on the right side of the Rect
	# by translating the Rect (adjusting the x and y offsets).
	def midright=(midright)
		self.right, self.centery = midright
		return midright
	end

	alias mr midright
	alias mr= midright=;

	# Return the x and y coordinates of the midpoint on the left side of the
	# Rect.
	def midbottom; return self.centerx, self.bottom; end	

	# Set the x and y coordinates of the midpoint on the bottom side of the
	# Rect by translating the Rect (adjusting the x and y offsets).
	def midbottom=(midbottom)
		self.centerx, self.bottom = midbottom
		return midbottom
	end

	alias mb midbottom
	alias mb= midbottom=;

	#--
	# UTILITY METHODS
	#++


	# As #clamp!, but the original caller is not changed.
	def clamp(rect)
		self.dup.clamp!(rect)
	end

	# Translate the calling Rect to be entirely inside the given Rect. If 
	# the caller is too large along either axis to fit in the given rect,
	# it is centered with respect to the given rect, along that axis.
	def clamp!(rect)
		nself = self.normalize
		rect = Rect.new_from_object(rect)
		#If self is inside given, there is no need to move self
		unless rect.contain?(nself)

			#If self is too wide:
			if nself[2] >= rect[2]
				self[0] = rect.centerx - nself[2]/2
				#Else self is not too wide
			else
				#If self is to the left of arg
				if nself[0] < rect[0]
					self[0] = rect[0]
				#If self is to the right of arg
				elsif nself.right > rect.right
					self[0] = rect.right - nself[2]
				#Otherwise, leave x alone
				end
			end

			#If self is too tall:
			if nself[3] >= rect[3]
				self[1] = rect.centery - nself[3]/2
				#Else self is not too tall
			else
				#If self is above arg
				if nself[1] < rect[1]
					self[1] = rect[1]
				#If self below arg
				elsif nself.bottom > rect.bottom
					self[1] = rect.bottom - nself[3]
				#Otherwise, leave y alone
				end
			end
		end
		return self
	end

	# As #clip!, but the original caller is not changed.
	def clip(rect)
		self.dup.clip!(rect)
	end

	# Crop the calling Rect to be entirely inside the given Rect. If the
	# caller does not intersect the given Rect at all, its width and height
	# are set to zero, but its x and y offsets are not changed.
	# 
	# As a side effect, the Rect is normalized.
	def clip!(rect)
		nself = self.normalize
		rect = Rect.new_from_object(rect).normalize
		if self.collide_rect?(rect)
			self[0] = min(nself.right, rect.right) - nself[0]
			self[3] = min(nself.bottom, rect.bottom) - nself[1]
			self[0] = max(nself[0], rect[1])
			self[1] = max(nself[1], rect[1])
			#if they do not intersect at all:
		else
			self[0], self[1] = nself.topleft
			self[2], self[3] = 0, 0
		end
		return self
	end

	# Iterate through all key/value pairs in the given hash table, and
	# return the first pair whose value is a Rect that collides with the
	# caller.
	#
	# Because a hash table is unordered, you should not expect any 
	# particular Rect to be returned first.
	def collide_hash(hash_rects)
		hash_rects.each { |key,value|
			if value.collide_rect?+(self); return [key,value]; end
		}
		return nil
	end

	# Iterate through all key/value pairs in the given hash table, and
	# return an Array of every pair whose value is a Rect that collides
	# the caller.
	# 
	# Because a hash table is unordered, you should not expect the returned
	# pairs to be in any particular order.
	def collide_hash_all(hash_rects)
		collection = []
		hash_rects.each { |key,value|
			if value.collide_rect?+(self); collection += [key,value]; end
		}
		return collection
	end

	# Iterate through all elements in the given Array, and return
	# the *index* of the first element which is a Rect that collides with
	# the caller.
	def collide_array(array_rects)
		for i in 0..(array_rects.length)
			if array_rects[i].collide_rect?(self)
				return i
			end
		end
		return nil
	end

	# Iterate through all elements in the given Array, and return
	# an Array containing the *indices* of every element that is a Rect
	# that collides with the caller.
	def collide_array_all(array_rects)
		indexes = []
		for i in (0..(array_rects.length))
			if array_rects[i].collide_rect?(self)
				indexes += [i]
			end
		end
		return indexes
	end

	# True if the point is inside (including on the border) of the caller.
	# The point can be given as either an Array or separate coordinates.
	def collide_point?(x,y=nil)
		unless y
			x, y = x[0..1]
			unless y
				raise ArgumentError("You must pass 2 Numerics or 1 Array.")
			end
		end
		nself = self.normalize
		return ((nself.left..nself.right).include? x and
					(nself.top..nself.bottom).include? y)
	end

	# True if the caller and the given Rect overlap (or touch) at all.
	def collide_rect?(rect)
		nself = self.normalize
		rect = Rect.new_from_object(rect).normalize
		return ((( (rect.l)..(rect.r) ).include?(nself.l)  or\
						 ((nself.l)..(nself.r)).include?( rect.l)) and\
						(( (rect.t)..(rect.b) ).include?(nself.t)  or\
						 ((nself.t)..(nself.b)).include?( rect.t)))
	end

	# True if the given Rect is totally within the caller. Borders may
	# overlap.
	def contain?(rect)
		nself = self.normalize
		rect = Rect.new_from_object(rect).normalize
		return (nself.left <= rect.left and rect.right <= nself.right and
					nself.top <= rect.top and rect.bottom <= nself.bottom)
	end

	# As #inflate!, but the original caller is not changed.
	def inflate(x,y=nil)
		unless y
			x, y = x[0..1]
			unless y
				raise ArgumentError("You must pass 2 Numerics or 1 Array.")
			end
		end

		return self.class.new(self[0] - x/2,
													self[1] - y/2,
													self[2] + x,
													self[3] + y)
	end

	# Increase the Rect's size is the x and y directions, while keeping the
	# same center point. For best results, expand by an even number.
	# X and y inflation can be given as an Array or as separate values.
	def inflate!(x,y=nil)
		unless y
			x, y = x[0..1]
			unless y
				raise ArgumentError("You must pass 2 Numerics or 1 Array.")
			end
		end

		self[0] -= x/2
		self[1] -= y/2
		self[2] += x
		self[3] += y
		return self
	end

	# As #move!, but the original caller is not changed.
	def move(x,y=nil)
		self.dup.move!(x,y)
	end

	# Translate the Rect by the given amounts in the x and y directions.
	# Positive values are rightward for x and downward for y.
	# X and y movement can be given as an Array or as separate values.
	def move!(x,y=nil)
		unless y
			x, y = x
			unless y
				raise ArgumentError("You must pass 2 Numerics or 1 Array.")
			end
		end

		self[0]+=x; self[1]+=y
		return self
	end

	# As #normalize!, but the original caller is not changed.
	def normalize
		self.dup.normalize!()
	end

	# Fix Rects that have negative width or height, without changing the
	# area it represents. Has no effect on Rects with non-negative width
	# and height. Some Rect methods will automatically normalize the Rect.
	def normalize!
		if self[2] < 0
			self[0], self[2] = self[0]+self[2], -self[2]
		end
		if self[3] < 0
			self[1], self[3] = self[1]+self[3], -self[3]
		end
		return self
	end

	# As #union!, but the original caller is not changed.
	def union(rect)
		self.dup.union!(rect)
	end

	# Expand the caller to also cover the given Rect. The Rect is still a 
	# rectangle, so it may also cover areas that neither of the original
	# Rects did, for example areas between the two Rects.
	def union!(rect)
		left, top, right, bottom = self.normalize
		rect = Rect.new_from_object(rect).normalize

		left = min(left,rect.left)
		top = min(top,rect.top)
		right = max(right,rect.right)
		bottom = max(bottom,rect.bottom)

		self[0..3] = left, top, right - left, bottom-top
		return self
	end

	# As #union_all!, but the original caller is not changed.
	def union_all(array_rects)
		self.dup.union_all!(array_rects)
	end

	# Expand the caller to cover all of the given Rects. See also #union!
	def union_all!(array_rects)
		left, top, right, bottom = self.normalize
		array_rects.each do |r|
			r = Rect.new_from_object(r).normalize
			left = min(left,r.left)
			top = min(top,r.top)
			right = max(right,r.right)
			bottom = max(bottom,r.bottom)
		end
		self[0..3] = left, top, right - left, bottom-top
		return self
	end


end # class Rect


class Surface
	# Return a Rect with the same width and height as the Surface, positioned
	# at (0,0).
	def make_rect()
		return Rect.new(0,0,self.width,self.height)
	end
end

end # module Rubygame
