# Copyright (c) 2007 Michael Twomey
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import re
import time
from datetime import datetime, timedelta, tzinfo

__all__ = ["parse_date", "ParseError"]

# Adapted from http://delete.me.uk/2005/03/iso8601.html
ISO8601_REGEX_RAW = "(?P<year>[0-9]{4})-(?P<month>[0-9]{1,2})-(?P<day>[0-9]{1,2})" \
                    "T(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(.(?P<fraction>[0-9]+))?)?" \
                    "(?P<timezone>Z|[-+][0-9]{2}(:?[0-9]{2})?)?"
ISO8601_REGEX = re.compile(ISO8601_REGEX_RAW)
TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})?")


class ParseError(Exception):
    """Raised when there is a problem parsing a date string"""


# Yoinked from python docs
ZERO = timedelta(0)


class Utc(tzinfo):
    """UTC

    """

    def utcoffset(self, dt):
        return ZERO

    def tzname(self, dt):
        return "UTC"

    def dst(self, dt):
        return ZERO


UTC = Utc()


class FixedOffset(tzinfo):
    """Fixed offset in hours and minutes from UTC

    """

    def __init__(self, name, offset_hours, offset_minutes, offset_seconds=0):
        self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes, seconds=offset_seconds)
        self.__name = name

    def utcoffset(self, dt):
        return self.__offset

    def tzname(self, dt):
        return self.__name

    def dst(self, dt):
        return ZERO

    def __repr__(self):
        return "<FixedOffset %r>" % self.__name


def parse_timezone(tzstring):
    """Parses ISO 8601 time zone specs into tzinfo offsets

    """
    if tzstring == "Z":
        return UTC

    if tzstring is None:
        zone_sec = -time.timezone
        return FixedOffset(name=time.tzname[0], offset_hours=(zone_sec / 3600), offset_minutes=(zone_sec % 3600) / 60,
                           offset_seconds=zone_sec % 60)

    m = TIMEZONE_REGEX.match(tzstring)
    prefix, hours, minutes = m.groups()
    if minutes is None:
        minutes = 0
    else:
        minutes = int(minutes)
    hours = int(hours)
    if prefix == "-":
        hours = -hours
        minutes = -minutes
    return FixedOffset(tzstring, hours, minutes)


def parse_date(datestring):
    """Parses ISO 8601 dates into datetime objects

    The timezone is parsed from the date string. However it is quite common to
    have dates without a timezone (not strictly correct). In this case the
    default timezone specified in default_timezone is used. This is UTC by
    default.
    """
    if not isinstance(datestring, str):
        raise ValueError("Expecting a string %r" % datestring)
    m = ISO8601_REGEX.match(datestring)
    if not m:
        raise ParseError("Unable to parse date string %r" % datestring)
    groups = m.groupdict()
    tz = parse_timezone(groups["timezone"])
    if groups["fraction"] is None:
        groups["fraction"] = 0
    else:
        groups["fraction"] = int(float("0.{}".format(groups["fraction"])) * 1e6)

    try:
        return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]),
                        int(groups["hour"]), int(groups["minute"]), int(groups["second"]),
                        int(groups["fraction"]), tz)
    except Exception as e:
        raise ParseError("Failed to create a valid datetime record due to: {}".format(e))
