--[[ **********************************************************************************************************************************
*** Option registration ***************************************************************************************************************
*************************************************************************************************************************************]]
--[[
    std::string     name;           // unique option identifier (allow Lua code to replace exising options)
    TOptionHandler  parser;         // function that process the option value and returns true on success
    int             priority;       // options with higher priorities are processed first - required to handle several too similar options
    StringVector    texts;          // list of option texts as displayed in the help screen (only the first two are displayed)
    std::string     help;           // textual option description for displayed help
    bool            for_preparsing; // option that should be recognized at the preparsing stage
    StringVector    prefixes;       // list of option prefixes (autogenerated - text minus value description)
    TOptionParser   parser;         // Action called to process recognized opttion; should set errcode if option value can't be accepted
    TOptionPostProcessor post_process; // Action called after all options are processed to perform action defined by the option
]]
options  = List.new()       -- List of option descriptors (for all options supported by the program)
optvalue = optvalue or {}   -- Default handlers store value of option NAME in optvalue.NAME

function parse_num(str)
    str = str:gsub("[',]", '')
    return tonumber(str)
end

-- Parse str as memory amount, according to its last char or `spec` parameter
function parse_mem(str,spec)
    spec = spec or 'b'
    if str:sub(-1) == 'b' then
        str = str:sub(1,-2)
        spec = 'b'
    end
    local last_char = str:sub(-1)
    if last_char:one_of(MEM_SUFFIXES) then
        str = str:sub(1,-2)
        spec = last_char
    end
    local n = parse_num(str)
    if n then
        if spec=='^' then
            n = 2^n
        else
            local pos = string.find(MEM_SUFFIXES,spec,1,true)
            if pos  then n = n*(1024^pos) end
        end
    end
    return n
end
MEM_SUFFIXES = 'kmgtpezy^'

-- Parse str either as memory size or percents of RAM size
function parse_phys_mem(str,spec)
    if str:sub(-1):one_of('%p') then
        local percents = parse_num(str:sub(1,-2))
        if percents then return percents*(RAM_SIZE/100) end
    else
        return parse_mem(str,spec)
    end
end

function try_parse_num(name,str)
    local n = parse_num(str)
    if n then
        optvalue[name] = n
    else
        return 'cannot parse "'..str..'" as number'
    end
end

function try_parse_mem(name,str,spec)
    local size = parse_mem(str,spec)
    if size then
        optvalue[name] = size
    else
        return 'cannot parse "'..str..'" as size'
    end
end

function try_parse_phys_mem(name,str,spec)
    local size = parse_phys_mem(str,spec)
    if size then
        optvalue[name] = size
    else
        return 'cannot parse "'..str..'" as memory size'
    end
end

function try_parse_bool(name,str)
    if str=='' then   -- boolean options should have no extra data after the option name
        optvalue[name] = true
    else
        return 'extra text "'..str..'" in the option that doesn\'t take any parameters'
    end
end

OPTION_BYTES     = 'b'
OPTION_KILOBYTES = 'k'
OPTION_MEGABYTES = 'm'
OPTION_GIGABYTES = 'g'
OPTION_TERABYTES = 't'
OPTION_PETABYTES = 'p'
OPTION_POWER     = '^'
OPTION_NUMBER    = '%'
OPTION_STRING    = '$'
OPTION_LIST      = '*'
OPTION_BOOL      = '?'

-- Add/modify option named `name` in the `options` list
function AddOption(option)
    local name, text, help, parser = table.unpack(option)
    local texts = stringx.split(text or '')
    if text:match('^%s*%-%-')  then texts:insert(1,'') end   -- the first help column should contain only options starting with a single '-'
    local post_processing = option.post_processing

    if type(parser) == "string" then
        local spec = parser
        if string.find (MEM_SUFFIXES..'b', spec, 1, true)  then  parser = function(param,name);  return try_parse_mem(name,param,spec);  end
        elseif spec==OPTION_NUMBER                         then  parser = function(param,name);  return try_parse_num(name,param);       end
        elseif spec==OPTION_STRING                         then  parser = function(param,name);  optvalue[name] = param;                 end
        elseif spec==OPTION_LIST                           then  parser = function(param,name);  optvalue[name] = optvalue[name] or {};  table.insert(optvalue[name], param);  end
        elseif spec==OPTION_BOOL                           then  parser = function(param,name);  return try_parse_bool(name,param);      end
        else assert (nil, string.format('Invalid option type: "%s"', spec))
        end
        if post_processing then
            local original_post_processing = post_processing
            post_processing = function(...)
                if optvalue[name]  then return original_post_processing (optvalue[name], ...) end
            end
        end
    end

    if parser then
        local original_parser = parser
        parser = function(param)
            local errmsg  =  original_parser (param, name)
            if errmsg then
                return errmsg
            elseif option.cpp and optvalue[name]~=nil then  -- to do: replace this ugly hack with better solution
                command[name] = optvalue[name]   -- Successfully parsed C++ option values are copied into the command structure
            end
        end
    else
        parser = function(param, name)
            return "Lua code hasn't assigned parsing function to the option "..name
        end
    end

    -- Add option name to the list of params sent to post_processing handler
    if post_processing then
        local original_post_processing = post_processing
        post_processing = function(...)
            original_post_processing (name, ...)
        end
    end

    local index = nil
    for i,x in ipairs(options) do
        if x.name==name  then index=i end
    end

    if index then   -- modify parameters of existing option named `name`
        local x = options[index]
        if text  then x.texts = texts end
        x.help           = option.help           or x.help
        x.priority       = option.priority       or x.priority
        x.post_priority  = option.post_priority  or x.post_priority
        x.default_value  = option.default_value  or x.default_value
        x.for_preparsing = option.for_preparsing or x.for_preparsing
        if parser then  -- modify option handlers only together
            x.parser = parser
            x.post_processing = post_processing
        end
    else -- add new option
        options:append {name=name, texts=texts, help=help, parser=parser, post_processing=post_processing,
                        priority        = option.priority       or NORMAL_PRIORITY,
                        post_priority   = option.post_priority  or NORMAL_PRIORITY,
                        default_value   = option.default_value,
                        for_preparsing  = option.for_preparsing or false}
    end
    options.already_sorted = false
end

-- Add batch of options
function AddOptions(options)
    tablex.foreach (options, AddOption)
end

-- Remove option named `name` from the `options` list
function RemoveOption(name)
    local index = options:find_if (function(x)  return x.name==name end)
    if index  then options:remove(index) end
end

-- Evaluate the default option value
function get_default_option_value(option)
    local default = option.default_value
    return (type(default)=='function' and default() or default)
end

-- Return description for options
function GetOptionsHelp (width0, width1, width2)
    return options
        -- Map option structure to description string
        :map(function(option)
            return (stringx.ljust('',                    width0)..
                    stringx.ljust(option.texts[1] or '', width1)..' '..
                    stringx.ljust(option.texts[2] or '', width2)..' '..
                    option.help:gsub('DEFAULT_VALUE', function()  return get_default_option_value(option) end))
        end)
        -- Sort options by their texts and place options having short form to the front
        :sorted()
        :partition_bool (function(text)  return text:sub(width0+1,width0+1)~=' ' end)
        :join('\n')
end


--[[ **********************************************************************************************************************************
*** C++ options *****************************************************************************************************************
*************************************************************************************************************************************]]
function AddCppOption(option)
    local name, text, help, parser = table.unpack(option)
    if option.default_value==nil then
        option.default_value = command[name]    -- read default option value right from the C++ variable
    else
        command[name] = option.default_value    -- set initial value of C++ variable from default option value
    end

    if not parser then
        local t = type(command[name])
        local subst = {number=OPTION_NUMBER, string=OPTION_STRING, boolean=OPTION_BOOL}
        parser  =  subst[t]  or  error('C++ option '..name..' has unsupported type '..t)
    end

    option[4] = parser
    option.cpp = true
    AddOption(option)
end

-- Add batch of C++ options
function AddCppOptions(options)
    tablex.foreach (options, AddCppOption)
end



--[[ **********************************************************************************************************************************
*** Option preparsing, parsing and post-processing ************************************************************************************
*************************************************************************************************************************************]]
DeclareEvents[[PreparseOption Option PostOption]]

-- Try to parse `arg` as one of the options
function ScanOptions (arg, preparsing)
    local result = ''
    for _,option in ipairs(options) do
        if not preparsing or option.for_preparsing then
            for i,prefix in ipairs(option.prefixes) do
                local param  =  prefix>'' and arg:start_with(prefix)
                if param then
                    local errmsg = option.parser(param)
                    if not errmsg then
                        return  -- successfully parsed the option
                    else
                        result = result .. "\n    as '" .. option.texts[i] .. "' option" .. (errmsg=='' and '' or ': '..errmsg)
                    end
                end
            end
        end
    end
    return result  -- cannot parse option, return collection of error messages from all matched option handlers
end

-- Preparse `arg` as option
onPreparseOption ('MainParser', function(arg)
    return ScanOptions(arg,true)
end)

-- Parse `arg` as option
onOption ('MainParser', function(arg)
    return ScanOptions(arg,false)
end)


-- Fill option.prefixes and sort options in order required for parsing
function PrepareForParsing()
    if options.already_sorted  then return end
    options.already_sorted = true

    -- Autogenerate option.prefixes from option.texts by removing value descriptions
    for _,option in ipairs(options) do
        -- Remove capital letters and "[].+" at end of prefix (these chars are required only in the displayed help)
        option.prefixes  =  option.texts:map (string.gsub, '[A-Z.+[%]]+$', '')
    end

    -- Put options with higher priority and longer first prefix to the front
    options:sort (function(a,b)
        if a.priority ~= b.priority
            then return a.priority > b.priority             -- put options with higher priority first
            else return #a.prefixes[1] > #b.prefixes[1]     -- otherwise, compare prefix lengths
        end
    end)
end

-- Assign default values to options
function InitializeOptions()
    for _,option in ipairs(options) do
        local default = get_default_option_value(option)
        if default  then optvalue[option.name] = default end
    end
end

-- Preparsing - determine the `cmd` and values of -cfg/-env options
function PreparseOptions(arguments)
    PrepareForParsing()
    local no_more_options,cmd_found = false,false
    for _,arg in ipairs(arguments) do
        if no_more_options then
            if not cmd_found  then command.cmd = arg  end
            break
        elseif arg == '--' then
            no_more_options = true
        elseif start_with(arg,'-') then
            RunEvent.PreparseOption(arg)  -- ignore error messages from unrecognized options
        else
            if not cmd_found  then command.cmd = arg;  cmd_found = true  end
        end
    end
end

function ParseOptions (arguments, cur_location, allow_filenames)
    PrepareForParsing()
    local filenames = {}
    local allow_options = true
    local cmd_assigned, arcname_assigned = false, false

    -- Save list of prior-to-cmdline options (received from envvar and config files)
    if not allow_filenames then
        optvalue.additional_options = table.append (optvalue.additional_options or {}, arguments)
    end

    local function cmdline_assert (condition, msg, details, arg)
        if not condition  then error(msg.." '"..arg.."' in the "..cur_location..(details=='' and '' or ':'..details)) end
    end

    -- Non-options in cmdline should be assigned in order to command/arcname/filenames
    -- Filenames are sent to C++ side in batches of 500
    local function ProcessName(name)
        if not cmd_assigned then
            command.cmd = name
            cmd_assigned = true
        elseif not arcname_assigned then
            command.arcname = name
            arcname_assigned = true
        else
            table.insert(filenames, name)
            if #filenames >= 500 then
                AddFilenames(filenames)
                filenames = {}
            end
        end
    end

    -- Send cached filenames to C++ side prior to filelist processing
    local function ProcessListfile(name)
        AddFilenames(filenames)
        filenames = {}
        AddListfile(name)
    end

    -- Main loop processing all arguments in order
    for _,arg in ipairs(arguments) do
        local arg1 = arg:sub(1,1)
        if allow_options and arg1=='-' then
            if arg=='--' then
                allow_options = false                   -- no more options, parse all remaining strings as names
            else
                local errmsg = RunEvent.Option(arg)     -- try to parse the option by the onOption() Lua event or high-level option parser
                cmdline_assert (not errmsg, (errmsg=='' and 'Unrecognized option' or 'Failed parsing of'), errmsg, arg)
            end
        else
            cmdline_assert (allow_filenames, 'Unexpected non-option', '', arg)
            if arg1=='@' then
                ProcessListfile(arg:sub(2))
            else
                ProcessName(arg)                        -- not an option nor filelist - copy it to the filenames list
            end
        end
    end
    AddFilenames(filenames)
end

-- Execute PostOption event by executing both PostOption handlers and option.post_processing handlers, sorted together by their priority
function RunEvent.PostOption(...)
    -- Join both handler types in the single table
    local event_handlers = tablex.copy(EventHandlers.PostOption)
    for _,option in ipairs(options) do
        if option.post_processing then
            table.insert (event_handlers, {label=option.name, priority=option.post_priority, handler=option.post_processing})
        end
    end

    -- Put handlers with higher priority first
    table.sort (event_handlers,  function (a,b) return (a.priority > b.priority) end)

    -- Execute all handlers in descending priority order until any handler returned an error message (any true value)
    for _,obj in ipairs(event_handlers) do
        local result = obj.handler(...)
        if result then return result end
    end
end

-- Procedure called after all options are processed to perform actions defined by the options
function PostProcessOptions()
    local errmsg = RunEvent.PostOption()
    assert (not errmsg,  (errmsg=='' and 'Failed option postprocessing' or errmsg))
end


--[[ **********************************************************************************************************************************
*** File-filter event: boolean function checked on each file found ********************************************************************
*************************************************************************************************************************************]]
DeclareEvent('FileFilter')

-- FileFilter event require custom processing - return false if any handler returned false, true otherwise
function RunEvent.FileFilter(...)
    for _,obj in ipairs(EventHandlers.FileFilter) do
        result = obj.handler(...)
        if not result then return result end
    end
    return true
end
FileFilter = RunEvent.FileFilter  -- speed up access from C++

-- Checked from the host program
function noFileFilters()
    return #EventHandlers.FileFilter==0
end

-- Single-expression file filters are combined together into one function for improved performance
function AddFileFilter(filter)
    table.insert (ExpressionFileFilters, filter)
end
ExpressionFileFilters = ExpressionFileFilters or {}

onPostOption ('RegisterExpressionFileFilters', LOWEST_PRIORITY, function()
    if #ExpressionFileFilters > 0 then
        onFileFilter ('ExpressionFileFilters', 'function (name, type, size, time, attr); return ('..table.concat(ExpressionFileFilters,') and (')..'); end')
    end
end)
