--
-- base/oven.lua
--
-- Process the workspaces, projects, and configurations that were specified
-- by the project script, and make them suitable for use by the exporters
-- and actions. Fills in computed values (e.g. object directories) and
-- optimizes the layout of the data for faster fetches.
--
-- Copyright (c) 2002-2014 Jason Perkins and the Premake project
--

	local p = premake

	p.oven = {}

	local oven = p.oven
	local context = p.context


--
-- These fields get special treatment, "bubbling up" from the configurations
-- to the project. This allows you to express, for example: "use this config
-- map if this configuration is present in the project", and saves the step
-- of clearing the current configuration filter before creating the map.
--

	p.oven.bubbledFields = {
		configmap = true,
		vpaths = true
	}



---
-- Traverses the container hierarchy built up by the project scripts and
-- filters, merges, and munges the information based on the current runtime
-- environment in preparation for doing work on the results, like exporting
-- project files.
--
-- This call replaces the existing the container objects with their
-- processed replacements. If you are using the provided container APIs
-- (p.global.*, p.workspace.*, etc.) this will be transparent.
---

	function oven.bake()
		-- reset the root _isBaked state.
		-- this really only affects the unit-tests, since that is the only place
		-- where multiple bakes per 'exe run' happen.
		local root = p.api.rootContainer()
		root._isBaked = false;

		p.container.bake(root)
	end

	function oven.bakeWorkspace(wks)
		return p.container.bake(wks)
	end

	p.alias(oven, "bakeWorkspace", "bakeSolution")


	local function addCommonContextFilters(self)
		context.addFilter(self, "_ACTION", _ACTION)
		context.addFilter(self, "action", _ACTION)

		self.system = self.system or os.target()
		context.addFilter(self, "system", os.getSystemTags(self.system))
		context.addFilter(self, "host", os.getSystemTags(os.host()))

		-- Add command line options to the filtering options
		local options = {}
		for key, value in pairs(_OPTIONS) do
			local term = key
			if value ~= "" then
				term = term .. "=" .. tostring(value)
			end
			table.insert(options, term)
		end
		context.addFilter(self, "_OPTIONS", options)
		context.addFilter(self, "options", options)
	end

---
-- Bakes a specific workspace object.
---

	function p.workspace.bake(self)
		-- Add filtering terms to the context and then compile the results. These
		-- terms describe the "operating environment"; only results contained by
		-- configuration blocks which match these terms will be returned.

		addCommonContextFilters(self)

		-- Set up my token expansion environment

		self.environ = {
			wks = self,
			sln = self,
		}

		context.compile(self)

		-- Specify the workspaces's file system location; when path tokens are
		-- expanded in workspace values, they will be made relative to this.

		self.location = self.location or self.basedir
		context.basedir(self, self.location)

		-- Build a master list of configuration/platform pairs from all of the
		-- projects contained by the workspace; I will need this when generating
		-- workspace files in order to provide a map from workspace configurations
		-- to project configurations.

		self.configs = oven.bakeConfigs(self)

		-- Now bake down all of the projects contained in the workspace, and
		-- store that for future reference

		p.container.bakeChildren(self)

		-- I now have enough information to assign unique object directories
		-- to each project configuration in the workspace.

		oven.bakeObjDirs(self)

		-- now we can post process the projects for 'buildoutputs' files
		-- that have the 'compilebuildoutputs' flag
		oven.addGeneratedFiles(self)
	end


	function oven.addGeneratedFiles(wks)

		local function addGeneratedFile(cfg, source, filename)
			-- mark that we have generated files.
			cfg.project.hasGeneratedFiles = true

			-- add generated file to the project.
			local files = cfg.project._.files
			local node = files[filename]
			if not node then
				node = p.fileconfig.new(filename, cfg.project)
				files[filename] = node
				table.insert(files, node)
			end

			-- always overwrite the dependency information.
			node.dependsOn = source
			node.generated = true

			-- add to config if not already added.
			if not p.fileconfig.getconfig(node, cfg) then
				p.fileconfig.addconfig(node, cfg)
			end
		end

		local function addFile(cfg, node)
			local filecfg = p.fileconfig.getconfig(node, cfg)
			if not filecfg or filecfg.flags.ExcludeFromBuild or not filecfg.compilebuildoutputs then
				return
			end

			if p.fileconfig.hasCustomBuildRule(filecfg) then
				local buildoutputs = filecfg.buildoutputs
				if buildoutputs and #buildoutputs > 0 then
					for _, output in ipairs(buildoutputs) do
						if not path.islinkable(output) then
							addGeneratedFile(cfg, node, output)
						end
					end
				end
			end
		end


		for prj in p.workspace.eachproject(wks) do
			local files = table.shallowcopy(prj._.files)
			for cfg in p.project.eachconfig(prj) do
				table.foreachi(files, function(node)
					addFile(cfg, node)
				end)
			end

			-- generated files might screw up the object sequences.
			if prj.hasGeneratedFiles and p.project.isnative(prj) then
				oven.assignObjectSequences(prj)
			end
		end
	end


	function p.project.bake(self)
		verbosef('    Baking %s...', self.name)

		self.solution = self.workspace
		self.global = self.workspace.global

		local wks = self.workspace

		-- Add filtering terms to the context to make it as specific as I can.
		-- Start with the same filtering that was applied at the workspace level.

		context.copyFilters(self, wks)

		-- Now filter on the current system and architecture, allowing the
		-- values that might already in the context to override my defaults.

		self.system = self.system or os.target()
		context.addFilter(self, "system", os.getSystemTags(self.system))
		context.addFilter(self, "host", os.getSystemTags(os.host()))
		context.addFilter(self, "architecture", self.architecture)
		context.addFilter(self, "tags", self.tags)

		-- The kind is a configuration level value, but if it has been set at the
		-- project level allow that to influence the other project-level results.

		context.addFilter(self, "kind", self.kind)

		-- Allow the project object to also be treated like a configuration

		self.project = self

		-- Populate the token expansion environment

		self.environ = {
			wks = wks,
			sln = wks,
			prj = self,
		}

		-- Go ahead and distill all of that down now; this is my new project object

		context.compile(self)

		p.container.bakeChildren(self)

		-- Set the context's base directory to the project's file system
		-- location. Any path tokens which are expanded in non-path fields
		-- are made relative to this, ensuring a portable generated project.

		self.location = self.location or self.basedir
		context.basedir(self, self.location)

		-- This bit could use some work: create a canonical set of configurations
		-- for the project, along with a mapping from the workspace's configurations.
		-- This works, but it could probably be simplified.

		local cfgs = table.fold(self.configurations or {}, self.platforms or {})
		oven.bubbleFields(self, self, cfgs)
		self._cfglist = oven.bakeConfigList(self, cfgs)

		-- Don't allow a project-level system setting to influence the configurations

		local projectSystem = self.system
		self.system = nil

		-- Finally, step through the list of configurations I built above and
		-- bake all of those down into configuration contexts as well. Store
		-- the results with the project.

		self.configs = {}

		for _, pairing in ipairs(self._cfglist) do
			local buildcfg = pairing[1]
			local platform = pairing[2]
			local cfg = oven.bakeConfig(wks, self, buildcfg, platform)

			if p.action.supportsconfig(p.action.current(), cfg) then
				self.configs[(buildcfg or "*") .. (platform or "")] = cfg
			end
		end

		-- Process the sub-objects that are contained by this project. The
		-- configuration build stuff above really belongs in here now.

		self._ = {}
		self._.files = oven.bakeFiles(self)

		-- If this type of project generates object files, look for files that will
		-- generate object name collisions (i.e. src/hello.cpp and tests/hello.cpp
		-- both create hello.o) and assign unique sequence numbers to each. I need
		-- to do this up front to make sure the sequence numbers are the same for
		-- all the tools, even they reorder the source file list.

		if p.project.isnative(self) then
			oven.assignObjectSequences(self)
		end

		-- at the end, restore the system, so it's usable elsewhere.
		self.system = projectSystem
	end


	function p.rule.bake(self)
		-- Add filtering terms to the context and then compile the results. These
		-- terms describe the "operating environment"; only results contained by
		-- configuration blocks which match these terms will be returned.

		addCommonContextFilters(self)

		-- Populate the token expansion environment

		self.environ = {
			rule = self,
		}

		-- Go ahead and distill all of that down now; this is my new rule object

		context.compile(self)

		-- sort the propertydefinition table.
		table.sort(self.propertydefinition, function (a, b)
			return a.name < b.name
		end)

		-- Set the context's base directory to the rule's file system
		-- location. Any path tokens which are expanded in non-path fields
		-- are made relative to this, ensuring a portable generated rule.

		self.location = self.location or self.basedir
		context.basedir(self, self.location)
	end



--
-- Assigns a unique objects directory to every configuration of every project
-- in the workspace, taking any objdir settings into account, to ensure builds
-- from different configurations won't step on each others' object files.
-- The path is built from these choices, in order:
--
--   [1] -> the objects directory as set in the config
--   [2] -> [1] + the platform name
--   [3] -> [2] + the build configuration name
--   [4] -> [3] + the project name
--
-- @param wks
--    The workspace to process. The directories are modified inline.
--

	function oven.bakeObjDirs(wks)
		-- function to compute the four options for a specific configuration
		local function getobjdirs(cfg)
			-- the "!" prefix indicates the directory is not to be touched
			local objdir = cfg.objdir or "obj"
			local i = objdir:find("!", 1, true)
			if i then
				cfg.objdir = objdir:sub(1, i - 1) .. objdir:sub(i + 1)
				return nil
			end

			local dirs = {}

			local dir = path.getabsolute(path.join(cfg.project.location, objdir))
			table.insert(dirs, dir)

			if cfg.platform then
				dir = path.join(dir, cfg.platform)
				table.insert(dirs, dir)
			end

			dir = path.join(dir, cfg.buildcfg)
			table.insert(dirs, dir)

			dir = path.join(dir, cfg.project.name)
			table.insert(dirs, dir)

			return dirs
		end

		-- walk all of the configs in the workspace, and count the number of
		-- times each obj dir gets used
		local counts = {}
		local configs = {}

		for prj in p.workspace.eachproject(wks) do
			for cfg in p.project.eachconfig(prj) do
				-- get the dirs for this config, and associate them together,
				-- and increment a counter for each one discovered
				local dirs = getobjdirs(cfg)
				if dirs then
					configs[cfg] = dirs
					for _, dir in ipairs(dirs or {}) do
						counts[dir] = (counts[dir] or 0) + 1
					end
				end
			end
		end

		-- now walk the list again, and assign the first unique value
		for cfg, dirs in pairs(configs) do
			for _, dir in ipairs(dirs) do
				if counts[dir] == 1 then
					cfg.objdir = dir
					break
				end
			end
		end
	end


--
-- Create a list of workspace-level build configuration/platform pairs.
--

	function oven.bakeConfigs(wks)
		local buildcfgs = wks.configurations or {}
		local platforms = wks.platforms or {}

		local configs = {}

		local pairings = table.fold(buildcfgs, platforms)
		for _, pairing in ipairs(pairings) do
			local cfg = oven.bakeConfig(wks, nil, pairing[1], pairing[2])
			if p.action.supportsconfig(p.action.current(), cfg) then
				table.insert(configs, cfg)
			end
		end

		return configs
	end


--
-- It can be useful to state "use this map if this configuration is present".
-- To allow this to happen, config maps that are specified within a project
-- configuration are allowed to "bubble up" to the top level. Currently,
-- maps are the only values that get this special behavior.
--
-- @param ctx
--    The project context information.
-- @param cset
--    The project's original configuration set, which contains the settings
--    of all the project configurations.
-- @param cfgs
--    The list of the project's build cfg/platform pairs.
--

	function oven.bubbleFields(ctx, cset, cfgs)
		-- build a query filter that will match any configuration name,
		-- within the existing constraints of the project

		local configurations = {}
		local platforms = {}

		for _, cfg in ipairs(cfgs) do
			if cfg[1] then
				table.insert(configurations, cfg[1]:lower())
			end
			if cfg[2] then
				table.insert(platforms, cfg[2]:lower())
			end
		end

		local terms = table.deepcopy(ctx.terms)
		terms.configurations = configurations
		terms.platforms = platforms

		for key in pairs(oven.bubbledFields) do
			local field = p.field.get(key)
			if not field then
				ctx[key] = rawget(ctx, key)
			else
				local value = p.configset.fetch(cset, field, terms, ctx)
				if value then
					ctx[key] = value
				end
			end
		end
	end


--
-- Builds a list of build configuration/platform pairs for a project,
-- along with a mapping between the workspace and project configurations.
--
-- @param ctx
--    The project context information.
-- @param cfgs
--    The list of the project's build cfg/platform pairs.
-- @return
--     An array of the project's build configuration/platform pairs,
--     based on any discovered mappings.
--

	function oven.bakeConfigList(ctx, cfgs)
		-- run them all through the project's config map
		for i, cfg in ipairs(cfgs) do
			cfgs[i] = p.project.mapconfig(ctx, cfg[1], cfg[2])
		end

		-- walk through the result and remove any duplicates
		local buildcfgs = {}
		local platforms = {}

		for _, pairing in ipairs(cfgs) do
			local buildcfg = pairing[1]
			local platform = pairing[2]

			if not table.contains(buildcfgs, buildcfg) then
				table.insert(buildcfgs, buildcfg)
			end

			if platform and not table.contains(platforms, platform) then
				table.insert(platforms, platform)
			end
		end

		-- merge these de-duped lists back into pairs for the final result
		return table.fold(buildcfgs, platforms)
	end


---
-- Flattens out the build settings for a particular build configuration and
-- platform pairing, and returns the result.
--
-- @param wks
--    The workpace which contains the configuration data.
-- @param prj
--    The project which contains the configuration data. Can be nil.
-- @param buildcfg
--    The target build configuration, a value from configurations().
-- @param platform
--    The target platform, a value from platforms().
-- @param extraFilters
--    Optional. Any extra filter terms to use when retrieving the data for
--    this configuration
---

	function oven.bakeConfig(wks, prj, buildcfg, platform, extraFilters)

		-- Set the default system and architecture values; if the platform's
		-- name matches a known system or architecture, use that as the default.
		-- More than a convenience; this is required to work properly with
		-- external Visual Studio project files.

		local system = os.target()
		local architecture = nil
		local toolset = p.action.current().toolset

		if platform then
			system = p.api.checkValue(p.fields.system, platform) or system
			architecture = p.api.checkValue(p.fields.architecture, platform) or architecture
			toolset = p.api.checkValue(p.fields.toolset, platform) or toolset
		end

		-- Wrap the projects's configuration set (which contains all of the information
		-- provided by the project script) with a context object. The context handles
		-- the expansion of tokens, and caching of retrieved values. The environment
		-- values are used when expanding tokens.

		local environ = {
			wks = wks,
			sln = wks,
			prj = prj,
		}

		local ctx = context.new(prj or wks, environ)

		ctx.project = prj
		ctx.workspace = wks
		ctx.solution = wks
		ctx.global = wks.global
		ctx.buildcfg = buildcfg
		ctx.platform = platform
		ctx.action = _ACTION

		-- Allow the configuration information to be accessed by tokens contained
		-- within the configuration itself

		environ.cfg = ctx

		-- Add filtering terms to the context and then compile the results. These
		-- terms describe the "operating environment"; only results contained by
		-- configuration blocks which match these terms will be returned. Start
		-- by copying over the top-level environment from the workspace. Don't
		-- copy the project terms though, so configurations can override those.

		context.copyFilters(ctx, wks)

		context.addFilter(ctx, "configurations", buildcfg)
		context.addFilter(ctx, "platforms", platform)
		if prj then
			context.addFilter(ctx, "language", prj.language)
		end

		-- allow the project script to override the default system
		ctx.system = ctx.system or system
		context.addFilter(ctx, "system", os.getSystemTags(ctx.system))
		context.addFilter(ctx, "host", os.getSystemTags(os.host()))

		-- allow the project script to override the default architecture
		ctx.architecture = ctx.architecture or architecture
		context.addFilter(ctx, "architecture", ctx.architecture)

		-- allow the project script to override the default toolset
		ctx.toolset = _OPTIONS.cc or ctx.toolset or toolset
		context.addFilter(ctx, "toolset", ctx.toolset)

		-- if a kind is set, allow that to influence the configuration
		context.addFilter(ctx, "kind", ctx.kind)

		-- if a sharedlibtype is set, allow that to influence the configuration
		context.addFilter(ctx, "sharedlibtype", ctx.sharedlibtype)

		-- if tags are set, allow that to influence the configuration
		context.addFilter(ctx, "tags", ctx.tags)

		-- if any extra filters were specified, can include them now
		if extraFilters then
			for k, v in pairs(extraFilters) do
				context.addFilter(ctx, k, v)
			end
		end

		context.compile(ctx)

		ctx.location = ctx.location or prj and prj.location
		context.basedir(ctx, ctx.location)

		-- Fill in a few calculated for the configuration, including the long
		-- and short names and the build and link target.

		oven.finishConfig(ctx)
		return ctx
	end


--
-- Create configuration objects for each file contained in the project. This
-- collects and collates all of the values specified in the project scripts,
-- and computes extra values like the relative path and object names.
--
-- @param prj
--    The project object being baked. The project
-- @return
--    A collection of file configurations, keyed by both the absolute file
--    path and an alpha-sorted index.
--

	function oven.bakeFiles(prj)

		local files = {}

		-- Start by building a comprehensive list of all the files contained by the
		-- project. Some files may only be included in a subset of configurations so
		-- I need to look at them all.

		for cfg in p.project.eachconfig(prj) do
			local function addFile(fname, i)

				-- If this is the first time I've seen this file, start a new
				-- file configuration for it. Track both by key for quick lookups
				-- and indexed for ordered iteration.
				local fcfg = files[fname]
				if not fcfg then
					fcfg = p.fileconfig.new(fname, prj)
					fcfg.order = i
					files[fname] = fcfg
					table.insert(files, fcfg)
				end

				p.fileconfig.addconfig(fcfg, cfg)
			end

			table.foreachi(cfg.files, addFile)

			-- If this project uses NuGet, we need to add the generated
			-- packages.config file to the project. Is there a better place to
			-- do this?

			if #prj.nuget > 0 and (_ACTION < "vs2017" or p.project.iscpp(prj)) then
				addFile("packages.config")
			end
		end

		-- Alpha sort the indices, so I will get consistent results in
		-- the exported project files.

		table.sort(files, function(a,b)
			return a.vpath < b.vpath
		end)

		return files
	end


--
-- Assign unique sequence numbers to any source code files that would generate
-- conflicting object file names (i.e. src/hello.cpp and tests/hello.cpp both
-- create hello.o).
--
-- a file list of: src/hello.cpp, tests/hello.cpp and src/hello1.cpp also generates
-- conflicting object file names - hello1.o

	function oven.uniqueSequence(f, cfg, seq, bases)
		while true do
			f.sequence = seq[cfg] or 0
			seq[cfg] = f.sequence + 1

			if f.sequence == 0 then
				-- first time seeing this objname
				break
			end

			-- getting here has changed our sequence number, but this new "basename"
			-- may still collide with files that actually end with this "sequence number"
			-- so we have to check the bases table now

			-- objname changes with the sequence number on every loop
			local lowerobj = f.objname:lower()
			if not bases[lowerobj] then
				-- this is the first appearance of a file that produces this objname
				-- intialize the table for any future basename that matches our objname
				bases[lowerobj] = {}
			end

			if not bases[lowerobj][cfg] then
				-- not a collision
				-- start a sequence for a future basename that matches our objname for this cfg
				bases[lowerobj][cfg] = 1
				break
			end
			-- else we have a objname collision, try the next sequence number
		end
	end


	function oven.assignObjectSequences(prj)

		-- Iterate over the file configurations which were prepared and cached in
		-- project.bakeFiles(); find buildable files with common base file names.

		local bases = {}
		table.foreachi(prj._.files, function(file)

			-- Only consider sources that actually generate object files

			if not path.isnativefile(file.abspath) then
				return
			end

			-- For each base file name encountered, keep a count of the number of
			-- collisions that have occurred for each project configuration. Use
			-- this collision count to generate the unique object file names.

			local lowerbase = file.basename:lower()
			if not bases[lowerbase] then
				bases[lowerbase] = {}
			end

			local sequences = bases[lowerbase]

			for cfg in p.project.eachconfig(prj) do
				local fcfg = p.fileconfig.getconfig(file, cfg)
				if fcfg ~= nil and not fcfg.flags.ExcludeFromBuild then
					oven.uniqueSequence(fcfg, cfg, sequences, bases)
				end
			end

			-- Makefiles don't use per-configuration object names yet; keep
			-- this around until they do. At which point I might consider just
			-- storing the sequence number instead of the whole object name

			oven.uniqueSequence(file, prj, sequences, bases)

		end)
	end


--
-- Finish the baking process for a workspace or project level configurations.
-- Doesn't bake per se, just fills in some calculated values.
--

	function oven.finishConfig(cfg)
		-- assign human-readable names
		cfg.longname = table.concat({ cfg.buildcfg, cfg.platform }, "|")
		cfg.shortname = table.concat({ cfg.buildcfg, cfg.platform }, " ")
		cfg.shortname = cfg.shortname:gsub(" ", "_"):lower()
		cfg.name = cfg.longname

		-- compute build and link targets
		if cfg.project and cfg.kind then
			cfg.buildtarget = p.config.gettargetinfo(cfg)
			cfg.buildtarget.relpath = p.project.getrelative(cfg.project, cfg.buildtarget.abspath)

			cfg.linktarget = p.config.getlinkinfo(cfg)
			cfg.linktarget.relpath = p.project.getrelative(cfg.project, cfg.linktarget.abspath)
		end
	end
