import datetime
import re

from . import base
from . import primitives
from . import domains
from .. import exceptions


# Patterns

REGEX_FROM_ISO8601 = r"""(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d)(?:T|\ )
    (?P<hour>\d\d):(?P<minute>\d\d)
    (?::(?P<second>\d\d)(?:(?:\.|,)(?P<sec_frac>\d{1,6}))?)?
    (?:(?P<tzd_offset>(?P<tzd_sign>[+−-])(?P<tzd_hour>\d\d):?(?P<tzd_minute>\d\d)?)
    |(?P<tzd_utc>Z))?$"""

REGEX_TO_ISO8601 = '%Y-%m-%dT%H:%M:%S.%f%z'

REGEX_FROM_TIME = r"""(?P<hour>\d\d):(?P<minute>\d\d)
    (?::(?P<second>\d\d)(?:(?:\.|,)(?P<sec_frac>\d{1,6}))?)?$"""


# Types

class DateTimeType(primitives.Primitive):
    NATIVE = datetime.datetime

    def __init__(self, regex=REGEX_FROM_ISO8601, **kw):
        super().__init__(**kw)
        domains.RegexDomain(self, regex, re.X)

    @base.skip_falsy
    def validate(self, value):
        super().validate(value)

    @base.skip_falsy
    def to_native(self, value):
        if self.is_type_match(value):
            return value

        elif not isinstance(value, str):
            e_msg = "Value is not a string: {}"
            raise exceptions.TypeException(e_msg.format(value))

        # Verify regex
        match = self._regex.match(value)
        if not match:
            e_msg = "Value did not match date pattern: {}"
            raise exceptions.TypeException(e_msg.format(value))

        # Map of regex keys that had values
        parts = {k: v for k, v in match.groupdict().items() if v is not None}

        # Accessor for regex parts as numbers
        p = lambda name: int(parts.get(name, 0))

        # Microseconds
        microsecond = 0
        if 'sec_frac' in parts:
            sec_frac = p('sec_frac')
            sf_len = sec_frac and len(parts['sec_frac'])
            microsecond = sec_frac and sec_frac * 10 ** (6 - sf_len)

        # Timezones
        tz = None
        if 'tzd_utc' in parts:
            tz = datetime.timezone.utc
        elif 'tzd_offset' in parts:
            tz_sign = 1 if parts['tzd_sign'] == '+' else -1
            tz_offset = (p('tzd_hour') * 60 + p('tzd_minute')) * tz_sign
            if tz_offset == 0:
                tz = datetime.timezone.utc
            else:
                tz = datetime.timezone(datetime.timedelta(minutes=tz_offset))

        return self.NATIVE(
            p('year'), p('month'), p('day'), p('hour'), p('minute'),
            p('second'), microsecond, tz
        )

    @base.skip_falsy
    def to_primitive(self, value, context=None):
        if isinstance(value, str) and self._regex.match(value):
            return value
        elif isinstance(value, self.NATIVE):
            return value.strftime(REGEX_TO_ISO8601)
        e_msg = "Value must be iso8601 string or datetime.datetime: {}"
        raise exceptions.TypeException(e_msg.format(value))


class TimeType(primitives.Primitive):
    NATIVE = datetime.time

    def __init__(self, regex=REGEX_FROM_TIME, **kw):
        super().__init__(**kw)
        domains.RegexDomain(self, regex, re.X)

    @base.skip_falsy
    def validate(self, value):
        super().validate(value)

    @base.skip_falsy
    def to_native(self, value):
        if isinstance(value, self.NATIVE):
            return value

        parts = {}
        match = self._regex.match(value)
        for k, v in match.groupdict().items():
            if v is not None:
                parts[k] = int(v)
        return datetime.time(**parts)

    @base.skip_falsy
    def to_primitive(self, value):
        if isinstance(value, self.NATIVE):
            return value.isoformat()
        if isinstance(value, str):
            return value
        e_msg = "Value must be time string or datetime.time: {}"
        raise exceptions.TypeException(e_msg.format(value))
