###################################################################################################################### 
#  Copyright 2019 Amazon.com, Inc. or its affiliates. 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. A copy of the License is located at                                                             # 
#                                                                                                                    # 
#      http://www.apache.org/licenses/                                                                               # 
#                                                                                                                    # 
#  or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES # 
#  OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions    # 
#  and limitations under the License.                                                                                # 
######################################################################################################################
import re

WILDCARD_CHAR = "*"
REGEX_CHAR = "\\"
FILTER_SEP = ","
NAME_VAL_SEP = "="
NOT_OPERATOR = "!"


class TagFilterSet(object):
    """
    Class for matching string and value pairs against a set of filters
    Filters are specified in a comma separated list of one or more filter.
    Filters can start, end or start and end with a * wildcard character for partial matching
    if the filter starts with a \ character it is a regular expression.
    For filtering key-value pairs set in dictionaries the format of the filters is <namefilter>=<valuefilter> where
    both items can start/end with a wildcard or with a \ for regular expressions. If the item has the
    format <namefilter> then the pairs will match this filter if the just keyname matches the expression.
    """

    def __init__(self, filters, name_val_sep=NAME_VAL_SEP, filter_sep=FILTER_SEP, regex_char=REGEX_CHAR,
                 wildcard_char=WILDCARD_CHAR):
        if filters.startswith("\\") or "=\\" in filters or "!=\\" in filters:
            self._filters = [filters.strip()]
        else:
            self._filters = [f.strip() for f in filters.split(filter_sep)]

        self._name_val_sep = name_val_sep
        self._regex_char = regex_char
        self._wildcard_char = wildcard_char

    # matches a single string against a single filter
    def match_string(self, filter_string, tested_string):
        """
        Matches a single string against a single filter
        :param filter_string: string containing the filer to match against. The string can start or end with a wildcard
        character, contain
        just the wildcard character of a regular expression starting with a \ character.
        :param tested_string: The string to test
        :return: True id the string matches, False if not
        """

        # empty or none matches empty or none
        if filter_string == "" or filter_string is None:
            return tested_string == "" or tested_string is None

        # filter is regex
        try:
            if filter_string.startswith(self._regex_char) and len(filter_string) > 1:
                return re.match(filter_string[1:], tested_string) is not None
        except re.error as ex:
            raise ValueError("\"{}\" is not a valid regular expression ({})".format(filter_string[1:], ex))

        # just "*" matches any value
        if filter_string == self._wildcard_char:
            return True

        if filter_string.startswith(self._wildcard_char):
            if filter_string.endswith(self._wildcard_char):
                # *contained*
                return filter_string[1:-1] in tested_string
            else:
                # *endswith
                return tested_string.endswith(filter_string[1:])

        if filter_string.endswith(self._wildcard_char):
            # startswith*
            return tested_string.startswith(filter_string[:-1])
        else:
            # exact match
            return filter_string == tested_string

    def tag_names(self):
        names = [f.split(self._name_val_sep)[0] for f in self._filters]
        return [n[1:] if n.startswith(NOT_OPERATOR) else n for n in names]

    def matches_name_value_pair(self, filter_str, pair_key, pair_value):
        """
        Matches a filter against a name value pair
        :param filter_str: filter in the format [!]<key>[!]=<value> or [!]<key> where both key and value can contain
        wildcards or a regex.
        :param pair_key: keyname to test
        :param pair_value: value to test
        :return: True if the filter matched the key pair, False if not
        """

        tag_name, tag_value, not_equal, not_tag = self._split_filter(filter_str)
        if self.match_string(tag_name, pair_key) != not_tag:
            return len(tag_value) == 0 or (self.match_string(tag_value, pair_value) != not_equal)
        return False

    def pairs_matching_any_filter(self, key_pairs):
        """
        Selects key value pairs match any of a set of name value filters
        :param key_pairs: Dictionary containing the pairs to test
        :return: Dictionary containing all keypairs that match at least one filter
        """
        result = {}
        for key_name in key_pairs:
            if any([self.matches_name_value_pair(f, key_name, key_pairs[key_name]) for f in self._filters]):
                result[key_name] = key_pairs[key_name]
        return result

    def all_pairs_matching_filter(self, key_pairs):
        for key_name in key_pairs:
            if not any([self.matches_name_value_pair(f, key_name, key_pairs[key_name]) for f in self._filters]):
                return False
        return True

    @classmethod
    def _split_filter(cls, filter_str):
        not_equal = False
        not_tag = False

        temp = filter_str.split(NAME_VAL_SEP, 1)
        tag_name = temp[0].strip()
        if tag_name.startswith(NOT_OPERATOR):
            not_tag = True
            tag_name = tag_name[1:].strip()

        if len(temp) > 1:
            tag_value = temp[1].strip()
            if len(tag_value) > 1:
                if tag_value[0] == NOT_OPERATOR:
                    not_equal = True
                    tag_value = tag_value[1:].strip()
        else:
            tag_value = ""
        return tag_name, tag_value, not_equal, not_tag

    def has_not_operator(self):
        for f in self._filters:
            if f.startswith(NOT_OPERATOR):
                return True
        return False