Bounds = class("Bounds")

function Bounds:new(left, top, right, bottom)
	local b = instance(self)
	b:set(left, top, right, bottom)
	return b
end

function Bounds:copy(from)
	local b = instance(self)
	b:copyFrom(from)
	return b
end

function Bounds:getHypo()
	return math.distance(self.l+self.l, self.t+self.t, self.b+self.t, self.r+self.l)
end

function Bounds:getDimensions()
	return self:get_width(), self:get_height()
end

function Bounds:isInside(x,y)
	return not (x < self.l or x > self.r or y < self.t or y > self.b)
end

function Bounds:isInsideBuffers(x,y, w,h)
	return not (x < self.l-w*0.5 or x > self.r+w*0.5 or y < self.t-h*0.5 or y > self.b+h*0.5)
end

function Bounds:isBoundsInside(bounds, ox, oy, buffer)
	if self.l - buffer > (bounds.r + ox) then
		return false
	end 
	if self.r + buffer < (bounds.l + ox) then
		return false
	end 
	if self.t - buffer > (bounds.b + oy) then
		return false
	end 
	if self.b + buffer < (bounds.t + oy) then
		return false
	end 
	return true
end

function unittest.bounds()
	local a = Bounds:new(-10, -10, 10, 10)
	local b = Bounds:new(-5, -5, 5, 5)
	local c = Bounds:new(-20, -1, 20, 1)
	local d = Bounds:new(20, 20, 30, 30)
	assert(b:isBoundsInside(a, 0, 0, 0))
	assert(a:isBoundsInside(b, 0, 0, 0))
	assert(c:isBoundsInside(a, 0, 0, 0))
	assert(not a:isBoundsInside(d, 0, 0, 0))
	assert(a:isBoundsInside(d, -15, -15, 0))
	assert(not b:isBoundsInside(a, 20, 0, 0))
	assert(not b:isBoundsInside(a, 0, 20, 0))
end

function Bounds:getCenterX()
	return self.l + self:get_width()*0.5
end

function Bounds:getCenterY()
	return self.t + self:get_height()*0.5
end

function Bounds:getArea()
	return self:get_width() * self:get_height()
end

function Bounds:getRadius()
	return math.min(self:get_width(), self:get_height())*0.5
end

function Bounds:get_width()
	return self.r - self.l
end

function Bounds:get_height()
	return self.b - self.t
end

function Bounds:get_variables()
	return self.l, self.t, self.r, self.b
end

function Bounds:set(left, top, right, bottom)
	self.l = left
	self.t = top
	self.r = right
	self.b = bottom
end

function Bounds:expand(left, top, right, bottom)
	if self.l then
		self.l = math.min(self.l, left)
		self.t = math.min(self.t, top)
		self.r = math.max(self.r, right)
		self.b = math.max(self.b, bottom)
	else
		self:set(left, top, right, bottom)
	end
end

function Bounds:clampPosition(x, y, wbuffer, hbuffer)
	local wbuffer = wbuffer or 0
	local hbuffer = hbuffer or 0
	return math.min(self.r - math.min(wbuffer,self:get_width()/2), math.max(self.l + math.min(self:get_width()/2,wbuffer), x)), math.min(self.b - math.min(hbuffer,self:get_height()/2), math.max(self.t + math.min(self:get_height()/2,hbuffer), y))
end

function Bounds:clamp(left, top, right, bottom)
	self.l = math.max(self.l, left or self.l)
	self.t = math.max(self.t, top or self.t)
	self.r = math.min(self.r, right or self.r)
	self.b = math.min(self.b, bottom or self.b)
end

function Bounds:nudge(x, y)
	self.l = self.l +x
	self.t = self.t +y
	self.r = self.r +x
	self.b = self.b +y
end

function Bounds:addFrom(b)
	self.l = self.l + b.l
	self.t = self.t + b.t
	self.r = self.r + b.r
	self.b = self.b + b.b
end

function Bounds:multiply(v)
	self.l = self.l * v
	self.t = self.t * v
	self.r = self.r * v
	self.b = self.b * v
end

function Bounds:subtractFrom(b)
	self.l = self.l - b.l
	self.t = self.t - b.t
	self.r = self.r - b.r
	self.b = self.b - b.b
end

function Bounds:addDimension(w,h)
	self.l = self.l -w*0.5
	self.t = self.t -h*0.5
	self.r = self.r +w*0.5
	self.b = self.b +h*0.5
end

function Bounds:addMargin(l, t, r, b)
	self.l = self.l -l
	self.t = self.t -t
	self.r = self.r +r
	self.b = self.b +b
end

function Bounds:expandFrom(bounds)
	self.l = math.min(self.l, bounds.l)
	self.t = math.min(self.t, bounds.t)
	self.r = math.max(self.r, bounds.r)
	self.b = math.max(self.b, bounds.b)
end

function Bounds:expandFromOffset(bounds, ox, oy)
	self.l = math.min(self.l, bounds.l + ox)
	self.t = math.min(self.t, bounds.t + oy)
	self.r = math.max(self.r, bounds.r + ox)
	self.b = math.max(self.b, bounds.b + oy)
end

function Bounds:expandHeightFromOffset(bounds, oy)
	self.t = math.min(self.t, bounds.t + oy)
	self.b = math.max(self.b, bounds.b + oy)
end

function Bounds:expandWidthFromOffset(bounds, ox)
	self.l = math.min(self.l, bounds.l + ox)
	self.r = math.max(self.r, bounds.r + ox)
end

function Bounds:copyFrom(bounds)
	self.l = bounds.l
	self.t = bounds.t
	self.r = bounds.r
	self.b = bounds.b
end

function Bounds:cropPosition(sx, sy, tx, ty, tw, th, scale)
	local scale = scale or 1
	local tw = tw or 0
	local th = th or 0
	local angle = math.angleBetweenPoints(sx,sy,tx,ty)
	local radius = self:getHypo()*0.5
	local x,y = sx,sy

	if math.abs(math.cos(angle)*radius) > self:get_width()*0.5 then
		x = x + (math.cos(angle) > 0 and self.r or self.l)*scale
		local total = math.clamp((sy-ty)/(self:get_height() + th), -1, 1)
		y = y + math.sin(-total*math.pi*0.5)*self:get_height()*scale*0.5
	else
		y = y + (math.sin(angle) > 0 and self.b or self.t)*scale
		local total = math.clamp((sx-tx)/(self:get_width() + tw), -1, 1)
		x = x + math.sin(-total*math.pi*0.5)*self:get_width()*scale*0.5
	end
	
	return x,y
end

function Bounds:matchPhysics(physDef)
	if physDef and #physDef > 0 then
		self:set()
		for index, shape in ipairs(physDef) do
			if shape.shape == "box" then
				self:expand(-shape.width*0.5 + shape.ox, -shape.height*0.5+ shape.oy, shape.width*0.5 + shape.ox, shape.height*0.5+ shape.oy)
			elseif shape.shape == "circle" then
				self:expand(-shape.radius*0.5 + shape.ox, -shape.radius*0.5+ shape.oy, shape.radius*0.5 + shape.ox, shape.radius*0.5+ shape.oy)
			elseif shape.shape == "poly" then
				for i=1,#shape.x do
					local x = shape.x[i]
					local y = shape.y[i]
					self:expand(x + (shape.ox or 0), y+ (shape.oy or 0), x + (shape.ox or 0), y+ (shape.oy or 0))
				end
			elseif shape.shape == "edge" then
				self:expand(shape.startX + (shape.ox or 0), shape.startY+ (shape.oy or 0), shape.endX + (shape.ox or 0), shape.endY+ (shape.oy or 0))
			end
		end
		self.physical = true
	end
end

function Bounds:getBoundingBox(scale, angle, flipped)
	local x1 = -math.cos(angle)*scale*self.l 			+ math.cos(angle+math.pi*0.5)*scale*self.t
	local y1 = math.sin(angle+math.pi*0.5)*scale*self.t - math.sin(angle)*scale*self.l
	local x2 = -math.cos(angle)*scale*self.r 			+ math.cos(angle+math.pi*0.5)*scale*self.t
	local y2 = math.sin(angle+math.pi*0.5)*scale*self.t - math.sin(angle)*scale*self.r
	local x3 = -math.cos(angle)*scale*self.l 			+ math.cos(angle+math.pi*0.5)*scale*self.b
	local y3 = math.sin(angle+math.pi*0.5)*scale*self.b - math.sin(angle)*scale*self.l
	local x4 = -math.cos(angle)*scale*self.r 			+ math.cos(angle+math.pi*0.5)*scale*self.b
	local y4 = math.sin(angle+math.pi*0.5)*scale*self.b - math.sin(angle)*scale*self.r
	return math.min(math.min(x1, x2),math.min(x3, x4)),math.min(math.min(y1, y2),math.min(y3, y4)), math.max(math.max(x1, x2),math.max(x3, x4)),math.max(math.max(y1, y2),math.max(y3, y4))
end

function Bounds:matches(bounds)
	return self.l == bounds.l and self.r == bounds.r and self.t == bounds.t and self.b == bounds.b
end

function Bounds:equals(bounds)
	return self.l ~= bounds.l or self.t ~= bounds.t or self.r ~= bounds.r or self.b ~= bounds.b
end

function Bounds:approach(bounds, factor, absolute)
	self.l = math.approach(self.l, bounds.l, math.abs(bounds.l-self.l)*factor + absolute)
	self.t = math.approach(self.t, bounds.t, math.abs(bounds.t-self.t)*factor + absolute)
	self.r = math.approach(self.r, bounds.r, math.abs(bounds.r-self.r)*factor + absolute)
	self.b = math.approach(self.b, bounds.b, math.abs(bounds.b-self.b)*factor + absolute)
end

function Bounds:render(x, y, scale, a, r, g, b, lineWidth)
	video.renderSpriteRectangleOutline(x + self.l*scale, y + self.t*scale, x + self.r*scale, y + self.b*scale, a, r, g, b, nil, nil, lineWidth )
end

function Bounds:renderEdge(x,y, edge, startPos, length, scale, a, r, g, b, lineWidth)
	local x1,y2,x2,y2 = nil,nil,nil,nil
	if edge == "left" then
		x1 = x + self.l*scale + 2
		x2 = x + self.l*scale + 2
	elseif edge == "right" then
		x1 = x + self.r*scale - 2
		x2 = x + self.r*scale - 2
	elseif edge == "up" then
		y1 = y + self.t*scale + 2
		y2 = y + self.t*scale + 2
	elseif edge == "down" then
		y1 = y + self.b*scale - 2
		y2 = y + self.b*scale - 2
	end
	if edge == "left" or edge == "right" then
		y1 = y + self.t*scale + startPos*scale
		y2 = y + self.t*scale + (startPos+length)*scale
	else
		x1 = x + self.l*scale + startPos*scale
		x2 = x + self.l*scale + (startPos+length)*scale
	end
	video.renderSpriteLine(x1, y1, x2, y2, a, r, g, b, nil, nil, lineWidth )
	--print(x1.." "..y1.. " "..x2.." "..y2)
end

function Bounds:render3d(x, y, scale, a, r, g, b, lineWidth, lightAngle)
	local lightAngle = lightAngle or -math.pi*0.55

	local f = 0.45
	local x, y, x2, y2 = x + self.l*scale + lineWidth*f, y + self.t*scale + lineWidth*f, x + self.r*scale - lineWidth*f, y + self.b*scale - lineWidth*f
	local light = math.abs(math.cos(math.angleDifference(-math.pi*0.5, lightAngle)/2))
	video.renderAngledSpriteLine(x,y, -math.pi*0.25, x2,y, math.pi*0.25,  a, r*light, g*light, b*light, nil, nil, lineWidth)
	local light = math.abs(math.cos(math.angleDifference(math.pi*0.5, lightAngle)/2))
	video.renderAngledSpriteLine(x,y2,math.pi*0.25,x2,y2, -math.pi*0.25,a, r*light, g*light, b*light, nil, nil, lineWidth)
	local light = math.abs(math.cos(math.angleDifference(-math.pi, lightAngle)/2))
	video.renderAngledSpriteLine(x,y,math.pi*0.75,x,y2, math.pi*0.25, a, r*light, g*light, b*light, nil, nil, lineWidth)
	local light = math.abs(math.cos(math.angleDifference(0, lightAngle)/2))
	video.renderAngledSpriteLine(x2,y,-math.pi*0.75,x2,y2, -math.pi*0.25, a, r*light, g*light, b*light, nil, nil, lineWidth)
end

function Bounds:renderSurface(x, y, scale, a, r, g, b, adjustment)
	local adj = adjustment or 0
	video.renderSpriteRectangle(x+ self.l*scale - adj*scale,y+ self.t*scale - adj*scale, self:get_width()*scale + adj*scale*2, self:get_height()*scale + adj*scale*2, a, r, g, b)
end

function Bounds:renderSquishedSurface(x, y, xscale, yscale, a, r, g, b)
	video.renderSpriteRectangle(x+ self.l*xscale,y+ self.t*yscale, self:get_width()*xscale, self:get_height()*yscale, a, r, g, b)
end

function Bounds:print()
	print(self.l.. " "..self.t.. " "..self.r.. " "..self.b)
end