# Copyright (c) 2010-2024 openpyxl

"""Manage Excel date weirdness."""

# Python stdlib imports
import datetime
from math import isnan
import re


# constants
MAC_EPOCH = datetime.datetime(1904, 1, 1)
WINDOWS_EPOCH = datetime.datetime(1899, 12, 30)
CALENDAR_WINDOWS_1900 = 2415018.5   # Julian date of WINDOWS_EPOCH
CALENDAR_MAC_1904 = 2416480.5       # Julian date of MAC_EPOCH
CALENDAR_WINDOWS_1900 = WINDOWS_EPOCH
CALENDAR_MAC_1904 = MAC_EPOCH
SECS_PER_DAY = 86400

ISO_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
ISO_REGEX = re.compile(r'''
(?P<date>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}))?T?
(?P<time>(?P<hour>\d{2}):(?P<minute>\d{2})(:(?P<second>\d{2})(?P<microsecond>\.\d{1,3})?)?)?Z?''',
                                       re.VERBOSE)
ISO_DURATION = re.compile(r'PT((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+(\.\d{1,3})?)S)?')


def to_ISO8601(dt):
    """Convert from a datetime to a timestamp string."""
    if hasattr(dt, "microsecond") and dt.microsecond:
        return dt.isoformat(timespec="milliseconds")
    return dt.isoformat()


def from_ISO8601(formatted_string):
    """Convert from a timestamp string to a datetime object. According to
    18.17.4 in the specification the following ISO 8601 formats are
    supported.

    Dates B.1.1 and B.2.1
    Times B.1.2 and B.2.2
    Datetimes B.1.3 and B.2.3

    There is no concept of timedeltas in the specification, but Excel
    writes them (in strict OOXML mode), so these are also understood.
    """
    if not formatted_string:
        return None

    match = ISO_REGEX.match(formatted_string)
    if match and any(match.groups()):
        parts = match.groupdict(0)
        for key in ["year", "month", "day", "hour", "minute", "second"]:
            if parts[key]:
                parts[key] = int(parts[key])

        if parts["microsecond"]:
            parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)

        if not parts["date"]:
            dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
        elif not parts["time"]:
            dt = datetime.date(parts['year'], parts['month'], parts['day'])
        else:
            del parts["time"]
            del parts["date"]
            dt = datetime.datetime(**parts)
        return dt

    match = ISO_DURATION.match(formatted_string)
    if match and any(match.groups()):
        parts = match.groupdict(0)
        for key, val in parts.items():
            if val:
                parts[key] = float(val)
        return datetime.timedelta(**parts)

    raise ValueError("Invalid datetime value {}".format(formatted_string))


def to_excel(dt, epoch=WINDOWS_EPOCH):
    """Convert Python datetime to Excel serial"""
    if isinstance(dt, datetime.time):
        return time_to_days(dt)
    if isinstance(dt, datetime.timedelta):
        return timedelta_to_days(dt)
    if isnan(dt.year):  # Pandas supports Not a Date
        return

    if not hasattr(dt, "date"):
        dt = datetime.datetime.combine(dt, datetime.time())

    # rebase on epoch and adjust for < 1900-03-01
    days = (dt - epoch).days
    if 0 < days <= 60 and epoch == WINDOWS_EPOCH:
        days -= 1
    return days + time_to_days(dt)


def from_excel(value, epoch=WINDOWS_EPOCH, timedelta=False):
    """Convert Excel serial to Python datetime"""
    if value is None:
        return

    if timedelta:
        td = datetime.timedelta(days=value)
        if td.microseconds:
            # round to millisecond precision
            td = datetime.timedelta(seconds=td.total_seconds() // 1,
                                    microseconds=round(td.microseconds, -3))
        return td

    day, fraction = divmod(value, 1)
    diff = datetime.timedelta(milliseconds=round(fraction * SECS_PER_DAY * 1000))
    if 0 <= value < 1 and diff.days == 0:
        return days_to_time(diff)
    if 0 < value < 60 and epoch == WINDOWS_EPOCH:
        day += 1
    return epoch + datetime.timedelta(days=day) + diff


def time_to_days(value):
    """Convert a time value to fractions of day"""
    return (
        (value.hour * 3600)
        + (value.minute * 60)
        + value.second
        + value.microsecond / 10**6
        ) / SECS_PER_DAY


def timedelta_to_days(value):
    """Convert a timedelta value to fractions of a day"""
    return value.total_seconds() / SECS_PER_DAY


def days_to_time(value):
    mins, seconds = divmod(value.seconds, 60)
    hours, mins = divmod(mins, 60)
    return datetime.time(hours, mins, seconds, value.microseconds)
