
#
# spyne - Copyright (C) Spyne contributors.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#


import decimal
import uuid

from spyne.model.primitive import NATIVE_MAP
from spyne.util import six
from spyne.model._base import SimpleModel
from spyne.model.primitive._base import re_match_with_span


UUID_PATTERN = "%(x)s{8}-%(x)s{4}-%(x)s{4}-%(x)s{4}-%(x)s{12}" % \
                                                            {'x': '[a-fA-F0-9]'}

LTREE_PATTERN = r"\w+(\.\w+)*"

# Actual ltree max size is 65536 but it's advised to keep it under 2048.
LTREE_OPTIMAL_SIZE = 2048
LTREE_MAXIMUM_SIZE = 65536


def _gen_mime_type_pattern(strict, with_params):
    ows = "[ \\t]*"  # Optional WhiteSpace
    token = "[0-9A-Za-z!#$%&'*+.^_`|~-]+"
    quotedString = "\"(?:[^\"\\\\]|\\.)*\""
    if strict:
        main_type = "(" \
                "application|audio|font|example|image|message|model|multipart" \
                "|text|video|x-(?:" + token + ")" \
            ")"

    else:
        main_type = token

    param = token + "=" + "(?:" + token + "|" + quotedString + ");?" + ows
    params = ";" + ows + param + "(" + param + ")*"

    if not with_params:
        return main_type + "/" + "(" + token + ")"
    else:
        return main_type + "/" + "(" + token + ")" + params


MIME_TYPE_PATTERN_STRICT = \
    _gen_mime_type_pattern(strict=True, with_params=False)
MIME_TYPE_PATTERN_PERMISSIVE = \
    _gen_mime_type_pattern(strict=False, with_params=False)

MEDIA_TYPE_PATTERN_STRICT = \
    _gen_mime_type_pattern(strict=True, with_params=True)
MEDIA_TYPE_PATTERN_PERMISSIVE = \
    _gen_mime_type_pattern(strict=False, with_params=True)


class Unicode(SimpleModel):
    """The type to represent human-readable data. Its native format is `unicode`
    or `str` with given encoding.
    """

    __type_name__ = 'string'
    Value = six.text_type

    class Attributes(SimpleModel.Attributes):
        """Customizable attributes of the :class:`spyne.model.primitive.Unicode`
        type."""

        min_len = 0
        """Minimum length of string. Can be set to any positive integer"""

        max_len = decimal.Decimal('inf')
        """Maximum length of string. Can be set to ``decimal.Decimal('inf')`` to
        accept strings of arbitrary length. You may also need to adjust
        :const:`spyne.server.wsgi.MAX_CONTENT_LENGTH`."""

        pattern = None
        """A regular expression that matches the whole string. See here for more
        info: http://www.regular-expressions.info/xml.html"""

        unicode_pattern = None
        """Same as ``pattern``, but, will be compiled with ``re.UNICODE``.
        See: https://docs.python.org/2/library/re.html#re.UNICODE"""

        encoding = None
        """The encoding of binary data this class may have to deal with."""

        unicode_errors = 'strict'
        """The argument to the ``unicode`` builtin; one of 'strict', 'replace'
        or 'ignore'."""

        format = None
        """A regular python string formatting string. See here:
        http://docs.python.org/library/stdtypes.html#string-formatting"""

        cast = None
        """Type override callable for casting non-unicode input to unicode."""

    def __new__(cls, *args, **kwargs):
        assert len(args) <= 1

        if len(args) == 1:
            kwargs['max_len'] = args[0]

        retval = SimpleModel.__new__(cls,  ** kwargs)

        return retval

    @staticmethod
    def is_default(cls):
        return (    SimpleModel.is_default(cls)
                and cls.Attributes.min_len == Unicode.Attributes.min_len
                and cls.Attributes.max_len == Unicode.Attributes.max_len
                and cls.Attributes.pattern == Unicode.Attributes.pattern
            )

    @staticmethod
    def validate_string(cls, value):
        return (     SimpleModel.validate_string(cls, value)
            and (value is None or (
                cls.Attributes.min_len <= len(value) <= cls.Attributes.max_len
            )))

    @staticmethod
    def validate_native(cls, value):
        return (SimpleModel.validate_native(cls, value)
            and (value is None or (
                re_match_with_span(cls.Attributes, value)
            )))


class String(Unicode):
    pass

if not six.PY2:
    String = Unicode


class AnyUri(Unicode):
    """A special kind of String type designed to hold an uri."""

    __type_name__ = 'anyURI'

    class Attributes(String.Attributes):
        text = None
        """The text shown in link."""

        anchor_class = None
        """The class of the generated <a> tag."""

    class Value(object):
        """A special object that is just a better way of carrying the
        information carried with a link.

        :param href: The uri string.
        :param text: The text data that goes with the link. This is a
            ``str`` or a ``unicode`` instance.
        :param content: The structured data that goes with the link. This is an
            `lxml.etree.Element` instance.
        """

        def __init__(self, href, text=None, content=None):
            self.href = href
            self.text = text
            self.content = content

        def __repr__(self):
            return "Uri(href={0!r}, text={1!r}, content={2!r})" \
                                     .format(self.href, self.text, self.content)


class ImageUri(AnyUri):
    """A special kind of String that holds the uri of an image."""


def _uuid_validate_string(cls, value):
    return (     SimpleModel.validate_string(cls, value)
        and (value is None or (
            cls.Attributes.min_len <= len(value) <= cls.Attributes.max_len
            and re_match_with_span(cls.Attributes, value)
        )))


def _Tuuid_validate(key):
    from uuid import UUID

    def _uvalid(cls, v):
        try:
            UUID(**{key:v})
        except ValueError:
            return False
        return True
    return _uvalid


_uuid_validate = {
    None: _uuid_validate_string,
    'hex': _Tuuid_validate('hex'),
    'urn': _Tuuid_validate('urn'),
    six.binary_type: _Tuuid_validate('bytes'),
    'bytes': _Tuuid_validate('bytes'),
    'bytes_le': _Tuuid_validate('bytes_le'),
    'fields': _Tuuid_validate('fields'),
    int: _Tuuid_validate('int'),
    'int': _Tuuid_validate('int'),
}


class Uuid(Unicode(pattern=UUID_PATTERN)):
    """Unicode subclass for Universially-Unique Identifiers."""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'uuid'
    Value = uuid.UUID

    class Attributes(Unicode(pattern=UUID_PATTERN).Attributes):
        serialize_as = None

    @staticmethod
    def validate_string(cls, value):
        return _uuid_validate[cls.Attributes.serialize_as](cls, value)

    @staticmethod
    def validate_native(cls, value):
        return SimpleModel.validate_native(cls, value)


class Ltree(Unicode(LTREE_OPTIMAL_SIZE, unicode_pattern=LTREE_PATTERN)):
    """A special kind of String type designed to hold the Ltree type from
    Postgresql."""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'ltreeString'


class LtreeLarge(Unicode(LTREE_MAXIMUM_SIZE, unicode_pattern=LTREE_PATTERN)):
    """A special kind of String type designed to hold the Ltree type from
    Postgresql."""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'largeLtreeString'


class MimeTypeStrict(Unicode(unicode_pattern=MIME_TYPE_PATTERN_STRICT)):
    """A special kind of String type designed to hold a mime type as defined
    by IANA."""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'strictMimeTypeString'


class MimeType(Unicode(unicode_pattern=MIME_TYPE_PATTERN_PERMISSIVE)):
    """A special kind of String type designed to hold a forward-compatible
    mime type that can have any string as main type."""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'mimeTypeString'


class MediaTypeStrict(Unicode(unicode_pattern=MEDIA_TYPE_PATTERN_STRICT)):
    """A special kind of String type designed to hold a mime type as defined
    by IANA followed by arbitrary parameters.

    See: https://tools.ietf.org/html/rfc7231#section-3.1.1.1"""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'strictMediaTypeString'


class MediaType(Unicode(unicode_pattern=MEDIA_TYPE_PATTERN_PERMISSIVE)):
    """A special kind of String type designed to hold a forward-compatible
    media type that can have any string as main type. A media type is
    essentially a mime type plus parameters.

    See: https://tools.ietf.org/html/rfc7231#section-3.1.1.1"""

    __namespace__ = 'http://spyne.io/schema'
    __type_name__ = 'mediaTypeString'


if not six.PY2:
    NATIVE_MAP.update({
        str: Unicode,
    })

else:
    NATIVE_MAP.update({
        str: String,
        unicode: Unicode,
    })
