#!/usr/bin/env python
"""

uritemplate.variable
====================

This module contains the URIVariable class which powers the URITemplate class.

What treasures await you:

- URIVariable class

You see a hammer in front of you.
What do you do?
>

"""

import sys

# This import hack is for Python 2 support.
# TODO(b/234740725): Import collections.abc once BigQuery bq CLI doesn't require
# Python 2 support.
try:
    import collections.abc as collections_abc
except ImportError:
    import collections as collections_abc

if sys.version_info.major == 2:
    import urllib
elif sys.version_info.major == 3:
    import urllib.parse as urllib


class URIVariable(object):

    """This object validates everything inside the URITemplate object.

    It validates template expansions and will truncate length as decided by
    the template.

    Please note that just like the :class:`URITemplate <URITemplate>`, this
    object's ``__str__`` and ``__repr__`` methods do not return the same
    information. Calling ``str(var)`` will return the original variable.

    This object does the majority of the heavy lifting. The ``URITemplate``
    object finds the variables in the URI and then creates ``URIVariable``
    objects.  Expansions of the URI are handled by each ``URIVariable``
    object. ``URIVariable.expand()`` returns a dictionary of the original
    variable and the expanded value. Check that method's documentation for
    more information.

    """

    operators = ('+', '#', '.', '/', ';', '?', '&', '|', '!', '@')
    reserved = ":/?#[]@!$&'()*+,;="

    def __init__(self, var):
        #: The original string that comes through with the variable
        self.original = var
        #: The operator for the variable
        self.operator = ''
        #: List of safe characters when quoting the string
        self.safe = ''
        #: List of variables in this variable
        self.variables = []
        #: List of variable names
        self.variable_names = []
        #: List of defaults passed in
        self.defaults = {}
        # Parse the variable itself.
        self.parse()
        self.post_parse()

    def __repr__(self):
        return 'URIVariable(%s)' % self

    def __str__(self):
        return self.original

    def parse(self):
        """Parse the variable.

        This finds the:
            - operator,
            - set of safe characters,
            - variables, and
            - defaults.

        """
        var_list = self.original
        if self.original[0] in URIVariable.operators:
            self.operator = self.original[0]
            var_list = self.original[1:]

        if self.operator in URIVariable.operators[:2]:
            self.safe = URIVariable.reserved

        var_list = var_list.split(',')

        for var in var_list:
            default_val = None
            name = var
            if '=' in var:
                name, default_val = tuple(var.split('=', 1))

            explode = False
            if name.endswith('*'):
                explode = True
                name = name[:-1]

            prefix = None
            if ':' in name:
                name, prefix = tuple(name.split(':', 1))
                prefix = int(prefix)

            if default_val:
                self.defaults[name] = default_val

            self.variables.append(
                (name, {'explode': explode, 'prefix': prefix})
            )

        self.variable_names = [varname for (varname, _) in self.variables]

    def post_parse(self):
        """Set ``start``, ``join_str`` and ``safe`` attributes.

        After parsing the variable, we need to set up these attributes and it
        only makes sense to do it in a more easily testable way.
        """
        self.safe = ''
        self.start = self.join_str = self.operator
        if self.operator == '+':
            self.start = ''
        if self.operator in ('+', '#', ''):
            self.join_str = ','
        if self.operator == '#':
            self.start = '#'
        if self.operator == '?':
            self.start = '?'
            self.join_str = '&'

        if self.operator in ('+', '#'):
            self.safe = URIVariable.reserved

    def _query_expansion(self, name, value, explode, prefix):
        """Expansion method for the '?' and '&' operators."""
        if value is None:
            return None

        tuples, items = is_list_of_tuples(value)

        safe = self.safe
        if list_test(value) and not tuples:
            if not value:
                return None
            if explode:
                return self.join_str.join(
                    '{}={}'.format(name, quote(v, safe)) for v in value
                )
            else:
                value = ','.join(quote(v, safe) for v in value)
                return '{}={}'.format(name, value)

        if dict_test(value) or tuples:
            if not value:
                return None
            items = items or sorted(value.items())
            if explode:
                return self.join_str.join(
                    '{}={}'.format(
                        quote(k, safe), quote(v, safe)
                    ) for k, v in items
                )
            else:
                value = ','.join(
                    '{},{}'.format(
                        quote(k, safe), quote(v, safe)
                    ) for k, v in items
                )
                return '{}={}'.format(name, value)

        if value:
            value = value[:prefix] if prefix else value
            return '{}={}'.format(name, quote(value, safe))
        return name + '='

    def _label_path_expansion(self, name, value, explode, prefix):
        """Label and path expansion method.

        Expands for operators: '/', '.'

        """
        join_str = self.join_str
        safe = self.safe

        if value is None or (len(value) == 0 and value != ''):
            return None

        tuples, items = is_list_of_tuples(value)

        if list_test(value) and not tuples:
            if not explode:
                join_str = ','

            fragments = [quote(v, safe) for v in value if v is not None]
            return join_str.join(fragments) if fragments else None

        if dict_test(value) or tuples:
            items = items or sorted(value.items())
            format_str = '%s=%s'
            if not explode:
                format_str = '%s,%s'
                join_str = ','

            expanded = join_str.join(
                format_str % (
                    quote(k, safe), quote(v, safe)
                ) for k, v in items if v is not None
            )
            return expanded if expanded else None

        value = value[:prefix] if prefix else value
        return quote(value, safe)

    def _semi_path_expansion(self, name, value, explode, prefix):
        """Expansion method for ';' operator."""
        join_str = self.join_str
        safe = self.safe

        if value is None:
            return None

        if self.operator == '?':
            join_str = '&'

        tuples, items = is_list_of_tuples(value)

        if list_test(value) and not tuples:
            if explode:
                expanded = join_str.join(
                    '{}={}'.format(
                        name, quote(v, safe)
                    ) for v in value if v is not None
                )
                return expanded if expanded else None
            else:
                value = ','.join(quote(v, safe) for v in value)
                return '{}={}'.format(name, value)

        if dict_test(value) or tuples:
            items = items or sorted(value.items())

            if explode:
                return join_str.join(
                    '{}={}'.format(
                        quote(k, safe), quote(v, safe)
                    ) for k, v in items if v is not None
                )
            else:
                expanded = ','.join(
                    '{},{}'.format(
                        quote(k, safe), quote(v, safe)
                    ) for k, v in items if v is not None
                )
                return '{}={}'.format(name, expanded)

        value = value[:prefix] if prefix else value
        if value:
            return '{}={}'.format(name, quote(value, safe))

        return name

    def _string_expansion(self, name, value, explode, prefix):
        if value is None:
            return None

        tuples, items = is_list_of_tuples(value)

        if list_test(value) and not tuples:
            return ','.join(quote(v, self.safe) for v in value)

        if dict_test(value) or tuples:
            items = items or sorted(value.items())
            format_str = '%s=%s' if explode else '%s,%s'

            return ','.join(
                format_str % (
                    quote(k, self.safe), quote(v, self.safe)
                ) for k, v in items
            )

        value = value[:prefix] if prefix else value
        return quote(value, self.safe)

    def expand(self, var_dict=None):
        """Expand the variable in question.

        Using ``var_dict`` and the previously parsed defaults, expand this
        variable and subvariables.

        :param dict var_dict: dictionary of key-value pairs to be used during
            expansion
        :returns: dict(variable=value)

        Examples::

            # (1)
            v = URIVariable('/var')
            expansion = v.expand({'var': 'value'})
            print(expansion)
            # => {'/var': '/value'}

            # (2)
            v = URIVariable('?var,hello,x,y')
            expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
                                  'x': '1024', 'y': '768'})
            print(expansion)
            # => {'?var,hello,x,y':
            #     '?var=value&hello=Hello%20World%21&x=1024&y=768'}

        """
        return_values = []

        for name, opts in self.variables:
            value = var_dict.get(name, None)
            if not value and value != '' and name in self.defaults:
                value = self.defaults[name]

            if value is None:
                continue

            expanded = None
            if self.operator in ('/', '.'):
                expansion = self._label_path_expansion
            elif self.operator in ('?', '&'):
                expansion = self._query_expansion
            elif self.operator == ';':
                expansion = self._semi_path_expansion
            else:
                expansion = self._string_expansion

            expanded = expansion(name, value, opts['explode'], opts['prefix'])

            if expanded is not None:
                return_values.append(expanded)

        value = ''
        if return_values:
            value = self.start + self.join_str.join(return_values)
        return {self.original: value}


def is_list_of_tuples(value):
    if (not value or
            not isinstance(value, (list, tuple)) or
            not all(isinstance(t, tuple) and len(t) == 2 for t in value)):
        return False, None

    return True, value


def list_test(value):
    return isinstance(value, (list, tuple))


def dict_test(value):
    return isinstance(value, (dict, collections_abc.MutableMapping))


try:
    texttype = unicode
except NameError:  # Python 3
    texttype = str

stringlikes = (texttype, bytes)


def _encode(value, encoding='utf-8'):
    if (isinstance(value, texttype) and
            getattr(value, 'encode', None) is not None):
        return value.encode(encoding)
    return value


def quote(value, safe):
    if not isinstance(value, stringlikes):
        value = str(value)
    return urllib.quote(_encode(value), safe)
