Documentation for this module may be created at Module:array/doc

local export = {}

local debug_track_module = "Module:debug/track"
local function_module = "Module:fun"
local table_module = "Module:table"

local getmetatable = getmetatable
local ipairs = ipairs
local pairs = pairs
local rawget = rawget
local rawset = rawset
local require = require
local select = select
local setmetatable = setmetatable
local sort = table.sort
local type = type
local upper = string.upper

--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
	local function debug_track(...)
		debug_track = require(debug_track_module)
		return debug_track(...)
	end
	
	local function deepcopy(...)
		deepcopy = require(table_module).deepcopy
		return deepcopy(...)
	end
	
	local function list_to_set(...)
		list_to_set = require(table_module).listToSet
		return list_to_set(...)
	end
	
	local function shallowcopy(...)
		shallowcopy = require(table_module).shallowcopy
		return shallowcopy(...)
	end

--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
	local m_function
	local function get_m_function()
		m_function, get_m_function = require(function_module), nil
		return m_function
	end
	
	local m_table
	local function get_m_table()
		m_table, get_m_table = require(table_module), nil
		return m_table
	end
	
	-- Functions from [[Module:table]] that operate on arrays or sparse arrays.
	-- List copied from [[Module:table/documentation]].
	local m_table_array_funcs
	local function get_m_table_array_funcs()
		m_table_array_funcs = list_to_set{
			-- non-sparse
			"removeDuplicates", "length", "size", "contains", "serialCommaJoin",
			"reverseIpairs", "reverse", "invert", "listToSet", "isArray",
			-- sparse
			"numKeys", "maxIndex", "compressSparseArray", "indexPairs", "indexIpairs",
			"sparseIpairs",
			-- tables in general
			"shallowcopy", "deepcopy",
		}
		get_m_table_array_funcs = nil
		return m_table_array_funcs
	end
	
	-- Functions from [[Module:fun]] that take an array in the second argument.
	-- They just have to have the argument order reversed to work as methods of the
	-- array object.
	local m_function_array_funcs
	local function get_m_function_array_funcs()
		m_function_array_funcs = list_to_set{
			"map", "some", "all", "filter", "fold"
		}
		get_m_function_array_funcs = nil
		return m_function_array_funcs
	end
	
	-- Functions from [[Module:table]] that create an array or table.
	-- Not all of these operate on arrays.
	local m_table_new_array_funcs
	local function get_m_table_new_array_funcs()
		m_table_new_array_funcs = list_to_set{
			-- Array.
			"removeDuplicates", "numKeys", "compressSparseArray",
			"keysToList", "reverse",
			-- Array or table.
			"shallowcopy", "deepcopy",
		}
		get_m_table_new_array_funcs = nil
		return m_table_new_array_funcs
	end
	
	-- Functions from [[Module:fun]] that create an array or table.
	-- Not all of these operate on arrays.
	local m_function_new_array_funcs
	local function get_m_function_new_array_funcs()
		m_function_new_array_funcs = list_to_set{
			"map", "filter",
		}
		get_m_function_new_array_funcs = nil
		return m_function_new_array_funcs
	end

-- Add aliases for the functions from [[Module:table]] whose names
-- contain "array" or "list", which is redundant.
-- The key redirects to the value.
local alias_of = {
	compress = "compressSparseArray",
	keys = "keysToList",
	toSet = "listToSet",
}

local function underscore_to_camel_case(str)
	if type(str) ~= "string" then
		return str
	end
	return (str:gsub("_(.)", upper))
end

local function track_underscore_to_camel_case()
	debug_track("array/underscore to camel case")
end

local function get_module_function(key, module, module_name)
	return module[key] or
		error(("Cannot find %s in [[Module:%s]]"):format(mw.dumpObject(key), module_name))
end

local function wrap_in_array_constructor(func)
	return function (...)
		return export(func(...))
	end
end

local function maybe_wrap_in_array_constructor(func, key, new_array_funcs)
	if not new_array_funcs[key] then
		return func
	end
	return wrap_in_array_constructor(func)
end

local array_mt
local function get_array_mt()
	-- Copy table library so as not to unexpectedly change the behavior of code that
	-- uses it.
	local Array = deepcopy(table)
	Array.ipairs = ipairs
	Array.pairs = pairs
	Array.unpack = unpack
	Array.listToText = mw.text.listToText
	
	-- Create version of table.sort that returns the table.
	function Array:sort(comp)
		sort(self, comp)
		return self
	end
	
	function Array:type()
		local mt = getmetatable(self)
		return mt and type(mt) == "table" and rawget(mt, "__type") or nil
	end
	
	function Array:adjustIndex(index)
		index = index - index % 1
		if index < 0 then
			index = self:length() + index + 1
		end
		return index
	end
	
	-- string.sub-style slicing.
	function Array:slice(i, j)
		if i == nil then
			i = 1
		elseif type(i) == "number" then
			i = self:adjust_index(i)
		else
			error("Expected number, got " .. type(i))
		end
		
		if j == nil or type(j) == "number" then
			j = self:adjust_index(j or -1)
		else
			error("Expected number, got " .. type(j))
		end
		
		local new_arr = export()
		local k = 0
		for index = i, j do
			k = k + 1
			new_arr[k] = self[index]
		end
		return new_arr
	end
	
	local Array_mt = {}
	setmetatable(Array, Array_mt)
	
	function Array_mt:__index(key)
		if type(key) ~= "string" then
			return nil
		end
		
		-- Convert underscores to camel case: num_keys -> numKeys.
		-- FIXME: this is pointless overhead: remove once nothing relies on it.
		local normalized_key = underscore_to_camel_case(key)
		if normalized_key ~= key then
			key = normalized_key
			normalized_key = true
		else
			normalized_key = false
		end
		key = alias_of[key] or key
		
		local func = rawget(self, key)
		if func ~= nil then
			if normalized_key then
				track_underscore_to_camel_case()
			end
			return func
		elseif (m_table_array_funcs or get_m_table_array_funcs())[key] then
			func = maybe_wrap_in_array_constructor(
				get_module_function(key, m_table or get_m_table(), "table"),
				key,
				m_table_new_array_funcs or get_m_table_new_array_funcs()
			)
		elseif (m_function_array_funcs or get_m_function_array_funcs())[key] then
			local raw_func = get_module_function(key, m_function or get_m_function(), "fun")
			if key == "fold" then
				function func(t, f, accum)
					return raw_func(f, t, accum)
				end
			else
				function func(a, b)
					return raw_func(b, a, true)
				end
			end
			func = maybe_wrap_in_array_constructor(
				func,
				key,
				m_function_new_array_funcs or get_m_function_new_array_funcs()
			)
		else
			return nil
		end
		
		if normalized_key then
			track_underscore_to_camel_case()
		end
		
		rawset(Array, key, func)
		return func
	end
	
	array_mt = {
		__index = Array,
		__type = "array",
	}
	
	function array_mt.__add(a, b)
		if not (type(a) == "table" and type(b) == "table") then
			return a - b -- Force arithmetic error.
		end
		local new_arr = export.shallowcopy(a)
		for _, val in ipairs(b) do
			new_arr:insert(val)
		end
		return new_arr
	end
	
	get_array_mt = nil
	return array_mt
end

-- A function to convert string key-table modules such
-- as [[Module:languages/data/2]] into arrays.
-- "from" is a bad name.
-- field_for_key supplies the field name in which the
-- key will be stored.
function export.from(map, field_for_key)
	local arr, i = {}, 0
	for key, val in pairs(map) do
		i = i + 1
		local new_val = shallowcopy(val)
		if field_for_key then
			new_val[field_for_key] = key
		end
		arr[i] = new_val
	end
	return export(arr)
end

local export_mt = {}

function export_mt:__call(...)
	local arr
	if select("#", ...) == 1 and type((...)) == "table" then
		arr = ...
		local mt = getmetatable(arr)
		-- If table has been loaded with mw.loadData, copy it to avoid the
		-- limitations of it being a virtual table.
		if mt and type(mt) == "table" and rawget(mt, "mw_loadData") == true then
			arr = shallowcopy(arr)
		end
	else
		arr = {...}
	end
	return setmetatable(arr, array_mt or get_array_mt())
end

function export_mt:__index(key)
	-- Convert underscores to camel case: num_keys -> numKeys.
	-- FIXME: this is pointless overhead: remove once nothing relies on it.
	local normalized_key = underscore_to_camel_case(key)
	if normalized_key ~= key then
		key = normalized_key
		normalized_key = true
	else
		normalized_key = false
	end
	key = alias_of[key] or key
	
	local func = rawget(self, key)
	if func ~= nil then
		if normalized_key then
			track_underscore_to_camel_case()
		end
		return func
	elseif (m_table_new_array_funcs or get_m_table_new_array_funcs())[key] then
		func = get_module_function(key, m_table or get_m_table(), "table")
	elseif (m_function_new_array_funcs or get_m_function_new_array_funcs())[key] then
		func = get_module_function(key, m_function or get_m_function(), "fun")
	else
		return nil
	end
	
	if normalized_key then
		track_underscore_to_camel_case()
	end
	
	func = wrap_in_array_constructor(func)
	
	rawset(export, key, func)
	return func
end

return setmetatable(export, export_mt)