--
-- api.lua
-- Implementation of the workspace, project, and configuration APIs.
-- Author Jason Perkins
-- Copyright (c) 2002-2015 Jason Perkins and the Premake project
--

	local p = premake
	p.api = {}

	local api = p.api
	local configset = p.configset



---
-- Set up a place to store the current active objects in each configuration
-- scope (e.g. wprkspaces, projects, groups, and configurations). This likely
-- ought to be internal scope, but it is useful for testing.
---

	api.scope = {}



---
-- Define a new class of configuration container. A container can receive and
-- store configuration blocks, which are what hold the individial settings
-- from the scripts. A container can also hold one or more kinds of child
-- containers; a workspace can contain projects, for instance.
--
-- @param containerName
--    The name of the new container type, e.g. "workspace". Used to define a
--    corresponding global function, e.g. workspace() to create new instances
--    of the container.
-- @param parentContainer (optional)
--    The container that can contain this one. For a project, this would be
--    the workspace container class.
-- @param extraScopes (optional)
--    Each container can hold fields scoped to itself (by putting the container's
--    class name into its scope attribute), or any of the container's children.
--    If a container can hold scopes other than these (i.e. "config"), it can
--    provide a list of those scopes in this argument.
-- @returns
--    The newly defined container class.
---

	function api.container(containerName, parentContainer, extraScopes)
		local class, err = p.container.newClass(containerName, parentContainer, extraScopes)
		if not class then
			error(err, 2)
		end

		_G[containerName] = function(name)
			local c = api._setContainer(class, name)
			if api._isIncludingExternal then
				c.external = true
			end
			return c
		end

		_G["external" .. containerName] = function(name)
			local c = _G[containerName](name)
			c.external = true
			return c
		end

		-- for backward compatibility
		p.alias(_G, "external" .. containerName, "external" .. containerName:capitalized())

		return class
	end



---
-- Register a general-purpose includeExternal() call which works just like
-- include(), but marks any containers created while evaluating the included
-- scripts as external. It also, loads the file regardless of how many times
-- it has been loaded already.
---

	function includeexternal(fname)
		local fullPath = p.findProjectScript(fname)
		api._isIncludingExternal = true
		fname = fullPath or fname
		dofile(fname)
		api._isIncludingExternal = nil
	end

	p.alias(_G, "includeexternal", "includeExternal")



---
-- Return the global configuration container.
---

	function api.rootContainer()
		return api.scope.global
	end



---
-- Activate a new configuration container, making it the target for all
-- subsequent configuration settings. When you call workspace() or project()
-- to active a container, that call comes here (see api.container() for the
-- details on how that happens).
--
-- @param class
--    The container class being activated, e.g. a project or workspace.
-- @param name
--    The name of the container instance to be activated. If a container
--    (e.g. project) with this name does not already exist it will be
--    created. If name is not set, the last activated container of this
--    class will be made current again.
-- @return
--    The container instance.
---

	function api._setContainer(class, name)
		local instance

		-- for backward compatibility, "*" activates the parent container
		if name == "*" then
			return api._setContainer(class.parent)
		end

		-- if name is not set, use whatever was last made current
		if not name then
			instance = api.scope[class.name]
			if not instance then
				error("no " .. class.name .. " in scope", 3)
			end
		end

		-- otherwise, look up the instance by name
		local parent
		if not instance and class.parent then
			parent = api.scope[class.parent.name]
			if not parent then
				error("no " .. class.parent.name .. " in scope", 3)
			end
			instance = p.container.getChild(parent, class, name)
		end

		-- if I have an existing instance, create a new configuration
		-- block for it so I don't pick up an old filter
		if instance then
			configset.addFilter(instance, {}, os.getcwd())
		end

		-- otherwise, a new instance
		if not instance then
			instance = class.new(name, parent)
			if parent then
				p.container.addChild(parent, instance)
			end
		end

		-- clear out any active child containers that might be active
		-- (recursive call, so needs to be its own function)
		api._clearContainerChildren(class)

		-- active this container, as well as it ancestors
		if not class.placeholder then
			api.scope.current = instance
		end

		while instance do
			api.scope[instance.class.name] = instance
			if instance.class.alias then
				api.scope[instance.class.alias] = instance
			end
			instance = instance.parent
		end

		return api.scope.current
	end

	function api._clearContainerChildren(class)
		for childClass in p.container.eachChildClass(class) do
			api.scope[childClass.name] = nil
			if childClass.alias then
				api.scope[childClass.alias] = nil
			end
			api._clearContainerChildren(childClass)
		end
	end



---
-- Register a new API function. See the built-in API definitions in
-- _premake_init.lua for lots of usage examples.
--
-- A new global function will be created to receive values for the field.
-- List fields will also receive a `remove...()` function to remove values.
--
-- @param field
--    A table describing the new field, with these keys:
--
--     name     The API name of the new field. This is used to create a global
--              function with the same name, and so should follow Lua symbol
--              naming conventions. (required)
--     scope    The scoping level at which this value can be used; see list
--              below. (required)
--     kind     The type of values that can be stored into this field; see
--              list below. (required)
--     allowed  An array of valid values for this field, or a function which
--              accepts a value as input and returns the canonical value as a
--              result, or nil if the input value is invalid. (optional)
--     tokens   A boolean indicating whether token expansion should be
--              performed on this field.
--
--   The available field scopes are:
--
--     project  The field applies to workspaces and projects.
--     config   The field applies to workspaces, projects, and individual build
--              configurations.
--
--   The available field kinds are:
--
--     string     A simple string value.
--     path       A file system path. The value will be made into an absolute
--                path, but no wildcard expansion will be performed.
--     file       One or more file names. Wilcard expansion will be performed,
--                and the results made absolute. Implies a list.
--     directory  One of more directory names. Wildcard expansion will be
--                performed, and the results made absolute. Implies a list.
--     mixed      A mix of simple string values and file system paths. Values
--                which contain a directory separator ("/") will be made
--                absolute; other values will be left intact.
--     table      A table of values. If the input value is not a table, it is
--                wrapped in one.
---

	function api.register(field)
		-- verify the name
		local name = field.name
		if not name then
			error("missing name", 2)
		end

		if rawget(_G, name) then
			error("name '" .. name .. "' in use", 2)
		end

		-- add this new field to my master list
		field, err = p.field.new(field)
		if not field then
			error(err)
		end


		-- Flag fields which contain filesystem paths. The context object will
		-- use this information when expanding tokens, to ensure that the paths
		-- are still well-formed after replacements.

		field.paths = p.field.property(field, "paths")

		-- Add preprocessed, lowercase keys to the allowed and aliased value
		-- lists to speed up value checking later on.

		if type(field.allowed) == "table" then
			for i, item in ipairs(field.allowed) do
				field.allowed[item:lower()] = item
			end
		end

		if type(field.aliases) == "table" then
			local keys = table.keys(field.aliases)
			for i, key in ipairs(keys) do
				field.aliases[key:lower()] = field.aliases[key]
			end
		end

		-- create a setter function for it
		_G[name] = function(value)
			return api.storeField(field, value)
		end

		if p.field.removes(field) then
			_G["remove" .. name] = function(value)
				return api.remove(field, value)
			end
		end

		return field
	end



---
-- Unregister a field definition, removing its functions and field
-- list entries.
---

	function api.unregister(field)
		if type(field) == "string" then
			field = p.field.get(field)
		end
		p.field.unregister(field)
		_G[field.name] = nil
		_G["remove" .. field.name] = nil
	end



---
-- Create an alias to one of the canonical API functions. This creates
-- new setter and remover names pointing to the same functions.
--
-- @param original
--    The name of the function to be aliased (a string value).
-- @param alias
--    The alias name (another string value).
---

	function api.alias(original, alias)
		p.alias(_G, original, alias)
		if _G["remove" .. original] then
			p.alias(_G, "remove" .. original, "remove" .. alias)
		end
	end



--
-- Add a new value to a field's list of allowed values.
--
-- @param fieldName
--    The name of the field to which to add the value.
-- @param value
--    The value to add. May be a single string value, or an array
--    of values.
--

	function api.addAllowed(fieldName, value)
		local field = p.field.get(fieldName)
		if not field then
			error("No such field: " .. fieldName, 2)
		end

		if type(value) == "table" then
			for i, item in ipairs(value) do
				api.addAllowed(fieldName, item)
			end
		else
			field.allowed = field.allowed or {}
			if field.allowed[value:lower()] == nil then
				table.insert(field.allowed, value)
				field.allowed[value:lower()] = value
			end
		end
	end



--
-- Add a new value to a field's list of allowed values.
--
-- @param fieldName
--    The name of the field to which to add the value.
-- @param value
--    The value to add. May be a single string value, or an array
--    of values.
--

	function api.addAliases(fieldName, value)
		local field = p.field.get(fieldName)
		if not field then
			error("No such field: " .. fieldName, 2)
		end

		field.aliases = field.aliases or {}
		for k, v in pairs(value) do
			field.aliases[k] = v
			field.aliases[k:lower()] = v
		end
	end



--
-- Mark an API field as deprecated.
--
-- @param name
--    The name of the field to mark as deprecated.
-- @param message
--    A optional message providing more information, to be shown
--    as part of the deprecation warning message.
-- @param handler
--    A function to call when the field is used. Passes the value
--    provided to the field as the only argument.
--

	function api.deprecateField(name, message, handler)
		p.fields[name].deprecated = {
			handler = handler,
			message = message
		}
	end


--
-- Mark a specific value of a field as deprecated.
--
-- @param name
--    The name of the field containing the value.
-- @param value
--    The value or values to mark as deprecated. May be a string
--    for a single value or an array of multiple values.
-- @param message
--    A optional message providing more information, to be shown
--    as part of the deprecation warning message.
-- @param addHandler
--    A function to call when the value is used, receiving the
--    value as its only argument.
-- @param removeHandler
--    A function to call when the value is removed from a list
--    field, receiving the value as its only argument (optional).
--

	function api.deprecateValue(name, value, message, addHandler, removeHandler)
		if type(value) == "table" then
			for _, v in pairs(value) do
				api.deprecateValue(name, v, message, addHandler, removeHandler)
			end
		else
			local field = p.fields[name]
			field.deprecated = field.deprecated or {}
			field.deprecated[value] = {
				add = addHandler,
				remove = removeHandler,
				message = message
			}
		end
	end


--
-- Control the handling of API deprecations.
--
-- @param value
--    One of "on" to enable the deprecation behavior, "off" to disable it,
--    and "error" to raise an error instead of logging a warning.
--

	function api.deprecations(value)
		value = value:lower()
		if not table.contains({ "on", "off", "error"}, value) then
			error("Invalid value: " .. value, 2)
		end
		api._deprecations = value:lower()
	end

	api._deprecations = "on"



---
-- Return the target container instance for a field.
--
-- @param field
--    The field being set or fetched.
-- @return
--    The currently active container instance if one is available, or nil if
--    active container is of the wrong class.
---

	function api.target(field)
		if p.container.classCanContain(api.scope.current.class, field.scope) then
			return api.scope.current
		end
		return nil
	end



--
-- Callback for all API functions; everything comes here first, and then
-- gets parceled out to the individual set...() functions.
--

	function api.storeField(field, value)
		if value == nil then
			return
		end

		if field.deprecated and type(field.deprecated.handler) == "function" then
			field.deprecated.handler(value)
			if field.deprecated.message and api._deprecations ~= "off" then
				local caller = filelineinfo(2)
				local key = field.name .. "_" .. caller
				p.warnOnce(key, "the field %s has been deprecated and will be removed.\n   %s\n   @%s\n", field.name, field.deprecated.message, caller)
				if api._deprecations == "error" then
					error("deprecation errors enabled", 3)
				end
			end
		end

		local target = api.target(field)
		if not target then
			local err = string.format("unable to set %s in %s scope, should be %s", field.name, api.scope.current.class.name, table.concat(field.scopes, ", "))
			error(err, 3)
		end

		local status, err = configset.store(target, field, value)
		if err then
			error(err, 3)
		end
	end



--
-- The remover: adds values to be removed to the "removes" field on
-- current configuration. Removes are keyed by the associated field,
-- so the call `removedefines("X")` will add the entry:
--  cfg.removes["defines"] = { "X" }
--

	function api.remove(field, value)
		-- right now, ignore calls with no value; later might want to
		-- return the current baked value
		if value == nil then return end

		local target = api.target(field)
		if not target then
			local err = string.format("unable to remove %s from %s scope, should be %s", field.name, api.scope.current.class.name, table.concat(field.scopes, ", "))
			error(err, 3)
		end

		local hasDeprecatedValues = (type(field.deprecated) == "table")

		-- Build a list of values to be removed. If this field has deprecated
		-- values, check to see if any of those are going to be removed by this
		-- call (which means matching against any provided wildcards) and call
		-- the appropriate logic for removing that value.

		local removes = {}

		local function check(value)
			if field.deprecated[value] then
				local handler = field.deprecated[value]
				if handler.remove then handler.remove(value) end
				if handler.message and api._deprecations ~= "off" then
					local caller = filelineinfo(8)
					local key = field.name .. "_" .. value .. "_" .. caller
					p.warnOnce(key, "the %s value %s has been deprecated and will be removed.\n   %s\n   @%s\n", field.name, value, handler.message, caller)
					if api._deprecations == "error" then
						error { msg="deprecation errors enabled" }
					end
				end
			end
		end

		local function recurse(value)
			if type(value) == "table" then
				table.foreachi(value, recurse)

			elseif hasDeprecatedValues and value:contains("*") then
				local current = configset.fetch(target, field, {
					matcher = function(cset, block, filter)
						local current = cset.current
						return criteria.matches(current._criteria, block._criteria.terms or {}) or
							   criteria.matches(block._criteria, current._criteria.terms or {})
					end
				})

				local mask = path.wildcards(value)
				for _, item in ipairs(current) do
					if item:match(mask) == item then
						recurse(item)
					end
				end
			else
				local value, err, additional = api.checkValue(field, value)
				if err then
					error { msg=err }
				end

				if field.deprecated then
					check(value)
				end

				table.insert(removes, value)
				if additional then
					table.insert(removes, additional)
				end
			end
		end

		local ok, err = pcall(function ()
			recurse(value)
		end)

		if not ok then
			if type(err) == "table" then
				err = err.msg
			end
			error(err, 3)
		end

		configset.remove(target, field, removes)
	end



--
-- Check to see if a value is valid for a particular field.
--
-- @param field
--    The field to check against.
-- @param value
--    The value to check.
-- @param kind
--    The kind of data currently being checked, corresponding to
--    one segment of the field's kind string (e.g. "string"). If
--    not set, defaults to "string".
-- @return
--    If the value is valid for this field, the canonical version
--    of that value is returned. If the value is not valid two
--    values are returned: nil, and an error message.
--

	function api.checkValue(field, value, kind)
		if not field.allowed then
			return value
		end

		local canonical, result
		local lowerValue = value:lower()

		if field.aliases then
			canonical = field.aliases[lowerValue]
		end

		if not canonical then
			if type(field.allowed) == "function" then
				canonical = field.allowed(value, kind or "string")
			else
				canonical = field.allowed[lowerValue]
			end
		end

		if not canonical then
			return nil, "invalid value '" .. value .. "' for " .. field.name
		end

		if field.deprecated and field.deprecated[canonical] then
			local handler = field.deprecated[canonical]
			handler.add(canonical)
			if handler.message and api._deprecations ~= "off" then
				local caller =  filelineinfo(9)
				local key = field.name .. "_" .. value .. "_" .. caller
				p.warnOnce(key, "the %s value %s has been deprecated and will be removed.\n   %s\n   @%s\n", field.name, canonical, handler.message, caller)
				if api._deprecations == "error" then
					return nil, "deprecation errors enabled"
				end
			end
		end

		return canonical
	end



---
-- Reset the API system, clearing out any temporary or cached values.
-- Used by the automated testing framework to clear state between
-- individual test runs.
---

	function api.reset()
		for containerClass in p.container.eachChildClass(p.global) do
			api.scope.global[containerClass.pluralName] = {}
		end
	end



--
-- Arrays are integer indexed tables; unlike lists, a new array value
-- will replace the old one, rather than merging both.
--

	premake.field.kind("array", {
		store = function(field, current, value, processor)
			if type(value) ~= "table" then
				value = { value }
			end

			for i, item in ipairs(value) do
				value[i] = processor(field, nil, value[i])
			end

			return value
		end,
		compare = function(field, a, b, processor)
			if a == nil or b == nil or #a ~= #b then
				return false
			end
			for i = 1, #a do
				if not processor(field, a[i], b[i]) then
					return false
				end
			end
			return true
		end
	})



---
-- Boolean field kind; converts common yes/no strings into true/false values.
---

	premake.field.kind("boolean", {
		store = function(field, current, value, processor)
			local mapping = {
				["false"] = false,
				["no"] = false,
				["off"] = false,
				["on"] = true,
				["true"] = true,
				["yes"] = true,
			}

			if type(value) == "string" then
				value = mapping[value:lower()]
				if value == nil then
					error { msg="expected boolean; got " .. value }
				end
				return value
			end

			if type(value) == "boolean" then
				return value
			end

			if type(value) == "number" then
				return (value ~= 0)
			end

			return (value ~= nil)
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})




--
-- Directory data kind; performs wildcard directory searches, converts
-- results to absolute paths.
--

	premake.field.kind("directory", {
		paths = true,
		store = function(field, current, value, processor)
			return path.getabsolute(value)
		end,
		remove = function(field, current, value, processor)
			return path.getabsolute(value)
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end,

		translate = function(field, current, _, processor)
			if current:find("*") then
				return os.matchdirs(current)
			end
			return { current }
		end
	})



--
-- File data kind; performs wildcard file searches, converts results
-- to absolute paths.
--

	premake.field.kind("file", {
		paths = true,
		store = function(field, current, value, processor)
			return path.getabsolute(value)
		end,
		remove = function(field, current, value, processor)
			return path.getabsolute(value)
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end,

		translate = function(field, current, _, processor)
			if current:find("*") then
				return os.matchfiles(current)
			end
			return { current }
		end
	})



--
-- Function data kind; this isn't terribly useful right now, but makes
-- a nice extension point for modules to build on.
--

	premake.field.kind("function", {
		store = function(field, current, value, processor)
			local t = type(value)
			if t ~= "function" then
				error { msg="expected function; got " .. t }
			end
			return value
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})



--
-- Integer data kind; validates inputs.
--

	premake.field.kind("integer", {
		store = function(field, current, value, processor)
			local t = type(value)
			if t ~= "number" then
				error { msg="expected number; got " .. t }
			end
			if math.floor(value) ~= value then
				error { msg="expected integer; got " .. tostring(value) }
			end
			return value
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})



---
-- Key-value data kind definition. Merges key domains; values may be any kind.
---

	local function storeKeyed(field, current, value, processor)
		current = current or {}

		for k, v in pairs(value) do
			if processor then
				v = processor(field, current[k], v)
			end
			current[k] = v
		end

		return current
	end


	local function mergeKeyed(field, current, value, processor)
		value = value or {}
		for k, v in pairs(value) do
			current[k] = v
		end
		return current
	end


	premake.field.kind("keyed", {
		store = storeKeyed,
		merge = mergeKeyed,
		compare = function(field, a, b, processor)
			if a == nil or b == nil then
				return false
			end
			for k in pairs(a) do
				if not processor(field, a[k], b[k]) then
					return false
				end
			end
			return true
		end,

		translate = function(field, current, _, processor)
			if not processor then
				return { current }
			end
			for k, v in pairs(current) do
				current[k] = processor(field, v, nil)[1]
			end
			return { current }
		end
	})


---
-- List data kind definition. Actually a misnomer, lists are more like sets in
-- that duplicate values are weeded out; each will only appear once. Can
-- contain any other kind of data.
---

	local function storeListItem(current, item, allowDuplicates)
		if not allowDuplicates and current[item] then
			table.remove(current, table.indexof(current, item))
		end
		table.insert(current, item)
		current[item] = item
	end


	local function storeList(field, current, value, processor)
		if type(value) == "table" then
			-- Flatten out incoming arrays of values
			if #value > 0 then
				for i = 1, #value do
					current = storeList(field, current, value[i], processor)
				end
				return current
			end

			-- Ignore empty lists
			if table.isempty(value) then
				return current
			end
		end

		current = current or {}

		if processor then
			value = processor(field, nil, value)
		end

		if type(value) == "table" then
			if #value > 0 then
				for i = 1, #value do
					storeListItem(current, value[i], field.allowDuplicates)
				end
			elseif not table.isempty(value) then
				storeListItem(current, value, field.allowDuplicates)
			end
		elseif value then
			storeListItem(current, value, field.allowDuplicates)
		end

		return current
	end


	local function mergeList(field, current, value, processor)
		value = value or {}
		for i = 1, #value do
			storeListItem(current, value[i], field.allowDuplicates)
		end
		return current
	end


	premake.field.kind("list", {
		store = storeList,
		remove = storeList,
		merge = mergeList,
		compare = function(field, a, b, processor)
			if a == nil or b == nil or #a ~= #b then
				return false
			end
			for i = 1, #a do
				if not processor(field, a[i], b[i]) then
					return false
				end
			end
			return true
		end,

		translate = function(field, current, _, processor)
			if not processor then
				return { current }
			end
			local ret = {}
			for _, value in ipairs(current) do
				for _, processed in ipairs(processor(field, value, nil)) do
					table.insert(ret, processed)
				end
			end
			return { ret }
		end
	})



--
-- Mixed data kind; values containing a directory separator "/" are converted
-- to absolute paths, other values left as-is. Used for links, where system
-- libraries and local library paths can be mixed into a single list.
--

	premake.field.kind("mixed", {
		paths = true,
		store = function(field, current, value, processor)
			if type(value) == "string" and value:find('/', nil, true) then
				if string.sub(value, 1, 2) ~= "%{" then
					value = path.getabsolute(value)
				end
			end
			return value
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})



--
-- Number data kind; validates inputs.
--

	premake.field.kind("number", {
		store = function(field, current, value, processor)
			local t = type(value)
			if t ~= "number" then
				error { msg="expected number; got " .. t }
			end
			return value
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})



--
-- Path data kind; converts all inputs to absolute paths.
--

	premake.field.kind("path", {
		paths = true,
		store = function(field, current, value, processor)
			if string.sub(value, 1, 2) == "%{" then
				return value
			end
			return path.getabsolute(value)
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})



--
-- String data kind; performs validation against allowed fields, checks for
-- value deprecations.
--

	premake.field.kind("string", {
		store = function(field, current, value, processor)
			if type(value) == "table" then
				error { msg="expected string; got table" }
			end

			if value ~= nil then
				local err
				value, err = api.checkValue(field, value)
				if err then
					error { msg=err }
				end
			end

			return value
		end,
		compare = function(field, a, b, processor)
			return (a == b)
		end
	})


--
-- Table data kind; wraps simple values into a table, returns others as-is.
--

	premake.field.kind("table", {
		store = function(field, current, value, processor)
			if type(value) ~= "table" then
				value = { value }
			end
			return value
		end,
		compare = function(field, a, b, processor)
			-- TODO: is there a reliable way to check this?
			return true
		end
	})



---
-- Start a new block of configuration settings, using the old, "open"
-- style of matching without field prefixes.
---

	function configuration(terms)
		if terms then
			if (type(terms) == "table" and #terms == 1 and terms[1] == "*") or (terms == "*") then
				terms = nil
			end
			configset.addblock(api.scope.current, {terms}, os.getcwd())
		end
		return api.scope.current
	end



---
-- Start a new block of configuration settings, using the new prefixed
-- style of pattern matching.
---

	function filter(terms)
		if terms then
			if (type(terms) == "table" and #terms == 1 and terms[1] == "*") or (terms == "*") then
				terms = nil
			end
			local ok, err = configset.addFilter(api.scope.current, {terms}, os.getcwd())
			if not ok then
				error(err, 2)
			end
		end
	end



--
-- Define a new action.
--
-- @param a
--    The new action object.
--

	function newaction(a)
		p.action.add(a)
	end


--
-- Define a new option.
--
-- @param opt
--    The new option object.
--

	function newoption(opt)
		p.option.add(opt)
	end
