--
-- vs2026_solution.lua
-- Generate a Visual Studio 2026 solution file.
-- Copyright (c) Jess Perkins and the Premake project
--

local p = premake
p.vstudio.sln2026 = {}

local vstudio = p.vstudio
local sln2026 = p.vstudio.sln2026
local project = p.project
local tree = p.tree

local m = sln2026

m.elements = {}

m.elements.solution = function(wks)
	return {
		m.solution,
		m.configurations,
		m.projects,
	}
end

m.elements.project = function(prj)
	return {
		m.projectDependencies,
		m.projectConfigMap,
		m.projectExclusion,
	}
end

function sln2026.generate(wks)
	p.utf8()
	p.callArray(m.elements.solution, wks)
	p.pop('</Solution>')
end

function m.solution(wks)
	local action = p.action.current()
	p.push('<Solution Description="Visual Studio slnx file generated by Premake" Version="%s">', action.vstudio.solutionVersion)
end

function m.configurations(wks)
	p.push('<Configurations>')

	local cfgs = {}
	local platforms = {}

	for cfg in p.workspace.eachconfig(wks) do
		local platform = vstudio.solutionPlatform(cfg)
		table.insert(platforms, platform)
		table.insert(cfgs, cfg.buildcfg)
	end

	cfgs = table.unique(cfgs)
	platforms = table.unique(platforms)

	table.sort(cfgs, function(a, b) return a:lower() < b:lower() end)
	table.sort(platforms, function(a, b) return a:lower() < b:lower() end)

	for _, buildcfg in ipairs(cfgs) do
		p.push('<BuildType Name="%s" />', buildcfg)
		p.pop()
	end

	for _, platform in ipairs(platforms) do
		p.push('<Platform Name="%s" />', platform)
		p.pop()
	end

	p.pop('</Configurations>')
end


function m.buildRelativePath(prj)
	local prjpath = vstudio.projectfile(prj)
	prjpath = vstudio.path(prj.workspace, prjpath)

	-- Unlike projects, solutions must use old-school %...% DOS style
	-- for environment variables.
	return prjpath:gsub("$%((.-)%)", "%%%1%%")
end


function m.projects(wks)
	local tr = p.workspace.grouptree(wks)

	-- Key-value pairs, key is full group path, value is list of projects
	local groups = {}
	local currentKey = ""

	tree.traverse(tr, {
		onbranch = function(n)
			-- If the current key is empty, this is the root node
			-- Root starts with a slash
			if currentKey == "" then
				currentKey = "/"
			end

			currentKey = currentKey .. n.name
			if currentKey:sub(-1) ~= "/" then
				currentKey = currentKey .. "/"
			end
		end,

		onbranchexit = function(n)
			-- Remove the last portion of the Key
			local keyParts = {}
			for part in currentKey:gmatch("[^/]+") do
				table.insert(keyParts, part)
			end
			table.remove(keyParts) -- Remove the last part
			currentKey = table.concat(keyParts, "/")
			if currentKey ~= "" then
				currentKey = currentKey .. "/"
			end

			-- Ensure the currentKey starts with a slash
			if currentKey ~= "" and currentKey:sub(1, 1) ~= "/" then
				currentKey = "/" .. currentKey
			end
		end,

		onleaf = function(n)
			-- check if group already exists
			if not groups[currentKey] then
				groups[currentKey] = {}
			end

			table.insert(groups[currentKey], n.project)
		end
	})

	-- Fetch the startup project from the workspace, if any exists
	local startupProject = nil
	if wks.startproject then
		startupProject = p.workspace.findproject(wks, wks.startproject)
	end

	-- Sort to maintain stability, as traversal order of tree is not required
	-- to be the same each time.
	local sortedKeys = {}
	for k, _ in pairs(groups) do
		table.insert(sortedKeys, k)
	end
	table.sort(sortedKeys)

	for _, groupName in ipairs(sortedKeys) do
		local projects = groups[groupName]
		table.sort(projects, function(a, b) return a.name:lower() < b.name:lower() end)

		if groupName ~= "" then
			p.push('<Folder Name="%s">', groupName)
		end
		for _, prj in ipairs(projects) do
			if startupProject and startupProject.name == prj.name then
				p.push('<Project Path="%s" Id="%s" DefaultStartup="true">', m.buildRelativePath(prj), prj.uuid)
			else
				p.push('<Project Path="%s" Id="%s">', m.buildRelativePath(prj), prj.uuid)
			end

			p.callArray(m.elements.project, prj)
			p.pop('</Project>')
		end
		if groupName ~= "" then
			p.pop('</Folder>')
		end
	end
end


function m.projectDependencies(prj)
	local deps = p.project.getdependencies(prj, 'dependOnly')
	if #deps > 0 then
		for _, dep in ipairs(deps) do
			p.push('<BuildDependency Project="%s" />', m.buildRelativePath(dep))
			p.pop()
		end
	end
end


function m.projectConfigMap(prj)
	local cfgmap = prj.configmap or {}
	for mi = 1, #cfgmap do
		-- Key may be a string (buildcfg only) or a table (buildcfg + platform)
		-- Sort the keys to ensure stable output
		-- If the key is a table, sort by buildcfg first, then platform
		-- On comparison of a string and a table
		--   If the string matches the buildcfg portion of the table, the string is "less than" the table
		--   If the string does not match the buildcfg portion of the table, sort by buildcfg alphabetically
		local slncfgmaps = cfgmap[mi]
		local sortedkeys = {}
		for slncfg, _ in pairs(slncfgmaps) do
			table.insert(sortedkeys, slncfg)
		end

		table.sort(sortedkeys, function(a, b)
			if type(a) == "string" and type(b) == "string" then
				return a:lower() < b:lower()
			elseif type(a) == "string" then
				return a:lower() < b[1]:lower()
			elseif type(b) == "string" then
				return false
			else
				if a[1]:lower() == b[1]:lower() then
					return a[2]:lower() < b[2]:lower()
				else
					return a[1]:lower() < b[1]:lower()
				end
			end
		end)

		-- Iterate over each sorted key
		for _, cfg in ipairs(sortedkeys) do
			local target = slncfgmaps[cfg]
			-- Determine if the target is a platform or a configuration, or both
			-- Target may be a length 1 table (buildcfg or platform) or a length 2 table (buildcfg + platform)
			
			local getplatform = function(tgt)
				if type(tgt) == "string" then
					if table.contains(prj.workspace.platforms, tgt) then
						return tgt
					end
				elseif type(tgt) == "table" then
					for _, plat in ipairs(tgt) do
						if table.contains(prj.workspace.platforms, plat) then
							return plat
						end
					end
				end
				return nil
			end

			local getbuildcfg = function(tgt)
				if type(tgt) == "string" then
					if table.contains(prj.workspace.configurations, tgt) then
						return tgt
					end
				elseif type(tgt) == "table" then
					for _, cfg in ipairs(tgt) do
						if table.contains(prj.workspace.configurations, cfg) then
							return cfg
						end
					end
				end
				return nil
			end

			local platform = getplatform(target)
			local buildcfg = getbuildcfg(target)

			local output = function(cfg, platform, tag)
				if type(cfg) == "string" then
					local isplatform = getplatform(cfg) ~= nil
					local isbuildcfg = getbuildcfg(cfg) ~= nil
					if isplatform then
						p.push('<%s Solution="*|%s" Project="%s" />', tag, cfg, platform)
						p.pop()
					elseif isbuildcfg then
						p.push('<%s Solution="%s|*" Project="%s" />', tag, cfg, platform)
						p.pop()
					end
				else
					local isplatform = getplatform(cfg) ~= nil
					local isbuildcfg = getbuildcfg(cfg) ~= nil
					
					if isplatform and isbuildcfg then
						p.push('<%s Solution="%s|%s" Project="%s" />', tag, cfg[1], cfg[2], platform)
						p.pop()
					elseif isplatform then
						p.push('<%s Solution="*|%s" Project="%s" />', tag, cfg[2], platform)
						p.pop()
					elseif isbuildcfg then
						p.push('<%s Solution="%s|*" Project="%s" />', tag, cfg[1], platform)
						p.pop()
					end
				end
			end

			if platform ~= nil then
				output(cfg, platform, "Platform")
			end

			if buildcfg ~= nil then
				output(cfg, buildcfg, "BuildType")
			end
		end
	end
end


function m.projectExclusion(prj)
	-- For each configuration + platform in the solution, find the matching project configuration
	-- If the project configuration is missing or excluded, add an exclusion entry
	local wks = prj.workspace

	for solcfg in p.workspace.eachconfig(wks) do
		local prjcfg = project.getconfig(prj, solcfg.buildcfg, solcfg.platform)
		if prjcfg == nil or prjcfg.flags.ExcludeFromBuild then
			local platform = vstudio.solutionPlatform(solcfg)
			p.push('<Build Solution="%s|%s" Project="false" />', solcfg.buildcfg, platform or "*")
			p.pop()
		end
	end
end
