-- It is intended for use by other Lua modules, and should not be called from #invoke directly.

local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType

local arguments = {}

local nilArg = {} -- Used for memoizing nil arguments in metaArgs.

-- Generate four different tidyVal functions, so that we don't have to check the options every time we call it.

local function tidyValDefault
	if type(val) == 'string' then
		val = val:match('^%s*(.-)%s*$')
		if val == '' then
			return nil
		else
			return val
		end
	else
		return val
	end
end

local function tidyValTrimOnly
	if type(val) == 'string' then
		return val:match('^%s*(.-)%s*$')
	else
		return val
	end
end

local function tidyValRemoveBlanksOnly
	if type(val) == 'string' then
		if val:find('%S') then
			return val
		else
			return nil
		end
	else
		return val
	end
end

local function tidyValNoChange
	return val
end

function arguments.getArgs
	checkType
	checkType
	frame = frame or {}
	options = options or {}

	-- Get the arguments from the frame object if available. If the frame object is not available, we are being called
	-- from another Lua module or from the debug console, so assign the args to a new variable so we can differentiate them.
	local fargs, pargs, luaArgs
	if type(frame.args) == 'table' and type(frame.getParent) == 'function' then
		if not options.parentOnly then
			fargs = frame.args
		end
		if not options.frameOnly then
			pargs = frame:getParent.args
		end
		if options.parentFirst then
			fargs, pargs = pargs, fargs
		end
	else
		luaArgs = frame
	end

	-- Set up the args and metaArgs tables. args will be the one accessed from functions, and metaArgs will hold the actual arguments.
	-- The metatable connects the two together.
	local args, metaArgs, metatable = {}, {}, {}
	setmetatable

	-- Generate the tidyVal function. If it has been specified by the user, we use that; if not, we choose one of four functions
	-- depending on the options chosen. This is so that we don't have to call the options table every time the function is called.
	local tidyVal = options.valueFunc
	if tidyVal then
		if type(tidyVal) ~= 'function' then
			error("bad value assigned to option 'valueFunc' (function expected, got " .. type(tidyVal) .. ')', 2)
		end
	elseif options.trim ~= false then
		if options.removeBlanks ~= false then
			tidyVal = tidyValDefault
		else
			tidyVal = tidyValTrimOnly
		end
	else
		if options.removeBlanks ~= false then
			tidyVal = tidyValRemoveBlanksOnly
		else
			tidyVal = tidyValNoChange
		end
	end

	local function mergeArgs
		-- Accepts multiple tables as input and merges their keys and values into one table using the specified iterator.
		-- If a value is already present it is not overwritten; tables listed earlier have precedence.
		-- We are also memoizing nil values, but those values can be overwritten.
		for _, t in ipairs(tables) do
			for key, val in iterator(t) do
				local metaArgsVal = metaArgs[key]
				if metaArgsVal == nil or metaArgsVal == nilArg then
					local tidiedVal = tidyVal
					if tidiedVal == nil then
						metaArgs[key] = nilArg
					else
						metaArgs[key] = tidiedVal
					end
				end
			end
		end
	end

	-- Set the order of precedence of the argument tables. If the variables are nil, nothing will be added to the table,
	-- which is how we avoid clashes between the frame/parent args and the Lua args.	
	local argTables = {fargs}
	argTables[#argTables + 1] = pargs
	argTables[#argTables + 1] = luaArgs

	--
	-- Define metatable behaviour. Arguments are memoized in the metaArgs table, and are only fetched from the
	-- argument tables once. Nil arguments are also memoized using the nilArg variable in order to increase
	-- performance. Also, we keep a record in the metatable of when pairs and ipairs have been called, so we
	-- do not run pairs and ipairs on fargs and pargs more than once. We also do not run ipairs on fargs and
	-- pargs if pairs has already been run, as all the arguments will already have been copied over.
	--

	metatable.__index = function 
		local val = metaArgs[key]
		if val ~= nil then
			if val == nilArg then
				return nil
			else
				return val
			end
		end
		for _, argTable in ipairs(argTables) do
			local argTableVal = tidyVal
			if argTableVal == nil then
				metaArgs[key] = nilArg
			else
				metaArgs[key] = argTableVal
				return argTableVal
			end
		end
		return nil
	end

	metatable.__newindex = function 
		if options.readOnly then
			error('could not write to argument table key "' .. tostring(key) .. '"; the table is read-only', 2)
		elseif options.noOverwrite and args[key] ~= nil then
			error('could not write to argument table key "' .. tostring(key) .. '"; overwriting existing arguments is not permitted', 2)
		elseif val == nil then
			metaArgs[key] = nilArg -- Memoize nils.
		else
			metaArgs[key] = val
		end
	end

	metatable.__pairs = function 
		if not metatable.donePairs then
			mergeArgs
			metatable.donePairs = true
			metatable.doneIpairs = true
		end
		return function 
			local nk, val = next
			if val == nilArg then
				val = nil
			end
			return nk, val
		end
	end

	metatable.__ipairs = function 
		if not metatable.doneIpairs then
			mergeArgs
			metatable.doneIpairs = true
		end
		return function 
			local val = metaArgs[i + 1]
			if val == nil then
				return nil
			elseif val == nilArg then
				val = nil
			end
			return i + 1, val
		end, nil, 0
	end

	return args
end