#include <bits/errno-return.h>
#include <luna/Check.h>
#include <luna/Format.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>

#define SECONDS_PER_DAY 86400

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

static int make_yday(int year, int month)
{
    static const short int upto[12] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
    int yd;

    yd = upto[month - 1];
    if (month > 2 && isleap(year)) yd++;
    return yd;
}

static int day_of_week(int year, int mon, int day)
{
    static int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
    year -= mon < 3;
    return (year + year / 4 - year / 100 + year / 400 + t[mon - 1] + day) % 7;
}

static void time_to_struct_tm(time_t time, struct tm* result)
{
    result->tm_isdst = 0; // No DST/timezone support for now.

    int year = 1970;

    while (time > 0)
    {
        time_t seconds_in_year = (isleap(year) ? 366 : 365) * SECONDS_PER_DAY;
        if (seconds_in_year <= time)
        {
            year++;
            time -= seconds_in_year;
            continue;
        }
        break;
    }

    int month_days[] = { 31, isleap(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    int month = 0;

    while (1)
    {
        time_t seconds_in_month = month_days[month] * SECONDS_PER_DAY;
        if (seconds_in_month <= time)
        {
            month++;
            time -= seconds_in_month;
            continue;
        }
        break;
    }

    int day = (int)(time / SECONDS_PER_DAY);
    time %= SECONDS_PER_DAY;

    check(day < month_days[month]);

    int hour = (int)(time / 3600);
    time %= 3600;

    int min = (int)(time / 60);
    time %= 60;

    result->tm_year = year - 1900;
    result->tm_mon = month;
    result->tm_yday = make_yday(year, month + 1) + day;
    result->tm_wday = day_of_week(year, month + 1, day + 1);
    result->tm_mday = day + 1;
    result->tm_hour = hour;
    result->tm_min = min;
    result->tm_sec = (int)time;
}

extern "C"
{
    int clock_gettime(clockid_t id, struct timespec* ts)
    {
        long rc = syscall(SYS_clock_gettime, id, ts);
        __errno_return(rc, int);
    }

    time_t time(time_t* tp)
    {
        struct timespec ts;
        if (clock_gettime(CLOCK_REALTIME, &ts) < 0) return (time_t)-1;

        if (tp) *tp = ts.tv_sec;

        return ts.tv_sec;
    }

    struct tm* localtime_r(const time_t* tp, struct tm* out)
    {
        time_to_struct_tm(*tp, out);
        return out;
    }

    struct tm* gmtime_r(const time_t* tp, struct tm* out)
    {
        time_to_struct_tm(*tp, out);
        return out;
    }

    struct tm* localtime(const time_t* tp)
    {
        static struct tm out;
        return localtime_r(tp, &out);
    }

    struct tm* gmtime(const time_t* tp)
    {
        static struct tm out;
        return gmtime_r(tp, &out);
    }

    char* asctime_r(const struct tm* tm, char buf[26])
    {
        strftime(buf, 26, "%a %b %d %H:%M:%S %Y\n", tm);
        return buf;
    }

    char* asctime(const struct tm* tm)
    {
        static char buf[26];
        return asctime_r(tm, buf);
    }

    char* ctime_r(const time_t* tp, char buf[26])
    {
        struct tm out;
        return asctime_r(localtime_r(tp, &out), buf);
    }

    char* ctime(const time_t* tp)
    {
        return asctime(localtime(tp));
    }
}