#=
    Input Parsing (for text) API
    ----------------------------

Parsing is performed by calling:

    parse_input(env, request)

where:
    `env::SearchEnv` is the search environment object
    `request::InternalRequest` is the request

The input_parser is generated by `parse_input` and its signature should be of the form:

    some_input_parser(env, request [;separator=DEFAULT_QUERY_PARSING_SEPARATOR, searchable_filters=[])

where:
    `env::SearchEnv` is the data environment object (i.e. no `searchers` field in the named tuple)
    `request::InternalRequest` is the request
    `separator` is a separator key-value separator
    `searchable_filters` are names of columns in the data whose values are to be added to the search query

The input parser function returns a named tuple with fields:
    • `search::AbstractString` which is the search query
    • `filter::Dict{Symbol, Any}` which represents filtering information
=#
function parse_input(env, request)
    input_parser = safe_symbol_eval(request.input_parser, DEFAULT_INPUT_PARSER_NAME)
    env_data = build_data_env(env)
    return input_parser(env_data, request; searchable_filters=request.searchable_filters)
end


# The pre-parser supports specification of another parser (including itself)
# into the query. It searches for the "magic" pattern, in this case
# `parser_name>`. For example, the query:
#
# "pre_parser> pre_parser> noop_input_parser> query text"
#
# has 3 parser specifications and willrecursively call the pre-parser 2 times until
# another parser, in the case the `noop_input_parser` is ecountered.
# If no other parser is available, it will default to the DEFAULT_INPUT_PARSER_NAME
# by virtue of evaluating an empty parser name.
function pre_parser(env, request, args...; magic=r"^[\s]*[\w]+[\s]*>", kwargs...)
    m = match(magic, request.query)
    parser_name = DEFAULT_INPUT_PARSER_NAME
    if m !== nothing
        request.query = strip(replace(request.query, magic=>""))                # strip parser magic from query
        parser_name = Symbol(strip(replace(m.match, last(magic.pattern)=>"")))  # extract parser name
    end
    input_parser = safe_symbol_eval(parser_name, DEFAULT_INPUT_PARSER_NAME)     # evaluate parser
    @debug "PRE-Parser: selecting \"$(input_parser)\"..."
    return input_parser(env, request; searchable_filters=request.searchable_filters)  # call parser
end


# This function shoud parse an input query and return a Dict with the fields and
# values necessary to filter the data later on.
function base_input_parser(env,
                           request;
                           separator=DEFAULT_QUERY_PARSING_SEPARATOR,
                           searchable_filters=Symbol[])
    @debug "BASE Input Parser: Running..."
    query = request.query
    dbschema = db_create_schema(env.dbdata)

    # Function that removes all tokens containing punctuation from a string
    function remove_punct_tokens(input, punct_regex=r"[\"!?:,.\[\]\(\)\*\&\^\%\$]")
        toks = filter!(token->!occursin(punct_regex, token), split(input))
        return join(toks, " ")
    end

    # Function that parses a string into a type
    function text2type(::Type{T}, valstr::AbstractString) where {T}
        if startswith(valstr, r"(\(|\[|\")") && endswith(valstr, r"(\)|\]|\")")
            return eval(Meta.parse(valstr))  # a vector or tuple, evaluate
        else
            return __parse(T, valstr)  # leave unchanged
        end
    end

    text2searchstring(val::AbstractString) = " " * val  # String get just passed
    text2searchstring(val::NTuple{N,T}) where {N, T<:AbstractString} = " " * join(val, " ")  # concatenate string values
    text2searchstring(val) = ""                 # Other filter values types i.e. Vectors, Numbers are not supported

    # Define expression to match
    #MATCH_EXPR = Regex("\\w+\\s*$separator(\\s*\\w+|\\s*(\\[|\\().*(\\]|\\)))")
    #MATCH_EXPR = Regex("[_a-zA-Z0-9]+$separator[_a-zA-Z0-9\\(\\[\\]\\),\"]+")
    REGEX_ALPHANUM = "_a-zA-Z0-9\\."
    MATCH_EXPR = Regex("[$REGEX_ALPHANUM]+"*
                       "$separator"*
                       "([$REGEX_ALPHANUM]+|"*
                       "\\([$REGEX_ALPHANUM,\"\\s]+\\)|"*
                       "\\[[$REGEX_ALPHANUM,\"\\s]+\\]|"*
                       "\"[$REGEX_ALPHANUM,\\s]+\")")

    # The search query simply is the reminder from the query after
    # removing all key:value(s) matches and tokens that contain punctuation
    search_query = strip(remove_punct_tokens(replace(query, MATCH_EXPR => "")))
    filteronly = isempty(search_query)

    # Initialize filter query for index columns with Colon() i.e. all
    columns = getproperty.(dbschema, :column)
    filter_query = Dict{Symbol, Any}()

    # Populate with other filter conditions and add to any
    # filter values to search_query if the filter field is
    # in searchable_filters
    for m in eachmatch(MATCH_EXPR, query)
        keystr, valstr = strip.(split(m.match, separator))
        key = Symbol(keystr)
        try
            idxkey = findfirst(isequal(key), columns)
            if idxkey != nothing
                # Add to filter
                val = text2type(dbschema[idxkey].coltype, valstr)
                push!(filter_query, key => val)

                # Add to search_query if key is a searchable_filter
                # and there we do not have only filters i.e. empty search_query
                if key in searchable_filters && !filteronly
                    search_query *= text2searchstring(val)
                end
            end
        catch
            @debug "Parse error for key \"$(m.match)\", ignoring..."
        end

    end
    return (search=search_query, filter=filter_query)
end


# Parser that does nothing
function noop_input_parser(env, request, args...; kwargs...)
    @debug "NOOP Input Parser: Running..."
    search_query = request.query
    filter_query = Dict{Symbol, Any}()
    return (search=search_query, filter=filter_query)
end
