-- Only called on the client
function Grid:_onAddGridObjectRejected(objectTypeName, cellId, rotation, face)
	self.tempContentsManager:removeByName(objectTypeName .. "|" .. tostring(cellId))
end

local function destroyAllWelds(part)
	local weld = part:FindFirstChild("WeldConstraint")
	while weld do
		weld:Destroy()
		weld = part:FindFirstChild("WeldConstraint")
	end
end

function Grid:_onDestroyObject(part)
	if part:IsDescendantOf(self.contentsManager.folder) then
		local connectedParts = part:GetConnectedParts()
		self.contentsManager:removeByName(part.Name)

		local removedPartSet = { part }
		for _, connectedPart in ipairs(connectedParts) do
			if connectedPart and removedPartSet[connectedPart] == nil and (not connectedPart:IsGrounded()) then
				local recursivelyConnectedParts = connectedPart:GetConnectedParts(true)
				if not self.physicallySimulateDestruction then
					for _, recursivelyConnectedPart in ipairs(recursivelyConnectedParts) do
						if not SceneryManager.isScenery(recursivelyConnectedPart) then
							self.contentsManager:removeByName(recursivelyConnectedPart.Name)
						end
					end

					if not SceneryManager.isScenery(connectedPart) then
						self.contentsManager:removeByName(connectedPart.Name)
					end
				else
					local hasHitGround = false
					self:_performFadeOutAnimation(recursivelyConnectedParts)
					for _, recursivelyConnectedPart in ipairs(recursivelyConnectedParts) do
						if removedPartSet[recursivelyConnectedPart] == nil and not SceneryManager.isScenery(recursivelyConnectedPart) then
							self.contentsManager:removeByName(recursivelyConnectedPart.Name, nil, true)
							PhysicsService:SetPartCollisionGroup(recursivelyConnectedPart, "Default")
							removedPartSet[recursivelyConnectedPart] = true
							local connections = {}
							table.insert(connections, recursivelyConnectedPart.Touched:Connect(function(otherPart)
								if SceneryManager.isScenery(otherPart) and hasHitGround == false then
									hasHitGround = true
									for _, partToDestroy in ipairs(recursivelyConnectedParts) do
										destroyAllWelds(partToDestroy)
									end
									for _, connection in ipairs(connections) do
										connection:Disconnect()
									end
									connections = {}
								end
							end))
						end
					end
				end
			end
		end
	end
end

function Grid:_performFadeOutAnimation(recursivelyConnectedParts)
	for _, partToFade in ipairs(recursivelyConnectedParts) do
		CollectionService:AddTag(partToFade, "WeaponsSystemIgnore")
		local fadeOutTween = TweenService:Create(
			partToFade,
			TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 5),
			{ Transparency = 1 }
		)
		fadeOutTween.Completed:Connect(function()
			partToFade:Destroy()
		end)
		fadeOutTween:Play()
	end
end

function Grid:setPlacementCFrame(cframe, simpleObjectType, rotation, faceRotation)
	if cframe == nil or cframe.Position == nil or cframe.LookVector == nil or simpleObjectType == nil then
		self:_destroyPreview()
		return
	end

	local ignoreList = {
		Players,
		game.Workspace.CurrentCamera,
	}
	if self.preview and self.preview.part then
		table.insert(ignoreList, self.preview.part)
	end

	local _, hitPoint = penetrateCast(Ray.new(cframe.Position, cframe.LookVector.Unit * 100 * Grid.CELL_DIMENSIONS.Magnitude),
		ignoreList
	)
	local rayLength = math.min((hitPoint - cframe.Position).Magnitude, 100 * Grid.CELL_DIMENSIONS.Magnitude)

	self:_updatePreview(cframe, rayLength, simpleObjectType, rotation, faceRotation)
end

function Grid:placePreview()
	if self.preview and self.preview.objectType ~= nil and self.preview.cell ~= nil and self.preview.rotation ~= nil then
		local placed = self.tempContentsManager:add(self.preview.objectType, self.preview.cell.id)
		if placed then
			AddGridObject:FireServer(self.preview.objectType.name, self.preview.cell.id)
		end

		self:_destroyPreview()
	end
end

function Grid:_destroyPreview()
	if self.tempContentsManager ~= nil and self.preview ~= nil then
		if self.preview.part then
			self.preview.part.Transparency = 1
		end

		self.preview.cell = nil
		self.preview.rotation = nil
		self.preview.face = nil
	end
end

function Grid:renderPreview(objectType, cell)
	objectType:render(self.tempContentsManager, cell, true)
end

function Grid:canPlace(objectType, cell)
	return self.tempContentsManager:canPlace(objectType, cell.id)
end

local function getPrimaryAxisAndDirection(cframe)
	local lookUnitVector = cframe.LookVector.Unit
	local lookVectorMaxComponent = math.max(math.abs(lookUnitVector.X), math.abs(lookUnitVector.Y), math.abs(lookUnitVector.Z))

	if math.abs(lookUnitVector.X) == lookVectorMaxComponent then
		return Enum.Axis.X, math.sign(lookUnitVector.X)
	elseif math.abs(lookUnitVector.Z) == lookVectorMaxComponent then
		return Enum.Axis.Z, math.sign(lookUnitVector.Z)
	else
		return Enum.Axis.Y, math.sign(lookUnitVector.Y)
	end
end

local function cellWithinRange(originCell, targetCell)
	local cellDist = targetCell.id - originCell.id
	return math.abs(cellDist.X) <= 1 and math.abs(cellDist.Y) <= 1 and math.abs(cellDist.Z) <= 1
end

function Grid:_getCandidateCells(originCFrame, targetCFrame)
	local originCell = Cell.newFromWorldVector(self, originCFrame.Position)
	local targetCell = Cell.newFromWorldVector(self, targetCFrame.Position)
	local primaryLookAxis, axisDirection = getPrimaryAxisAndDirection(targetCFrame)

	local candidateCells = {}

	local axes = {
		Enum.Axis.X,
		Enum.Axis.Y,
		Enum.Axis.Z,
	}

	for _, axis in ipairs(axes) do
		if axis ~= primaryLookAxis then
			local axisAsVec3 = Vector3.FromAxis(axis)
			if axis ~= Enum.Axis.Y then
				if axisAsVec3:Dot(targetCFrame.LookVector.Unit) > 0.5 then
					local candidateCell = Cell.new(self, targetCell.id + axisAsVec3)
					if cellWithinRange(originCell, candidateCell) then
						table.insert(candidateCells, Cell.new(self, targetCell.id + axisAsVec3))
					end
				elseif (-1 * axisAsVec3):Dot(targetCFrame.LookVector.Unit) > 0.5 then
					local candidateCell = Cell.new(self, targetCell.id + -1 * axisAsVec3)
					if cellWithinRange(originCell, candidateCell) then
						table.insert(candidateCells, candidateCell)
					end
				end
			else
				local candidateCell = Cell.new(self, targetCell.id + axisAsVec3)
				if cellWithinRange(originCell, candidateCell) then
					table.insert(candidateCells, Cell.new(self, targetCell.id + axisAsVec3))
				end

				candidateCell = Cell.new(self, targetCell.id + -1 * axisAsVec3)
				if cellWithinRange(originCell, candidateCell) then
					table.insert(candidateCells, candidateCell)
				end
			end
		end
	end

	if targetCell.id ~= originCell.id then
		table.insert(candidateCells, targetCell)
	end

	-- TODO: Try to get the right feel without conditional logic on the Y angle
	local lookVector = targetCFrame.LookVector.Unit
	local lookVectorYAngle = math.deg(math.atan(lookVector.Y / math.sqrt(lookVector.X * lookVector.X + lookVector.Z * lookVector.Z)))
	if math.abs(lookVectorYAngle) > 65 then
		if targetCell.id ~= originCell.id then
			local candidateCell = Cell.new(self, targetCell.id + Vector3.FromAxis(primaryLookAxis) * -1 * axisDirection)
			if cellWithinRange(originCell, candidateCell) then
				table.insert(candidateCells, candidateCell)
			end
		else
			table.insert(candidateCells, targetCell)
		end
	end

	table.sort(candidateCells, function(cell1, cell2)
		return (originCFrame.Position - cell1.center).Magnitude < (originCFrame.Position - cell2.center).Magnitude
	end)

	return candidateCells
end

function Grid:_updatePreview(cframe, maxDist, simpleObjectType, rotation, faceRotation)
	local candidateCells = self:_getCandidateCells(cframe, CFrame.new(
		cframe.Position + math.min(Grid.CELL_DIMENSIONS.X, maxDist) * cframe.LookVector.Unit,
		cframe.Position + 2 * (math.min(Grid.CELL_DIMENSIONS.X, maxDist) * cframe.LookVector.Unit)
	))

	local lookRotation
	if math.sign(cframe.LookVector.Z) > 0 and math.abs(cframe.LookVector.Z) >= math.abs(cframe.LookVector.X) then
		lookRotation = 0
	elseif math.sign(cframe.LookVector.X) > 0 and math.abs(cframe.LookVector.Z) <= math.abs(cframe.LookVector.X) then
		lookRotation = 90
	elseif math.sign(cframe.LookVector.Z) < 0 and math.abs(cframe.LookVector.Z) >= math.abs(cframe.LookVector.X) then
		lookRotation = 180
	elseif math.sign(cframe.LookVector.X) < 0 and math.abs(cframe.LookVector.Z) <= math.abs(cframe.LookVector.X) then
		lookRotation = 270
	end

	if BitOps.getVerticalFaceFromOccupancy(ObjectTypeConfigurations[simpleObjectType].OCCUPANCY) then
		rotation = 0
	end

	local actualRotation = (lookRotation + rotation) % 360

	local objectTypeName = simpleObjectType .. "$" .. actualRotation .. "$" .. faceRotation
	local objectType = self.objectTypes[objectTypeName]
	if objectType == nil then
		error("Trying to place preview of unknown object type " .. objectTypeName)
	end

	for _, candidateCell in ipairs(candidateCells) do
		if self:canPlace(objectType, candidateCell) then
			self:renderPreview(objectType, candidateCell)
			return
		end
	end

	self:_destroyPreview()
end

function Grid:destroy()
	self:_destroyPreview()

	if self.contentsFolder then
		self.contentsFolder:Destroy()
		self.contentsFolder = nil
	end

	if self.tempContentsFolder then
		self.tempContentsFolder:Destroy()
		self.tempContentsFolder = nil
	end

	for _, connection in pairs(self.connections) do
		connection:Disconnect()
	end

	self.connections = nil
end

return Grid