--!native

local CollisionModule = {}

local sin = math.sin
local cos = math.cos
local sqrt = math.sqrt
local tan = math.tan
local floor = math.floor
local rad = math.rad

local max = math.max
local min = math.min
local huge = math.huge
local abs = math.abs
local sign = math.sign

local pi = math.pi

local function GetBoundingBox(points, offset)
	offset = offset or Vector3.zero
	
	local minX, minY, minZ = huge, huge, huge
	local maxX, maxY, maxZ = -huge, -huge, -huge

	for _, point in pairs(points) do
		local x, y, z = unpack(point)
		
		x = x + offset.X
		y = y + offset.Y
		z = z + offset.Z

		minX = min(minX, x)
		minY = min(minY, y)
		minZ = min(minZ, z)

		maxX = max(maxX, x)
		maxY = max(maxY, y)
		maxZ = max(maxZ, z)
	end

	return {
		Min = Vector3.new(minX, minY, minZ),
		Max = Vector3.new(maxX, maxY, maxZ)
	}
end

local function IsPointInsideModel(model, point)
	local bounds = GetBoundingBox(model.points)
	return point.X >= bounds.Min.X and point.X <= bounds.Max.X
		and point.Y >= bounds.Min.Y and point.Y <= bounds.Max.Y
		and point.Z >= bounds.Min.Z and point.Z <= bounds.Max.Z
end

local function DoesRayIntersectModel(ray, model)
	local origin = ray.Origin
	local direction = ray.Direction
	local intersectionPoint = nil

	-- Check if the ray's origin is inside the model
	if IsPointInsideModel(model, origin) then
		intersectionPoint = origin
		return true, intersectionPoint
	end

	-- Check for intersections with each model point
	local points = model.points
	for _, vertex in pairs(points) do
		local point = Vector3.new(vertex[1], vertex[2], vertex[3])
		local hitDistance = (point - origin):Dot(direction)
		if hitDistance >= 0 and IsPointInsideModel(model, origin + hitDistance * direction) then
			intersectionPoint = origin + hitDistance * direction
			return true, intersectionPoint
		end
	end

	return false, nil
end

local function CalculateMouseRay(mouseX, mouseY, Camera, viewportWidth, viewportHeight)
	local cameraCFrame = Camera:GetCFrame()
	local cameraInverseCFrame = cameraCFrame:Inverse()
	local mousePosition = Vector3.new(mouseX, mouseY, 0)

	local mouseRayDirection = (mousePosition - Vector3.new(viewportWidth / 2, viewportHeight / 2, 0)) / sqrt(viewportWidth ^ 2 + viewportHeight ^ 2) * 2

	local mouseRayWorldDirection = (cameraInverseCFrame * mouseRayDirection).unit
	return Ray.new(Camera.Position, mouseRayWorldDirection)
end

local function GetIntersectionPoint(ray, model)
	local origin = ray.Origin
	local direction = ray.Direction

	local closestDistance = huge
	local intersectionPoint = nil

	-- Check for intersections with each model point
	local points = model.points
	for _, vertex in pairs(points) do
		local point = Vector3.new(vertex[1], vertex[2], vertex[3])
		local hitDistance = (point - origin):Dot(direction)
		if hitDistance >= 0 and IsPointInsideModel(model, origin + hitDistance * direction) then
			-- Update the closest intersection point if this point is closer to the ray's origin
			if hitDistance < closestDistance then
				closestDistance = hitDistance
				intersectionPoint = origin + hitDistance * direction
			end
		end
	end

	return intersectionPoint
end

local function CalculateSurfaceNormal(model, hitPoint)
	local points = model.points
	local normalsSum = Vector3.zero
	
	local closestNormal = Vector3.zero
	local closestDistance = huge

	for _, triangle in pairs(model.triangles) do
		local vertex1 = Vector3.new(points[triangle[1]][1], points[triangle[1]][2], points[triangle[1]][3])
		local vertex2 = Vector3.new(points[triangle[2]][1], points[triangle[2]][2], points[triangle[2]][3])
		local vertex3 = Vector3.new(points[triangle[3]][1], points[triangle[3]][2], points[triangle[3]][3])

		local edge1 = vertex2 - vertex1
		local edge2 = vertex3 - vertex1
		local normal = edge1:Cross(edge2).unit

		local distanceToPlane = normal:Dot(vertex1 - hitPoint)
		if abs(distanceToPlane) < 0.001 then
			normalsSum = normalsSum + normal
		end
		
		if distanceToPlane < closestDistance then
			closestNormal = normal
			closestDistance = distanceToPlane
		end
		
	end

	if normalsSum.Magnitude == 0 then
		return closestNormal.unit
	else
		return normalsSum.unit
	end
end

function CollisionModule.Raycast(origin, direction, whitelist)
	local ray = Ray.new(origin, direction)

	-- Check for collisions with each model in the whitelist
	for _, model in pairs(whitelist) do
		local doesIntersect, intersectionPoint = DoesRayIntersectModel(ray, model)
		if doesIntersect then
			return model.Name, intersectionPoint
		end
	end

	return nil, nil
end

function CollisionModule.RaycastWithNormals(origin, direction, blacklist, registry)
	local ray = Ray.new(origin, direction)
	local hitModel, hitPoint, surfaceNormal = nil, nil, nil
	local closestDistance = huge

	for i, model in pairs(registry) do
		if table.find(blacklist, model.Name) == nil then
			local doesIntersect, intersectionPoint = DoesRayIntersectModel(ray, model)

			if doesIntersect then
				local hitDistance = (intersectionPoint - origin).Magnitude

				if hitDistance < closestDistance then
					hitModel = model
					hitPoint = intersectionPoint
					surfaceNormal = CalculateSurfaceNormal(model, hitPoint)
					closestDistance = hitDistance
				end
			end
		end
	end

	return hitModel, hitPoint, surfaceNormal
end

function CollisionModule.AABBCollision(model1, model2, offset)
	local bounds1 = GetBoundingBox(model1.points, offset)
	local bounds2 = GetBoundingBox(model2.points)

	-- Check for collision along each axis (X, Y, Z)
	local collisionX = bounds1.Min.X <= bounds2.Max.X and bounds1.Max.X >= bounds2.Min.X
	local collisionY = bounds1.Min.Y <= bounds2.Max.Y and bounds1.Max.Y >= bounds2.Min.Y
	local collisionZ = bounds1.Min.Z <= bounds2.Max.Z and bounds1.Max.Z >= bounds2.Min.Z

	-- If there's a collision along all three axes, there's a collision
	if collisionX and collisionY and collisionZ then
		-- Calculate the collision normal (direction of collision)
		local collisionNormal = Vector3.new(0, 0, 0)

		-- Determine which side was hit based on the minimum penetration depth along each axis
		local minPenetrationX = min(bounds1.Max.X - bounds2.Min.X, bounds2.Max.X - bounds1.Min.X)
		local minPenetrationY = min(bounds1.Max.Y - bounds2.Min.Y, bounds2.Max.Y - bounds1.Min.Y)
		local minPenetrationZ = min(bounds1.Max.Z - bounds2.Min.Z, bounds2.Max.Z - bounds1.Min.Z)

		-- Find the axis with the minimum penetration depth
		local minPenetration = min(minPenetrationX, minPenetrationY, minPenetrationZ)

		if minPenetration == minPenetrationX then
			collisionNormal = Vector3.new(sign(bounds1.Max.X - bounds2.Min.X), 0, 0)
		elseif minPenetration == minPenetrationY then
			collisionNormal = Vector3.new(0, sign(bounds1.Max.Y - bounds2.Min.Y), 0)
		else
			collisionNormal = Vector3.new(0, 0, sign(bounds1.Max.Z - bounds2.Min.Z))
		end

		return true, collisionNormal
	end

	return false, nil, 0
end

function CollisionModule.IsModelInside(model1, model2)
	local bounds1 = GetBoundingBox(model1.points)
	local bounds2 = GetBoundingBox(model2.points)

	return bounds1.Min.X >= bounds2.Min.X and bounds1.Min.Y >= bounds2.Min.Y and bounds1.Min.Z >= bounds2.Min.Z
		and bounds1.Max.X <= bounds2.Max.X and bounds1.Max.Y <= bounds2.Max.Y and bounds1.Max.Z <= bounds2.Max.Z
end



return CollisionModule
