Transform a simple query with one or more filter expressions
    into a MongoDB query expression.

    :param qry: Filter expression(s), see function docstring for details.
    :type qry: str or list
    :return: MongoDB query
    :rtype: dict
    :raises: BadExpression, if one of the input expressions cannot be parsed

    Expressions have three parts, called in order ``field``, ``operator``,
    and ``value``.

        - `field` is the name of a field in a MongoDB document
        - `value` is the value to compare against:
            * numeric
            * string, you MUST use 'single' or "double" quotes
            * boolean: true, false
        - `operator` is a comparison operator:
            * inequalities: >, <, =, <=, >=, !=
            * PCRE regular expression: ~
            * data type: int, float, string, or bool
            * exists: boolean (true/false) whether field exists in record
            * size: for array fields, an inequality for the array size, given as
              a suffix to the operator: size>, size<

    Multiple expressions can be a single string, or a list.
    In either case, the form is a "disjunction of conjunctions".

    In the string form:

        * "and" joins expressions into groups
        * "or" joins one or more expression groups

    In the list form:

        * The inner list is a group of "and"ed expressions
        * The outer list "or"s the expression groups together.

    In the string form, parentheses before or after the "or"ed expression
    groups [note: even non-sensical ones like '((('], are ignored.
    So these can be used to clarify the groupings.

    **Examples**

    Two sets of filters, return records where either is true:

    >>> to_mongo('(a > 3 and b = "hello") or (c > 1 and d = "goodbye")')
    {'$or': [{'a': {'$gt': 3}, 'b': 'hello'}, {'c': {'$gt': 1}, 'd': 'goodbye'}]}

    Same as previous, but without parentheses:

    >>> to_mongo('a > 3 and b = "hello" or c > 1 and d = "goodbye"')
    {'$or': [{'a': {'$gt': 3}, 'b': 'hello'}, {'c': {'$gt': 1}, 'd': 'goodbye'}]}

    Same as previous, but using lists rather than "and"/"or":

    >>> to_mongo([['a > 3', 'b = "hello"'], ['c > 1', 'd = "goodbye"']])
    {'$or': [{'a': {'$gt': 3}, 'b': 'hello'}, {'c': {'$gt': 1}, 'd': 'goodbye'}]}

def to_mongo(qry):
    """Transform a simple query with one or more filter expressions
    into a MongoDB query expression.

    :param qry: Filter expression(s), see function docstring for details.
    :type qry: str or list
    :return: MongoDB query
    :rtype: dict
    :raises: BadExpression, if one of the input expressions cannot be parsed

    Expressions have three parts, called in order ``field``, ``operator``,
    and ``value``.

        - `field` is the name of a field in a MongoDB document
        - `value` is the value to compare against:
            * numeric
            * string, you MUST use 'single' or "double" quotes
            * boolean: true, false
        - `operator` is a comparison operator:
            * inequalities: >, <, =, <=, >=, !=
            * PCRE regular expression: ~
            * data type: int, float, string, or bool
            * exists: boolean (true/false) whether field exists in record
            * size: for array fields, an inequality for the array size, given as
              a suffix to the operator: size>, size<

    Multiple expressions can be a single string, or a list.
    In either case, the form is a "disjunction of conjunctions".

    In the string form:

        * "and" joins expressions into groups
        * "or" joins one or more expression groups

    In the list form:

        * The inner list is a group of "and"ed expressions
        * The outer list "or"s the expression groups together.

    In the string form, parentheses before or after the "or"ed expression
    groups [note: even non-sensical ones like '((('], are ignored.
    So these can be used to clarify the groupings.

    **Examples**

    Two sets of filters, return records where either is true:

    >>> to_mongo('(a > 3 and b = "hello") or (c > 1 and d = "goodbye")')
    {'$or': [{'a': {'$gt': 3}, 'b': 'hello'}, {'c': {'$gt': 1}, 'd': 'goodbye'}]}

    Same as previous, but without parentheses:

    >>> to_mongo('a > 3 and b = "hello" or c > 1 and d = "goodbye"')
    {'$or': [{'a': {'$gt': 3}, 'b': 'hello'}, {'c': {'$gt': 1}, 'd': 'goodbye'}]}

    Same as previous, but using lists rather than "and"/"or":

    >>> to_mongo([['a > 3', 'b = "hello"'], ['c > 1', 'd = "goodbye"']])
    {'$or': [{'a': {'$gt': 3}, 'b': 'hello'}, {'c': {'$gt': 1}, 'd': 'goodbye'}]}
    """
    rev = False     # filters, not constraints
    # special case for empty string/list
    if qry == "" or qry == []:
        return {}
    # break input into groups of filters
    unpar = lambda s: s.strip().strip('()')
    if isinstance(qry, str):
        groups = []
        if _TOK_OR in qry:
            groups = [unpar(g).split(_TOK_AND) for g in qry.split(_TOK_OR)]
        else:
            groups = [unpar(qry).split(_TOK_AND)]
    else:
        if isinstance(qry[0], list) or isinstance(qry[0], tuple):
            groups = qry
        else:
            groups = [qry]
    # generate mongodb queries for each filter group
    filters = []
    for filter_exprs in groups:
        mq = MongoQuery()
        for e in filter_exprs:
            try:
                e = unpar(e)
            except AttributeError:
                raise BadExpression(e, "expected string, got '{t}'".format(t=type(e)))
            try:
                constraint = Constraint(*parse_expr(e))
            except ValueError as err:
                raise BadExpression(e, err)
            clause = MongoClause(constraint, rev=rev)
            mq.add_clause(clause)
        filters.append(mq.to_mongo(rev))
    # combine together filters, or strip down the one filter
    if len(filters) > 1:
        result = {'$or': filters}
    else:
        result = filters[0]
    return result