-- Written by Kip Turner, Copyright ROBLOX 2015

local CoreGui = Game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")

local Utility = require(Modules:FindFirstChild('Utility'))
local GlobalSettings = require(Modules:FindFirstChild('GlobalSettings'))

local GuiService = game:GetService('GuiService')

local DEFAULT_WINDOW_SIZE = UDim2.new(1,0,1,0)


local function ScrollingGrid()

	local this = {}

	this.Enum =
	{
		ScrollDirection = {["Vertical"] = 1; ["Horizontal"] = 2;};
		StartCorner = {["UpperLeft"] = 1; ["UpperRight"] = 2; ["BottomLeft"] = 3; ["BottomRight"] = 4;};
		--ChildAlignment = {["UpperLeft"] = 1; ["UpperRight"] = 2; ["BottomLeft"] = 3; ["BottomRight"] = 4;};
	}


	this.GridItems = {}
	this.ItemSet = {}

	this.ScrollDirection = this.Enum.ScrollDirection.Vertical

	this.StartCorner = this.Enum.StartCorner.UpperLeft

	this.FixedRowColumnCount = nil

	--this.ChildAlignment = nil

	this.CellSize = Vector2.new(100,100)
	this.Padding = Vector2.new(0,0)
	this.Spacing = Vector2.new(0,0)




	function this:GetPadding()
		return self.Padding
	end

	function this:SetPadding(newPadding)
		if newPadding ~= self.Padding then
			self.Padding = newPadding
			self:RecalcLayout()
		end
	end

	function this:GetSpacing()
		return self.Spacing
	end

	function this:SetSpacing(newSpacing)
		if newSpacing ~= self.Spacing then
			self.Spacing = newSpacing
			self:RecalcLayout()
		end
	end

	function this:GetCellSize()
		return self.CellSize
	end

	function this:SetCellSize(cellSize)
		if cellSize ~= self.CellSize then
			self.CellSize = cellSize
			self:RecalcLayout()
		end
	end

	function this:GetScrollDirection()
		return self.ScrollDirection
	end

	function this:SetScrollDirection(newDirection)
		if newDirection ~= self.ScrollDirection then
			self.ScrollDirection = newDirection
			self:RecalcLayout()
		end
	end

	function this:GetStartCorner()
		return self.StartCorner
	end

	function this:SetStartCorner(newStartCorner)
		if newStartCorner ~= self.StartCorner then
			self.StartCorner = newStartCorner
			self:RecalcLayout()
		end
	end

	function this:GetRowColumnConstraint()
		return self.FixedRowColumnCount
	end

	function this:SetRowColumnConstraint(fixedRowColumnCount)
		if fixedRowColumnCount < 1 then
			fixedRowColumnCount = nil
		end
		if fixedRowColumnCount ~= self.FixedRowColumnCount then
			self.FixedRowColumnCount = fixedRowColumnCount
			self:RecalcLayout()
		end
	end


	function this:GetClipping()
		return self.Container.ClipsDescendants
	end

	function this:SetClipping(clippingEnabled)
		self.Container.ClipsDescendants = clippingEnabled;
	end

	function this:GetVisible()
		return self.Container.Visible
	end

	function this:SetVisible(isVisible)
		self.Container.Visible = isVisible;
	end

	function this:GetSize()
		return self.Container.Size
	end

	function this:SetSize(size)
		self.Container.Size = size
	end

	function this:GetPosition()
		return self.Container.Position
	end

	function this:SetPosition(position)
		self.Container.Position = position
	end

	function this:GetParent()
		return self.Container.Parent
	end

	function this:SetParent(parent)
		self.Container.Parent = parent
	end

	function this:GetGuiObject()
		return self.Container
	end

	-- Default selection handles the case of removing the last item in the grid while it is selected
	-- Set to nil if do not want a default selection
	function this:SetDefaultSelection(selectionObject)
		self.DefaultSelection = selectionObject
	end

	function this:ResetDefaultSelection()
		self.DefaultSelection = self.Container
	end

	----


	function this:ContainsItem(gridItem)
		return self.ItemSet[gridItem] ~= nil
	end

	function this:SortItems(sortFunc)
		table.sort(self.GridItems, sortFunc)
		self:RecalcLayout()
	end

	function this:AddItem(gridItem)
		if not self:ContainsItem(gridItem) then
			table.insert(self.GridItems, gridItem)
			self.ItemSet[gridItem] = true
			gridItem.Parent = self.Container
			if GuiService.SelectedCoreObject == self.DefaultSelection then
				GuiService.SelectedCoreObject = gridItem
			end
			self:RecalcLayout()
		end
	end

	function this:RemoveItem(gridItem)
		if self:ContainsItem(gridItem) then
			for i, otherItem in pairs(self.GridItems) do
				if otherItem == gridItem then
					table.remove(self.GridItems, i)
					-- Assign a new selection
					if GuiService.SelectedCoreObject == gridItem then
						GuiService.SelectedCoreObject = self.GridItems[i] or self.GridItems[i-1] or self.GridItems[1] or self.DefaultSelection
					end
					-- Clean-up
					self.ItemSet[gridItem] = nil
					gridItem.Parent = nil
					self:RecalcLayout()
					return
				end
			end
		end
	end

	function this:RemoveAllItems()
		local wasSelected = false
		do
			local currentSelection = GuiService.SelectedCoreObject
			while currentSelection ~= nil and wasSelected == false do
	  			wasSelected = wasSelected or self:ContainsItem(currentSelection)
	  			currentSelection = currentSelection.Parent
	  		end
		end
		for i = #self.GridItems, 1, -1 do
			local removed = table.remove(self.GridItems, i)
			self.ItemSet[removed] = nil
			removed.Parent = nil
		end

		if wasSelected then
			GuiService.SelectedCoreObject = self.Container
		end

		self:RecalcLayout()
		self.Container.CanvasPosition = Vector2.new(0, 0)
	end

	function this:Get2DGridIndex(index)
		-- 0 base index
		local zerobasedIndex = index - 1
		local rows, columns = self:GetNumRowsColumns()
		local row, column;

		-- TODO: implement StartCorner here
		if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
			row = math.floor(zerobasedIndex / columns)
			column = zerobasedIndex % columns
		else
			column = math.floor(zerobasedIndex / rows)
			row = zerobasedIndex % rows
		end

		return row, column
	end

	function this:GetNumRowsColumns()
		local rows, columns = 0, 0

		local windowSize = self.Container.AbsoluteWindowSize
		local padding = self:GetPadding()
		local cellSize = self:GetCellSize()
		local cellSpacing = self:GetSpacing()
		local adjustedWindowSize = Utility.ClampVector2(Vector2.new(0, 0), windowSize - padding, windowSize - padding)
		local absoluteCellSize = Utility.ClampVector2(Vector2.new(1,1), cellSize + cellSpacing, cellSize + cellSpacing)
		local windowSizeCalc = (adjustedWindowSize + cellSpacing) / absoluteCellSize

		if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
			columns = math.max(1, self:GetRowColumnConstraint() or math.floor(windowSizeCalc.x))
			rows = math.ceil(math.max(1, #self.GridItems) / columns)
		else
			rows = math.max(1, self:GetRowColumnConstraint() or math.floor(windowSizeCalc.y))
			columns = math.ceil(math.max(1, #self.GridItems) / rows)
		end

		return rows, columns
	end

	function this:GetGridPosition(row, column, gridItemSize)
		local cellSize = self:GetCellSize()
		local spacing = self:GetSpacing()
		local padding = self:GetPadding()
		return UDim2.new(0, padding.X + column * cellSize.X + column * spacing.X,
						 0, padding.Y + row * cellSize.Y + row * spacing.Y)
	end

	function this:GetGridItemSize()
		return self.CellSize
		--[[
		if self.CellSize then
			return self.CellSize
		end
		return UDim2.new(0, (self.Container.AbsoluteSize.X - ((self.Columns + 1) * self.CellPadding.X)) / self.Columns,
			             0, (self.Container.AbsoluteSize.Y - ((self.Rows + 1) * self.CellPadding.Y)) / self.Rows)
		--]]
		-- if self.ScrollDirection == EnumScrollDirection.Vertical then
		-- 	return UDim2.new(0,(self.Container.AbsoluteSize.X - ((self.Columns + 1) * self.CellPadding.X)) / self.Columns, 0, self.Container.AbsoluteSize.Y / self.Rows)
		-- else
		-- 	return UDim2.new(0,self.Container.AbsoluteSize.Y / self.Rows, 0, (self.Container.AbsoluteSize.Y - ((self.Rows + 1) * self.CellPadding.Y)) / self.Rows)
		-- end
	end

	function this:GetCanvasPositionForOffscreenItem(selectedObject)
		if self.Container and selectedObject and self:ContainsItem(selectedObject) then
			if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
				if selectedObject.AbsolutePosition.Y < self.Container.AbsolutePosition.Y then
					return Utility.ClampCanvasPosition(self.Container, Vector2.new(0, selectedObject.Position.Y.Offset)) -- - selectedObject.AbsoluteSize.Y/2))
				elseif selectedObject.AbsolutePosition.Y + selectedObject.AbsoluteSize.Y > self.Container.AbsolutePosition.Y + self.Container.AbsoluteWindowSize.Y then
					return Utility.ClampCanvasPosition(self.Container, Vector2.new(0, -(self.Container.AbsoluteWindowSize.Y - selectedObject.Position.Y.Offset - selectedObject.AbsoluteSize.Y)  )) --+ selectedObject.AbsoluteSize.Y/2))
				end
			else -- Horizontal
				if selectedObject.AbsolutePosition.X < self.Container.AbsolutePosition.X then
					return Utility.ClampCanvasPosition(self.Container, Vector2.new(selectedObject.Position.X.Offset - selectedObject.AbsoluteSize.X/2, 0))
				elseif selectedObject.AbsolutePosition.X + selectedObject.AbsoluteSize.X > self.Container.AbsolutePosition.X + self.Container.AbsoluteWindowSize.X then
					return Utility.ClampCanvasPosition(self.Container, Vector2.new(-(self.Container.AbsoluteWindowSize.X - selectedObject.Position.X.Offset - selectedObject.AbsoluteSize.X) + selectedObject.AbsoluteSize.X/2, 0))
				end
			end
		end
	end

	function this:RecalcLayout()
		local padding = self:GetPadding()
		local cellSpacing = self:GetSpacing()
		local gridItemSize = self:GetGridItemSize()
		local rows, columns = self:GetNumRowsColumns()

		if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
			self.Container.CanvasSize = UDim2.new(self.Container.Size.X.Scale, self.Container.Size.X.Offset, 0, padding.Y * 2 + rows * gridItemSize.Y + (math.max(0, rows - 1)) * cellSpacing.Y)
		else
			self.Container.CanvasSize = UDim2.new(0, padding.X * 2 + columns * gridItemSize.X + (math.max(0, columns - 1)) * cellSpacing.X, self.Container.Size.Y.Scale, self.Container.Size.Y.Offset)
		end

		local grid2DtoIndex = {}
		for i = 1, #self.GridItems do
			local row, column = self:Get2DGridIndex(i)
			local gridItem = self.GridItems[i]

			gridItem.Size = UDim2.new(0, gridItemSize.X, 0, gridItemSize.Y)
			gridItem.Position = self:GetGridPosition(row, column, gridItemSize)

			grid2DtoIndex[row] = grid2DtoIndex[row] or {}
			grid2DtoIndex[row][column] = gridItem
		end

		for rowNum, row in pairs(grid2DtoIndex) do
			for columnNum, column in pairs(row) do
				local gridItem = grid2DtoIndex[rowNum][columnNum]
				if gridItem then
					if self.ScrollDirection == self.Enum.ScrollDirection.Vertical then
						gridItem.NextSelectionUp = grid2DtoIndex[rowNum - 1] and grid2DtoIndex[rowNum - 1][columnNum] or nil
						gridItem.NextSelectionDown = grid2DtoIndex[rowNum + 1] and grid2DtoIndex[rowNum + 1][columnNum] or nil
						if gridItem.NextSelectionDown == nil and grid2DtoIndex[rowNum + 1] ~= nil then
							gridItem.NextSelectionDown = self.GridItems[#self.GridItems]
						end
						gridItem.NextSelectionLeft = nil
						gridItem.NextSelectionRight = nil
					else
						gridItem.NextSelectionLeft = grid2DtoIndex[rowNum] and grid2DtoIndex[rowNum][columnNum - 1] or nil
						gridItem.NextSelectionRight = grid2DtoIndex[rowNum] and grid2DtoIndex[rowNum][columnNum + 1] or nil
						if gridItem.NextSelectionRight == nil and grid2DtoIndex[0] and grid2DtoIndex[0][columnNum + 1] then
								gridItem.NextSelectionRight = self.GridItems[#self.GridItems]
						end
						gridItem.NextSelectionUp = nil
						gridItem.NextSelectionDown = nil
					end
				end
			end
		end
	end


	function this:Destroy()
		if self.Container then
			self.Container:Destroy()
		end
	end

	do
		local container = Utility.Create'ScrollingFrame'
		{
			Size = DEFAULT_WINDOW_SIZE;
			Name = "Container";
			BackgroundTransparency = 1;
			ClipsDescendants = true;
			ScrollingEnabled = false;
			ScrollBarThickness = 0;
			Selectable = false;
		}
		this.Container = container
		this.DefaultSelection = this.Container

		this.Container.Changed:connect(function(prop)
			if prop == 'AbsoluteSize' then
				this:RecalcLayout()
			end
		end)

		this:RecalcLayout()


		function this:FindAncestorGridItem(object)
			if object ~= nil then
				if self:ContainsItem(object) then
					return object
				end
				return self:FindAncestorGridItem(object.Parent)
			end
		end

		local currentTween = nil
		local lastSelectedObject = nil
		GuiService.Changed:connect(function(prop)
			if prop == 'SelectedCoreObject' then
				local selectedObject = this:FindAncestorGridItem(GuiService.SelectedCoreObject)
				if selectedObject and this:ContainsItem(selectedObject) then
					-- print(selectedObject.NextSelectionUp, selectedObject.NextSelectionDown, selectedObject.NextSelectionLeft, selectedObject.NextSelectionRight)

					local upDirection = (this.ScrollDirection == this.Enum.ScrollDirection.Vertical) and 'NextSelectionUp' or 'NextSelectionLeft'
					local downDirection = (this.ScrollDirection == this.Enum.ScrollDirection.Vertical) and 'NextSelectionDown' or 'NextSelectionRight'

					local nextPos, upPos, downPos;


					local gridItemSize = this:GetGridItemSize()
					local thisPos = this:GetCanvasPositionForOffscreenItem(selectedObject)

					if lastSelectedObject and lastSelectedObject[upDirection] == selectedObject and selectedObject[upDirection] then
						upPos = this:GetCanvasPositionForOffscreenItem(selectedObject[upDirection])
						upPos = upPos and upPos + gridItemSize / 2
					elseif lastSelectedObject and lastSelectedObject[downDirection] == selectedObject and selectedObject[downDirection] then
						downPos = this:GetCanvasPositionForOffscreenItem(selectedObject[downDirection])
						downPos = downPos and downPos - gridItemSize / 2
					end

					if upPos and (upPos.Y < this.Container.CanvasPosition.Y or upPos.X < this.Container.CanvasPosition.X) then
						nextPos = upPos
						--print('up' , nextPos , selectedObject.Position, lastSelectedObject and lastSelectedObject.Position)
					elseif downPos and (downPos.Y > this.Container.CanvasPosition.Y or downPos.X > this.Container.CanvasPosition.X) then
						nextPos = downPos
						--print('down' , nextPos , selectedObject.Position, lastSelectedObject and lastSelectedObject.Position)
					else
						nextPos = thisPos
						--print('this' , nextPos , selectedObject.Position, lastSelectedObject and lastSelectedObject.Position)
					end

					if nextPos then
						nextPos = Utility.ClampCanvasPosition(this.Container, nextPos)
						if currentTween then
							local final = currentTween:GetFinal()
							currentTween:Cancel()
							--if final then
							--	this.Container.CanvasPosition = final
							--end
						end
						if thisPos then --and thisPos ~= nextPos then
							-- Sort of a hack to not snap on the last one
							if (selectedObject[upDirection] and selectedObject[downDirection]) then
								this.Container.CanvasPosition = thisPos
							end
						end
						currentTween = Utility.PropertyTweener(this.Container, 'CanvasPosition', this.Container.CanvasPosition, nextPos, 0.2, Utility.EaseOutQuad)
					end
					lastSelectedObject = selectedObject
				else
					lastSelectedObject = nil
				end
			end
		end)

	end

	return this
end

return ScrollingGrid
