Документация
require( 'strict' )
local p = {}

local mwLang = mw.getContentLanguage()
local getArgs = require('Module:Arguments').getArgs
local DebugLog = require('Module:DebugLog')
local config
local DEFAULT_CONFIG = 'Module:Песочница/Abiyoyo/Autosorting/config'
local schemas = mw.loadData('Module:Песочница/Abiyoyo/Autosorting/schemas')

local debLog = DebugLog:new()
local globals = {
	ignoreNSChecks = false,
}

--------------------------------------------------------------------------------
-- Функции общего назначения
--------------------------------------------------------------------------------

-- проверка пустого значения
local function isEmpty(val)
	return val == nil or val == ''
end

-- Проверка наличия value среди значений tab
local function hasValue( tab, value )
    for _, val in pairs(tab) do
        if val == value then return true end
    end
    return false
end

--------------------------------------------------------------------------------
--- Фильтры
-- вызываются из processFilters() в виде filterFunc(options, frameArgs)
--------------------------------------------------------------------------------
local function 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

local function isNotEmptyParam(options, frameArgs)
	debLog:write('invoked', 'isNotEmptyParam')
	return not isEmptyParam(options, frameArgs)
end
local function isEqParam(options, frameArgs)
	return true
end
local function isNotEqParam(options, frameArgs)
	return true
end

--- Nasmespace filter
local function 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
local function 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

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,
}

--------------------------------------------------------------------------------
-- Функции для формирования имён категорий
--------------------------------------------------------------------------------

-- получает из пресета preset части имени категории и возвращает
-- таблицу с ними
local function catParts(preset)
	local result = {}
	result.sep      = preset.sep
	result.innersep = preset.innersep
	result.lbracket = preset.lbracket
	result.rbracket = preset.rbracket
	result.prefix   = preset.prefix
	result.postfix  = preset.postfix

	return result
end

-- Формирует строку с "сортировочной" частью имени категории, используется в  
-- функции formatCategoryName() для обработки параметра values. 
-- values - или (1) строка <string1>
--          или (2) таблица вида {<string1>, <string2>, ... }
--          или (3) таблица вида { { <string11>, <string12> }, ... }
-- на выходе выдает:
-- (1) => <string1>
-- (2) => <string1><sep><string2>...
-- (3) => <string11><innersep><string12><sep><string21><innersep><string22>...
local function formatChain(values , sep, innersep)
	values = values or ''
	sep = sep or ';'
	innersep = innersep or ':'
	
	if type(values) == 'string' then return values  end -- вариант (1)
	if type(values) ~= 'table'  then return '' end
	if next(values) == nil      then return '' end
	
	if type( select(2, next(values)) ) == 'string' then -- вариант (2)
		return table.concat(values, sep)
	elseif type( select(2, next(values)) ) == 'table' then -- вариант (3)
		local result = ''
		for k, subtable in ipairs(values) do
			result = result .. table.concat(subtable, innersep)
		end
		return result
	end
	deblog:write('unexpected value', 'formatSortChain', 'error')
	return ''
end
	
-- Формирует название категории. Возвращает строку с именем категории по шаблону,
-- заданому в конфиге (config.presets),
--
-- preset - таблица с пресетом
-- values - содержание "сортировочной" части. Формат описан в formatChain()
local function formatCategoryName(preset, values)
	values = values or ''
	local result = ''

	-- Получение фрагментов имени категории
	local p = catParts(preset)
	local chain = formatChain(values , p.sep, p.innersep)
	return table.concat{ p.prefix, p.lbracket, chain, p.rbracket, p.postfix }
end

-- форматирует строку категории по имени и ключу сортировки
local function formatCategory(name, sortKey)
	if isEmpty(name) then
		debLog:write('No category name', 'formatCategory', 'warn')
		return ''
	end
	local sortStr = ( isEmpty(sortKey) and '' ) or ( '|' .. tostring(sortKey) )
	return table.concat{'[', '[Category:', tostring(name), sortStr, ']', ']'}
end

--------------------------------------------------------------------------------
-- Функции, используемые обработчиками
--------------------------------------------------------------------------------

-- Получить соответствие категории критериям
local function isValidCategory(preset, catName, property, checkLimits)
	local allowRed = false
	if not isEmpty(preset) then 
		allowRed = preset.allowred or false
	end
	
	if not checkLimits then
		local success, title = pcall(mw.title.new, 'Category:' .. catName)
		if success and allowRed then
			return true
		elseif success and not isEmpty(title) and title.exists then
			return true
		end
	end
	
	return false
end

-- Получить категорию для отсутствия изображений
local function getFileCategory( frame, name, property, entityId )
	if isEmpty(name) then
		return ''
	end
	frame = frame or mw.getCurrentFrame()
	
	local propValues = getWikidataProperty( frame, property, entityId, 3 )
	
	local catName = getCategoryName( name, property )
	local instanceOf = getWikidataProperty( frame, 'p31', entityId, 3 )
	instanceOf = #instanceOf > 0 and instanceOf[ 1 ] or ''
	local pageTitle = mw.title.getCurrentTitle().fullText
	
	local result = string.format('['..'[Category:%s|%s%s]'..']', catName,
		                         instanceOf, pageTitle )
	return #propValues, result
end

-- Берет таблицу schema и на сновании неё возвращает пару значений: статус и
-- таблицу значений в соответствии со схемой. Значения берутся из конфига или из 
-- параметров фрейма. Используется для загрузки настроек обработчиков.
local function populateSettings(func, schema, frameArgs, currentRule)
	local myname = 'populateSettings'
	local result = {}
	
	-- обертка для mw.ustring.find
	local function patternCheckPassed(settingVal, pattern)
		if pattern ~= nil and type(settingVal) == 'string' then
			return mw.ustring.find(settingVal, pattern)
		end
		return true
	end
	-- проверка типов, тип м.б. передан строкой или таблицей
	local function typeCheckPassed(settingVal, typeDesc)
		if isEmpty(typeDesc) then return true end
		if type(typeDesc) == 'string' then
			typeDesc = {typeDesc}
		end
		for _, curType in ipairs(typeDesc) do
			if type(settingVal) == curType then return true end
		end
		return false
	end

	for settingName, propsTab in pairs(schema) do
		local valueIndex = propsTab.valueIndex or settingName
		local settingVal = currentRule.options[settingName]
		
		-- нет в конфиге
		if settingVal == nil then
			if propsTab.mandatory then
				debLog:write('Mandatory setting is empty: '..settingName, myname, 'warn')
				return false
			-- записываем только если не установлен флаг overrided или значения
			-- нет. Если есть и флаг true, пропускаем
			elseif not propsTab['overrided'] or isEmpty(result[valueIndex]) then
				-- пишем дважды на случай, если settingName ~= valueIndex
				result[settingName] = propsTab.default 
				result[valueIndex]  = propsTab.default
			end
		-- тип не совпадает
		elseif not typeCheckPassed(settingVal, propsTab['type']) then
			debLog:write('Type mismatch in config: ' .. settingName, myname, 'warn')
			return false
		-- паттерн не совпадает
		elseif not patternCheckPassed(settingVal, propsTab['pattern']) then
			local msg = string.format('Pattern mismatch: %s', settingName)
			debLog:write(msg, myname, 'warn')
			return false
		elseif propsTab['external'] then --настройки, читаемые из фрейма
			local settExtVal = frameArgs[settingVal]
			if not patternCheckPassed(settExtVal, propsTab['valuePattern']) then
				local err = string.format('Pattern mismatch: %s', settingVal)
				debLog:write(err, myname, 'warn')
				return false
			elseif not propsTab['overrided'] or isEmpty(result[valueIndex]) then
				-- если помечено overrided, то заполняем если иных значений нет
				result[settingName] = settingVal
				result[valueIndex] = settExtVal
			elseif propsTab['overrided'] then
				-- в любом случае пишем сам параметр
				result[settingName] = settingVal
			end
		elseif not propsTab['overrided'] or isEmpty(result[valueIndex]) then
			result[settingName] = settingVal
			result[valueIndex] = settingVal
		elseif propsTab['overrided'] then
			result[settingName] = settingVal
		end
	end
	return true, result
end

--------------------------------------------------------------------------------
--[[ Обработчики ]]
-- Вызываются из processRule() в виде
-- p[funcName](frameArgs, preset, currentRule)
-- frameArgs - параметры фрейма
-- preset - таблица с пресетом. Валидность проверяется 
--             на этапе препроцессинга. В обработчиках не проверяется.
-- currentRule - таблица с описанием иекущего правила из конфига. Валидируется 
--            на этапе препроцессинга.
--------------------------------------------------------------------------------

--[[ function p._sortByProperty() ]]
-- Формирует категории в разрезе указанного свойства
-- Описание используемых настроек конфига и соответствующей логики см. в коде.
function p._sortByProperty( frameArgs, preset, currentRule, entityId )
	local myname = '_sortByProperty'
	debLog:write('Invoked', myname)

	local settingsSchema = schemas.workers._sortByProperty.configOptions 
	------
	-- 1) загрузка из конфига и валидация по схеме в таблицу configOptions
	-- 2) getSettings (configOptions, frameArgs) => settings - здесь описаны правила трансформации
	-- в виде { [<setting name>] = f({configOptions}, {frameArgs}) }
	-- например f может быть:
	-- setting1 = configOption[index1] or frameArgs[index2] - аргументы фрейма используются как дефолтное значение
	-- setting2 = frameArgs[index3] or configOption[index4] - аргументы фрейма перекрывают конфиг
	-- setting3 = frameArgs[ configOption[index5] ] or configOption[index6] - из фрейма берется параметр с именем,
	--                                                                        заданным в конфиге, и перекрывает конфиг
	-- setting4 = configOption[i7] or frameArgs[ configOption[i8] ] - из фрейма бьерется параметр с именем,
	--                                                                заданным в конфиге, и используется как дефолт
	-- setting5 = configOption[i9] or configOption[i10]   - одна из двух опций конфига должна быть задана, первая с приоритетом,
	--                                                      может использоваться как булева опция "или"
	-- setting6 = configOption[i10] and configOption[i11] - "обе должны быть заданы" (как булева опция "и")
	--                                                      или "вторая опция при условии наличия первой"
	
	-- итого набор правил трансформации:
	-- A [or/and] B, где А и В принимают значения из:
	--    configOption[<index>]
	--    frameArgs[<index>]
	--    frameArgs[ configOption[<index>] ]
	
	-- configOptions:
	-- ...
	-- ifNotEmptyPar = 'not-empty-argument',
	-- ifNotEmpty = 'config value',
	--...
	-- in {settings} result should be:
	-- settings.ifNotEmptyVal =  frameArgs[ configOption[ifNotEmptyPar] ] or configOption[ifNotEmpty]
	-- settings.checkIfNotEmptyVal = configOption[ifNotEmptyPar] or configOption[ifNotEmpty] // effectively boolean
	
	
	local succ, settings = populateSettings(myname, settingsSchema,
											frameArgs, currentRule)
	if succ then
		debLog:write('Settings populated: ' .. mw.dumpObject(settings), myname)
	else
		debLog:write('Settings population failed', myname, 'warn')
		return nil
	end
	
	-- [[логические препроверки]]
	-- передано значение, которое д.б. пустым
	if settings.ifEmptyVal and settings.ifEmptyPar then
		local msg = string.format('Value not empty: |%s=%s, skipped',
								  settings.ifEmptyPar, settings.ifEmptyVal)
		debLog:write(msg, myname )
		return ''
	end
	-- не передано значение, которое д.б. НЕпустым
	if not settings.ifNotEmptyVal and settings.ifNotEmptyPar then
		local msg = string.format('Value is empty: |%s=%s, skipped',
								 settings.ifNotEmptyPar, settings.ifNotEmptyVal)
		debLog:write(msg, myname)
		return ''
	end

	-- получение ID сущности Викиданных
	entityId = entityId or frameArgs['from']
	if isEmpty(entityId) then
		entityId = mw.wikibase.getEntityIdForCurrentPage()
	end
	-- Если сущности нет
	if isEmpty(entityId) then
		debLog:write('No Entity ID for current page', myname)
		-- попытка использовать дефолтное значение
		if not isEmpty(settings.propDefVal) then 
		
			-- сюда формирование результата на основе дефолтного значения
			
		else -- дефолтного тоже нет
			
			-- сюда результаты сортировки вида "тип не указан"/"значение св-ва не указано"
			
			return ''
		end
		
		-- сюда часть снизу
		
	end
	
	-- получение свойства
	local statements = mw.wikibase.getBestStatements(entityId, settings.property)
	if isEmpty(statements) or next(statements) == nil then
		debLog:write('No claims with property ' .. settings.property, myname)
		return ''
	end
	--временно:
	debLog:write('Result from WD: ' .. mw.dumpObject(statements[1].mainsnak.datavalue.value), myname)
	
	
	-- на данном этапе проперти загружены и есть как минимум один результат с клеймом
	
	-- постпроверки условий (или перенести перед?)
	
	-- доделать заглушки
	
	-- проверка, что свойство propId содержит значение propVal
	local function ifPropContains(entityId, propId, propVal)
		-- заглушка
		return true
	end
	-- проверка, что свойство propId НЕ содержит значение propVal
	local function ifPropNotContains(entityId, propId, propVal)
		-- заглушка
		return true
	end
	
	local function ifEmptyProp(entityId, propId)
		-- заглушка
		return true
	end
	
	local function ifNotEmptyProp(entityId, propId)
		-- заглушка
		return true
	end
	
	-- Проверка, что свойство содержит требуемое в настройках значение
	if ifPropContains(entityId, settings.ifPropContainsID, settings.ifPropContainsVal) then
		debLog:write('Property contains required value, ok', myname )
	else 
		debLog:write('Property does not contain required value', myname)
		return ''
	end
	-- Проверка, что свойство НЕ содержит требуемое в настройках значение
	if ifPropNotContains(entityId, settings.ifPropContainsID, settings.ifPropContainsVal) then
		debLog:write('Property does not contain value required to be not contained, ok', myname )
	else 
		debLog:write('Property contains value required to be not contained', myname)
		return ''
	end
	-- Проверка, что свойства не содержат никаких утверждений
	if ifEmptyProp(entityId, settings.ifEmptyPropsVal) then
		debLog:write('Properties required to be empty are empty, ok', myname )
	else 
		debLog:write('Properties required to be empty are not empty', myname)
		return ''
	end
	-- Проверка, что свойства содержат хоть какие-то утверждения
	if ifNotEmptyProp(entityId, settings.ifNotEmptyPropsVal) then
		debLog:write('Properties required to be not empty are not empty, ok', myname )
	else 
		debLog:write('Properties required to be not empty are empty', myname)
		return ''
	end
	
	local function getPropLabel(entityId, propId)
		-- заглушка
		return 'someproplabel'
	end
	local propLabel = settings.propLabel
	if isEmpty(propLabel) then
		propLabel = getPropLabel(entityId, settings.property)
	end
	
	-- ... формирование результата и проверка категорий
	
	local chains = {}
	for i, statement in ipairs(statements) do
		-- тут может быть обычное вэлью, надо дописывать 
		debLog:write(statement.mainsnak.datavalue.value, myname,'warn')
		if type(statement.mainsnak.datavalue.value) == 'table' then
			local valueLabel = mw.wikibase.getLabel(statement.mainsnak.datavalue.value.id) 
			local chain = {}
			chains[i]={propLabel, valueLabel} -- это неправильно, не хватает уровня вложенности, но потом все равно менять
		end
	end
	
	-- это временно и неправильно
	local catName = formatCategoryName(preset, chains)
	local cat = formatCategory (catName)
	local result = cat
	--[[
	-- проверка, что есть следующий уровень
	local thenBy = currentRule.thenBy
	-- рекурсивный вызов
	if not isEmpty( thenBy ) and type( thenBy ) == 'table' then
		p._sortByProperty(frameArgs, preset, thenBy)
	end
	--]]
	return result
end

function p._catByProperty(frameArgs, preset, currentRule)
	return nil
end

function p._catLocalFileWithoutWD(frameArgs, preset, currentRule)
	return nil
end

function p._catNoEntity(frameArgs, preset, currentRule)
	return nil
end

function p._category(frameArgs, preset, currentRule)
	if currentRule.options.category ~= nil then
		return formatCategory(currentRule.options.category)
	else 
		return nil
	end
end

--------------------------------------------------------------------------------
-- General processing
--------------------------------------------------------------------------------

--- Load config from path 'configPath'
--@param configPath string
--@return true on success or or nil
local function getConfig(configPath)
	if configPath == nil or configPath == '' then
		debLog:write('No log provided', 'getConfig', 'error')
		return nil
	end
	local success, result = pcall(mw.loadData, configPath)
	if success then
		config = result
		debLog:write('Config loaded: '..configPath, 'getConfig')
		return true
	end
	debLog:write(result, 'getConfig', 'error')
	return nil
end

--- Get 'options' frome config and sets them to 'globals'
--@param node - 'config' node
--@return globals table or nil if nothing is set
local function setGlobalOptions()
	if type(config.options) ~= 'table' then return nil end
	for k, option in pairs(config.options) do
		if option == 'ignoreNSChecks' then 
			globals.ignoreNSChecks = globals.ignoreNSChecks or option
		else
			globals[k] = option
		end
	end
	debLog:write('Globals set: '..mw.dumpObject(globals), 'setGlobalOptions')
	return globals
end

--- Global options processing
--@param frameArgs
--@return true on ok, false on checks failed
local function processGlobalOptions(frameArgs)
	local nocat = frameArgs[globals.nocatParamName]
	if nocat ~= nil and nocat ~= '' then
		globals.nocat = nocat
		debLog:write('Categorization denied', 'cp:processGlobalOptions')
		return false
	end
	local from = frameArgs[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
		globals.from = from
	end
	debLog:write('Globals: '.. mw.dumpObject(globals), 'processGlobalOptions')
	return true
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 = 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 = 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

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

--- Loads list of strings with rules' IDs from array 'node', 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 = 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 funcOptions = node.options -- вставить в вызов вместо пресета
	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

--------------------------------------------------------------------------------
-- Методы
--------------------------------------------------------------------------------
-- временная обертка для старых методов, потом содержимое перенести в main
local function pcall_main(func, ...)
	local success, mainstate, result = pcall(p._main, unpack(arg))
	if not success then
		debLog:write(mainstate, '_main', 'error')
		return ''
	elseif not mainstate then
		debLog:write(result, '_main')
		return ''
	else 
		return result
	end
end
-- Шаблон сортировки по типам
function p.byType(frame)
	local frameArgs = getArgs(frame)
	local presetName = frameArgs[1]
	return pcall_main(p._main, frameArgs, presetName)
end

-- Шаблон сортировки по странам
function p.byCountry(frame)
	local frameArgs = getArgs(frame)
	local presetName = frameArgs[1]
	return pcall_main(p._main, frameArgs, presetName)
end

-- Шаблон сортировки по изображениям
function p.byImage(frame)
	local frameArgs = getArgs(frame)
	local presetName = 'статьи без изображений'
	return pcall_main(p._main, frameArgs, presetName)
end

--- Call for debug and logs
function p.loggedCall(frame)
	globals.ignoreNSChecks = true
	debLog.enabled = true
	local frameArgs = getArgs(frame)
	debLog:write('Invoked with args: '.. mw.dumpObject(frameArgs), 'loggedCall')
	
	local functionName = frameArgs['function']
	local success, result = pcall(p[functionName], frame)
	if not success then
		debLog:write(result, 'loggedCall', 'error')
		return debLog:getAll()
	end
	return debLog:getAll() .. mw.text.nowiki(result)
end

---Основная функция.
-- параметр presetName нужен для поддержки старых методов типа byImage и т.п.
-- Больше низачем не нужен, потом надо убрать
function p._main (frameArgs, presetName)
	debLog:write('Invoked with args: '..mw.dumpObject(frameArgs), '_main')
	--совместимость со старыми методами вызова
	local presetName = presetName or frameArgs['preset']
	
	-- inits and checks
	local configPath = frameArgs['config'] or DEFAULT_CONFIG
	if not getConfig(configPath) then
		return nil, 'Config load failed'
	end

	setGlobalOptions()
	if not processGlobalOptions(frameArgs) then
		return nil, 'Global options interruption'	
	end

	-- get preset enriched with defaults
	local workPreset = preparePreset(presetName)
	if workPreset == nil then
		return nil, 'Preset is empty'
	end

	-- check page against preset filters
	local filters = getFilters(workPreset)
	if not processFilters(FILTER_FUNCTIONS, filters, frameArgs) then
		return nil, 'No filter matches'
	end

	-- get valid rules from preset and check vs. rules' definitions
	local ruleList = getRules(config.rules, workPreset)
	if ruleList == nil then 
		return nil, 'Rule list is nil'
	end
	
	-- process rules and concat result
	local rulesResult = processRuleList(ruleList, config.rules, frameArgs, workPreset)
	if rulesResult == nil then 
		return nil, 'Rules result is nil'
	end
	
	return true, rulesResult
end

function p.main(frame)
	local frameArgs = getArgs(frame)
	return pcall_main(p._main, frameArgs)
end

return p