-- Save next to a local variable for better performance
local next = next

local isServer = RunService:IsServer()

local ADDED_OBJECT = "ADDED_OBJECT_CHANGE_TYPE"
local REMOVED_OBJECT = "REMOVED_OBJECT_CHANGE_TYPE"

local DEBUG = false

local ContentsManager = {}
ContentsManager.__index = ContentsManager

function ContentsManager.new(grid, folder, masterContentsManager)
	local newContentsManager = {}
	newContentsManager.contents = {}
	newContentsManager.grid = grid
	newContentsManager.masterContentsManager = masterContentsManager
	newContentsManager.connections = {}
	newContentsManager.contentsListenerCount = 0
	newContentsManager.contentsListeners = {}
	setmetatable(newContentsManager, ContentsManager)

	if type(folder) == "string" then
		newContentsManager.folder = Instance.new("Folder")
		newContentsManager.folder.Name = folder
		newContentsManager.folder.Parent = game.Workspace
	else
		newContentsManager.folder = folder

		for _, child in ipairs(newContentsManager.folder:GetChildren()) do
			newContentsManager:_onChildAddedToContentsFolder(child)
		end
		newContentsManager.connections.objectAddedToContentsFolder =
			newContentsManager.folder.ChildAdded:Connect(function(childAdded)
				newContentsManager:_onChildAddedToContentsFolder(childAdded)
			end)
	end

	if masterContentsManager then
		masterContentsManager:addContentsListener(function(change)
			newContentsManager:_onMasterContentsChanged(change)
		end)
	end

	return newContentsManager
end

local function Vector3FromString(str)
	local vector3Values = {}
	for num in str:gmatch("[^,]+") do
		table.insert(vector3Values, tonumber(num))
	end

	return Vector3.new(vector3Values[1], vector3Values[2], vector3Values[3])
end

function ContentsManager:_onChildAddedToContentsFolder(child)
	local childName = child.Name
	local objectTypeName = string.sub(childName, 1, string.find(childName, "|") - 1)
	local objectType = self.grid.objectTypes[objectTypeName]
	if objectType then
		local _, _, _, cellName = objectType.fromString(childName)

		self:add(objectType, self:_allocateCell(Vector3FromString(cellName)).id, child, true)
	else
		warn("Child with improper name " .. child.Name .. " added to contents folder")
	end
end

function ContentsManager:addContentsListener(func)
	if not self.contentsListeners[func] then
		self.contentsListeners[func] = true
		self.contentsListenerCount = self.contentsListenerCount + 1
	end
end

function ContentsManager:removeContentsListener(func)
	if self.contentsListener[func] then
		self.contentsListener[func] = nil
		self.contentsListenerCount = self.contentsListenerCount - 1
	end
end

function ContentsManager:_onMasterContentsChanged(change)
	-- TODO: Remove conflicting temp objects and
	-- temp objects that are no longer connected to the rest of the structure

	if change.type == ADDED_OBJECT then
		self:remove(change.objectType, tostring(change.cellId))
	elseif change.type == REMOVED_OBJECT then
		self:remove(change.objectType, tostring(change.cellId), true, change.doNotDestroy)
	else
		error("Received master contents change with invalid type")
	end
end

function ContentsManager:canPlace(objectType, cellId)
	return self:_spaceAvailable(objectType, cellId) and self:_connected(objectType, cellId)
end

function ContentsManager:_spaceAvailable(objectType, cellId)
	local masterSpaceAvailable = true
	if self.masterContentsManager then
		masterSpaceAvailable = self.masterContentsManager:_spaceAvailable(objectType, cellId)
	end

	local spaceAvailable = not bit32.btest(objectType.occupancy, self:getCell(cellId).occupancy)

	return masterSpaceAvailable and spaceAvailable
end

function ContentsManager:getCell(cellId)
	local cell = self.contents[tostring(cellId)]
	if cell then
		return cell
	end

	return Cell.new(self.grid, cellId)
end

function ContentsManager:_connected(objectType, cellId)
	local cell = self:getCell(cellId)

	if self.masterContentsManager and self.masterContentsManager:_connected(objectType, cellId) then
		return true
	end

	local connections = bit32.band(objectType.connectivity, cell.connectivity)
	if BitOps.numOfSetBits(connections) >= 2 then
		-- Check that at least two connections are to the same adjacent grid object
		local setOfConnectedObjects = {}
		for i = 0, 23 do
			local key = tostring(i)
			if bit32.extract(objectType.connectivity, i) == 1 and cell.connectivityMap[key] ~= nil then
				for object in pairs(cell.connectivityMap[key]) do
					if setOfConnectedObjects[object] then
						return true
					else
						setOfConnectedObjects[object] = true
					end
				end
			end
		end
	end

	local touchingScenery = self:_getTouchingScenery(cellId, objectType)
	local touchingScenerySize = 0
	for _ in pairs(touchingScenery) do
		touchingScenerySize = touchingScenerySize + 1
	end

	if touchingScenerySize > 0 then
		return true
	end

	return false
end