-- TODO: Raycast from perimeter of objectType mesh instead of cell

function ContentsManager:_getTouchingScenery(cellId, objectType)
	local scenerySet = {}
	local cell = self:getCell(cellId)
	local rayDirection = Vector3.new(0, -self.grid.CELL_DIMENSIONS.Y, 0)
	local rayOrigins = {
		cell.center,
		cell.center + Vector3.new(self.grid.CELL_DIMENSIONS.X / 2, 0, 0),
		cell.center - Vector3.new(self.grid.CELL_DIMENSIONS.X / 2, 0, 0),
		cell.center + Vector3.new(0, 0, self.grid.CELL_DIMENSIONS.Z / 2),
		cell.center - Vector3.new(0, 0, self.grid.CELL_DIMENSIONS.Z / 2)
	}

	for _, rayOrigin in ipairs(rayOrigins) do
		local part = workspace:FindPartOnRayWithWhitelist(Ray.new(rayOrigin, rayDirection), Util.getTableKeys(SceneryManager.scenery), true)
		if part then
			scenerySet[part] = true
		end
	end

	return scenerySet
end

function ContentsManager:addForced(objectType, cell)
	return self:add(objectType, cell, nil, true)
end

function ContentsManager:add(objectType, cellId, instance, forced)
	if forced or self:canPlace(objectType, cellId) then
		if instance ~= nil then
			self:_addToContents(instance, objectType, cellId)
		else
			local gridObject = objectType:render(self, self:getCell(cellId))
			self:_addToContents(gridObject, objectType, cellId)

			-- TODO: Move this check into Grid/init.lua
			if isServer then
				local touchingObjects = self:getTouchingObjects(gridObject, objectType, self:getCell(cellId))
				weldTouchingObjects(gridObject, touchingObjects)
				gridObject.Anchored = false
			end
		end

		return true
	else
		return false
	end
end

function ContentsManager:_allocateCell(cellId)
	local cellName = tostring(cellId)
	self.contents[cellName] = self:getCell(cellId)
	return self.contents[cellName]
end

function ContentsManager:_deallocateCell(cellId)
	local cellName = tostring(cellId)
	self.contents[cellName] = nil
end

function ContentsManager:_allocateConnectivityMapForIndex(cellId, index)
	local cell = self:_allocateCell(cellId)
	local indexAsString = tostring(index)

	if cell.connectivityMap[indexAsString] == nil then
		cell.connectivityMap[indexAsString] = {}
	end

	return cell.connectivityMap[indexAsString]
end

function ContentsManager:_deallocateConnectivityMapForIndex(cellId, index)
	local cell = self:getCell(cellId)
	local indexAsString = tostring(index)

	cell.connectivityMap[indexAsString] = nil
end

function ContentsManager:_addToContents(object, objectType, cellId)
	local cell = self:_allocateCell(cellId)

	cell.contents.objects[objectType] = object

	-- Update occupancy for cell
	cell.occupancy = bit32.bor(objectType.occupancy, cell.occupancy)

	-- Update occupancyMap for cell
	for index = 0, 13 do
		if bit32.extract(objectType.occupancy, index) == 1 then
			cell.occupancyMap[tostring(index)] = object
		end
	end

	-- Update occupancy and contents for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		local adjacentCell = self:_allocateCell(cellId + face)
		adjacentCell.occupancy =
			BitOps.applyFaceOccupancy(adjacentCell.occupancy, -1 * face, objectType.occupancy, face)

		if bit32.btest(BitOps.OCCUPANCY_FACE_MASK[tostring(BitOps.normalizeVec3(face))], objectType.occupancy) then
			local simpleObjectType, objectTypeRotation, objectFaceRotation = objectType.fromString(objectType.name)
			local adjacentObjectTypeRotation = (objectTypeRotation + 180) % 360
			local adjacentObjectType = self.grid.objectTypes[simpleObjectType .. "$" .. tostring(adjacentObjectTypeRotation) .. "$" .. tostring(objectFaceRotation)]
			adjacentCell.contents.objects[adjacentObjectType] = object
		end
	end

	-- Update occupancyMap for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		local occupancyFaceIndex = BitOps.OCCUPANCY_INDEX_FOR_FACE[tostring(BitOps.normalizeVec3(face))]
		if bit32.extract(objectType.occupancy, occupancyFaceIndex) == 1 then
			-- TODO: Don't allocate cell unless you have to
			local adjacentCell = self:_allocateCell(cellId + face)
			local occupancyIndexForAdjacentFace = BitOps.OCCUPANCY_INDEX_FOR_FACE[tostring(BitOps.normalizeVec3(-1 * face))]
			adjacentCell.occupancyMap[tostring(occupancyIndexForAdjacentFace)] = object
		end
	end

	-- Update connectivity for cell
	cell.connectivity = bit32.bor(objectType.connectivity, cell.connectivity)

	-- Update connectivityMap for cell
	for index = 0, 23 do
		if bit32.extract(objectType.connectivity, index) == 1 then
			local setOfObjectsConnectedToPoint = self:_allocateConnectivityMapForIndex(cellId, index)
			setOfObjectsConnectedToPoint[object] = true
		end
	end

	-- Update connectivity for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		-- TODO: Don't allocate cell unless you have to
		local adjacentCell = self:_allocateCell(cellId + face)
		adjacentCell.connectivity =
			BitOps.applyFaceConnectivity(adjacentCell.connectivity, -1 * face, objectType.connectivity, face)
	end

	-- Update connectivityMap for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		for listIndex, bitmaskIndex in ipairs(BitOps.CONNECTIVITY_INDEXES_FOR_FACE[tostring(BitOps.normalizeVec3(face))]) do
			if bit32.extract(objectType.connectivity, bitmaskIndex) == 1 then
				local connectivityIndexesForAdjacentFace = BitOps.CONNECTIVITY_INDEXES_FOR_FACE[tostring(BitOps.normalizeVec3(-1 * face))]
				local adjacentBitmaskIndex = connectivityIndexesForAdjacentFace[listIndex]
				local setOfObjectsConnectedToAdjacentPoint = self:_allocateConnectivityMapForIndex(cellId + face, adjacentBitmaskIndex)
				setOfObjectsConnectedToAdjacentPoint[object] = true
			end
		end
	end

	-- Update connectivity and connectivityMap for diagonally adjacent cells
	for edgeAsString, connectivityIndexes in pairs(BitOps.CONNECTIVITY_INDEXES_FOR_EDGE) do
		local edge = Vector3FromString(edgeAsString)
		local connectivityIndexesSize = #connectivityIndexes
		for i, connectivityIndex in ipairs(connectivityIndexes) do
			if bit32.extract(objectType.connectivity, connectivityIndex) ~= 0 then
				-- TODO: Don't allocate cell unless you have to
				local diagonallyAdjacentCell = self:_allocateCell(cellId + edge)
				local diagonallyAdjacentEdge = BitOps.normalizeVec3(-1 * edge)
				local diagonallyAdjacentEdgeConnectivityIndexes = BitOps.CONNECTIVITY_INDEXES_FOR_EDGE[tostring(diagonallyAdjacentEdge)]

				-- Flip the order you traverse the diagonally adjacent edge's
				-- connectivity indexes if the edge is perpendicular to the Y axis
				local j = i
				if edge.Y ~= 0 then
					j = connectivityIndexesSize + 1 - j
				end

				diagonallyAdjacentCell.connectivity =
					bit32.replace(diagonallyAdjacentCell.connectivity, 1, diagonallyAdjacentEdgeConnectivityIndexes[j])

				local setOfObjectsConnectedToAdjacentPoint = self:_allocateConnectivityMapForIndex(
					diagonallyAdjacentCell.id,
					diagonallyAdjacentEdgeConnectivityIndexes[j]
				)
				setOfObjectsConnectedToAdjacentPoint[object] = true
			end
		end
	end

	if DEBUG then
		local system = "CLIENT - "
		if isServer then
			system = "SERVER - "
		end
		print(system .. "ContentsManager contents for folder " .. self.folder.Name .. " after adding " .. objectType.name .. " to " .. tostring(cellId))
		print(Util.debugPrint(self.contents))
	end

	self:broadcastContentsChange({
		type = ADDED_OBJECT,
		cellId = cellId,
		objectType = objectType,
		object = object,
	})
end

function ContentsManager:broadcastContentsChange(change)
	if self.contentsListenerCount > 0 then
		for callback in pairs(self.contentsListeners) do
			callback(change)
		end
	end
end

function ContentsManager:getTouchingObjects(object, objectType, cell)
	local result = {}

	-- Must be connected to the same object by at least two connectivity points
	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 touchingObject in pairs(cell.connectivityMap[key]) do
				if setOfConnectedObjects[touchingObject] then
					result[touchingObject] = true
				else
					setOfConnectedObjects[touchingObject] = true
				end
			end
		end
	end

	for sceneryObject in pairs(self:_getTouchingScenery(cell.id, objectType)) do
		result[sceneryObject] = true
	end

	return result
end

function ContentsManager:remove(objectType, cellName, suppressWarning, doNotDestroy)
	local cell = self:getCell(Vector3FromString(cellName))

	-- Get object
	local object = cell.contents.objects[objectType]
	if object == nil then
		if not suppressWarning then
			warn("Tried to remove " .. objectType.name .. " in cell " .. tostring(cell.id) .. " where none existed")
		end

		return
	end

	-- Update occupancy for cell
	cell.occupancy = bit32.band(bit32.bnot(objectType.occupancy), cell.occupancy)

	-- Update occupancyMap for cell
	for index = 0, 13 do
		if bit32.extract(objectType.occupancy, index) == 1 then
			cell.occupancyMap[tostring(index)] = nil
		end
	end

	-- Update occupancy for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		local adjacentCell = self:getCell(cell.id + face)
		adjacentCell.occupancy =
			BitOps.removeFaceOccupancy(adjacentCell.occupancy, -1 * face, objectType.occupancy, face)
	end

	-- Update occupancyMap for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		local occupancyFaceIndex = BitOps.OCCUPANCY_INDEX_FOR_FACE[tostring(BitOps.normalizeVec3(face))]
		if bit32.extract(objectType.occupancy, occupancyFaceIndex) == 1 then
			local adjacentCell = self:getCell(cell.id + face)
			local occupancyIndexForAdjacentFace = BitOps.OCCUPANCY_INDEX_FOR_FACE[tostring(BitOps.normalizeVec3(-1 * face))]
			adjacentCell.occupancyMap[tostring(occupancyIndexForAdjacentFace)] = nil

			local simpleObjectType, objectTypeRotation, objectFaceRotation = objectType.fromString(objectType.name)
			local adjacentObjectTypeRotation = (objectTypeRotation + 180) % 360
			local adjacentObjectType = self.grid.objectTypes[simpleObjectType .. "$" .. tostring(adjacentObjectTypeRotation) .. "$" .. tostring(objectFaceRotation)]
			adjacentCell.contents.objects[adjacentObjectType] = nil
		end
	end

	-- Update connectivity and connectivityMap for cell
	for index = 0, 23 do
		if bit32.extract(objectType.connectivity, index) == 1 then
			local setOfObjectsConnectedToPoint = self:_allocateConnectivityMapForIndex(cell.id, index)
			setOfObjectsConnectedToPoint[object] = nil
			if next(setOfObjectsConnectedToPoint) == nil then
				self:_deallocateConnectivityMapForIndex(cell.id, index)
				cell.connectivity = bit32.replace(cell.connectivity, 0, index)
			end
		end
	end

	-- Update connectivity and connectivityMap for orthogonally adjacent cells
	for _, face in pairs(BitOps.FACES) do
		for listIndex, bitmaskIndex in ipairs(BitOps.CONNECTIVITY_INDEXES_FOR_FACE[tostring(BitOps.normalizeVec3(face))]) do
			if bit32.extract(objectType.connectivity, bitmaskIndex) == 1 then
				local connectivityIndexesForAdjacentFace = BitOps.CONNECTIVITY_INDEXES_FOR_FACE[tostring(BitOps.normalizeVec3(-1 * face))]
				local adjacentBitmaskIndex = connectivityIndexesForAdjacentFace[listIndex]
				local setOfObjectsConnectedToAdjacentPoint = self:_allocateConnectivityMapForIndex(cell.id + face, adjacentBitmaskIndex)
				setOfObjectsConnectedToAdjacentPoint[object] = nil
				if next(setOfObjectsConnectedToAdjacentPoint) == nil then
					local adjacentCell = self:getCell(cell.id + face)
					self:_deallocateConnectivityMapForIndex(adjacentCell.id, adjacentBitmaskIndex)
					adjacentCell.connectivity = bit32.replace(adjacentCell.connectivity, 0, adjacentBitmaskIndex)
				end
			end
		end
	end

	-- Update connectivity and connectivityMap for diagonally adjacent cells
	for edgeAsString, connectivityIndexes in pairs(BitOps.CONNECTIVITY_INDEXES_FOR_EDGE) do
		local edge = Vector3FromString(edgeAsString)
		local connectivityIndexesSize = #connectivityIndexes
		for i, connectivityIndex in ipairs(connectivityIndexes) do
			if bit32.extract(objectType.connectivity, connectivityIndex) ~= 0 then
				local diagonallyAdjacentCell = self:getCell(cell.id + edge)
				local diagonallyAdjacentEdge = BitOps.normalizeVec3(-1 * edge)
				local diagonallyAdjacentEdgeConnectivityIndexes = BitOps.CONNECTIVITY_INDEXES_FOR_EDGE[tostring(diagonallyAdjacentEdge)]

				-- Flip the order you traverse the diagonally adjacent edge's
				-- connectivity indexes if the edge is perpendicular to the Y axis
				local j = i
				if edge.Y ~= 0 then
					j = connectivityIndexesSize + 1 - j
				end

				local setOfObjectsConnectedToAdjacentPoint = self:_allocateConnectivityMapForIndex(
					diagonallyAdjacentCell.id,
					diagonallyAdjacentEdgeConnectivityIndexes[j]
				)
				setOfObjectsConnectedToAdjacentPoint[object] = nil
				if next(setOfObjectsConnectedToAdjacentPoint) == nil then
					diagonallyAdjacentCell.connectivity =
						bit32.replace(diagonallyAdjacentCell.connectivity, 0, diagonallyAdjacentEdgeConnectivityIndexes[j])
				end
			end
		end
	end

	-- Remove object from cell contents
	cell.contents.objects[objectType] = nil

	-- Remove object from adjacent cell contents
	for _, face in pairs(BitOps.FACES) do
		local occupancyFaceIndex = BitOps.OCCUPANCY_INDEX_FOR_FACE[tostring(BitOps.normalizeVec3(face))]
		if bit32.extract(objectType.occupancy, occupancyFaceIndex) == 1 then
			local adjacentCell = self:getCell(cell.id + face)
			local simpleObjectType, objectTypeRotation, objectFaceRotation = objectType.fromString(objectType.name)
			local adjacentObjectTypeRotation = (objectTypeRotation + 180) % 360
			local adjacentObjectType = self.grid.objectTypes[simpleObjectType .. "$" .. tostring(adjacentObjectTypeRotation) .. "$" .. tostring(objectFaceRotation)]
			adjacentCell.contents.objects[adjacentObjectType] = nil
		end
	end

	-- Destroy object
	if not doNotDestroy then
		object:Destroy()
	end

	-- Deallocate all affected cells that are empty and disconnected
	local affectedCells = { cell }
	for _, face in pairs(BitOps.FACES) do
		table.insert(affectedCells, self:getCell(cell.id + face))
	end
	for edge in pairs(BitOps.CONNECTIVITY_INDEXES_FOR_EDGE) do
		table.insert(affectedCells, self:getCell(cell.id + Vector3FromString(edge)))
	end

	for _, affectedCell in ipairs(affectedCells) do
		if affectedCell.occupancy == 0 and affectedCell.connectivity == 0 then
			self:_deallocateCell(affectedCell.id)
		end
	end

	if DEBUG then
		local system = "CLIENT - "
		if isServer then
			system = "SERVER - "
		end
		print(system .. "ContentsManager contents for folder " .. self.folder.Name .. " after removing " .. objectType.name .. " to " .. cellName)
		print(Util.debugPrint(self.contents))
	end

	self:broadcastContentsChange({
		type = REMOVED_OBJECT,
		cellId = cell.id,
		objectType = objectType,
		doNotDestroy = doNotDestroy,
	})

	-- TODO: This should be moved into Grid/init.lua
	if isServer then
		RemoveGridObject:FireAllClients(objectType:toString(cell), doNotDestroy)
	end
end

function ContentsManager:removeByName(name, suppressWarning, doNotDestroy)
	local parsedName = {}
	for word in name:gmatch("[^|]+") do
		table.insert(parsedName, word)
	end

	local objectTypeName = parsedName[1]
	local cellName = parsedName[2]

	if self.grid.objectTypes[objectTypeName] then
		self:remove(self.grid.objectTypes[objectTypeName], cellName, suppressWarning, doNotDestroy)
	else
		warn("Could not find object type " .. objectTypeName)
	end
end

function ContentsManager:destroy()
	-- TODO
end

return ContentsManager
