# Source code for cassandra.encoder

    # Copyright DataStax, Inc.
    #
    # 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.
    """
    These functions are used to convert Python objects into CQL strings.
    When non-prepared statements are executed, these encoder functions are
    called on each query parameter.
    """

    import logging
    log = logging.getLogger(__name__)

    from binascii import hexlify
    import calendar
    import datetime
    import math
    import sys
    import types
    from uuid import UUID
    import six

    if six.PY3:
        import ipaddress

    from cassandra.util import (OrderedDict, OrderedMap, OrderedMapSerializedKey,
                                sortedset, Time, Date)

    if six.PY3:
        long = int

    def cql_quote(term):
        # The ordering of this method is important for the result of this method to
        # be a native str type (for both Python 2 and 3)

        if isinstance(term, str):
            return "'%s'" % str(term).replace("'", "''")
        # This branch of the if statement will only be used by Python 2 to catch
        # unicode strings, text_type is used to prevent type errors with Python 3.
        elif isinstance(term, six.text_type):
            return "'%s'" % term.encode('utf8').replace("'", "''")
        else:
            return str(term)

    class ValueSequence(list):
        pass

    class Encoder(object):
        """
        A container for mapping python types to CQL string literals when working
        with non-prepared statements.  The type :attr:`~.Encoder.mapping` can be
        directly customized by users.
        """

        mapping = None
        """
        A map of python types to encoder functions.
        """

        def __init__(self):
            self.mapping = {
                float: self.cql_encode_float,
                bytearray: self.cql_encode_bytes,
                str: self.cql_encode_str,
                int: self.cql_encode_object,
                UUID: self.cql_encode_object,
                datetime.datetime: self.cql_encode_datetime,
                datetime.date: self.cql_encode_date,
                datetime.time: self.cql_encode_time,
                Date: self.cql_encode_date_ext,
                Time: self.cql_encode_time,
                dict: self.cql_encode_map_collection,
                OrderedDict: self.cql_encode_map_collection,
                OrderedMap: self.cql_encode_map_collection,
                OrderedMapSerializedKey: self.cql_encode_map_collection,
                list: self.cql_encode_list_collection,
                tuple: self.cql_encode_list_collection,  # TODO: change to tuple in next major
                set: self.cql_encode_set_collection,
                sortedset: self.cql_encode_set_collection,
                frozenset: self.cql_encode_set_collection,
                types.GeneratorType: self.cql_encode_list_collection,
                ValueSequence: self.cql_encode_sequence
            }

            if six.PY2:
                self.mapping.update({
                    unicode: self.cql_encode_unicode,
                    buffer: self.cql_encode_bytes,
                    long: self.cql_encode_object,
                    types.NoneType: self.cql_encode_none,
                })
            else:
                self.mapping.update({
                    memoryview: self.cql_encode_bytes,
                    bytes: self.cql_encode_bytes,
                    type(None): self.cql_encode_none,
                    ipaddress.IPv4Address: self.cql_encode_ipaddress,
                    ipaddress.IPv6Address: self.cql_encode_ipaddress
                })

        def cql_encode_none(self, val):
            """
            Converts :const:`None` to the string 'NULL'.
            """
            return 'NULL'

        def cql_encode_unicode(self, val):
            """
            Converts :class:`unicode` objects to UTF-8 encoded strings with quote escaping.
            """
            return cql_quote(val.encode('utf-8'))

        def cql_encode_str(self, val):
            """
            Escapes quotes in :class:`str` objects.
            """
            return cql_quote(val)

        if six.PY3:
            def cql_encode_bytes(self, val):
                return (b'0x' + hexlify(val)).decode('utf-8')
        elif sys.version_info >= (2, 7):
            def cql_encode_bytes(self, val):  # noqa
                return b'0x' + hexlify(val)
        else:
            # python 2.6 requires string or read-only buffer for hexlify
            def cql_encode_bytes(self, val):  # noqa
                return b'0x' + hexlify(buffer(val))

        def cql_encode_object(self, val):
            """
            Default encoder for all objects that do not have a specific encoder function
            registered. This function simply calls :meth:`str()` on the object.
            """
            return str(val)

        def cql_encode_float(self, val):
            """
            Encode floats using repr to preserve precision
            """
            if math.isinf(val):
                return 'Infinity' if val > 0 else '-Infinity'
            elif math.isnan(val):
                return 'NaN'
            else:
                return repr(val)

        def cql_encode_datetime(self, val):
            """
            Converts a :class:`datetime.datetime` object to a (string) integer timestamp
            with millisecond precision.
            """
            timestamp = calendar.timegm(val.utctimetuple())
            return str(long(timestamp * 1e3 + getattr(val, 'microsecond', 0) / 1e3))

        def cql_encode_date(self, val):
            """
            Converts a :class:`datetime.date` object to a string with format
            ``YYYY-MM-DD``.
            """
            return "'%s'" % val.strftime('%Y-%m-%d')

        def cql_encode_time(self, val):
            """
            Converts a :class:`cassandra.util.Time` object to a string with format
            ``HH:MM:SS.mmmuuunnn``.
            """
            return "'%s'" % val

        def cql_encode_date_ext(self, val):
            """
            Encodes a :class:`cassandra.util.Date` object as an integer
            """
            # using the int form in case the Date exceeds datetime.[MIN|MAX]YEAR
            return str(val.days_from_epoch + 2 ** 31)

        def cql_encode_sequence(self, val):
            """
            Converts a sequence to a string of the form ``(item1, item2, ...)``.  This
            is suitable for ``IN`` value lists.
            """
            return '(%s)' % ', '.join(self.mapping.get(type(v), self.cql_encode_object)(v)
                                         for v in val)

        cql_encode_tuple = cql_encode_sequence
        """
        Converts a sequence to a string of the form ``(item1, item2, ...)``.  This
        is suitable for ``tuple`` type columns.
        """

        def cql_encode_map_collection(self, val):
            """
            Converts a dict into a string of the form ``{key1: val1, key2: val2, ...}``.
            This is suitable for ``map`` type columns.
            """
            return '{%s}' % ', '.join('%s: %s' % (
                self.mapping.get(type(k), self.cql_encode_object)(k),
                self.mapping.get(type(v), self.cql_encode_object)(v)
            ) for k, v in six.iteritems(val))

        def cql_encode_list_collection(self, val):
            """
            Converts a sequence to a string of the form ``[item1, item2, ...]``.  This
            is suitable for ``list`` type columns.
            """
            return '[%s]' % ', '.join(self.mapping.get(type(v), self.cql_encode_object)(v) for v in val)

        def cql_encode_set_collection(self, val):
            """
            Converts a sequence to a string of the form ``{item1, item2, ...}``.  This
            is suitable for ``set`` type columns.
            """
            return '{%s}' % ', '.join(self.mapping.get(type(v), self.cql_encode_object)(v) for v in val)

        def cql_encode_all_types(self, val, as_text_type=False):
            """
            Converts any type into a CQL string, defaulting to ``cql_encode_object``
            if :attr:`~Encoder.mapping` does not contain an entry for the type.
            """
            encoded = self.mapping.get(type(val), self.cql_encode_object)(val)
            if as_text_type and not isinstance(encoded, six.text_type):
                return encoded.decode('utf-8')
            return encoded

        if six.PY3:
            def cql_encode_ipaddress(self, val):
                """
                Converts an ipaddress (IPV4Address, IPV6Address) to a CQL string. This
                is suitable for ``inet`` type columns.
                """
                return "'%s'" % val.compressed