///                MentOS, The Mentoring Operating system project
/// @file time.c
/// @brief Clock functions.
/// @copyright (c) 2014-2021 This file is distributed under the MIT License.
/// See LICENSE.md for details.

#include "time.h"
#include "system/syscall_types.h"
#include "sys/errno.h"
#include "string.h"
#include "stdio.h"

static const char *weekdays[] = {
    "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};

static const char *months[] = {
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
};

_syscall1(time_t, time, time_t *, t)

time_t difftime(time_t time1, time_t time2)
{
    return time1 - time2;
}

/// @brief Computes day of week
/// @param y Year
/// @param m Month of year (in range 1 to 12)
/// @param d Day of month (in range 1 to 31)
/// @return Day of week (in range 1 to 7)
static inline int day_of_week(unsigned int y, unsigned int m, unsigned int d)
{
    int h, j, k;
    // January and February are counted as months 13 and 14 of the previous year
    if (m <= 2) {
        m += 12;
        y -= 1;
    }
    // J is the century
    j = (int)(y / 100);
    // K the year of the century
    k = (int)(y % 100);
    // Compute H using Zeller's congruence
    h = (int)(d + (26 * (m + 1) / 10) + k + (k / 4) + (5 * j) + (j / 4));
    // Return the day of the week
    return ((h + 5) % 7) + 1;
}

tm_t *localtime(const time_t *time)
{
    static tm_t date;
    unsigned int a, b, c, d, e, f;
    time_t t = *time;
    // Negative Unix time values are not supported
    if (t < 1) {
        t = 0;
    }
    //Retrieve hours, minutes and seconds
    date.tm_sec = (int)(t % 60);
    t /= 60;
    date.tm_min = (int)(t % 60);
    t /= 60;
    date.tm_hour = (int)(t % 24);
    t /= 24;
    // Convert Unix time to date
    a = (unsigned int)((4 * t + 102032) / 146097 + 15);
    b = (unsigned int)(t + 2442113 + a - (a / 4));
    c = (20 * b - 2442) / 7305;
    d = b - 365 * c - (c / 4);
    e = d * 1000 / 30601;
    f = d - e * 30 - e * 601 / 1000;
    // January and February are counted as months 13 and 14 of the previous year
    if (e <= 13) {
        c -= 4716;
        e -= 1;
    } else {
        c -= 4715;
        e -= 13;
    }
    //Retrieve year, month and day
    date.tm_year = (int)c;
    date.tm_mon  = (int)e;
    date.tm_mday = (int)f;
    // Calculate day of week.
    date.tm_wday = day_of_week(date.tm_year, date.tm_mon, date.tm_mday);
    return &date;
}

size_t strftime(char *str, size_t maxsize, const char *format, const tm_t *timeptr)
{
    if (format == NULL || str == NULL || maxsize <= 0) {
        return 0;
    }
    char *f = (char *)format;
    char *s = str;
    int value;
    size_t ret = 0;
    while ((*f) != '\0') {
        if ((*f) != '%') {
            (*s++) = (*f);
        } else {
            ++f;
            switch (*f) {
#if 0
                case 'a':    /* locale's abbreviated weekday name */
                    PUT_STRN(weekdays[timeptr->tm_wday], 3);
                    break;

                case 'A':    /* locale's full weekday name */
                    PUT_STR(weekdays[timeptr->tm_wday]);
                    break;
#endif
            case 'b': // Abbreviated month name
            {
                strncat(s, months[timeptr->tm_mon], 3);
                break;
            }
            case 'B': // Full month name
            {
                unsigned int i = (unsigned int)timeptr->tm_mon;
                strcat(s, months[i]);
                break;
            }
            case 'd': /* day of month as decimal number (01-31) */
            {
                value  = timeptr->tm_mday;
                (*s++) = (char)('0' + ((value / 10) % 10));
                (*s++) = (char)('0' + (value % 10));
                break;
            }
            case 'H': /* hour (24-hour clock) as decimal (00-23) */
            {
                value  = timeptr->tm_hour;
                (*s++) = (char)('0' + ((value / 10) % 10));
                (*s++) = (char)('0' + (value % 10));
                break;
            }
#if 0
                case 'I':    /* hour (12-hour clock) as decimal (01-12) */
                    SPRINTF("%02d", ((timeptr->tm_hour + 11) % 12) + 1);
                    break;
#endif
            case 'j': // Day of year as decimal number (001-366)
            {
                value  = timeptr->tm_yday;
                (*s++) = (char)('0' + ((value / 10) % 10));
                (*s++) = (char)('0' + (value % 10));
                break;
            }
            case 'm': // Month as decimal number (01-12)
            {
                value  = timeptr->tm_mon;
                (*s++) = (char)('0' + ((value / 10) % 10));
                (*s++) = (char)('0' + (value % 10));
                break;
            }
#if 0
                case 'M':    /* month as decimal number (00-59) */
                    SPRINTF("%02d", timeptr->tm_min);
                    break;

                case 'p':    /* AM/PM designations (12-hour clock) */
                    PUT_STR(timeptr->tm_hour < 12 ? "AM" : "PM");
                    /* s.b. locale-specific */
                    break;

                case 'S':    /* second as decimal number (00-61) */
                    SPRINTF("%02d", timeptr->tm_sec);
                    break;

                case 'U':    /* week of year (first Sunday = 1) (00-53) */
                case 'W':    /* week of year (first Monday = 1) (00-53) */
                {
                    int fudge = timeptr->tm_wday - timeptr->tm_yday % 7;
                    if (*fp == 'W') {
                        fudge--;
                    }
                    fudge = (fudge + 7) % 7;      /* +7 so not negative */
                    SPRINTF("%02d", (timeptr->tm_yday + fudge) / 7);
                    break;
                }

                case 'w':    /* weekday (0-6), Sunday is 0 */
                    SPRINTF("%02d", timeptr->tm_wday);
                    break;

                case 'x':    /* appropriate date representation */
                    RECUR("%B %d, %Y");    /* s.b. locale-specific */
                    break;

                case 'X':    /* appropriate time representation */
                    RECUR("%H:%M:%S");    /* s.b. locale-specific */
                    break;

                case 'y':    /* year without century as decimal (00-99) */
                    SPRINTF("%02d", timeptr->tm_year % 100);
                    break;

                case 'Y':    /* year with century as decimal */
                    SPRINTF("%d", timeptr->tm_year + 1900);
                    break;

                case 'Z':    /* time zone name or abbreviation */
                    /* XXX %%% */
                    break;

                case '%':
                    PUT_CH('%');
                    break;
#endif
            default:
                (*s++) = '%';
                (*s++) = (*f);
            }
        }
        ++f;
    }

#if 0
#define PUT_CH(c) (ret < maxsize ? (*dp++ = (c), ret++) : 0)
#define PUT_STR(str)   \
    p = (char *)(str); \
    goto dostr;
#define PUT_STRN(str, n) \
    p   = (char *)(str); \
    len = (n);           \
    goto dostrn;
#define SPRINTF(fmt, arg)      \
    sprintf(tmpbuf, fmt, arg); \
    p = tmpbuf;                \
    goto dostr;
#define RECUR(fmt)                                            \
    {                                                         \
        size_t r = strftime(dp, maxsize - ret, fmt, timeptr); \
        if (r <= 0)                                           \
            return r;                                         \
        dp += r;                                              \
        ret += r;                                             \
    }

    char* fp = (char*) format;
    char* dp = s;
    size_t ret = 0;

    if (format == NULL || s == NULL || maxsize <= 0) {
        return 0;
    }

    while (*fp != '\0') {
        char* p;
        int len;
        char tmpbuf[10];

        if (*fp != '%') {
            PUT_CH(*fp++);
            continue;
        }

        fp++;

        switch (*fp) {

        case 'a':    /* locale's abbreviated weekday name */
        PUT_STRN(weekdays[timeptr->tm_wday], 3);
            break;

        case 'A':    /* locale's full weekday name */
        PUT_STR(weekdays[timeptr->tm_wday]);
            break;

        case 'b':    /* locale's abbreviated month name */
        PUT_STRN(months[timeptr->tm_mon], 3);
            break;

        case 'B':    /* locale's full month name */
        PUT_STR(months[timeptr->tm_mon]);
            break;

        case 'c':    /* appropriate date and time representation */
        RECUR("%X %x");        /* s.b. locale-specific */
            break;

        case 'd':    /* day of month as decimal number (01-31) */
        SPRINTF("%02d", timeptr->tm_mday);
            break;

        case 'H':    /* hour (24-hour clock) as decimal (00-23) */
        SPRINTF("%02d", timeptr->tm_hour);
            break;

        case 'I':    /* hour (12-hour clock) as decimal (01-12) */
        SPRINTF("%02d", ((timeptr->tm_hour + 11) % 12) + 1);
            break;

        case 'j':    /* day of year as decimal number (001-366) */
        SPRINTF("%03d", timeptr->tm_yday + 1);
            break;

        case 'm':    /* month as decimal number (01-12) */
        SPRINTF("%02d", timeptr->tm_mon + 1);
            break;

        case 'M':    /* month as decimal number (00-59) */
        SPRINTF("%02d", timeptr->tm_min);
            break;

        case 'p':    /* AM/PM designations (12-hour clock) */
        PUT_STR(timeptr->tm_hour < 12 ? "AM" : "PM");
            /* s.b. locale-specific */
            break;

        case 'S':    /* second as decimal number (00-61) */
        SPRINTF("%02d", timeptr->tm_sec);
            break;

        case 'U':    /* week of year (first Sunday = 1) (00-53) */
        case 'W':    /* week of year (first Monday = 1) (00-53) */
        {
            int fudge = timeptr->tm_wday - timeptr->tm_yday % 7;
            if (*fp == 'W') {
                fudge--;
            }
            fudge = (fudge + 7) % 7;      /* +7 so not negative */
            SPRINTF("%02d", (timeptr->tm_yday + fudge) / 7);
            break;
        }

        case 'w':    /* weekday (0-6), Sunday is 0 */
        SPRINTF("%02d", timeptr->tm_wday);
            break;

        case 'x':    /* appropriate date representation */
        RECUR("%B %d, %Y");    /* s.b. locale-specific */
            break;

        case 'X':    /* appropriate time representation */
        RECUR("%H:%M:%S");    /* s.b. locale-specific */
            break;

        case 'y':    /* year without century as decimal (00-99) */
        SPRINTF("%02d", timeptr->tm_year % 100);
            break;

        case 'Y':    /* year with century as decimal */
        SPRINTF("%d", timeptr->tm_year + 1900);
            break;

        case 'Z':    /* time zone name or abbreviation */
            /* XXX %%% */
            break;

        case '%':
            PUT_CH('%');
            break;

        dostr:
            while (*p != '\0')
                PUT_CH(*p++);
            break;

        dostrn:
            while (len-- > 0)
                PUT_CH(*p++);
            break;
        }

        ++fp;
    }
    if (ret >= maxsize) {
        s[maxsize - 1] = '\0';
        return 0;
    }
    *dp = '\0';
#endif
    return ret;
}

_syscall2(int, nanosleep, const timespec *, req, timespec *, rem)

unsigned int sleep(unsigned int seconds)
{
    timespec req, rem;
    req.tv_sec = seconds;
    // Call the nanosleep.
    int __ret = nanosleep(&req, &rem);
    // If the call to the nanosleep is interrupted by a signal handler,
    // then it returns -1 with errno set to EINTR.
    if ((__ret == -1) && (errno == EINTR)) {
        return rem.tv_sec;
    }
    return 0;
}

_syscall2(int, getitimer, int, which, itimerval *, curr_value)

_syscall3(int, setitimer, int, which, const itimerval *, new_value, itimerval *, old_value)