--[[
The MIT License (MIT)

Copyright (c) 2018 SSYGEN

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
--

local path = ... .. "."
local wf = {}
wf.Math = require(path .. "mlib.mlib")

World = {}
World.__index = World

function wf.newWorld(xg, yg, sleep)
	local world = wf.World.new(wf, xg, yg, sleep)

	world.box2d_world:setCallbacks(
		world.collisionOnEnter,
		world.collisionOnExit,
		world.collisionPre,
		world.collisionPost
	)
	world:collisionClear()
	world:addCollisionClass("Default")

	-- Points all box2d_world functions to this wf.World object
	-- This means that the user can call world:setGravity for instance without having to say world.box2d_world:setGravity
	for k, v in pairs(world.box2d_world.__index) do
		if
			k ~= "__gc"
			and k ~= "__eq"
			and k ~= "__index"
			and k ~= "__tostring"
			and k ~= "update"
			and k ~= "destroy"
			and k ~= "type"
			and k ~= "typeOf"
		then
			world[k] = function(self, ...)
				return v(self.box2d_world, ...)
			end
		end
	end

	return world
end

function World.new(wf, xg, yg, sleep)
	local self = {}
	local settings = settings or {}
	self.wf = wf

	self.draw_query_for_n_frames = 10
	self.query_debug_drawing_enabled = false
	self.explicit_collision_events = false
	self.collision_classes = {}
	self.masks = {}
	self.is_sensor_memo = {}
	self.query_debug_draw = {}

	love.physics.setMeter(32)
	self.box2d_world = love.physics.newWorld(xg, yg, sleep)

	return setmetatable(self, World)
end

function World:update(dt)
	self:collisionEventsClear()
	self.box2d_world:update(dt)
end

function World:draw(alpha)
	-- get the current color values to reapply
	local r, g, b, a = love.graphics.getColor()
	-- alpha value is optional
	alpha = alpha or 255
	-- Colliders debug
	love.graphics.setColor(222, 222, 222, alpha)
	local bodies = self.box2d_world:getBodies()
	for _, body in ipairs(bodies) do
		local fixtures = body:getFixtures()
		for _, fixture in ipairs(fixtures) do
			if fixture:getShape():type() == "PolygonShape" then
				love.graphics.polygon("line", body:getWorldPoints(fixture:getShape():getPoints()))
			elseif fixture:getShape():type() == "EdgeShape" or fixture:getShape():type() == "ChainShape" then
				local points = { body:getWorldPoints(fixture:getShape():getPoints()) }
				for i = 1, #points, 2 do
					if i < #points - 2 then
						love.graphics.line(points[i], points[i + 1], points[i + 2], points[i + 3])
					end
				end
			elseif fixture:getShape():type() == "CircleShape" then
				local body_x, body_y = body:getPosition()
				local shape_x, shape_y = fixture:getShape():getPoint()
				local r = fixture:getShape():getRadius()
				love.graphics.circle("line", body_x + shape_x, body_y + shape_y, r, 360)
			end
		end
	end
	love.graphics.setColor(255, 255, 255, alpha)

	-- Joint debug
	love.graphics.setColor(222, 128, 64, alpha)
	local joints = self.box2d_world:getJoints()
	for _, joint in ipairs(joints) do
		local x1, y1, x2, y2 = joint:getAnchors()
		if x1 and y1 then
			love.graphics.circle("line", x1, y1, 4)
		end
		if x2 and y2 then
			love.graphics.circle("line", x2, y2, 4)
		end
	end
	love.graphics.setColor(255, 255, 255, alpha)

	-- Query debug
	love.graphics.setColor(64, 64, 222, alpha)
	for _, query_draw in ipairs(self.query_debug_draw) do
		query_draw.frames = query_draw.frames - 1
		if query_draw.type == "circle" then
			love.graphics.circle("line", query_draw.x, query_draw.y, query_draw.r)
		elseif query_draw.type == "rectangle" then
			love.graphics.rectangle("line", query_draw.x, query_draw.y, query_draw.w, query_draw.h)
		elseif query_draw.type == "line" then
			love.graphics.line(query_draw.x1, query_draw.y1, query_draw.x2, query_draw.y2)
		elseif query_draw.type == "polygon" then
			local triangles = love.math.triangulate(query_draw.vertices)
			for _, triangle in ipairs(triangles) do
				love.graphics.polygon("line", triangle)
			end
		end
	end
	for i = #self.query_debug_draw, 1, -1 do
		if self.query_debug_draw[i].frames <= 0 then
			table.remove(self.query_debug_draw, i)
		end
	end
	love.graphics.setColor(r, g, b, a)
end

function World:setQueryDebugDrawing(value)
	self.query_debug_drawing_enabled = value
end

function World:setExplicitCollisionEvents(value)
	self.explicit_collision_events = value
end

function World:addCollisionClass(collision_class_name, collision_class)
	if self.collision_classes[collision_class_name] then
		error("Collision class " .. collision_class_name .. " already exists.")
	end

	if self.explicit_collision_events then
		self.collision_classes[collision_class_name] = collision_class or {}
	else
		self.collision_classes[collision_class_name] = collision_class or {}
		self.collision_classes[collision_class_name].enter = {}
		self.collision_classes[collision_class_name].exit = {}
		self.collision_classes[collision_class_name].pre = {}
		self.collision_classes[collision_class_name].post = {}
		for c_class_name, _ in pairs(self.collision_classes) do
			table.insert(self.collision_classes[collision_class_name].enter, c_class_name)
			table.insert(self.collision_classes[collision_class_name].exit, c_class_name)
			table.insert(self.collision_classes[collision_class_name].pre, c_class_name)
			table.insert(self.collision_classes[collision_class_name].post, c_class_name)
		end
		for c_class_name, _ in pairs(self.collision_classes) do
			table.insert(self.collision_classes[c_class_name].enter, collision_class_name)
			table.insert(self.collision_classes[c_class_name].exit, collision_class_name)
			table.insert(self.collision_classes[c_class_name].pre, collision_class_name)
			table.insert(self.collision_classes[c_class_name].post, collision_class_name)
		end
	end

	self:collisionClassesSet()
end

function World:collisionClassesSet()
	self:generateCategoriesMasks()

	self:collisionClear()
	local collision_table = self:getCollisionCallbacksTable()
	for collision_class_name, collision_list in pairs(collision_table) do
		for _, collision_info in ipairs(collision_list) do
			if collision_info.type == "enter" then
				self:addCollisionEnter(collision_class_name, collision_info.other)
			end
			if collision_info.type == "exit" then
				self:addCollisionExit(collision_class_name, collision_info.other)
			end
			if collision_info.type == "pre" then
				self:addCollisionPre(collision_class_name, collision_info.other)
			end
			if collision_info.type == "post" then
				self:addCollisionPost(collision_class_name, collision_info.other)
			end
		end
	end

	self:collisionEventsClear()
end

function World:collisionClear()
	self.collisions = {}
	self.collisions.on_enter = {}
	self.collisions.on_enter.sensor = {}
	self.collisions.on_enter.non_sensor = {}
	self.collisions.on_exit = {}
	self.collisions.on_exit.sensor = {}
	self.collisions.on_exit.non_sensor = {}
	self.collisions.pre = {}
	self.collisions.pre.sensor = {}
	self.collisions.pre.non_sensor = {}
	self.collisions.post = {}
	self.collisions.post.sensor = {}
	self.collisions.post.non_sensor = {}
end

function World:collisionEventsClear()
	local bodies = self.box2d_world:getBodies()
	for _, body in ipairs(bodies) do
		local collider = body:getFixtures()[1]:getUserData()
		collider:collisionEventsClear()
	end
end

function World:addCollisionEnter(type1, type2)
	if not self:isCollisionBetweenSensors(type1, type2) then
		table.insert(self.collisions.on_enter.non_sensor, { type1 = type1, type2 = type2 })
	else
		table.insert(self.collisions.on_enter.sensor, { type1 = type1, type2 = type2 })
	end
end

function World:addCollisionExit(type1, type2)
	if not self:isCollisionBetweenSensors(type1, type2) then
		table.insert(self.collisions.on_exit.non_sensor, { type1 = type1, type2 = type2 })
	else
		table.insert(self.collisions.on_exit.sensor, { type1 = type1, type2 = type2 })
	end
end

function World:addCollisionPre(type1, type2)
	if not self:isCollisionBetweenSensors(type1, type2) then
		table.insert(self.collisions.pre.non_sensor, { type1 = type1, type2 = type2 })
	else
		table.insert(self.collisions.pre.sensor, { type1 = type1, type2 = type2 })
	end
end

function World:addCollisionPost(type1, type2)
	if not self:isCollisionBetweenSensors(type1, type2) then
		table.insert(self.collisions.post.non_sensor, { type1 = type1, type2 = type2 })
	else
		table.insert(self.collisions.post.sensor, { type1 = type1, type2 = type2 })
	end
end

function World:doesType1IgnoreType2(type1, type2)
	local collision_ignores = {}
	for collision_class_name, collision_class in pairs(self.collision_classes) do
		collision_ignores[collision_class_name] = collision_class.ignores or {}
	end
	local all = {}
	for collision_class_name, _ in pairs(collision_ignores) do
		table.insert(all, collision_class_name)
	end
	local ignored_types = {}
	for _, collision_class_type in ipairs(collision_ignores[type1]) do
		if collision_class_type == "All" then
			for _, collision_class_name in ipairs(all) do
				table.insert(ignored_types, collision_class_name)
			end
		else
			table.insert(ignored_types, collision_class_type)
		end
	end
	for key, _ in pairs(collision_ignores[type1]) do
		if key == "except" then
			for _, except_type in ipairs(collision_ignores[type1].except) do
				for i = #ignored_types, 1, -1 do
					if ignored_types[i] == except_type then
						table.remove(ignored_types, i)
					end
				end
			end
		end
	end
	for _, ignored_type in ipairs(ignored_types) do
		if ignored_type == type2 then
			return true
		end
	end
end

function World:isCollisionBetweenSensors(type1, type2)
	if not self.is_sensor_memo[type1] then
		self.is_sensor_memo[type1] = {}
	end
	if not self.is_sensor_memo[type1][type2] then
		self.is_sensor_memo[type1][type2] = (
			self:doesType1IgnoreType2(type1, type2) or self:doesType1IgnoreType2(type2, type1)
		)
	end
	if self.is_sensor_memo[type1][type2] then
		return true
	else
		return false
	end
end

-- https://love2d.org/forums/viewtopic.php?f=4&t=75441
function World:generateCategoriesMasks()
	local collision_ignores = {}
	for collision_class_name, collision_class in pairs(self.collision_classes) do
		collision_ignores[collision_class_name] = collision_class.ignores or {}
	end
	local incoming = {}
	local expanded = {}
	local all = {}
	for object_type, _ in pairs(collision_ignores) do
		incoming[object_type] = {}
		expanded[object_type] = {}
		table.insert(all, object_type)
	end
	for object_type, ignore_list in pairs(collision_ignores) do
		for key, ignored_type in pairs(ignore_list) do
			if ignored_type == "All" then
				for _, all_object_type in ipairs(all) do
					table.insert(incoming[all_object_type], object_type)
					table.insert(expanded[object_type], all_object_type)
				end
			elseif type(ignored_type) == "string" then
				if ignored_type ~= "All" then
					table.insert(incoming[ignored_type], object_type)
					table.insert(expanded[object_type], ignored_type)
				end
			end
			if key == "except" then
				for _, except_ignored_type in ipairs(ignored_type) do
					for i, v in ipairs(incoming[except_ignored_type]) do
						if v == object_type then
							table.remove(incoming[except_ignored_type], i)
							break
						end
					end
				end
				for _, except_ignored_type in ipairs(ignored_type) do
					for i, v in ipairs(expanded[object_type]) do
						if v == except_ignored_type then
							table.remove(expanded[object_type], i)
							break
						end
					end
				end
			end
		end
	end
	local edge_groups = {}
	for k, v in pairs(incoming) do
		table.sort(v, function(a, b)
			return string.lower(a) < string.lower(b)
		end)
	end
	local i = 0
	for k, v in pairs(incoming) do
		local str = ""
		for _, c in ipairs(v) do
			str = str .. c
		end
		if not edge_groups[str] then
			i = i + 1
			edge_groups[str] = { n = i }
		end
		table.insert(edge_groups[str], k)
	end
	local categories = {}
	for k, _ in pairs(collision_ignores) do
		categories[k] = {}
	end
	for k, v in pairs(edge_groups) do
		for i, c in ipairs(v) do
			categories[c] = v.n
		end
	end
	for k, v in pairs(expanded) do
		local category = { categories[k] }
		local current_masks = {}
		for _, c in ipairs(v) do
			table.insert(current_masks, categories[c])
		end
		self.masks[k] = { categories = category, masks = current_masks }
	end
end

function World:getCollisionCallbacksTable()
	local collision_table = {}
	for collision_class_name, collision_class in pairs(self.collision_classes) do
		collision_table[collision_class_name] = {}
		for _, v in ipairs(collision_class.enter or {}) do
			table.insert(collision_table[collision_class_name], { type = "enter", other = v })
		end
		for _, v in ipairs(collision_class.exit or {}) do
			table.insert(collision_table[collision_class_name], { type = "exit", other = v })
		end
		for _, v in ipairs(collision_class.pre or {}) do
			table.insert(collision_table[collision_class_name], { type = "pre", other = v })
		end
		for _, v in ipairs(collision_class.post or {}) do
			table.insert(collision_table[collision_class_name], { type = "post", other = v })
		end
	end
	return collision_table
end

local function collEnsure(collision_class_name1, a, collision_class_name2, b)
	if a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1 then
		return b, a
	else
		return a, b
	end
end

local function collIf(collision_class_name1, collision_class_name2, a, b)
	if
		(a.collision_class == collision_class_name1 and b.collision_class == collision_class_name2)
		or (a.collision_class == collision_class_name2 and b.collision_class == collision_class_name1)
	then
		return true
	else
		return false
	end
end

function World.collisionOnEnter(fixture_a, fixture_b, contact)
	local a, b = fixture_a:getUserData(), fixture_b:getUserData()

	if fixture_a:isSensor() and fixture_b:isSensor() then
		if a and b then
			for _, collision in ipairs(a.world.collisions.on_enter.sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					table.insert(
						a.collision_events[collision.type2],
						{ collision_type = "enter", collider_1 = a, collider_2 = b, contact = contact }
					)
					if collision.type1 == collision.type2 then
						table.insert(
							b.collision_events[collision.type1],
							{ collision_type = "enter", collider_1 = b, collider_2 = a, contact = contact }
						)
					end
				end
			end
		end
	elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
		if a and b then
			for _, collision in ipairs(a.world.collisions.on_enter.non_sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					table.insert(
						a.collision_events[collision.type2],
						{ collision_type = "enter", collider_1 = a, collider_2 = b, contact = contact }
					)
					if collision.type1 == collision.type2 then
						table.insert(
							b.collision_events[collision.type1],
							{ collision_type = "enter", collider_1 = b, collider_2 = a, contact = contact }
						)
					end
				end
			end
		end
	end
end

function World.collisionOnExit(fixture_a, fixture_b, contact)
	local a, b = fixture_a:getUserData(), fixture_b:getUserData()

	if fixture_a:isSensor() and fixture_b:isSensor() then
		if a and b then
			for _, collision in ipairs(a.world.collisions.on_exit.sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					table.insert(
						a.collision_events[collision.type2],
						{ collision_type = "exit", collider_1 = a, collider_2 = b, contact = contact }
					)
					if collision.type1 == collision.type2 then
						table.insert(
							b.collision_events[collision.type1],
							{ collision_type = "exit", collider_1 = b, collider_2 = a, contact = contact }
						)
					end
				end
			end
		end
	elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
		if a and b then
			for _, collision in ipairs(a.world.collisions.on_exit.non_sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					table.insert(
						a.collision_events[collision.type2],
						{ collision_type = "exit", collider_1 = a, collider_2 = b, contact = contact }
					)
					if collision.type1 == collision.type2 then
						table.insert(
							b.collision_events[collision.type1],
							{ collision_type = "exit", collider_1 = b, collider_2 = a, contact = contact }
						)
					end
				end
			end
		end
	end
end

function World.collisionPre(fixture_a, fixture_b, contact)
	local a, b = fixture_a:getUserData(), fixture_b:getUserData()

	if fixture_a:isSensor() and fixture_b:isSensor() then
		if a and b then
			for _, collision in ipairs(a.world.collisions.pre.sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					a:preSolve(b, contact)
					if collision.type1 == collision.type2 then
						b:preSolve(a, contact)
					end
				end
			end
		end
	elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
		if a and b then
			for _, collision in ipairs(a.world.collisions.pre.non_sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					a:preSolve(b, contact)
					if collision.type1 == collision.type2 then
						b:preSolve(a, contact)
					end
				end
			end
		end
	end
end

function World.collisionPost(fixture_a, fixture_b, contact, ni1, ti1, ni2, ti2)
	local a, b = fixture_a:getUserData(), fixture_b:getUserData()

	if fixture_a:isSensor() and fixture_b:isSensor() then
		if a and b then
			for _, collision in ipairs(a.world.collisions.post.sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					a:postSolve(b, contact, ni1, ti1, ni2, ti2)
					if collision.type1 == collision.type2 then
						b:postSolve(a, contact, ni1, ti1, ni2, ti2)
					end
				end
			end
		end
	elseif not (fixture_a:isSensor() or fixture_b:isSensor()) then
		if a and b then
			for _, collision in ipairs(a.world.collisions.post.non_sensor) do
				if collIf(collision.type1, collision.type2, a, b) then
					a, b = collEnsure(collision.type1, a, collision.type2, b)
					a:postSolve(b, contact, ni1, ti1, ni2, ti2)
					if collision.type1 == collision.type2 then
						b:postSolve(a, contact, ni1, ti1, ni2, ti2)
					end
				end
			end
		end
	end
end

function World:newCircleCollider(x, y, r, settings)
	return self.wf.Collider.new(self, "Circle", x, y, r, settings)
end

function World:newRectangleCollider(x, y, w, h, settings)
	return self.wf.Collider.new(self, "Rectangle", x, y, w, h, settings)
end

function World:newBSGRectangleCollider(x, y, w, h, corner_cut_size, settings)
	return self.wf.Collider.new(self, "BSGRectangle", x, y, w, h, corner_cut_size, settings)
end

function World:newPolygonCollider(vertices, settings)
	return self.wf.Collider.new(self, "Polygon", vertices, settings)
end

function World:newLineCollider(x1, y1, x2, y2, settings)
	return self.wf.Collider.new(self, "Line", x1, y1, x2, y2, settings)
end

function World:newChainCollider(vertices, loop, settings)
	return self.wf.Collider.new(self, "Chain", vertices, loop, settings)
end

-- Internal AABB box2d query used before going for more specific and precise computations.
function World:_queryBoundingBox(x1, y1, x2, y2)
	local colliders = {}
	local callback = function(fixture)
		if not fixture:isSensor() then
			table.insert(colliders, fixture:getUserData())
		end
		return true
	end
	self.box2d_world:queryBoundingBox(x1, y1, x2, y2, callback)
	return colliders
end

function World:collisionClassInCollisionClassesList(collision_class, collision_classes)
	if collision_classes[1] == "All" then
		local all_collision_classes = {}
		for class, _ in pairs(self.collision_classes) do
			table.insert(all_collision_classes, class)
		end
		if collision_classes.except then
			for _, except in ipairs(collision_classes.except) do
				for i, class in ipairs(all_collision_classes) do
					if class == except then
						table.remove(all_collision_classes, i)
						break
					end
				end
			end
		end
		for _, class in ipairs(all_collision_classes) do
			if class == collision_class then
				return true
			end
		end
	else
		for _, class in ipairs(collision_classes) do
			if class == collision_class then
				return true
			end
		end
	end
end

function World:queryCircleArea(x, y, radius, collision_class_names)
	if not collision_class_names then
		collision_class_names = { "All" }
	end
	if self.query_debug_drawing_enabled then
		table.insert(
			self.query_debug_draw,
			{ type = "circle", x = x, y = y, r = radius, frames = self.draw_query_for_n_frames }
		)
	end

	local colliders = self:_queryBoundingBox(x - radius, y - radius, x + radius, y + radius)
	local outs = {}
	for _, collider in ipairs(colliders) do
		if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
			for _, fixture in ipairs(collider.body:getFixtures()) do
				if
					self.wf.Math.polygon.getCircleIntersection(
						x,
						y,
						radius,
						{ collider.body:getWorldPoints(fixture:getShape():getPoints()) }
					)
				then
					table.insert(outs, collider)
					break
				end
			end
		end
	end
	return outs
end

function World:queryRectangleArea(x, y, w, h, collision_class_names)
	if not collision_class_names then
		collision_class_names = { "All" }
	end
	if self.query_debug_drawing_enabled then
		table.insert(
			self.query_debug_draw,
			{ type = "rectangle", x = x, y = y, w = w, h = h, frames = self.draw_query_for_n_frames }
		)
	end

	local colliders = self:_queryBoundingBox(x, y, x + w, y + h)
	local outs = {}
	for _, collider in ipairs(colliders) do
		if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
			for _, fixture in ipairs(collider.body:getFixtures()) do
				if
					self.wf.Math.polygon.isPolygonInside(
						{ x, y, x + w, y, x + w, y + h, x, y + h },
						{ collider.body:getWorldPoints(fixture:getShape():getPoints()) }
					)
				then
					table.insert(outs, collider)
					break
				end
			end
		end
	end
	return outs
end

function World:queryPolygonArea(vertices, collision_class_names)
	if not collision_class_names then
		collision_class_names = { "All" }
	end
	if self.query_debug_drawing_enabled then
		table.insert(
			self.query_debug_draw,
			{ type = "polygon", vertices = vertices, frames = self.draw_query_for_n_frames }
		)
	end

	local cx, cy = self.wf.Math.polygon.getCentroid(vertices)
	local d_max = 0
	for i = 1, #vertices, 2 do
		local d = self.wf.Math.line.getLength(cx, cy, vertices[i], vertices[i + 1])
		if d > d_max then
			d_max = d
		end
	end
	local colliders = self:_queryBoundingBox(cx - d_max, cy - d_max, cx + d_max, cy + d_max)
	local outs = {}
	for _, collider in ipairs(colliders) do
		if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
			for _, fixture in ipairs(collider.body:getFixtures()) do
				if
					self.wf.Math.polygon.isPolygonInside(
						vertices,
						{ collider.body:getWorldPoints(fixture:getShape():getPoints()) }
					)
				then
					table.insert(outs, collider)
					break
				end
			end
		end
	end
	return outs
end

function World:queryLine(x1, y1, x2, y2, collision_class_names)
	if not collision_class_names then
		collision_class_names = { "All" }
	end
	if self.query_debug_drawing_enabled then
		table.insert(
			self.query_debug_draw,
			{ type = "line", x1 = x1, y1 = y1, x2 = x2, y2 = y2, frames = self.draw_query_for_n_frames }
		)
	end

	local colliders = {}
	local callback = function(fixture, ...)
		if not fixture:isSensor() then
			table.insert(colliders, fixture:getUserData())
		end
		return 1
	end
	self.box2d_world:rayCast(x1, y1, x2, y2, callback)

	local outs = {}
	for _, collider in ipairs(colliders) do
		if self:collisionClassInCollisionClassesList(collider.collision_class, collision_class_names) then
			table.insert(outs, collider)
		end
	end
	return outs
end

function World:addJoint(joint_type, ...)
	local args = { ... }
	if args[1].body then
		args[1] = args[1].body
	end
	if type(args[2]) == "table" and args[2].body then
		args[2] = args[2].body
	end
	local joint = love.physics["new" .. joint_type](unpack(args))
	return joint
end

function World:removeJoint(joint)
	joint:destroy()
end

function World:destroy()
	local bodies = self.box2d_world:getBodies()
	for _, body in ipairs(bodies) do
		local collider = body:getFixtures()[1]:getUserData()
		collider:destroy()
	end
	local joints = self.box2d_world:getJoints()
	for _, joint in ipairs(joints) do
		joint:destroy()
	end
	self.box2d_world:destroy()
	self.box2d_world = nil
end

local Collider = {}
Collider.__index = Collider

local generator = love.math.newRandomGenerator(os.time())
local function UUID()
	local fn = function(x)
		local r = generator:random(16) - 1
		r = (x == "x") and (r + 1) or (r % 4) + 9
		return ("0123456789abcdef"):sub(r, r)
	end
	return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end

function Collider.new(world, collider_type, ...)
	local self = {}
	self.id = UUID()
	self.world = world
	self.type = collider_type
	self.object = nil

	self.shapes = {}
	self.fixtures = {}
	self.sensors = {}

	self.collision_events = {}
	self.collision_stay = {}
	self.enter_collision_data = {}
	self.exit_collision_data = {}
	self.stay_collision_data = {}

	local args = { ... }
	local shape, fixture
	if self.type == "Circle" then
		self.collision_class = (args[4] and args[4].collision_class) or "Default"
		self.body =
			love.physics.newBody(self.world.box2d_world, args[1], args[2], (args[4] and args[4].body_type) or "dynamic")
		shape = love.physics.newCircleShape(args[3])
	elseif self.type == "Rectangle" then
		self.collision_class = (args[5] and args[5].collision_class) or "Default"
		self.body = love.physics.newBody(
			self.world.box2d_world,
			args[1] + args[3] / 2,
			args[2] + args[4] / 2,
			(args[5] and args[5].body_type) or "dynamic"
		)
		shape = love.physics.newRectangleShape(args[3], args[4])
	elseif self.type == "BSGRectangle" then
		self.collision_class = (args[6] and args[6].collision_class) or "Default"
		self.body = love.physics.newBody(
			self.world.box2d_world,
			args[1] + args[3] / 2,
			args[2] + args[4] / 2,
			(args[6] and args[6].body_type) or "dynamic"
		)
		local w, h, s = args[3], args[4], args[5]
		shape = love.physics.newPolygonShape({
			-w / 2,
			-h / 2 + s,
			-w / 2 + s,
			-h / 2,
			w / 2 - s,
			-h / 2,
			w / 2,
			-h / 2 + s,
			w / 2,
			h / 2 - s,
			w / 2 - s,
			h / 2,
			-w / 2 + s,
			h / 2,
			-w / 2,
			h / 2 - s,
		})
	elseif self.type == "Polygon" then
		self.collision_class = (args[2] and args[2].collision_class) or "Default"
		self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[2] and args[2].body_type) or "dynamic")
		shape = love.physics.newPolygonShape(unpack(args[1]))
	elseif self.type == "Line" then
		self.collision_class = (args[5] and args[5].collision_class) or "Default"
		self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[5] and args[5].body_type) or "dynamic")
		shape = love.physics.newEdgeShape(args[1], args[2], args[3], args[4])
	elseif self.type == "Chain" then
		self.collision_class = (args[3] and args[3].collision_class) or "Default"
		self.body = love.physics.newBody(self.world.box2d_world, 0, 0, (args[3] and args[3].body_type) or "dynamic")
		shape = love.physics.newChainShape(args[1], unpack(args[2]))
	end

	-- Define collision classes and attach them to fixture and sensor
	fixture = love.physics.newFixture(self.body, shape)
	if self.world.masks[self.collision_class] then
		fixture:setCategory(unpack(self.world.masks[self.collision_class].categories))
		fixture:setMask(unpack(self.world.masks[self.collision_class].masks))
	end
	fixture:setUserData(self)
	local sensor = love.physics.newFixture(self.body, shape)
	sensor:setSensor(true)
	sensor:setUserData(self)

	self.shapes["main"] = shape
	self.fixtures["main"] = fixture
	self.sensors["main"] = sensor
	self.shape = shape
	self.fixture = fixture

	self.preSolve = function() end
	self.postSolve = function() end

	-- Points all body, fixture and shape functions to this wf.Collider object
	-- This means that the user can call collider:setLinearVelocity for instance without having to say collider.body:setLinearVelocity
	for k, v in pairs(self.body.__index) do
		if
			k ~= "__gc"
			and k ~= "__eq"
			and k ~= "__index"
			and k ~= "__tostring"
			and k ~= "destroy"
			and k ~= "type"
			and k ~= "typeOf"
		then
			self[k] = function(self, ...)
				return v(self.body, ...)
			end
		end
	end
	for k, v in pairs(self.fixture.__index) do
		if
			k ~= "__gc"
			and k ~= "__eq"
			and k ~= "__index"
			and k ~= "__tostring"
			and k ~= "destroy"
			and k ~= "type"
			and k ~= "typeOf"
		then
			self[k] = function(self, ...)
				return v(self.fixture, ...)
			end
		end
	end
	for k, v in pairs(self.shape.__index) do
		if
			k ~= "__gc"
			and k ~= "__eq"
			and k ~= "__index"
			and k ~= "__tostring"
			and k ~= "destroy"
			and k ~= "type"
			and k ~= "typeOf"
		then
			self[k] = function(self, ...)
				return v(self.shape, ...)
			end
		end
	end

	return setmetatable(self, Collider)
end

function Collider:collisionEventsClear()
	self.collision_events = {}
	for other, _ in pairs(self.world.collision_classes) do
		self.collision_events[other] = {}
	end
end

function Collider:setCollisionClass(collision_class_name)
	if not self.world.collision_classes[collision_class_name] then
		error("Collision class " .. collision_class_name .. " doesn't exist.")
	end
	self.collision_class = collision_class_name
	for _, fixture in pairs(self.fixtures) do
		if self.world.masks[collision_class_name] then
			fixture:setCategory(unpack(self.world.masks[collision_class_name].categories))
			fixture:setMask(unpack(self.world.masks[collision_class_name].masks))
		end
	end
end

function Collider:enter(other_collision_class_name)
	local events = self.collision_events[other_collision_class_name]
	if events and #events >= 1 then
		for _, e in ipairs(events) do
			if e.collision_type == "enter" then
				if not self.collision_stay[other_collision_class_name] then
					self.collision_stay[other_collision_class_name] = {}
				end
				table.insert(
					self.collision_stay[other_collision_class_name],
					{ collider = e.collider_2, contact = e.contact }
				)
				self.enter_collision_data[other_collision_class_name] = { collider = e.collider_2, contact = e.contact }
				return true
			end
		end
	end
end

function Collider:getEnterCollisionData(other_collision_class_name)
	return self.enter_collision_data[other_collision_class_name]
end

function Collider:exit(other_collision_class_name)
	local events = self.collision_events[other_collision_class_name]
	if events and #events >= 1 then
		for _, e in ipairs(events) do
			if e.collision_type == "exit" then
				if self.collision_stay[other_collision_class_name] then
					for i = #self.collision_stay[other_collision_class_name], 1, -1 do
						local collision_stay = self.collision_stay[other_collision_class_name][i]
						if collision_stay.collider.id == e.collider_2.id then
							table.remove(self.collision_stay[other_collision_class_name], i)
						end
					end
				end
				self.exit_collision_data[other_collision_class_name] = { collider = e.collider_2, contact = e.contact }
				return true
			end
		end
	end
end

function Collider:getExitCollisionData(other_collision_class_name)
	return self.exit_collision_data[other_collision_class_name]
end

function Collider:stay(other_collision_class_name)
	if self.collision_stay[other_collision_class_name] then
		if #self.collision_stay[other_collision_class_name] >= 1 then
			return true
		end
	end
end

function Collider:getStayCollisionData(other_collision_class_name)
	return self.collision_stay[other_collision_class_name]
end

function Collider:setPreSolve(callback)
	self.preSolve = callback
end

function Collider:setPostSolve(callback)
	self.postSolve = callback
end

function Collider:setObject(object)
	self.object = object
end

function Collider:getObject()
	return self.object
end

function Collider:addShape(shape_name, shape_type, ...)
	if self.shapes[shape_name] or self.fixtures[shape_name] then
		error("Shape/fixture " .. shape_name .. " already exists.")
	end
	local args = { ... }
	local shape = love.physics["new" .. shape_type](unpack(args))
	local fixture = love.physics.newFixture(self.body, shape)
	if self.world.masks[self.collision_class] then
		fixture:setCategory(unpack(self.world.masks[self.collision_class].categories))
		fixture:setMask(unpack(self.world.masks[self.collision_class].masks))
	end
	fixture:setUserData(self)
	local sensor = love.physics.newFixture(self.body, shape)
	sensor:setSensor(true)
	sensor:setUserData(self)

	self.shapes[shape_name] = shape
	self.fixtures[shape_name] = fixture
	self.sensors[shape_name] = sensor
end

function Collider:removeShape(shape_name)
	if not self.shapes[shape_name] then
		return
	end
	self.shapes[shape_name] = nil
	self.fixtures[shape_name]:setUserData(nil)
	self.fixtures[shape_name]:destroy()
	self.fixtures[shape_name] = nil
	self.sensors[shape_name]:setUserData(nil)
	self.sensors[shape_name]:destroy()
	self.sensors[shape_name] = nil
end

function Collider:destroy()
	self.collision_stay = nil
	self.enter_collision_data = nil
	self.exit_collision_data = nil
	self:collisionEventsClear()

	self:setObject(nil)
	for name, _ in pairs(self.fixtures) do
		self.shapes[name] = nil
		self.fixtures[name]:setUserData(nil)
		self.fixtures[name] = nil
		self.sensors[name]:setUserData(nil)
		self.sensors[name] = nil
	end
	self.body:destroy()
	self.body = nil
end

wf.World = World
wf.Collider = Collider

return wf
