import re
import string
import sys
from random import choice
from functools import wraps


def cached(func):
    """Cache function results.
    (cached) call the function func as usual: func(args)
    clear cache for function func as: func.clear()
    """
    cache = {}

    @wraps(func)
    def wrapper(*args, **kw):
        key = (args, frozenset(kw.items()))
        if key in cache:
            return cache[key]
        else:
            value = func(*args, **kw)
            cache[key] = value
            return value

    wrapper.clear = lambda: cache.clear()
    return wrapper


def is_ipv4(ip_address):
    """Check if specified string is an IPv4 address

    :type ip_address: str | unicode
    :rtype: bool
    """

    # We cannot use socket.inet_pton because it is not available on Windows
    # and may be some Unix OSes
    # https://docs.python.org/2/library/socket.html#socket.inet_pton
    # Availability: Unix (maybe not all platforms).

    # Simple solution taken from:
    # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python
    # This solution is:
    # - quite simple and
    # - does not require any external libraries
    # - works on any platform

    pattern = re.compile(r"""
        ^
        (?:
            # Dotted variants:
            (?:
                # Decimal 1-255 (no leading 0's)
                [3-9]\d?|2(?:5[0-5]|[0-4]?\d)?|1\d{0,2}
            |
                0x0*[0-9a-f]{1,2}  # Hexadecimal 0x0 - 0xFF (possible leading 0's)
            |
                0+[1-3]?[0-7]{0,2} # Octal 0 - 0377 (possible leading 0's)
            )
            (?:                  # Repeat 0-3 times, separated by a dot
                \.
                (?:
                    [3-9]\d?|2(?:5[0-5]|[0-4]?\d)?|1\d{0,2}
                |
                    0x0*[0-9a-f]{1,2}
                |
                    0+[1-3]?[0-7]{0,2}
                )
            ){0,3}
        |
            0x0*[0-9a-f]{1,8}    # Hexadecimal notation, 0x0 - 0xffffffff
        |
            0+[0-3]?[0-7]{0,10}  # Octal notation, 0 - 037777777777
        |
            # Decimal notation, 1-4294967295:
            429496729[0-5]|42949672[0-8]\d|4294967[01]\d\d|429496[0-6]\d{3}|
            42949[0-5]\d{4}|4294[0-8]\d{5}|429[0-3]\d{6}|42[0-8]\d{7}|
            4[01]\d{8}|[1-3]\d{0,9}|[4-9]\d{0,8}
        )
        $
    """, re.VERBOSE | re.IGNORECASE)

    return pattern.match(ip_address) is not None


def is_ipv6(ip_address):
    """Check if specified string is an IPv6 address

    :type ip_address: str | unicode
    :rtype: bool
    """

    # See comment inside is_ipv4 function - the same is applicable here.

    pattern = re.compile(r"""
        ^
        \s*                      # Leading whitespace
        (?!.*::.*::)             # Only a single whildcard allowed
        (?:(?!:)|:(?=:))         # Colon iff it would be part of a wildcard
        (?:                      # Repeat 6 times:
            [0-9a-f]{0,4}        #   A group of at most four hexadecimal digits
            (?:(?<=::)|(?<!::):) #   Colon unless preceded by wildcard
        ){6}                     #
        (?:                      # Either
            [0-9a-f]{0,4}        #   Another group
            (?:(?<=::)|(?<!::):) #   Colon unless preceded by wildcard
            [0-9a-f]{0,4}        #   Last group
            (?: (?<=::)          #   Colon iff preceded by exacly one colon
                |  (?<!:)           #
                |  (?<=:) (?<!::) : #
            )                   # OR
            |                       #   A v4 address with NO leading zeros
            (?:25[0-4]|2[0-4]\d|1\d\d|[1-9]?\d)
            (?: \.
                (?:25[0-4]|2[0-4]\d|1\d\d|[1-9]?\d)
            ){3}
        )
        \s*                         # Trailing whitespace
        $
    """, re.VERBOSE | re.IGNORECASE | re.DOTALL)

    return pattern.match(ip_address) is not None


def safe_get_dict_value(userdict, *args):
    """Return value from nested dictionaries if key chain exists else None

    :param userdict: dictionary
    :param args: key chain
    :return: any
    """
    existing_value = userdict
    for x in args:
        if isinstance(existing_value, dict) and x in existing_value:
            existing_value = existing_value[x]
        else:
            return None
    return existing_value


def safe_format(*args, **kwargs):
    """Fault-tolerant string format. Use this function for non-critical formatting, for example logging and reports.

    1) This function allows formatting binary data into user's messages.
    If some argument is a binary string and it can not be converted to unicode -
    it will be inserted as Python representation.
    2) This function makes code more fault-tolerant.
    If some argument is missing for format string, it will be left as is. No exception will be thrown.

    First positional argument is a format string. All the other arguments must be named arguments.

    Output is always an unicode string that could be logged, saved to report file, etc.

    :rtype: unicode
    """
    if len(args) == 0:
        return u''

    safe_kwargs = {
        k: safe_string_repr(v, omit_quotes=True) for k, v in kwargs.items()
    }

    # limit to avoid
    max_missing_keys = 100

    if sys.version_info.major >= 3:
        max_missing_keys_range = range(max_missing_keys)
        str_type = str
    else:
        max_missing_keys_range = xrange(max_missing_keys)
        str_type = unicode

    for _ in max_missing_keys_range:
        try:
            return str_type(args[0]).format(**safe_kwargs)
        except KeyError as e:
            missing_key = e.args[0]
            safe_kwargs[missing_key] = '{%s}' % missing_key


def safe_string_repr(s, omit_quotes=False, encoding=None):
    """If object can be interpreted as unicode, then return it as unicode. Otherwise return _repr_ of its _str_.

    Could be useful for safe logging, when usually you want to get just plain string, but if there are some issues
    with encoding - at least get its Python representation.

    :type s: any
    :type omit_quotes: bool
    :type encoding: str
    :rtype: unicode
    """
    if sys.version_info.major >= 3:
        bytes_type = bytes
        str_type = str
    else:
        bytes_type = basestring
        str_type = unicode
    if isinstance(s, str_type):
        return s
    else:
        try:
            if encoding is not None:
                s = str(s).decode(encoding)

            return str_type(s)
        except UnicodeError:
            if omit_quotes and isinstance(s, bytes_type):
                return (u"%r" % (s,))[1:-1]
            else:
                return u"%r" % (str(s),)


def safe_idn_decode(domain_name):
    """Safely decode domain name from punycode to unicode

    If domain name is already in unicode - return it as is. If any error occurred during decoding - return domain
    name as is.

    :type domain_name: str | unicode
    :rtype: unicode
    """
    if not is_ascii_string(domain_name):
        return domain_name

    try:
        return domain_name.decode('idna')
    except Exception:
        return domain_name


def normalize_path(path):
    """Normalize Windows path for comparison - use single slash style, remove leading slash, convert to lowercase

    :type path: str | unicode
    :rtype: str | unicode
    """
    return path.replace('/', '\\').rstrip('\\').lower()


def normalize_domain_name(domain_name):
    """Normalize domain name - convert to lowercase punycode, convert wildcard subdomains from "*" to "_" notation

    Use this function for comparison of domain names.

    :rtype str | unicode
    """
    return domain_name.encode('idna').lower().replace('*', '_')


def join_nonempty_strs(strs, delim=' '):
    nonempty_strs = [str for str in strs if str is not None and str.strip() != '']
    return delim.join(nonempty_strs)


def is_ascii_string(string):
    try:
        string.decode('ascii')
    except UnicodeError:
        return False
    return True


def parse_key_value_line(line, key, separator='=', default=None, strip_symbols=None):
    """Return value for key and separator in string
    
    :type line: str | unicode 
    :type key: str | unicode 
    :type separator: str | unicode 
    :type default: str | unicode | None
    :type strip_symbols: str | unicode | None
    :rtype: str | unicode | None
    """
    if not line or not key:
        return default
    splitted_line = line.split(separator, 1)
    if len(splitted_line) != 2:
        return default
    if splitted_line[0].strip() != key:
        return default
    result = splitted_line[1].strip()
    if strip_symbols:
        result = result.strip(strip_symbols)
    return result


def generate_password(length=8):
    if sys.version_info.major >= 3:
        length_range = range(length)
        string_letters = string.ascii_letters
    else:
        length_range = xrange(length)
        string_letters = string.letters
    chars = string_letters + string.digits + "!@#$%^&*?~"
    return ''.join([choice(chars) for _ in length_range])


def list_priority_sequence(numbers, max_value):
    """
    :param numbers: list of values to sort and pseudo-normalize
    :type numbers: list
    :type max_value: int
    :return: dict of relations {numbers: normalized values}
    :rtype: dict
    """
    return_dict = dict()
    if type(numbers) != list or len(numbers) == 0:
        return return_dict
    # remove duplicates and sort
    numbers = list(set(numbers))
    numbers.sort()
    for i, number in list(enumerate(numbers)):
        if i <= max_value:
            return_dict[number] = i
        else:
            return_dict[number] = max_value
    return return_dict
