CoreScriptsRoot/Modules/ActionBindingsTab.lua
local CoreGui = game:GetService("CoreGui")
local ContextActionService = game:GetService("ContextActionService")
local TweenService = game:GetService("TweenService")

local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Utility = require(RobloxGui.Modules.Settings.Utility)

local INPUT_TYPE_ROW_HEIGHT = 30
local ACTION_ROW_HEIGHT = 20
local ROW_PADDING = 5
local COLUMN_PADDING = 5

local CORE_SECURITY_COLUMN_COLOR = Color3.new(0.1, 0, 0)
local DEV_SECURITY_COLUMN_COLOR = Color3.new(0, 0, 0)

local EXPAND_ROTATE_IMAGE_TWEEN_OUT = TweenInfo.new(0.150, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local EXPAND_ROTATE_IMAGE_TWEEN_IN = TweenInfo.new(0.150, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)

local ROW_PULSE = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, true, 0)
local CONTAINER_SCROLL = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)

local container = nil
local boundInputTypeRows = {}
local boundInputTypesByRows = {}
local boundInputTypeActionRows = {}
local boundActionInfoByRows = {}
local inputTypesByActionRows = {}
local inputTypesByHeaders = {}
local headersByInputTypes = {}
local inputTypesExpanded = {}

local layoutOrderDirty = true

local rowTypePrecedence = { 
	BoundInputType = 3, 
	TableHeader = 2, 
	BoundAction = 1 
} 

local function sortInputTypeRows(a, b)
	local inputTypeA = boundInputTypesByRows[a]
	local inputTypeB = boundInputTypesByRows[b]
	return tostring(inputTypeA) < tostring(inputTypeB)
end

local function sortActionRows(a, b) 
	local actionA = boundActionInfoByRows[a] 
	local actionB = boundActionInfoByRows[b] 
	if actionA and actionB then 
		local rowInputTypeA = inputTypesByActionRows[a] 
		local rowInputTypeB = inputTypesByActionRows[b] 
		if rowInputTypeA ~= rowInputTypeB then 
			return tostring(rowInputTypeA) < tostring(rowInputTypeB)
		end 
		if actionA.isCore and not actionB.isCore then 
			return true 
		elseif not actionA.isCore and actionB.isCore then 
			return false 
		end 
		local stackOrderA = actionA.stackOrder 
		local stackOrderB = actionB.stackOrder 
		if stackOrderA and stackOrderB then 
			return stackOrderA > stackOrderB --descending sort 
		else 
			return true 
		end 
	else 
		return true 
	end 
	return true 
 end

local function createEmptyRow(name, height)
	local row = Utility:Create("Frame") {
		Name = name,
		BackgroundTransparency = 1,
		ZIndex = 6,
		Size = UDim2.new(1, 0, 0, height or 0)
	}
	local columnList = Utility:Create("UIListLayout") {
		FillDirection = Enum.FillDirection.Horizontal,
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = UDim.new(0, COLUMN_PADDING),
		Parent = row
	}
	return row
end

local function createButtonRow(name, height)
	local row = Utility:Create("TextButton") {
		Name = name,
		BackgroundTransparency = 1,
		ZIndex = 6,
		Text = "",
		Size = UDim2.new(1, 0, 0, height or 0),
	}
	local columnList = Utility:Create("UIListLayout") {
		FillDirection = Enum.FillDirection.Horizontal,
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = UDim.new(0, COLUMN_PADDING),
		Parent = row
	}
	return row
end

local function createEmptyColumn(row, columnName)
	local column = Utility:Create("Frame") {
		Name = columnName,
		BackgroundColor3 = Color3.new(0, 0, 0),
		BackgroundTransparency = 0.75,
		BorderSizePixel = 0,
		Size = UDim2.new(1, 0, 1, 0),
		ZIndex = 6,
		ClipsDescendants = true,
		Parent = row
	}

	return column
end

local function createImageColumn(row, columnName, image, aspectRatio, imageSize)
	local column = createEmptyColumn(row, columnName)

	local aspectRatioConstraint = Utility:Create("UIAspectRatioConstraint") {
		AspectRatio = aspectRatio or 1,
		Parent = column 
	}

	local imageLabel = Utility:Create("ImageLabel") {
		Name = "ColumnImage",
		BackgroundTransparency = 1,
		Position = UDim2.new(0.5, 0, 0.5, 0),
		Size = UDim2.new(imageSize or 1, 0, imageSize or 1, 0),
		AnchorPoint = Vector2.new(0.5, 0.5),
		ZIndex = 6,
		Image = image,
		Parent = column
	}

	return column
end

local function createTextColumn(row, columnName, text)
	local column = createEmptyColumn(row, columnName)

	local textLabel = Utility:Create("TextLabel") {
		Name = "ColumnText",
		BackgroundTransparency = 1,
		Position = UDim2.new(0.5, 0, 0.5, 0),
		Size = UDim2.new(1, -10, 1, -10),
		AnchorPoint = Vector2.new(0.5, 0.5),
		ZIndex = 6,
		Text = text,
		TextSize = 18,
		TextColor3 = Color3.new(1, 1, 1),
		TextXAlignment = Enum.TextXAlignment.Left,
		Font = Enum.Font.SourceSans,
		Parent = column
	}

	return column
end

local function createActionColumns(row, backgroundColor)
	local x = 0

	local insetWidth = ACTION_ROW_HEIGHT + (COLUMN_PADDING * 2)
	local insetCol = createEmptyColumn(row, "Inset")
	insetCol.LayoutOrder = 0
	insetCol.BackgroundTransparency = 1
	insetCol.Size = UDim2.new(0, insetWidth, 1, 0)
	x = x + insetWidth + COLUMN_PADDING

	local priorityWidth = 80
	local priorityCol = createTextColumn(row, "Priority", "Priority")
	priorityCol.LayoutOrder = 1
	priorityCol.BackgroundColor3 = backgroundColor
	priorityCol.Size = UDim2.new(0, priorityWidth, 1, 0)
	x = x + priorityWidth + COLUMN_PADDING

	local securityWidth = 80
	local securityCol = createTextColumn(row, "Security", "Security")
	securityCol.LayoutOrder = 2
	securityCol.BackgroundColor3 = backgroundColor
	securityCol.Size = UDim2.new(0, securityWidth, 1, 0)
	x = x + securityWidth + COLUMN_PADDING

	local nameCol = createTextColumn(row, "ActionName", "Action Name")
	nameCol.LayoutOrder = 3
	nameCol.BackgroundColor3 = backgroundColor
	nameCol.Size = UDim2.new(1/4, 0, 1, 0)

	local inputTypesCol = createTextColumn(row, "InputTypes", "Input Types")
	inputTypesCol.LayoutOrder = 4
	inputTypesCol.BackgroundColor3 = backgroundColor
	inputTypesCol.Size = UDim2.new(3/4, -x - COLUMN_PADDING, 1, 0)

	return insetCol, priorityCol, securityCol, nameCol, inputTypesCol
end

local function updateContainerCanvas()
	debug.profilebegin("updateContainerCanvas")

	if layoutOrderDirty then
		layoutOrderDirty = false
		local idx = 1

		local inputTypeRowList = {}

		for inputType, inputTypeRow in pairs(boundInputTypeRows) do
			table.insert(inputTypeRowList, inputTypeRow)
		end

		table.sort(inputTypeRowList, sortInputTypeRows)

		for i, inputTypeRow in pairs(inputTypeRowList) do
			local inputType = boundInputTypesByRows[inputTypeRow]
			inputTypeRow.LayoutOrder = idx; idx = idx + 1
			local headerRow = headersByInputTypes[inputType]
			if headerRow then
				headerRow.LayoutOrder = idx; idx = idx + 1
			end

			local actionRows = boundInputTypeActionRows[inputType]
			if actionRows then
				local actionRowList = {}
				for actionName, actionRow in pairs(actionRows) do
					table.insert(actionRowList, actionRow)
				end

				table.sort(actionRowList, sortActionRows)

				for i, actionRow in pairs(actionRowList) do
					actionRow.LayoutOrder = idx; idx = idx + 1
				end
			end
		end
	end

	debug.profileend()
end

local function scrollContainerToRow(row)
	local scrollOffset = row.AbsolutePosition.Y - container.AbsolutePosition.Y
	local newCanvasPosition = container.CanvasPosition + Vector2.new(0, scrollOffset)
	TweenService:Create(container, CONTAINER_SCROLL, { CanvasPosition = newCanvasPosition }):Play()
end

local ActionBindingsTab = {}

function ActionBindingsTab.initializeGui(tabFrame)
	local scrollingFrame = Utility:Create("ScrollingFrame") {
		Position = UDim2.new(0.5, 0, 0.5, 0),
		Size = UDim2.new(1, -10, 1, -10),
		AnchorPoint = Vector2.new(0.5, 0.5),
		BorderSizePixel = 0,
		ScrollBarThickness = 4,
		BackgroundTransparency = 1,
		ZIndex = 6,
		Parent = tabFrame
	}
	container = scrollingFrame

	local listLayout = Utility:Create("UIListLayout") {
		Padding = UDim.new(0, ROW_PADDING),
		Parent = scrollingFrame
	}
	listLayout.SortOrder = Enum.SortOrder.LayoutOrder
	listLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() container.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y) end)

	ActionBindingsTab.updateGuis()

	ContextActionService.BoundActionAdded:connect(function(actionName, createTouchButton, actionInfo, isCore)
		actionInfo.isCore = isCore
		ActionBindingsTab.updateActionRows(actionName, actionInfo)

		layoutOrderDirty = true
		updateContainerCanvas()
	end)
	ContextActionService.BoundActionRemoved:connect(function(actionName, actionInfo, isCore)
		actionInfo.isCore = isCore
		ActionBindingsTab.removeActionRows(actionName, actionInfo)

		layoutOrderDirty = true
		updateContainerCanvas()
	end)
end

function ActionBindingsTab.updateBoundInputTypeRow(inputType)
	local existingRow = boundInputTypeRows[inputType]
	if not existingRow then
		local row = createButtonRow("BoundInputType", INPUT_TYPE_ROW_HEIGHT)

		local expandImageCol = createImageColumn(row, "ExpandImage", "rbxasset://textures/ui/ExpandArrowSheet.png", 1, 0.35)
		expandImageCol.ColumnImage.ImageRectSize = Vector2.new(21, 21)
		expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(0, 0)

		local inputTypeCol = createTextColumn(row, "InputType", tostring(inputType))
		inputTypeCol.Size = UDim2.new(1, -INPUT_TYPE_ROW_HEIGHT - COLUMN_PADDING, 1, 0)
		inputTypeCol.ColumnText.Font = Enum.Font.SourceSansBold

		local tableHeaderRow = createEmptyRow("TableHeader", ACTION_ROW_HEIGHT)
		tableHeaderRow.Visible = false
		local _, priorityCol, securityCol, nameCol, inputTypesCol = createActionColumns(tableHeaderRow, DEV_SECURITY_COLUMN_COLOR)
		priorityCol.ColumnText.Font = Enum.Font.SourceSansBold
		securityCol.ColumnText.Font = Enum.Font.SourceSansBold
		nameCol.ColumnText.Font = Enum.Font.SourceSansBold
		inputTypesCol.ColumnText.Font = Enum.Font.SourceSansBold

		boundInputTypeRows[inputType] = row
		boundInputTypesByRows[row] = inputType
		inputTypesByHeaders[tableHeaderRow] = inputType
		headersByInputTypes[inputType] = tableHeaderRow

		tableHeaderRow.Parent = container
		row.Parent = container

		TweenService:Create(inputTypeCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()

		inputTypesExpanded[inputType] = false
		row.MouseButton1Click:connect(function()
			inputTypesExpanded[inputType] = not inputTypesExpanded[inputType]

			local inputTypeActionRows = boundInputTypeActionRows[inputType]
			if not inputTypesExpanded[inputType] then
				expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(0, 0)
				tableHeaderRow.Visible = false
				if inputTypeActionRows then
					for _, actionRow in pairs(inputTypeActionRows) do
						actionRow.Visible = false
					end
				end
			else
				expandImageCol.ColumnImage.ImageRectOffset = Vector2.new(21, 0)
				tableHeaderRow.Visible = true
				if inputTypeActionRows then
					for _, actionRow in pairs(inputTypeActionRows) do
						actionRow.Visible = true
					end
				end
			end

			updateContainerCanvas()
			if inputTypesExpanded[inputType] then
				TweenService:Create(inputTypeCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
				scrollContainerToRow(row)
			end
		end)
	end
end

function ActionBindingsTab.updateActionRowForInputType(actionName, actionInfo, inputType)
	local inputTypeActionRows = boundInputTypeActionRows[inputType]
	if not inputTypeActionRows then
		inputTypeActionRows = {}
		boundInputTypeActionRows[inputType] = inputTypeActionRows
	end

	local existingRow = inputTypeActionRows[actionName]
	if not existingRow then
		local row = createEmptyRow("BoundAction", ACTION_ROW_HEIGHT)
		row.Visible = inputTypesExpanded[inputType]

		local inputTypeNames = {}
		for i, inputType in pairs(actionInfo.inputTypes) do
			inputTypeNames[i] = tostring(inputType)
		end

		local insetCol, priorityCol, securityCol, nameCol, inputTypesCol = createActionColumns(row, actionInfo.isCore and CORE_SECURITY_COLUMN_COLOR or DEV_SECURITY_COLUMN_COLOR)
		priorityCol.ColumnText.Text = actionInfo.priorityLevel or "Default"
		securityCol.ColumnText.Text = actionInfo.isCore and "Core" or "Developer"
		nameCol.ColumnText.Text = actionName
		inputTypesCol.ColumnText.Text = table.concat(inputTypeNames, ", ")

		if actionInfo.isCore then
			priorityCol.ColumnText.Font = Enum.Font.SourceSansItalic
			securityCol.ColumnText.Font = Enum.Font.SourceSansItalic
			nameCol.ColumnText.Font = Enum.Font.SourceSansItalic
			inputTypesCol.ColumnText.Font = Enum.Font.SourceSansItalic
		end

		inputTypeActionRows[actionName] = row
		inputTypesByActionRows[row] = inputType
		boundActionInfoByRows[row] = actionInfo
		row.Parent = container

		if row.Visible then
			TweenService:Create(nameCol, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
		else
			local inputTypeRow = boundInputTypeRows[inputType]
			if inputTypeRow then
				TweenService:Create(inputTypeRow.InputType, ROW_PULSE, { BackgroundColor3 = Color3.new(0.5, 0.5, 0.5) }):Play()
			end
		end

		existingRow = row
	end
end

function ActionBindingsTab.updateActionRows(actionName, actionInfo)
	for _, inputType in pairs(actionInfo.inputTypes) do
		ActionBindingsTab.updateBoundInputTypeRow(inputType)
		ActionBindingsTab.updateActionRowForInputType(actionName, actionInfo, inputType)
	end
end

function ActionBindingsTab.removeActionRows(actionName, actionInfo)
	for _, inputType in pairs(actionInfo.inputTypes) do
		local inputTypeActionRows = boundInputTypeActionRows[inputType]
		if inputTypeActionRows then
			local row = inputTypeActionRows[actionName]
			row:Destroy()
			inputTypeActionRows[actionName] = nil

			--The following code looks weird. It's because Lua has no way to determine
			--if a table is explicitly empty in both the array and dictionary parts.
			--This does it though.
			local isEmpty = true
			for _, __ in pairs(inputTypeActionRows) do
				isEmpty = false
				break
			end

			if isEmpty then
				local inputTypeRow = boundInputTypeRows[inputType]
				if inputTypeRow then
					inputTypeRow:Destroy()
					boundInputTypeRows[inputType] = nil
				end
				local tableHeaderRow = headersByInputTypes[inputType]
				if tableHeaderRow then
					headersByInputTypes[tableHeaderRow] = nil
					tableHeaderRow:Destroy()
					headersByInputTypes[inputType] = nil
				end
			end
		end
	end

	updateContainerCanvas()
	container.UIListLayout:ApplyLayout()
end

function ActionBindingsTab.updateGuis()
	local boundCoreActions = ContextActionService:GetAllBoundCoreActionInfo()
	for actionName, actionInfo in pairs(boundCoreActions) do
		actionInfo.isCore = true
		ActionBindingsTab.updateActionRows(actionName, actionInfo)
	end
	local boundActions = ContextActionService:GetAllBoundActionInfo()
	for actionName, actionInfo in pairs(boundActions) do
		actionInfo.isCore = false
		ActionBindingsTab.updateActionRows(actionName, actionInfo)
	end

	layoutOrderDirty = true
	updateContainerCanvas()
end

return ActionBindingsTab