/**********************************************************************
 *
 * Name:     cpl_time.cpp
 * Project:  CPL - Common Portability Library
 * Purpose:  Time functions.
 * Author:   Even Rouault, <even dot rouault at spatialys.com>
 *
 **********************************************************************
 *
 * CPLUnixTimeToYMDHMS() is derived from timesub() in localtime.c from
 * openbsd/freebsd/netbsd.
 *
 * CPLYMDHMSToUnixTime() has been implemented by Even Rouault and is in the
 * public domain.
 *
 * c.f.
 *http://svn.freebsd.org/viewvc/base/stable/7/lib/libc/stdtime/localtime.c?revision=178142&view=markup
 * localtime.c comes with the following header :
 *
 * This file is in the public domain, so clarified as of
 * 1996-06-05 by Arthur David Olson (arthur_david_olson@nih.gov).
 */

#include "cpl_time.h"
#include "cpl_string.h"

#include <cstring>
#include <ctime>

#include "cpl_error.h"

constexpr int SECSPERMIN = 60;
constexpr int MINSPERHOUR = 60;
constexpr int HOURSPERDAY = 24;
constexpr int SECSPERHOUR = SECSPERMIN * MINSPERHOUR;
constexpr int SECSPERDAY = SECSPERHOUR * HOURSPERDAY;
constexpr int DAYSPERWEEK = 7;
constexpr int MONSPERYEAR = 12;

constexpr int EPOCH_YEAR = 1970;
constexpr int EPOCH_WDAY = 4;
constexpr int TM_YEAR_BASE = 1900;
constexpr int DAYSPERNYEAR = 365;
constexpr int DAYSPERLYEAR = 366;

static bool isleap(int y)
{
    return ((y % 4) == 0 && (y % 100) != 0) || (y % 400) == 0;
}

static int LEAPS_THROUGH_END_OF(int y)
{
    return y / 4 - y / 100 + y / 400;
}

constexpr int mon_lengths[2][MONSPERYEAR] = {
    {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};

constexpr int year_lengths[2] = {DAYSPERNYEAR, DAYSPERLYEAR};

/************************************************************************/
/*                   CPLUnixTimeToYMDHMS()                              */
/************************************************************************/

/** Converts a time value since the Epoch (aka "unix" time) to a broken-down
 *  UTC time.
 *
 * This function is similar to gmtime_r().
 * This function will always set tm_isdst to 0.
 *
 * @param unixTime number of seconds since the Epoch.
 * @param pRet address of the return structure.
 *
 * @return the structure pointed by pRet filled with a broken-down UTC time.
 */

struct tm *CPLUnixTimeToYMDHMS(GIntBig unixTime, struct tm *pRet)
{
    GIntBig days = unixTime / SECSPERDAY;
    GIntBig rem = unixTime % SECSPERDAY;

    constexpr GIntBig TEN_THOUSAND_YEARS =
        static_cast<GIntBig>(10000) * SECSPERDAY * DAYSPERLYEAR;
    if (unixTime < -TEN_THOUSAND_YEARS || unixTime > TEN_THOUSAND_YEARS)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Invalid unixTime = " CPL_FRMT_GIB, unixTime);
        memset(pRet, 0, sizeof(*pRet));
        return pRet;
    }

    while (rem < 0)
    {
        rem += SECSPERDAY;
        --days;
    }

    pRet->tm_hour = static_cast<int>(rem / SECSPERHOUR);
    rem = rem % SECSPERHOUR;
    pRet->tm_min = static_cast<int>(rem / SECSPERMIN);
    /*
    ** A positive leap second requires a special
    ** representation.  This uses "... ??:59:60" et seq.
    */
    pRet->tm_sec = static_cast<int>(rem % SECSPERMIN);
    pRet->tm_wday = static_cast<int>((EPOCH_WDAY + days) % DAYSPERWEEK);
    if (pRet->tm_wday < 0)
        pRet->tm_wday += DAYSPERWEEK;

    int y = EPOCH_YEAR;
    int yleap = 0;
    int iters = 0;
    while (iters < 1000 &&
           (days < 0 ||
            days >= static_cast<GIntBig>(year_lengths[yleap = isleap(y)])))
    {
        int newy = y + static_cast<int>(days / DAYSPERNYEAR);
        if (days < 0)
            --newy;
        days -= static_cast<GIntBig>(newy - y) * DAYSPERNYEAR +
                LEAPS_THROUGH_END_OF(newy - 1) - LEAPS_THROUGH_END_OF(y - 1);
        y = newy;
        iters++;
    }
    if (iters == 1000)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Invalid unixTime = " CPL_FRMT_GIB, unixTime);
        memset(pRet, 0, sizeof(*pRet));
        return pRet;
    }

    pRet->tm_year = static_cast<int>(y - TM_YEAR_BASE);
    pRet->tm_yday = static_cast<int>(days);
    const int *ip = mon_lengths[yleap];

    for (pRet->tm_mon = 0; days >= static_cast<GIntBig>(ip[pRet->tm_mon]);
         ++(pRet->tm_mon))
        days = days - static_cast<GIntBig>(ip[pRet->tm_mon]);

    pRet->tm_mday = static_cast<int>((days + 1));
    pRet->tm_isdst = 0;

    return pRet;
}

/************************************************************************/
/*                      CPLYMDHMSToUnixTime()                           */
/************************************************************************/

/** Converts a broken-down UTC time into time since the Epoch (aka "unix" time).
 *
 * This function is similar to mktime(), but the passed structure is not
 * modified.  This function ignores the tm_wday, tm_yday and tm_isdst fields of
 * the passed value.  No timezone shift will be applied. This function
 * returns 0 for the 1/1/1970 00:00:00
 *
 * @param brokendowntime broken-downtime UTC time.
 *
 * @return a number of seconds since the Epoch encoded as a value of type
 *         GIntBig, or -1 if the time cannot be represented.
 */

GIntBig CPLYMDHMSToUnixTime(const struct tm *brokendowntime)
{

    if (brokendowntime->tm_mon < 0 || brokendowntime->tm_mon >= 12)
        return -1;

    // Number of days of the current month.
    GIntBig days = brokendowntime->tm_mday - 1;

    // Add the number of days of the current year.
    const int *ip = mon_lengths[static_cast<int>(
        isleap(TM_YEAR_BASE + brokendowntime->tm_year))];
    for (int mon = 0; mon < brokendowntime->tm_mon; mon++)
        days += ip[mon];

    // Add the number of days of the other years.
    days += (TM_YEAR_BASE + static_cast<GIntBig>(brokendowntime->tm_year) -
             EPOCH_YEAR) *
                DAYSPERNYEAR +
            LEAPS_THROUGH_END_OF(static_cast<int>(TM_YEAR_BASE) +
                                 static_cast<int>(brokendowntime->tm_year) -
                                 static_cast<int>(1)) -
            LEAPS_THROUGH_END_OF(EPOCH_YEAR - 1);

    // Now add the secondes, minutes and hours to the number of days
    // since EPOCH.
    return brokendowntime->tm_sec + brokendowntime->tm_min * SECSPERMIN +
           brokendowntime->tm_hour * SECSPERHOUR + days * SECSPERDAY;
}

/************************************************************************/
/*                      OGRParseRFC822DateTime()                        */
/************************************************************************/

static const char *const aszWeekDayStr[] = {"Mon", "Tue", "Wed", "Thu",
                                            "Fri", "Sat", "Sun"};

static const char *const aszMonthStr[] = {"Jan", "Feb", "Mar", "Apr",
                                          "May", "Jun", "Jul", "Aug",
                                          "Sep", "Oct", "Nov", "Dec"};

/** Parse a RFC822 formatted date-time string.
 *
 * Such as [Fri,] 28 Dec 2007 05:24[:17] GMT
 *
 * @param pszRFC822DateTime formatted string.
 * @param pnYear pointer to int receiving year (like 1980, 2000, etc...), or
 * NULL
 * @param pnMonth pointer to int receiving month (between 1 and 12), or NULL
 * @param pnDay pointer to int receiving day of month (between 1 and 31), or
 * NULL
 * @param pnHour pointer to int receiving hour of day (between 0 and 23), or
 * NULL
 * @param pnMinute pointer to int receiving minute (between 0 and 59), or NULL
 * @param pnSecond pointer to int receiving second (between 0 and 60, or -1 if
 * unknown), or NULL
 * @param pnTZFlag pointer to int receiving time zone flag (0=unknown, 100=GMT,
 *                 101=GMT+15minute, 99=GMT-15minute), or NULL
 * @param pnWeekDay pointer to int receiving day of week (between 1 and 7, or 0
 * if invalid/unset), or NULL
 * @return TRUE if parsing is successful
 *
 * @since GDAL 2.3
 */
int CPLParseRFC822DateTime(const char *pszRFC822DateTime, int *pnYear,
                           int *pnMonth, int *pnDay, int *pnHour, int *pnMinute,
                           int *pnSecond, int *pnTZFlag, int *pnWeekDay)
{
    // Following
    // https://www.w3.org/Protocols/rfc822/#z28 :
    // [Fri,] 28 Dec 2007 05:24[:17] GMT
    char **papszTokens =
        CSLTokenizeStringComplex(pszRFC822DateTime, " ,:", TRUE, FALSE);
    char **papszVal = papszTokens;
    int nTokens = CSLCount(papszTokens);
    if (nTokens < 5)
    {
        CSLDestroy(papszTokens);
        return false;
    }

    if (pnWeekDay)
        *pnWeekDay = 0;

    if (!((*papszVal)[0] >= '0' && (*papszVal)[0] <= '9'))
    {
        if (pnWeekDay)
        {
            for (size_t i = 0; i < CPL_ARRAYSIZE(aszWeekDayStr); ++i)
            {
                if (EQUAL(*papszVal, aszWeekDayStr[i]))
                {
                    *pnWeekDay = static_cast<int>(i + 1);
                    break;
                }
            }
        }

        ++papszVal;
    }

    int day = atoi(*papszVal);
    if (day <= 0 || day >= 32)
    {
        CSLDestroy(papszTokens);
        return false;
    }
    if (pnDay)
        *pnDay = day;
    ++papszVal;

    int month = 0;
    for (int i = 0; i < 12; ++i)
    {
        if (EQUAL(*papszVal, aszMonthStr[i]))
        {
            month = i + 1;
            break;
        }
    }
    if (month == 0)
    {
        CSLDestroy(papszTokens);
        return false;
    }
    if (pnMonth)
        *pnMonth = month;
    ++papszVal;

    int year = atoi(*papszVal);
    if (year < 100 && year >= 30)
        year += 1900;
    else if (year < 30 && year >= 0)
        year += 2000;
    if (pnYear)
        *pnYear = year;
    ++papszVal;

    int hour = atoi(*papszVal);
    if (hour < 0 || hour >= 24)
    {
        CSLDestroy(papszTokens);
        return false;
    }
    if (pnHour)
        *pnHour = hour;
    ++papszVal;

    if (*papszVal == nullptr)
    {
        CSLDestroy(papszTokens);
        return false;
    }
    int minute = atoi(*papszVal);
    if (minute < 0 || minute >= 60)
    {
        CSLDestroy(papszTokens);
        return false;
    }
    if (pnMinute)
        *pnMinute = minute;
    ++papszVal;

    if (*papszVal != nullptr && (*papszVal)[0] >= '0' && (*papszVal)[0] <= '9')
    {
        int second = atoi(*papszVal);
        if (second < 0 || second >= 61)
        {
            CSLDestroy(papszTokens);
            return false;
        }
        if (pnSecond)
            *pnSecond = second;
        ++papszVal;
    }
    else if (pnSecond)
        *pnSecond = -1;

    int TZ = 0;
    if (*papszVal == nullptr)
    {
    }
    else if (strlen(*papszVal) == 5 &&
             ((*papszVal)[0] == '+' || (*papszVal)[0] == '-'))
    {
        char szBuf[3] = {(*papszVal)[1], (*papszVal)[2], 0};
        const int TZHour = atoi(szBuf);
        if (TZHour < 0 || TZHour >= 15)
        {
            CSLDestroy(papszTokens);
            return false;
        }
        szBuf[0] = (*papszVal)[3];
        szBuf[1] = (*papszVal)[4];
        szBuf[2] = 0;
        const int TZMinute = atoi(szBuf);
        TZ = 100 + (((*papszVal)[0] == '+') ? 1 : -1) *
                       ((TZHour * 60 + TZMinute) / 15);
    }
    else
    {
        const char *aszTZStr[] = {"GMT", "UT",  "Z",   "EST", "EDT", "CST",
                                  "CDT", "MST", "MDT", "PST", "PDT"};
        const int anTZVal[] = {0, 0, 0, -5, -4, -6, -5, -7, -6, -8, -7};
        TZ = -1;
        for (int i = 0; i < 11; ++i)
        {
            if (EQUAL(*papszVal, aszTZStr[i]))
            {
                TZ = 100 + anTZVal[i] * 4;
                break;
            }
        }
        if (TZ < 0)
        {
            CSLDestroy(papszTokens);
            return false;
        }
    }

    if (pnTZFlag)
        *pnTZFlag = TZ;

    CSLDestroy(papszTokens);
    return true;
}
