# Copyright (C) 2022 Exaloop Inc. <https://exaloop.io>
# Parts of this file: (c) 2022 Python Software Foundation. All right reserved.
# - Currently does not support timezones
# - Timedeltas use a pure-microseconds representations for efficiency, meaning they
#   have a smaller range (+/- 292,471.2 years) but should be more than enough for
#   all practical uses
# License:
#    1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and
#    the Individual or Organization ("Licensee") accessing and otherwise using Python
#    3.10.2 software in source or binary form and its associated documentation.
#
#    2. Subject to the terms and conditions of this License Agreement, PSF hereby
#    grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
#    analyze, test, perform and/or display publicly, prepare derivative works,
#    distribute, and otherwise use Python 3.10.2 alone or in any derivative
#    version, provided, however, that PSF's License Agreement and PSF's notice of
#    copyright, i.e., "Copyright © 2001-2022 Python Software Foundation; All Rights
#    Reserved" are retained in Python 3.10.2 alone or in any derivative version
#    prepared by Licensee.
#
#    3. In the event Licensee prepares a derivative work that is based on or
#    incorporates Python 3.10.2 or any part thereof, and wants to make the
#    derivative work available to others as provided herein, then Licensee hereby
#    agrees to include in any such work a brief summary of the changes made to Python
#    3.10.2.
#
#    4. PSF is making Python 3.10.2 available to Licensee on an "AS IS" basis.
#    PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED.  BY WAY OF
#    EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR
#    WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE
#    USE OF PYTHON 3.10.2 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
#
#    5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.10.2
#    FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
#    MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.10.2, OR ANY DERIVATIVE
#    THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
#
#    6. This License Agreement will automatically terminate upon a material breach of
#    its terms and conditions.
#
#    7. Nothing in this License Agreement shall be deemed to create any relationship
#    of agency, partnership, or joint venture between PSF and Licensee.  This License
#    Agreement does not grant permission to use PSF trademarks or trade name in a
#    trademark sense to endorse or promote products or services of Licensee, or any
#    third party.
#
#    8. By copying, installing or otherwise using Python 3.10.2, Licensee agrees
#    to be bound by the terms and conditions of this License Agreement.

from time import localtime
from time import struct_time

#############
# constants #
#############

MINYEAR = 1
MAXYEAR = 9999
MAXORDINAL = 3652059
MAX_DELTA_DAYS = 999999999

_DI4Y = 1461
_DI100Y = 36524
_DI400Y = 146097

_ROUND_HALF_EVEN = 0
_ROUND_CEILING = 1
_ROUND_FLOOR = 2
_ROUND_UP = 3

#############
# utilities #
#############

def _signed_add_overflowed(result: int, i: int, j: int) -> bool:
    return ((result ^ i) & (result ^ j)) < 0

def _divmod(x: int, y: int) -> Tuple[int, int]:
    # assert y > 0
    quo = x // y
    r = x - quo * y
    if r < 0:
        quo -= 1
        r += y
    # assert 0 <= r < y
    return quo, r

def _divide_nearest(m: int, n: int) -> int:
    return m // n  # TODO

def _days_in_monthx(i: int) -> int:
    return (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[i]

def _days_before_monthx(i: int) -> int:
    return (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334)[i]

def _is_leap(year: int) -> bool:
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def _days_in_month(year: int, month: int) -> int:
    # assert 1 <= month <= 12
    if month == 2 and _is_leap(year):
        return 29
    else:
        return _days_in_monthx(month)

def _days_before_month(year: int, month: int) -> int:
    # assert 1 <= month <= 12
    days = _days_before_monthx(month)
    if month > 2 and _is_leap(year):
        days += 1
    return days

def _days_before_year(year: int) -> int:
    y = year - 1
    # assert year >= 1
    return y * 365 + y // 4 - y // 100 + y // 400

def _ord_to_ymd(ordinal: int) -> Tuple[int, int, int]:
    ordinal -= 1
    n400 = ordinal // _DI400Y
    n = ordinal % _DI400Y
    year = n400 * 400 + 1

    n100 = n // _DI100Y
    n = n % _DI100Y

    n4 = n // _DI4Y
    n = n % _DI4Y

    n1 = n // 365
    n = n % 365

    year += n100 * 100 + n4 * 4 + n1
    if n1 == 4 or n100 == 4:
        # assert n == 0
        year -= 1
        return (year, 12, 31)

    leapyear = (n1 == 3) and (n4 != 24 or n100 == 3)
    # assert leapyear == is_leap(year)
    month = (n + 50) >> 5
    preceding = _days_before_monthx(month) + int(month > 2 and leapyear)
    if preceding > n:
        month -= 1
        preceding -= _days_in_month(year, month)
    n -= preceding
    # assert 0 <= n
    # assert n < _days_in_month(year, month)
    day = n + 1
    return (year, month, day)

def _ymd_to_ord(year: int, month: int, day: int) -> int:
    return _days_before_year(year) + _days_before_month(year, month) + day

def _weekday(year: int, month: int, day: int) -> int:
    return (_ymd_to_ord(year, month, day) + 6) % 7

def _iso_week1_monday(year: int) -> int:
    first_day = _ymd_to_ord(year, 1, 1)
    first_weekday = (first_day + 6) % 7
    week1_monday = first_day - first_weekday
    if first_weekday > 3:
        week1_monday += 7
    return week1_monday

def _check_delta_day_range(days: int):
    if not (-MAX_DELTA_DAYS <= days <= MAX_DELTA_DAYS):
        raise OverflowError(f"days={days}; must have magnitude <= {MAX_DELTA_DAYS}")

def _check_date_args(year: int, month: int, day: int):
    if not (MINYEAR <= year <= MAXYEAR):
        raise ValueError(f"year {year} is out of range")
    if not (1 <= month <= 12):
        raise ValueError("month must be in 1..12")
    if not (1 <= day <= _days_in_month(year, month)):
        raise ValueError("day is out of range for month")

def _check_time_args(hour: int, minute: int, second: int, microsecond: int):
    if not (0 <= hour <= 23):
        raise ValueError("hour must be in 0..23")
    if not (0 <= minute <= 59):
        raise ValueError("minute must be in 0..59")
    if not (0 <= second <= 59):
        raise ValueError("second must be in 0..59")
    if not (0 <= microsecond <= 999999):
        raise ValueError("microsecond must be in 0..999999")

def _normalize_pair(hi: int, lo: int, factor: int) -> Tuple[int, int]:
    # assert factor > 0
    if lo < 0 or lo >= factor:
        num_hi, lo = _divmod(lo, factor)
        new_hi = hi + num_hi
        # assert not _signed_add_overflowed(new_hi, hi, num_hi)
        hi = new_hi
    # assert 0 <= lo < factor
    return hi, lo

def _normalize_d_s_us(d: int, s: int, us: int) -> Tuple[int, int, int]:
    if us < 0 or us >= 1000000:
        s, us = _normalize_pair(s, us, 1000000)
    if s < 0 or s >= 24 * 3600:
        d, s = _normalize_pair(d, s, 24 * 3600)
    # assert 0 <= s < 24*3600
    # assert 0 <= us < 1000000
    return d, s, us

def _normalize_y_m_d(y: int, m: int, d: int) -> Tuple[int, int, int]:
    def error():
        raise OverflowError("date value out of range")

    # assert 1 <= m <= 12
    dim = _days_in_month(y, m)
    if d < 1 or d > dim:
        if d == 0:
            m -= 1
            if m > 0:
                d = _days_in_month(y, m)
            else:
                y -= 1
                m = 12
                d = 31
        elif d == dim + 1:
            m += 1
            d = 1
            if m > 12:
                m = 1
                y += 1
        else:
            ordinal = _ymd_to_ord(y, m, 1) + d - 1
            if ordinal < 1 or ordinal > MAXORDINAL:
                error()
            else:
                return _ord_to_ymd(ordinal)
    # assert m > 0
    # assert d > 0
    if not (MINYEAR <= y <= MAXYEAR):
        error()
    return y, m, d

def _normalize_date(year: int, month: int, day: int) -> Tuple[int, int, int]:
    return _normalize_y_m_d(year, month, day)

def _normalize_datetime(
    year: int,
    month: int,
    day: int,
    hour: int,
    minute: int,
    second: int,
    microsecond: int,
) -> Tuple[int, int, int, int, int, int, int]:
    second, microsecond = _normalize_pair(second, microsecond, 1000000)
    minute, second = _normalize_pair(minute, second, 60)
    hour, minute = _normalize_pair(hour, minute, 60)
    day, hour = _normalize_pair(day, hour, 24)
    year, month, day = _normalize_date(year, month, day)
    return year, month, day, hour, minute, second, microsecond


def _parse_digits(digits: str, num_digits: int) -> Tuple[str, int]:
    if len(digits) < num_digits:
        return "", -1
    p = digits.ptr
    var = 0
    for i in range(num_digits):
        tmp = int(p[0]) - 48  # 48 == '0'
        if not (0 <= tmp <= 9):
            return "", -1
        var *= 10
        var += tmp
        p += 1
    return str(p, len(digits) - num_digits), var

def _isoformat_error(s: str):
    raise ValueError(f"Invalid isoformat string: {s}")

def _parse_isoformat_date(dtstr: str) -> Tuple[int, int, int]:
    p = dtstr
    p, year = _parse_digits(p, 4)
    if year < 0:
        _isoformat_error(dtstr)

    if not p or p[0] != "-":
        _isoformat_error(dtstr)
    p = p[1:]

    p, month = _parse_digits(p, 2)
    if month < 0:
        _isoformat_error(dtstr)

    if not p or p[0] != "-":
        _isoformat_error(dtstr)
    p = p[1:]

    p, day = _parse_digits(p, 2)
    if day < 0 or p:
        _isoformat_error(dtstr)

    return year, month, day

def _parse_hh_mm_ss_ff(tstr: str) -> Tuple[int, int, int, int]:
    hour, minute, second, microsecond = 0, 0, 0, 0

    p = tstr
    for i in range(3):
        p, val = _parse_digits(p, 2)
        if val < 0:
            _isoformat_error(tstr)

        if i == 0:
            hour = val
        if i == 1:
            minute = val
        if i == 2:
            second = val

        if not p:
            return hour, minute, second, microsecond
        c = p[0]
        p = p[1:]
        if c == ":":
            continue
        elif c == ".":
            break
        else:
            _isoformat_error(tstr)

    len_remains = len(p)
    if not (len_remains == 6 or len_remains == 3):
        _isoformat_error(tstr)

    p, microsecond = _parse_digits(p, len_remains)
    if microsecond < 0:
        _isoformat_error(tstr)

    if len_remains == 3:
        microsecond *= 1000

    return hour, minute, second, microsecond

def _parse_isoformat_time(dtstr: str) -> Tuple[int, int, int, int, int, int]:
    n = len(dtstr)
    tzinfo_pos = 0
    tzsign = 0
    while tzinfo_pos < n:
        c = dtstr[tzinfo_pos]
        if c == "+":
            tzsign = 1
            break
        if c == "-":
            tzsign = -1
            break
        tzinfo_pos += 1

    hour, minute, second, microsecond = _parse_hh_mm_ss_ff(dtstr[:tzinfo_pos])
    if tzinfo_pos == n:
        return hour, minute, second, microsecond, 0, 0

    tzlen = n - tzinfo_pos
    if not (tzlen == 6 or tzlen == 9 or tzlen == 16):
        _isoformat_error(dtstr)

    tzhour, tzminute, tzsecond, tzmicrosecond = _parse_hh_mm_ss_ff(
        dtstr[tzinfo_pos + 1 :]
    )
    tzoffset = tzsign * ((tzhour * 3600) + (tzminute * 60) + tzsecond)
    tzmicrosecond *= tzsign
    return hour, minute, second, microsecond, tzoffset, tzmicrosecond

def _format_ctime(
    year: int, month: int, day: int, hours: int, minutes: int, seconds: int
) -> str:
    DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
    MONTH_NAMES = (
        "Jan",
        "Feb",
        "Mar",
        "Apr",
        "May",
        "Jun",
        "Jul",
        "Aug",
        "Sep",
        "Oct",
        "Nov",
        "Dec",
    )
    wday = _weekday(year, month, day)
    return f"{DAY_NAMES[wday]} {MONTH_NAMES[month - 1]} {str(day).rjust(2)} {str(hours).zfill(2)}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)} {str(year).zfill(4)}"

def _utc_to_seconds(
    year: int, month: int, day: int, hour: int, minute: int, second: int
) -> int:
    if year < MINYEAR or year > MAXYEAR:
        raise ValueError(f"year {year} is out of range")
    ordinal = _ymd_to_ord(year, month, day)
    return ((ordinal * 24 + hour) * 60 + minute) * 60 + second

def _round_half_even(x: float) -> float:
    from math import fabs

    rounded = x.__round__()
    if fabs(x - rounded) == 0.5:
        rounded = 2.0 * (x / 2.0).__round__()
    return rounded

################
# core classes #
################

@tuple
class timedelta:
    min: ClassVar[timedelta] = timedelta._new(-9223372036854775808)
    max: ClassVar[timedelta] = timedelta._new(9223372036854775807)
    resolution: ClassVar[timedelta] = timedelta(microseconds=1)

    _microseconds: int

    def _new(microseconds: int) -> timedelta:
        return (microseconds,)

    @inline
    def _accum(sofar: int, leftover: float, num: int, factor: int) -> Tuple[int, float]:
        sofar += num * factor
        return sofar, leftover

    @inline
    def _accum(
        sofar: int, leftover: float, num: float, factor: int
    ) -> Tuple[int, float]:
        from math import modf

        fracpart, intpart = modf(num)
        prod = int(intpart) * factor
        s = sofar + prod

        if fracpart == 0.0:
            return s, leftover
        dnum = factor * fracpart
        fracpart, intpart = modf(dnum)
        y = s + int(intpart)
        return y, leftover + fracpart

    # override default constructor
    def __new__(days: int) -> timedelta:
        return timedelta(days, 0)

    def __new__(
        days: float = 0,
        seconds: float = 0,
        microseconds: float = 0,
        milliseconds: float = 0,
        minutes: float = 0,
        hours: float = 0,
        weeks: float = 0,
    ) -> timedelta:
        us = 0
        leftover = 0.0

        us, leftover = timedelta._accum(us, leftover, days, 24 * 60 * 60 * 1000000)
        us, leftover = timedelta._accum(us, leftover, seconds, 1000000)
        us, leftover = timedelta._accum(us, leftover, microseconds, 1)
        us, leftover = timedelta._accum(us, leftover, milliseconds, 1000)
        us, leftover = timedelta._accum(us, leftover, minutes, 60 * 1000000)
        us, leftover = timedelta._accum(us, leftover, hours, 60 * 60 * 1000000)
        us, leftover = timedelta._accum(us, leftover, weeks, 7 * 24 * 60 * 60 * 1000000)

        if leftover:
            from math import fabs

            whole_us = leftover.__round__()
            if fabs(whole_us - leftover) == 0.5:
                is_odd = us & 1
                whole_us = 2.0 * ((leftover + is_odd) * 0.5).__round__() - is_odd
            us += int(whole_us)

        return (us,)

    @property
    def days(self) -> int:
        days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds)
        return days

    @property
    def seconds(self) -> int:
        days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds)
        return seconds

    @property
    def microseconds(self) -> int:
        days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds)
        return microseconds

    def __repr__(self) -> str:
        days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds)
        if days == 0 and seconds == 0 and microseconds == 0:
            return "timedelta(0)"
        v = []
        if days:
            v.append(f"days={days}")
        if seconds:
            v.append(f"seconds={seconds}")
        if microseconds:
            v.append(f"microseconds={microseconds}")
        return f"timedelta({', '.join(v)})"

    def __str__(self) -> str:
        days, seconds, us = _normalize_d_s_us(0, 0, self._microseconds)
        minutes, seconds = _divmod(seconds, 60)
        hours, minutes = _divmod(minutes, 60)

        if days:
            if us:
                return f"{days} day{'' if days == 1 or days == -1 else 's'}, {hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}.{str(us).zfill(6)}"
            else:
                return f"{days} day{'' if days == 1 or days == -1 else 's'}, {hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}"
        else:
            if us:
                return f"{hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}.{str(us).zfill(6)}"
            else:
                return f"{hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}"

    def __add__(self, other: timedelta) -> timedelta:
        return timedelta._new(self._microseconds + other._microseconds)

    def __sub__(self, other: timedelta) -> timedelta:
        return timedelta._new(self._microseconds - other._microseconds)

    def __mul__(self, other: int) -> timedelta:
        return timedelta._new(self._microseconds * other)

    def __rmul__(self, other: int) -> timedelta:
        return self * other

    def __mul__(self, other: float) -> timedelta:
        return timedelta._new(int(_round_half_even(self._microseconds * other)))

    def __rmul__(self, other: float) -> timedelta:
        return self * other

    def __truediv__(self, other: timedelta) -> float:
        return self._microseconds / other._microseconds

    def __truediv__(self, other: float) -> timedelta:
        return timedelta._new(int(_round_half_even(self._microseconds / other)))

    def __truediv__(self, other: int) -> timedelta:
        return self / float(other)

    def __floordiv__(self, other: timedelta) -> int:
        return int((self._microseconds / other._microseconds).__floor__())

    def __floordiv__(self, other: int) -> timedelta:
        return timedelta._new(self._microseconds // other)

    def __mod__(self, other: timedelta) -> timedelta:
        n = self._microseconds
        M = other._microseconds
        m = self._microseconds % other._microseconds
        return timedelta._new(((n % M) + M) % M)

    def __divmod__(self, other: timedelta) -> Tuple[int, timedelta]:
        return self // other, self % other

    def __pos__(self) -> timedelta:
        return self

    def __neg__(self) -> timedelta:
        return timedelta._new(-self._microseconds)

    def __abs__(self) -> timedelta:
        return timedelta._new(abs(self._microseconds))

    def __eq__(self, other: timedelta) -> bool:
        return self._microseconds == other._microseconds

    def __ne__(self, other: timedelta) -> bool:
        return self._microseconds != other._microseconds

    def __lt__(self, other: timedelta) -> bool:
        return self._microseconds < other._microseconds

    def __le__(self, other: timedelta) -> bool:
        return self._microseconds <= other._microseconds

    def __gt__(self, other: timedelta) -> bool:
        return self._microseconds > other._microseconds

    def __ge__(self, other: timedelta) -> bool:
        return self._microseconds >= other._microseconds

    def __bool__(self) -> bool:
        return bool(self._microseconds)

    def total_seconds(self) -> float:
        return self._microseconds / 1e6

@tuple
class IsoCalendarDate:
    year: int
    week: int
    weekday: int

    def __repr__(self) -> str:
        return f"IsoCalendarDate(year={self.year}, week={self.week}, weekday={self.weekday})"

@tuple
class date:
    min: ClassVar[date] = date(MINYEAR, 1, 1)
    max: ClassVar[date] = date(MAXYEAR, 12, 31)
    resolution: ClassVar[timedelta] = timedelta(days=1)

    _value: UInt[32]

    def __new__(year: int, month: int, day: int) -> date:
        _check_date_args(year, month, day)
        v = (year << 16) | (month << 8) | day
        return date(UInt[32](v))

    @property
    def year(self) -> int:
        v = int(self._value)
        return v >> 16

    @property
    def month(self) -> int:
        v = int(self._value)
        return (v >> 8) & 0xFF

    @property
    def day(self) -> int:
        v = int(self._value)
        return v & 0xFF

    def __repr__(self) -> str:
        return f"date(year={self.year}, month={self.month}, day={self.day})"

    def today() -> date:
        from time import time as ttime

        return date.fromtimestamp(ttime())

    def fromtimestamp(timestamp) -> date:
        ts = int(timestamp)
        tm = localtime(ts)
        return date(tm.tm_year, tm.tm_mon, tm.tm_mday)

    def fromordinal(ordinal: int) -> date:
        return date(*_ord_to_ymd(ordinal))

    def fromisoformat(date_string: str) -> date:
        return date(*_parse_isoformat_date(date_string))

    def fromisocalendar(year, week, day) -> date:
        if year < MINYEAR or year > MAXYEAR:
            raise ValueError(f"Year is out of range: {year}")

        if week <= 0 or week >= 53:
            out_of_range = True
            if week == 53:
                first_weekday = _weekday(year, 1, 1)
                if first_weekday == 3 or (first_weekday == 2 and _is_leap(year)):
                    out_of_range = False

            if out_of_range:
                raise ValueError(f"Invalid week: {week}")

        if day <= 0 or day >= 8:
            raise ValueError(f"Invalid day: {day} (range is [1, 7])")

        day_1 = _iso_week1_monday(year)
        month = week
        day_offset = (month - 1) * 7 + day - 1
        return date(*_ord_to_ymd(day_1 + day_offset))

    def __add__(self, other: timedelta) -> date:
        days, seconds, microseconds = _normalize_d_s_us(0, 0, other._microseconds)
        day = self.day + days
        return date(*_normalize_date(self.year, self.month, day))

    def __sub__(self, other: timedelta) -> date:
        days, seconds, microseconds = _normalize_d_s_us(0, 0, other._microseconds)
        day = self.day - days
        return date(*_normalize_date(self.year, self.month, day))

    def __sub__(self, other: date) -> timedelta:
        left_ord = _ymd_to_ord(self.year, self.month, self.day)
        right_ord = _ymd_to_ord(other.year, other.month, other.day)
        return timedelta(days=left_ord - right_ord)

    def __eq__(self, other: date) -> bool:
        return self._value == other._value

    def __ne__(self, other: date) -> bool:
        return self._value != other._value

    def __lt__(self, other: date) -> bool:
        return self._value < other._value

    def __le__(self, other: date) -> bool:
        return self._value <= other._value

    def __gt__(self, other: date) -> bool:
        return self._value > other._value

    def __ge__(self, other: date) -> bool:
        return self._value >= other._value

    def __bool__(self) -> bool:
        return True

    def replace(self, year: int = -1, month: int = -1, day: int = -1) -> date:
        if year == -1:
            year = self.year
        if month == -1:
            month = self.month
        if day == -1:
            day = self.day
        return date(year, month, day)

    def timetuple(self) -> struct_time:
        yday = self.toordinal() - date(self.year, 1, 1).toordinal() + 1
        return struct_time(
            self.year, self.month, self.day, 0, 0, 0, self.weekday(), yday, -1
        )

    def toordinal(self) -> int:
        return _ymd_to_ord(self.year, self.month, self.day)

    def weekday(self) -> int:
        return _weekday(self.year, self.month, self.day)

    def isoweekday(self) -> int:
        return self.weekday() + 1

    def isocalendar(self) -> IsoCalendarDate:
        year = self.year
        week1_monday = _iso_week1_monday(year)
        today = _ymd_to_ord(year, self.month, self.day)
        week, day = _divmod(today - week1_monday, 7)
        if week < 0:
            year -= 1
            week1_monday = _iso_week1_monday(year)
            week, day = _divmod(today - week1_monday, 7)
        elif week >= 52 and today >= _iso_week1_monday(year + 1):
            year += 1
            week = 0
        return IsoCalendarDate(year, week + 1, day + 1)

    def isoformat(self) -> str:
        return f"{str(self.year).zfill(4)}-{str(self.month).zfill(2)}-{str(self.day).zfill(2)}"

    def __str__(self) -> str:
        return self.isoformat()

    def ctime(self) -> str:
        return _format_ctime(self.year, self.month, self.day, 0, 0, 0)

    # strftime() / __format__() not supported

@tuple
class time:
    min: ClassVar[time] = time(0, 0, 0, 0)
    max: ClassVar[time] = time(23, 59, 59, 999999)
    resolution: ClassVar[timedelta] = timedelta(microseconds=1)

    _value: int

    def __new__(
        hour: int = 0, minute: int = 0, second: int = 0, microsecond: int = 0
    ) -> time:
        _check_time_args(hour, minute, second, microsecond)
        v = (hour << 40) | (minute << 32) | (second << 24) | microsecond
        return (v,)

    @property
    def hour(self) -> int:
        v = self._value
        return v >> 40

    @property
    def minute(self) -> int:
        v = self._value
        return (v >> 32) & 0xFF

    @property
    def second(self) -> int:
        v = self._value
        return (v >> 24) & 0xFF

    @property
    def microsecond(self) -> int:
        v = self._value
        return v & 0xFFFFFF

    def __repr__(self) -> str:
        h, m, s, us = self.hour, self.minute, self.second, self.microsecond
        v = []
        v.append(f"hour={h}")
        v.append(f"minute={m}")
        if s or us:
            v.append(f"second={s}")
        if us:
            v.append(f"microsecond={us}")
        return f"time({', '.join(v)})"

    def __str__(self) -> str:
        return self.isoformat()

    def __bool__(self) -> bool:
        return True

    def fromisoformat(time_string: str) -> time:
        (
            hour,
            minute,
            second,
            microsecond,
            tzoffset,
            tzmicrosecond,
        ) = _parse_isoformat_time(time_string)
        # TODO: deal with timezone
        return time(hour, minute, second, microsecond)

    def replace(
        self, hour: int = -1, minute: int = -1, second: int = -1, microsecond: int = -1
    ) -> time:
        if hour == -1:
            hour = self.hour
        if second == -1:
            second = self.second
        if minute == -1:
            minute = self.minute
        if microsecond == -1:
            microsecond = self.microsecond
        return time(hour, minute, second, microsecond)

    def isoformat(self, timespec: Static[str] = "auto") -> str:
        hh = str(self.hour).zfill(2)
        mm = str(self.minute).zfill(2)
        ss = str(self.second).zfill(2)
        us = str(self.microsecond).zfill(6)
        ms = str(self.microsecond // 1000).zfill(3)

        if timespec == "auto":
            if self.microsecond:
                return f"{hh}:{mm}:{ss}.{us}"
            else:
                return f"{hh}:{mm}:{ss}"
        elif timespec == "hours":
            return hh
        elif timespec == "minutes":
            return f"{hh}:{mm}"
        elif timespec == "seconds":
            return f"{hh}:{mm}:{ss}"
        elif timespec == "milliseconds":
            return f"{hh}:{mm}:{ss}.{ms}"
        elif timespec == "microseconds":
            return f"{hh}:{mm}:{ss}.{us}"
        else:
            compile_error(
                "invalid timespec; valid ones are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'"
            )

@tuple
class datetime:
    min: ClassVar[datetime] = datetime(MINYEAR, 1, 1)
    max: ClassVar[datetime] = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999)
    resolution: ClassVar[timedelta] = timedelta(microseconds=1)

    _time: time
    _date: date

    def __new__(
        year: int,
        month: int,
        day: int,
        hour: int = 0,
        minute: int = 0,
        second: int = 0,
        microsecond: int = 0,
    ) -> datetime:
        return datetime(time(hour, minute, second, microsecond), date(year, month, day))

    def date(self) -> date:
        return self._date

    def time(self) -> time:
        return self._time

    @property
    def year(self) -> int:
        return self.date().year

    @property
    def month(self) -> int:
        return self.date().month

    @property
    def day(self) -> int:
        return self.date().day

    @property
    def hour(self) -> int:
        return self.time().hour

    @property
    def minute(self) -> int:
        return self.time().minute

    @property
    def second(self) -> int:
        return self.time().second

    @property
    def microsecond(self) -> int:
        return self.time().microsecond

    def __repr__(self) -> str:
        return f"datetime(year={self.year}, month={self.month}, day={self.day}, hour={self.hour}, minute={self.minute}, second={self.second}, microsecond={self.microsecond})"

    def __str__(self) -> str:
        return self.isoformat(sep=" ")

    def _from_timet_and_us(timet, us) -> datetime:
        tm = localtime(timet)
        year = tm.tm_year
        month = tm.tm_mon
        day = tm.tm_mday
        hour = tm.tm_hour
        minute = tm.tm_min
        second = min(59, tm.tm_sec)
        # TODO: timezone adjustments
        return datetime(year, month, day, hour, minute, second, us)

    def today() -> datetime:
        from time import time as ttime

        return datetime.fromtimestamp(ttime())

    # TODO: support timezone
    def now() -> datetime:
        return datetime.today()

    def utcnow() -> datetime:
        return datetime.now()

    # TODO: support timezone
    def fromtimestamp(timestamp) -> datetime:
        from time import _time_to_timeval, _ROUND_HALF_EVEN

        timet, us = _time_to_timeval(float(timestamp), _ROUND_HALF_EVEN)
        return datetime._from_timet_and_us(timet, us)

    def utcfromtimestamp(timestamp) -> datetime:
        return datetime.fromtimestamp(timestamp)

    def fromordinal(ordinal: int) -> datetime:
        return datetime.combine(date.fromordinal(ordinal), time())

    # TODO: support timezone
    def combine(date: date, time: time) -> datetime:
        return datetime(time, date)

    def fromisoformat(date_string: str) -> datetime:
        time_string = "" if len(date_string) < 10 else date_string[:10]
        year, month, day = _parse_isoformat_date(time_string)
        if len(date_string) == 10:
            return datetime(year=year, month=month, day=day)
        date_string = "" if len(date_string) < 12 else date_string[11:]
        hour, minute, second, microsecond = _parse_hh_mm_ss_ff(date_string)
        return datetime(
            year=year,
            month=month,
            day=day,
            hour=hour,
            minute=minute,
            second=second,
            microsecond=microsecond,
        )

    def fromisocalendar(year: int, week: int, day: int) -> datetime:
        return datetime.combine(date.fromisocalendar(year, week, day), time())

    def __add__(self, other: timedelta) -> datetime:
        td_days, td_seconds, td_microseconds = _normalize_d_s_us(
            0, 0, other._microseconds
        )
        year = self.year
        month = self.month
        day = self.day + td_days
        hour = self.hour
        minute = self.minute
        second = self.second + td_seconds
        microsecond = self.microsecond + td_microseconds
        return datetime(
            *_normalize_datetime(year, month, day, hour, minute, second, microsecond)
        )

    def __sub__(self, other: timedelta) -> datetime:
        td_days, td_seconds, td_microseconds = _normalize_d_s_us(
            0, 0, other._microseconds
        )
        year = self.year
        month = self.month
        day = self.day - td_days
        hour = self.hour
        minute = self.minute
        second = self.second - td_seconds
        microsecond = self.microsecond - td_microseconds
        return datetime(
            *_normalize_datetime(year, month, day, hour, minute, second, microsecond)
        )

    def __sub__(self, other: datetime) -> timedelta:
        delta_d = _ymd_to_ord(self.year, self.month, self.day) - _ymd_to_ord(
            other.year, other.month, other.day
        )
        delta_s = (
            (self.hour - other.hour) * 3600
            + (self.minute - other.minute) * 60
            + (self.second - other.second)
        )
        delta_us = self.microsecond - other.microsecond
        return timedelta(days=delta_d, seconds=delta_s, microseconds=delta_us)

    def __eq__(self, other: datetime) -> bool:
        return self.date() == other.date() and self.time() == other.time()

    def __ne__(self, other: datetime) -> bool:
        return not (self == other)

    def __lt__(self, other: datetime) -> bool:
        return (self.date(), self.time()) < (other.date(), other.time())

    def __le__(self, other: datetime) -> bool:
        return (self.date(), self.time()) <= (other.date(), other.time())

    def __gt__(self, other: datetime) -> bool:
        return (self.date(), self.time()) > (other.date(), other.time())

    def __ge__(self, other: datetime) -> bool:
        return (self.date(), self.time()) >= (other.date(), other.time())

    def __bool__(self) -> bool:
        return True

    def replace(
        self,
        year: int = -1,
        month: int = -1,
        day: int = -1,
        hour: int = -1,
        minute: int = -1,
        second: int = -1,
        microsecond: int = -1,
    ) -> datetime:
        return datetime(
            self.time().replace(hour, minute, second, microsecond),
            self.date().replace(year, month, day),
        )

    def timetuple(self) -> struct_time:
        yday = self.toordinal() - date(self.year, 1, 1).toordinal() + 1
        return struct_time(
            self.year,
            self.month,
            self.day,
            self.hour,
            self.minute,
            self.second,
            self.weekday(),
            yday,
            -1,
        )

    def utctimetuple(self) -> struct_time:
        return self.timetuple()

    def toordinal(self) -> int:
        return self.date().toordinal()

    def timestamp(self) -> float:
        return (self - datetime(1970, 1, 1)).total_seconds()

    def weekday(self) -> int:
        return self.date().weekday()

    def isoweekday(self) -> int:
        return self.date().isoweekday()

    def isocalendar(self) -> IsoCalendarDate:
        return self.date().isocalendar()

    def isoformat(self, sep: str = "T", timespec: Static[str] = "auto") -> str:
        date_part = str(self.date())
        time_part = self.time().isoformat(timespec=timespec)
        return f"{date_part}{sep}{time_part}"

    def ctime(self) -> str:
        date = self.date()
        time = self.time()
        return _format_ctime(
            date.year, date.month, date.day, time.hour, time.minute, time.second
        )

@extend
class timedelta:
    def __add__(self, other: date) -> date:
        return other + self

    def __add__(self, other: datetime) -> datetime:
        return other + self
