"""
"""
import base64
import copy
import inspect
import logging
import re
import urlparse

import pilo

from . import Expression, exp, types

logger = logging.getLogger(__name__)


class PathMixin(object):

    @property
    def path(self):
        return self.name


class BooleanMixin(Expression):

    inv = False

    def __eq__(self, other):
        return exp.FieldEqual(self, other)

    def __ne__(self, other):
        return exp.FieldNotEqual(self, other)

    def __invert__(self):
        other = copy.copy(self)
        other.inv = not other.inv
        return other

    # Expression

    def __call__(self, request):
        value = self.__get__(request)
        result = False if value is None else value
        if self.inv:
            result = not result
        return result

    def __str__(self):
        return '{inv}{op}'.format(
            inv='not ' if self.inv else '',
            op=self.name,
        )

    def compile(self, symbols):
        field_key = symbols.field(self)
        return '{inv}request[{field}]'.format(
            inv='not ' if self.inv else '',
            field=field_key,
        )


class Boolean(BooleanMixin, pilo.fields.Boolean, exp.UnaryOp, PathMixin):

    type = bool


class BooleanSubField(BooleanMixin, exp.SubField):

    type = bool


class StringMixin(object):

    def __eq__(self, other):
        return exp.FieldEqual(self, other)

    def __ne__(self, other):
        return exp.FieldNotEqual(self, other)

    def contains(self, item):
        return exp.FieldContains(self, item)

    def in_(self, others):
        return exp.FieldIn(self, others)

    def match(self, pattern, ignore_case=False):
        flags = 0
        if ignore_case:
            flags |= re.I
        pattern_re = re.compile(pattern, flags)
        return exp.FieldMatch(self, pattern_re)

    def startswith(self, prefix):
        return exp.FieldStartswith(self, prefix)

    def endswith(self, suffix):
        return exp.FieldEndswith(self, suffix)


class String(pilo.fields.String, PathMixin, StringMixin):

    type = str


class StringSubField(exp.SubField, StringMixin):

    type = str


class IntegerMixin(object):

    def __eq__(self, other):
        return exp.FieldEqual(self, other)

    def __ne__(self, other):
        return exp.FieldNotEqual(self, other)

    def __lt__(self, other):
        return exp.FieldLessThan(self, other)

    def __le__(self, other):
        return exp.FieldLessThanEqual(self, other)

    def __gt__(self, other):
        return exp.FieldGreaterThan(self, other)

    def __ge__(self, other):
        return exp.FieldGreaterThanEqual(self, other)

    def in_(self, others):
        return exp.FieldIn(self, others)


class Integer(pilo.fields.Integer, PathMixin, IntegerMixin):

    type = int


class NamedTuple(pilo.Field, PathMixin):

    type = pilo.NOT_SET

    def __init__(self, *args, **kwargs):
        self.type = kwargs.pop('type', pilo.NOT_SET)
        if self.type is pilo.NOT_SET:
            args, self.type = pilo.fields.pluck(args, lambda arg: (
                inspect.isclass(arg) and issubclass(arg, types.NamedTuple)
            ))
            if self.type is pilo.NOT_SET:
                raise TypeError('Missing type=NamedTuple')
        super(NamedTuple, self).__init__(*args, **kwargs)

    def __getattr__(self, name):
        field = getattr(self.type, name, None)
        if field is None:
            raise AttributeError(
                '{0}.{1} does not exist'.format(self.type, name)
            )
        if not hasattr(field, 'type'):
            raise AttributeError(
                '{0}.{1}.type does not exist'.format(self.type, name)
            )
        if issubclass(field.type, str):
            sub_field_type = StringSubField
        elif issubclass(field.type, bool):
            sub_field_type = BooleanSubField
        else:
            raise TypeError(
                '{0}.{1}.type={2} is not supported, must be on of {3}'
                .format(self.type, name, field.type, [str, bool])
            )
        sub_field = sub_field_type(self, name)
        setattr(self, name, sub_field)
        return sub_field


class StringHash(pilo.Field, PathMixin):

    type = types.StringHash

    def __getattr__(self, name):
        return StringSubField(self, name)

    def contains(self, item):
        return exp.FieldContains(self, item)


class ArgumentHash(pilo.Field, PathMixin):

    type = types.ArgumentHash

    def __getattr__(self, name):
        return StringSubField(self, name)

    def contains(self, item):
        return exp.FieldContains(self, item)


class IPAddress(pilo.Field, PathMixin):

    type = types.IPAddress

    def __eq__(self, other):
        return exp.FieldEqual(self, other)

    def __ne__(self, other):
        return exp.FieldNotEqual(self, other)

    def in_(self, others):
        return exp.FieldIn(self, others)


class Object(pilo.Field, PathMixin):

    type = object


class HeaderHash(pilo.fields.Group, PathMixin):

    type = types.HeaderHash

    def __init__(self, *args, **kwargs):
        super(HeaderHash, self).__init__(
            (re.compile('HTTP\_(.+)'), String()), *args, **kwargs
        )

    def _munge(self, value):
        return dict(
            (match.group(0).lower(), value)
            for _, match, value in super(HeaderHash, self)._munge(value)
        )

    def __getattr__(self, name):
        return StringSubField(self, name)

    def contains(self, item):
        return exp.FieldContains(self, item)


class BasicAuthorization(types.NamedTuple):

    username = String()

    password = String()


class Request(pilo.Form):
    """
    `environ`

    `router`

    """

    def __init__(self, environ, router=None):
        super(Request, self).__init__()
        self.src = pilo.source.DefaultSource(environ)
        self.router = router

    def context(self, symbols):
        """
        :param symbols:

        :return:
        """
        return exp.Context(self, symbols)

    method = String('REQUEST_METHOD')

    path = String('PATH_INFO')

    query_string = String('QUERY_STRING')

    query = ArgumentHash()

    @query.compute
    def query(self):
        if self.query_string:
            query = dict(
                (k, v[-1])
                for k, v in urlparse.parse_qs(self.query_string).iteritems()
            )
        else:
            query = {}
        query_hash = types.StringHash(**query)
        return query_hash

    content_type = String('CONTENT_TYPE', default=None)

    content_length = Integer('CONTENT_LENGTH', default=None)

    headers = HeaderHash()

    host = String('HTTP_HOST')

    authenticated = Boolean('HTTP_AUTHORIZATION', default=False)

    @authenticated.parse
    def authenticated(self, path):
        return path.exists and not path.is_null

    basic_authorization = NamedTuple(
        BasicAuthorization, 'HTTP_AUTHORIZATION', default=None
    )

    @basic_authorization.parse
    def basic_authorization(self, path):
        v = path.primitive(basestring)
        if not v.startswith('Basic '):
            return
        encoded = v[len('Basic '):]
        try:
            username, _, password = base64.b64decode(encoded).partition(':')
        except (TypeError, ValueError):
            return
        return BasicAuthorization(username=username, password=password)

    username = String(nullable=True)

    @username.compute
    def username(self):
        if self.basic_authorization:
            return self.basic_authorization.username

    password = String(nullable=True)

    @password.compute
    def password(self):
        if self.basic_authorization:
            return self.basic_authorization.password

    client_ip4 = IPAddress('REMOTE_ADDR')

    @client_ip4.parse
    def client_ip4(self, path):
        return types.IPAddress(path.primitive(basestring))

    has_content = Boolean()

    @has_content.compute
    def has_content(self):
        return (
            self.content_type is not None and
            self.content_length not in((0, None))
        )

    content = String('wsgi.input')

    @content.parse
    def content(self, path):
        if not self.has_content:
            return None
        io = path.value
        if not io:
            return b''
        return io.read(self.content_length)
