--[[
2D Geometry utilities

all of these apply to full rectangles { x = ..., y = ..., w = ..., h = ... }

some behaviour is defined for points { x = ..., y = ... }
some behaviour is defined for dimensions { w = ..., h = ... }

just use it on simple tables that have x, y and/or w, h
or define your own types using this as a metatable
]]--
Geom = {
	x = 0,
	y = 0,
	w = 0,
	h = 0
}

function Geom:new(o)
	local o = o or {}
	setmetatable(o, self)
	self.__index = self
	return o
end

function Geom:copy()
	local n = Geom:new()
	n.x = self.x
	n.y = self.y
	n.w = self.w
	n.h = self.h
	return n
end

function Geom:__tostring()
	return self.w.."x"..self.h.."+"..self.x.."+"..self.y
end

--[[
offset rectangle or point by relative values
]]--
function Geom:offsetBy(dx, dy)
	self.x = self.x + dx
	self.y = self.y + dy
	return self
end

--[[
offset rectangle or point to certain coordinates
]]--
function Geom:offsetTo(x, y)
	self.x = x
	self.y = y
	return self
end

--[[
scale rectangle (grow to bottom and to the right) or dimension

if a single factor is given, it is applied to both width and height
]]--
function Geom:scaleBy(zx, zy)
	self.w = self.w * zx
	self.h = self.h * (zy or zx)
	return self
end

--[[
this method also takes care of x and y
]]--
function Geom:transformByScale(zx, zy)
	self.x = self.x * zx
	self.y = self.y * (zx or zy)
	self:scaleBy(zx, zy)
end

--[[
enlarges or shrinks dimensions or rectangles

note that for rectangles the offset stays the same
]]--
function Geom:changeSizeBy(dw, dh)
	self.w = self.w + dw
	self.h = self.h + dh
	return self
end

--[[
return the outer rectangle that contains both us and a given rectangle

works for rectangles, dimensions and points
]]--
function Geom:combine(rect_b)
	local combined = Geom:new(self)
	if combined.x > rect_b.x then
		combined.x = rect_b.x
	end
	if combined.y > rect_b.y then
		combined.y = rect_b.y
	end
	if self.x + self.w > rect_b.x + rect_b.w then
		combined.w = self.x + self.w - combined.x
	else
		combined.w = rect_b.x + rect_b.w - combined.x
	end
	if self.y + self.h > rect_b.y + rect_b.h then
		combined.h = self.y + self.h - combined.y
	else
		combined.h = rect_b.y + rect_b.h - combined.y
	end
	return combined
end

--[[
returns a rectangle for the part that we and a given rectangle share

TODO: what happens if there is no rectangle shared? currently behaviour is undefined.
]]--
function Geom:intersect(rect_b)
	-- make a copy of self
	local intersected = self:copy()
	if self.x < rect_b.x then
		intersected.x = rect_b.x
	end
	if self.y < rect_b.y then
		intersected.y = rect_b.y
	end
	if self.x + self.w < rect_b.x + rect_b.w then
		intersected.w = self.x + self.w - intersected.x
	else
		intersected.w = rect_b.x + rect_b.w - intersected.x
	end
	if self.y + self.h < rect_b.y + rect_b.h then
		intersected.h = self.y + self.h - intersected.y
	else
		intersected.h = rect_b.y + rect_b.h - intersected.y
	end
	return intersected
end

--[[
return true if self does not share any area with rect_b
]]--
function Geom:notIntersectWith(rect_b)
	if (self.x >= (rect_b.x + rect_b.w))
	or (self.y >= (rect_b.y + rect_b.h))
	or (rect_b.x >= (self.x + self.w))
	or (rect_b.y >= (self.y + self.h)) then
		return true
	end
	return false
end

--[[
set size of dimension or rectangle to size of given dimension/rectangle
]]--
function Geom:setSizeTo(rect_b)
	self.w = rect_b.w
	self.h = rect_b.h
	return self
end

--[[
check whether rect_b is within current rectangle

works for dimensions, too
for points, it is basically an equality check
]]--
function Geom:contains(rect_b)
	if self.x <= rect_b.x
	and self.y <= rect_b.y
	and self.x + self.w >= rect_b.x + rect_b.w
	and self.y + self.h >= rect_b.y + rect_b.h
	then
		return true
	end
	return false
end

--[[
check for equality

works for rectangles, points, dimensions
]]--
function Geom:__eq(rect_b)
	if self.x == rect_b.x
	and self.y == rect_b.y
	and self:equalSize(rect_b)
	then
		return true
	end
	return false
end

--[[
check size of dimension/rectangle for equality
]]--
function Geom:equalSize(rect_b)
	if self.w == rect_b.w
	and self.h == rect_b.h
	then
		return true
	end
	return false
end

--[[
check if our size is smaller than the size of the given dimension/rectangle
]]--
function Geom:__lt(rect_b)
	DEBUG("lt:",self,rect_b)
	if self.w < rect_b.w and self.h < rect_b.h then
DEBUG("lt+")
		return true
	end
DEBUG("lt-")
	return false
end

--[[
check if our size is smaller or equal the size of the given dimension/rectangle
]]--
function Geom:__le(rect_b)
	if self.w <= rect_b.w and self.h <= rect_b.h then
		return true
	end
	return false
end

--[[
offset the current rectangle by dx, dy while fitting it into the space
of a given rectangle

this can also be called with dx=0 and dy=0, which will fit the current
rectangle into the given rectangle
]]--
function Geom:offsetWithin(rect_b, dx, dy)
	-- check size constraints and shrink us when we're too big
	if self.w > rect_b.w then
		self.w = rect_b.w
	end
	if self.h > rect_b.h then
		self.h = rect_b.h
	end
	-- offset
	self.x = self.x + dx
	self.y = self.y + dy
	-- check offsets
	if self.x < rect_b.x then
		self.x = rect_b.x
	end
	if self.y < rect_b.y then
		self.y = rect_b.y
	end
	if self.x + self.w > rect_b.x + rect_b.w then
		self.x = rect_b.x + rect_b.w - self.w
	end
	if self.y + self.h > rect_b.y + rect_b.h then
		self.y = rect_b.y + rect_b.h - self.h
	end
end

function Geom:shrinkInside(rect_b, dx, dy)
	self:offsetBy(dx, dy)
	return self:intersect(rect_b)
end

--[[
return the Euclidean distance between two geoms
]]--
function Geom:distance(geom)
	return math.sqrt(math.pow(self.x - geom.x, 2) + math.pow(self.y - geom.y, 2))
end

--[[
return the midpoint of two geoms
]]--
function Geom:midpoint(geom)
	return Geom:new{
		x = (self.x + geom.x) / 2,
		y = (self.y + geom.y) / 2,
		w = 0, h = 0,
	}
end

