Документация
require( 'strict' )
local getArgs = require('Module:Arguments').getArgs
local DebugLog = require('Module:DebugLog')

local debLog = DebugLog:new()
local cp={}

-- for debug
cp.log = debLog
cp.log.enabled = true

-- Constructor
function cp:new(o)
	o = o or {}
	setmetatable(o, self)
	self.__index = self
	o.globals = {}
	return o
end

--------------------------------------------------------------------------------
-- Func maps
--------------------------------------------------------------------------------
-- filter funcs declarations

--[[
local isEmptyParam, isNotEmptyParam, isEqParam, isNotEqParam, isNamespace
local isNotNamespace

local FILTER_FUNCTIONS = {
	['parameter-is-empty']   = isEmptyParam,
	['parameter-not-empty']  = isNotEmptyParam,
	['parameter-equals']     = isEqParam,
	['parameter-not-equals'] = isNotEqParam,
	['namespace-is']         = isNamespace,
	['namespace-is-not']     = isNotNamespace,
	default = function() return true end,
}
--]]
--------------------------------------------------------------------------------
--- Filters
--  called from  processFilters() as filterFunc(options, frameArgs)
--------------------------------------------------------------------------------
--[[

function cp.isEmptyParam(options, frameArgs)
	debLog:write('invoked', 'isEmptyParam')
	if not options              then return true end
	if type(options) ~= 'table' then return true end
	if options.param == nil      then return true end
	if frameArgs[options.param] == nil or frameArgs[options.param] == '' then
		return true
	end
	return false
end

function cp.isNotEmptyParam(options, frameArgs)
	debLog:write('invoked', 'isNotEmptyParam')
	return not cp.isEmptyParam(options, frameArgs)
end

--- Nasmespace filter
function cp.isNamespace(options, frameArgs)
	if not options               then return true end
	if type(options) ~= 'table'  then
		return globals.ignoreNSChecks or false
	end
	if options.namespaces == nil then return true end

	local namespace = mw.title.getCurrentTitle().namespace
	if hasValue(options.namespaces, namespace) then
		return true
	end
	return globals.ignoreNSChecks or false
end

--- Not nasmespace filter
function cp.isNotNamespace(options, frameArgs)
	if not options               then return true end
	if type(options) ~= 'table'  then return true end
	if options.namespaces == nil then return true end

	local namespace = mw.title.getCurrentTitle().namespace
	if hasValue( options.namespaces, namespace ) then
		return globals.ignoreNSChecks or false
	end
	return true
end

--]]
--------------------------------------------------------------------------------
-- General processing
--------------------------------------------------------------------------------

--- Takes a named node in config with name 'name from node 'upNode' 
-- and returns resulting table
-- @param upNode - previous node. e.g. 'a' for: a = { <name> = { } }
-- @param name string
-- @return table
local function getConfigNode(upNode, name)
	if type(name) ~= 'string' or name == '' then
		return nil, 'Name invalid'
	end
	if type(upNode) ~= 'table' then
		return nil, 'upper node is not a table'
	end
	return upNode[name]
end

--- Takes a named node in config with name 'name from node 'upNode', adds
-- default values, defined on the same level, and returns resulting table
-- @param upNode - previous node. e.g. 'a' for: a = { <name> = { } }
-- @param name string
local function getConfigNodeWithDefaults(upNode, name)
	local node = getConfigNode(upNode, name)
	local default = upNode['default']
	if type(node) == nil then return default end
	if type(node) ~= 'table' then return node end
	if type(default) ~= 'table' then return node end

	local result = {}
	for k, v in pairs(default) do
		result[k] = v
	end
	for k, v in pairs(node) do
		result[k] = v
	end
	return result
end

--- Gets filter table from table 'node'
-- @function getFilters
-- @param node
-- @return table or nil
local function getFilters(node)
	local filters = getConfigNode(node, 'filters')
	if type(filters) ~= 'table' then return nil end
	return filters
end

--- Вызывает один за другим фильтры из массива filters
-- Если фильтров нет или что-то сконфигурировано неверно, возвращает true
-- Если хотя бы один из фильтров отработал и выдал false, возвращает false
-- Если все фильтры вернули true, возвращает true
-- @function processFilters
-- @param filtMap таблица-список фильтров вида {<filterID> = <func>, ...}
-- @param filters массив с фильтрами вида
--                { {name = <filterID>, options = {<opt1> = val1, ... } ... }
-- @param frameArgs таблица с параметрами фрейма
-- @return true если все фильтры вернули true или проблемы с параметрами
--         false если хотя бы один вернул false
local function processFilters(filtMap, filters, frameArgs)
	debLog:write('invoked ', 'processFilters')
	if type(filtMap) ~= 'table' then return true end
	if type(filters) ~= 'table' then return true end
	
	local function default() return true end

	for i, filter in ipairs(filters) do
		debLog:write('processing name: '.. tostring(filter.id), 'processFilters')
		local filterFunc = filtMap[filter.id] or filtMap[default] or default
		
		if not filterFunc(filter.options, frameArgs) then
			return false
		end
	end
	return true
end

--- Loads list of strings with rules' IDs from array '<node>.rules', and returns an
-- array with those of them, which are also exist as indexes in 'ruleMap'
-- @return array with valid rules
local function getRules(ruleMap, node)
	local rules = getConfigNode(node, 'rules')
	if type(rules) ~= 'table' then return nil end
	
	local validRules = {}
	local i = 1
	for _, ruleName in ipairs(rules) do
		if ruleMap[ruleName] then
			validRules[i] = ruleName
			i = i + 1
		end
	end
	if next(validRules) == nil then
		debLog:write('No valid rulenames found', 'getRules', 'warn')
		return nil
	end
	debLog:write('Valid rules found: '.. mw.dumpObject(validRules), 'getRules')
	return validRules
end

--- Executes a function, corresponding to rule with id 'ruleName'
--@param ruleName string with rule id
--@param node node with rule
--@param frameArgs table
--@param preset table with preset
--@return result of a called function or nil
local function processRule(node, frameArgs, preset)
	local myname = 'processRule'
	debLog:write('Invoked', myname)
	
	-- check func name presence and validity
	if type(p[node.func]) ~= 'function' then
		debLog:write('Function does not exist: ' .. node.func, myname, 'warn')
		return nil
	end
	
	-- function call
	local funcResult = p[node.func](frameArgs, preset, node)
	if funcResult == nil then 
		debLog:write(node.func .. '() returned nil', myname, 'warn')
		return ''
	end
	debLog:write(node.func .. ' returned: '..mw.text.nowiki(funcResult), myname)
	return funcResult
end

---Processes rules from array of IDs in ruleList
--@return concatenated result of rules
local function processRuleList(ruleList, node, frameArgs, preset)
	local result = ''
	for _, ruleName in ipairs(ruleList) do
		local ruleResult = processRule(node[ruleName], frameArgs, preset)
		if ruleResult == nil then
			debLog:write('Rule returned nil', 'processRuleList', 'warn')
		else 
			result = table.concat{result, ruleResult}
		end
	end
	return result
end

--------------------------------------------------------------------------------
--- Methods
--------------------------------------------------------------------------------

--- Load config from path 'configPath'
--@param configPath string
--@return true on success or or nil
function cp:getConfig(configPath)
	if configPath == nil or configPath == '' then
		debLog:write('No log provided', 'cp:getConfig', 'error')
		return nil
	end
	local success, result = pcall(mw.loadData, configPath)
	if success then
		self.config = result
		debLog:write('Config loaded: '..configPath, 'cp:getConfig')
		return true
	end
	debLog:write(result, 'cp:getConfig', 'error')
	return nil
end
	
--- Get 'options' frome config and sets them to 'globals' table of 'self'
--@param node - 'config' node
--@return globals table or nil if nothing is set
function cp:setGlobalOptions(node)
	node = node or self.config
	debLog:write('invoked','cp:setGlobalOptions')
	debLog:write(self,'cp:setGlobalOptions')
	if type(node) ~= 'table' then return nil end
	if type(node.options) ~= 'table' then return nil end
	for k, option in pairs(node.options) do
		if option == 'ignoreNSChecks' then 
			self.globals.ignoreNSChecks = self.globals.ignoreNSChecks or option
		else
			self.globals[k] = option
		end
	end
	debLog:write('Globals set: '..mw.dumpObject(self.globals), 'setGlobalOptions')
	return self.globals
end

--- Global options processing
--@param frameArgs
--@return true on ok, false on checks failed
function cp:processGlobalOptions(frameArgs)
	local nocat = frameArgs[self.globals.nocatParamName]
	if nocat ~= nil and nocat ~= '' then
		self.globals.nocat = nocat
		debLog:write('Categorization denied', 'cp:processGlobalOptions')
		return false
	end
	local from = frameArgs[self.globals.fromParamName]
	if from ~= nil and from ~= '' then
		if not mw.wikibase.isValidEntityId(from) then
			debLog:write('EntityID not valid: '.. from, 'processGlobalOptions')
			return false
		end
		if not mw.wikibase.entityExists(from) then
			debLog:write('Entity does not exist:'.. from, 'processGlobalOptions')
			return false
		end
		self.globals.from = from
	end
	return true
end

--- Get preset 'name' from table 'node' and enriches it with default values
--@function getPreset
--@param name string
--@return table or nil
function cp:preparePreset(name)
	local node = self.config.presets
	local workPreset = getConfigNodeWithDefaults(node, name)
	if type(workPreset) ~= 'table' then
		return nil
	end
	self.workPreset = workPreset
	return true
end

--- Init function maps table
--@param funcMap table
--@return true or false
function cp:initFuncMaps(funcMap)
	if type(funcMap) ~= 'table' then return nil end
	self.funcMaps = funcMap
	return true
end

---Process filters from work preset
--@param frameArgs
--@return true on ok, false on checks failed
function cp:processPresetFilters(frameArgs)
	local filters = getFilters(self.workPreset)
	return processFilters(self.funcMaps.filters, filters, frameArgs)
end

---Get valid rules' list from workPreset and check vs. funcMaps
--@param
--@return
function cp:prepareRuleStack()
	getRules(self.funcMaps.tasks, self.workPreset)
end

--- Init 
--@param 
--@return
function cp:init()
	return getRules(ruleMap, node)
end

function cp:process()
	return	
end

local p = cp
return p