import datetime

import xml.etree.ElementTree as et

from simplegeneric import generic

import wsme.types
from wsme.exc import UnknownArgument, InvalidInput

import re

content_type = 'text/xml'
accept_content_types = [
    content_type,
]

time_re = re.compile(r'(?P<h>[0-2][0-9]):(?P<m>[0-5][0-9]):(?P<s>[0-6][0-9])')


def xml_indent(elem, level=0):
    i = "\n" + level * "  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        for e in elem:
            xml_indent(e, level + 1)
        if not e.tail or not e.tail.strip():
            e.tail = i
    if level and (not elem.tail or not elem.tail.strip()):
        elem.tail = i


@generic
def toxml(datatype, key, value):
    """
    A generic converter from python to xml elements.

    If a non-complex user specific type is to be used in the api,
    a specific toxml should be added::

        from wsme.protocol.restxml import toxml

        myspecialtype = object()

        @toxml.when_object(myspecialtype)
        def myspecialtype_toxml(datatype, key, value):
            el = et.Element(key)
            if value is None:
                el.set('nil', 'true')
            else:
                el.text = str(value)
            return el
    """
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        if wsme.types.isusertype(datatype):
            return toxml(datatype.basetype,
                         key, datatype.tobasetype(value))
        elif wsme.types.iscomplex(datatype):
            for attrdef in datatype._wsme_attributes:
                attrvalue = getattr(value, attrdef.key)
                if attrvalue is not wsme.types.Unset:
                    el.append(toxml(attrdef.datatype, attrdef.name,
                                    attrvalue))
        else:
            el.text = str(value)
    return el


@generic
def fromxml(datatype, element):
    """
    A generic converter from xml elements to python datatype.

    If a non-complex user specific type is to be used in the api,
    a specific fromxml should be added::

        from wsme.protocol.restxml import fromxml

        class MySpecialType(object):
            pass

        @fromxml.when_object(MySpecialType)
        def myspecialtype_fromxml(datatype, element):
            if element.get('nil', False):
                return None
            return MySpecialType(element.text)
    """
    if element.get('nil', False):
        return None
    if wsme.types.isusertype(datatype):
        return datatype.frombasetype(fromxml(datatype.basetype, element))
    if wsme.types.iscomplex(datatype):
        obj = datatype()
        for attrdef in wsme.types.list_attributes(datatype):
            sub = element.find(attrdef.name)
            if sub is not None:
                val_fromxml = fromxml(attrdef.datatype, sub)
                if getattr(attrdef, 'readonly', False):
                    raise InvalidInput(attrdef.name, val_fromxml,
                                       "Cannot set read only field.")
                setattr(obj, attrdef.key, val_fromxml)
            elif attrdef.mandatory:
                raise InvalidInput(attrdef.name, None,
                                   "Mandatory field missing.")
        return wsme.types.validate_value(datatype, obj)
    if datatype is wsme.types.bytes:
        return element.text.encode('ascii')
    return datatype(element.text)


@toxml.when_type(wsme.types.ArrayType)
def array_toxml(datatype, key, value):
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        for item in value:
            el.append(toxml(datatype.item_type, 'item', item))
    return el


@toxml.when_type(wsme.types.DictType)
def dict_toxml(datatype, key, value):
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        for item in value.items():
            key = toxml(datatype.key_type, 'key', item[0])
            value = toxml(datatype.value_type, 'value', item[1])
            node = et.Element('item')
            node.append(key)
            node.append(value)
            el.append(node)
    return el


@toxml.when_object(wsme.types.bytes)
def bytes_toxml(datatype, key, value):
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        el.text = value.decode('ascii')
    return el


@toxml.when_object(bool)
def bool_toxml(datatype, key, value):
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        el.text = value and 'true' or 'false'
    return el


@toxml.when_object(datetime.date)
def date_toxml(datatype, key, value):
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        el.text = value.isoformat()
    return el


@toxml.when_object(datetime.datetime)
def datetime_toxml(datatype, key, value):
    el = et.Element(key)
    if value is None:
        el.set('nil', 'true')
    else:
        el.text = value.isoformat()
    return el


@fromxml.when_type(wsme.types.ArrayType)
def array_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return [
        fromxml(datatype.item_type, item)
        for item in element.findall('item')
    ]


@fromxml.when_object(bool)
def bool_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return element.text.lower() != 'false'


@fromxml.when_type(wsme.types.DictType)
def dict_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return dict((
        (fromxml(datatype.key_type, item.find('key')),
            fromxml(datatype.value_type, item.find('value')))
        for item in element.findall('item')))


@fromxml.when_object(wsme.types.text)
def unicode_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return wsme.types.text(element.text) if element.text else ''


@fromxml.when_object(datetime.date)
def date_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return wsme.utils.parse_isodate(element.text)


@fromxml.when_object(datetime.time)
def time_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return wsme.utils.parse_isotime(element.text)


@fromxml.when_object(datetime.datetime)
def datetime_fromxml(datatype, element):
    if element.get('nil') == 'true':
        return None
    return wsme.utils.parse_isodatetime(element.text)


def parse(s, datatypes, bodyarg):
    if hasattr(s, 'read'):
        tree = et.parse(s)
    else:
        tree = et.fromstring(s)
    if bodyarg:
        name = list(datatypes.keys())[0]
        return {name: fromxml(datatypes[name], tree)}
    else:
        kw = {}
        extra_args = []
        for sub in tree:
            if sub.tag not in datatypes:
                extra_args.append(sub.tag)
            kw[sub.tag] = fromxml(datatypes[sub.tag], sub)
        if extra_args:
            raise UnknownArgument(', '.join(extra_args))
        return kw


def encode_result(value, datatype, **options):
    return et.tostring(toxml(
        datatype, options.get('nested_result_attrname', 'result'), value
    ))


def encode_error(context, errordetail):
    el = et.Element('error')
    et.SubElement(el, 'faultcode').text = errordetail['faultcode']
    et.SubElement(el, 'faultstring').text = errordetail['faultstring']
    if 'debuginfo' in errordetail:
        et.SubElement(el, 'debuginfo').text = errordetail['debuginfo']
    return et.tostring(el)


def encode_sample_value(datatype, value, format=False):
    r = toxml(datatype, 'value', value)
    if format:
        xml_indent(r)
    content = et.tostring(r)
    return ('xml', content)


def encode_sample_params(params, format=False):
    node = et.Element('parameters')
    for name, datatype, value in params:
        node.append(toxml(datatype, name, value))
    if format:
        xml_indent(node)
    content = et.tostring(node)
    return ('xml', content)


def encode_sample_result(datatype, value, format=False):
    r = toxml(datatype, 'result', value)
    if format:
        xml_indent(r)
    content = et.tostring(r)
    return ('xml', content)
