#include <sead/prim/seadStringUtil.h>
#include <sead/time/seadCalendarSpan.h>
#include <sead/time/seadDateUtil.h>

namespace sead
{
namespace DateUtil
{
bool isLeapYear(u32 year)
{
#ifdef MATCHING_HACK_NX_CLANG
    bool div100, div4;
    return (div100 = year % 100 == 0, div4 = year % 4 == 0, !div100 & div4) | (year % 400 == 0);
#else
    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
#endif
}

CalendarTime::Week calcWeekDay(const CalendarTime::Year& year, const CalendarTime::Month& month,
                               const CalendarTime::Day& day)
{
    int y = year.getValue();
    int m = month.getValueOneOrigin();
    int d = day.getValue();
    if (m < 3)
    {
        y -= 1;
        m += 12;
    }
    d += y + (y / 4) - (y / 100) + (y / 400);
    d += (26 * m + 16) / 10;
    return CalendarTime::Week(d % 7);
}

void calcSecondToCalendarSpan(CalendarSpan* out_span, u64 sec)
{
    if (!out_span)
        return;
    out_span->setDays(sec / (3600 * 24));
    out_span->setHours((sec % (3600 * 24)) / 3600);
    out_span->setMinutes((sec % 3600) / 60);
    out_span->setSeconds(sec % 60);
}

bool parseW3CDTFSubString(bool* ok, u32* value, SafeString* str, s32* str_length,
                          char* out_separator, s32 parse_length, const SafeString& separators,
                          bool allow_null_separator, u32 value_min, u32 value_max)
{
    if (*str_length < parse_length)
    {
        *ok = false;
        return true;
    }

    const char c = str->at(parse_length);
    if (!separators.include(c) && (!allow_null_separator || c != SafeString::cNullChar))
    {
        *ok = false;
        return true;
    }

    FixedSafeString<8> buffer;
    buffer.copy(*str, parse_length);

    if (!StringUtil::tryParseU32(value, buffer, StringUtil::CardinalNumber::Base10) ||
        *value < value_min || *value > value_max)
    {
        *ok = false;
        return true;
    }

    if (c == SafeString::cNullChar)
    {
        *ok = true;
        return true;
    }

    *str = str->getPart(parse_length + 1);
    *str_length -= parse_length + 1;
    if (out_separator)
        *out_separator = c;
    return false;
}

static bool parseW3CDTFStringImpl(u32* year, u32* month, u32* day, u32* hour, u32* minute,
                                  u32* second, s32* tz_hour, s32* tz_minute,
                                  const SafeString& string)
{
    s32 len = string.calcLength();
    bool ok = true;
    SafeString substr = string;
    char separator;

    if (parseW3CDTFSubString(&ok, year, &substr, &len, &separator, 4, "-", true, 0, 0xFFFFFFFF))
        return ok;
    if (parseW3CDTFSubString(&ok, month, &substr, &len, &separator, 2, "-", true, 1, 12))
        return ok;
    if (parseW3CDTFSubString(&ok, day, &substr, &len, &separator, 2, "T", true, 1, 31))
        return ok;
    if (parseW3CDTFSubString(&ok, hour, &substr, &len, &separator, 2, ":", false, 0, 23))
        return ok;
    if (parseW3CDTFSubString(&ok, minute, &substr, &len, &separator, 2, ":+-Z", true, 0, 59))
        return ok;

    if (separator == ':')
    {
        if (parseW3CDTFSubString(&ok, second, &substr, &len, &separator, 2, ".+-Z", true, 0, 59))
            return ok;

        if (separator == '.')
        {
            if (len == 0)
                return false;

            auto it = substr.tokenBegin("+-Z");
            ++it;
            auto end = substr.tokenEnd("+-Z");

            // No timezone information
            if (it == end)
            {
                *tz_hour = 0;
                *tz_minute = 0;
                return true;
            }

            separator = substr.at(it.getIndex() - 1);
            substr = substr.getPart(it.getIndex());
            len -= it.getIndex();
        }
    }

    if (separator != '+' && separator != '-')
    checkLength:
        return len == 0;

    bool done;
    u32 tz_hour_abs = 0;
    done = parseW3CDTFSubString(&ok, &tz_hour_abs, &substr, &len, nullptr, 2, ":", false, 0, 11);
    if (ok)
    {
        *tz_hour = tz_hour_abs;
        if (separator == '-')
            *tz_hour = -tz_hour_abs;
    }
    if (done)
        return ok;

    u32 tz_minute_abs = 0;
    done = parseW3CDTFSubString(&ok, &tz_minute_abs, &substr, &len, nullptr, 2, "", true, 0, 59);
    if (ok)
    {
        *tz_minute = tz_minute_abs;
        if (separator == '-')
            *tz_minute = -tz_minute_abs;

        if (done)
        {
            len -= 2;
            goto checkLength;
        }
    }

    return false;
}

bool parseW3CDTFString(CalendarTime* out_time, CalendarSpan* time_zone, const SafeString& string)
{
    u32 year = 1970;
    u32 month = 1;
    u32 day = 1;
    u32 hour = 0;
    u32 minute = 0;
    u32 second = 0;

    s32 tz_hour = 0;
    s32 tz_minute = 0;

    const bool ret = parseW3CDTFStringImpl(&year, &month, &day, &hour, &minute, &second, &tz_hour,
                                           &tz_minute, string);

    if (ret)
    {
        out_time->setDate({year, CalendarTime::Month::makeFromValueOneOrigin(month), day});
        out_time->setTime({hour, minute, second});

        time_zone->setDays(0);
        time_zone->setHours(tz_hour);
        time_zone->setMinutes(tz_minute);
        time_zone->setSeconds(0);
    }

    return ret;
}
}  // namespace DateUtil
}  // namespace sead
