# Copyright (c) 2011 OpenStack Foundation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import ast
import operator

import pyparsing
from pyparsing import Literal
from pyparsing import OneOrMore
from pyparsing import Regex


def _all_in(x, *y):
    x = ast.literal_eval(x)
    if not isinstance(x, list):
        raise TypeError("<all-in> must compare with a list literal"
                        " string, EG \"%s\"" % (['aes', 'mmx'],))
    return all(val in x for val in y)


op_methods = {
    # This one is special/odd,
    # TODO(harlowja): fix it so that it's not greater than or
    # equal, see here for the original @ https://review.openstack.org/#/c/8089/
    '=': lambda x, y: float(x) >= float(y),
    # More sane ops/methods
    '!=': lambda x, y: float(x) != float(y),
    '<=': lambda x, y: float(x) <= float(y),
    '<': lambda x, y: float(x) < float(y),
    '==': lambda x, y: float(x) == float(y),
    '>=': lambda x, y: float(x) >= float(y),
    '>': lambda x, y: float(x) > float(y),
    's!=': operator.ne,
    's<': operator.lt,
    's<=': operator.le,
    's==': operator.eq,
    's>': operator.gt,
    's>=': operator.ge,
    '<all-in>': _all_in,
    '<in>': lambda x, y: y in x,
    '<or>': lambda x, *y: any(x == a for a in y),
}


def make_grammar():
    """Creates the grammar to be used by a spec matcher."""
    # This is apparently how pyparsing recommends to be used,
    # as http://pyparsing.wikispaces.com/share/view/644825 states that
    # it is not thread-safe to use a parser across threads.

    unary_ops = (
        # Order matters here (so that '=' doesn't match before '==')
        Literal("==") | Literal("=") |
        Literal("!=") | Literal("<in>") |
        Literal(">=") | Literal("<=") |
        Literal(">") | Literal("<") |
        Literal("s==") | Literal("s!=") |
        # Order matters here (so that '<' doesn't match before '<=')
        Literal("s<=") | Literal("s<") |
        # Order matters here (so that '>' doesn't match before '>=')
        Literal("s>=") | Literal("s>"))

    all_in_nary_op = Literal("<all-in>")
    or_ = Literal("<or>")

    # An atom is anything not an keyword followed by anything but whitespace
    atom = ~(unary_ops | all_in_nary_op | or_) + Regex(r"\S+")

    unary = unary_ops + atom
    nary = all_in_nary_op + OneOrMore(atom)
    disjunction = OneOrMore(or_ + atom)

    # Even-numbered tokens will be '<or>', so we drop them
    disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2])

    expr = disjunction | nary | unary | atom
    return expr


def match(cmp_value, spec):
    """Match a given value to a given spec DSL."""
    expr = make_grammar()
    try:
        tree = expr.parseString(spec)
    except pyparsing.ParseException:
        tree = [spec]
    if len(tree) == 1:
        return tree[0] == cmp_value

    op = op_methods[tree[0]]
    return op(cmp_value, *tree[1:])
