/* Copyright (C) 2021       Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>

#ifndef HAVE_CLOCK_GETTIME
#if HAVE_GETTIMEOFDAY
#include <sys/time.h>
#elif HAVE_FTIME
#include <sys/timeb.h>
#endif
#endif

#if !defined(HAVE_CLOCK_NANOSLEEP) && !defined(HAVE_NANOSLEEP)
#if HAVE_SELECT
#if HAVE_SYS_SELECT_H
#include <sys/select.h>
#else
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#else
#include <unistd.h>
#endif
#endif

#include <igloo/time.h>
#include <igloo/error.h>

#define MARKER          ((uint32_t)0x1)
#define FLAG_NULL       ((uint32_t)0x2)
#define FLAG_NEGATIVE   ((uint32_t)0x4)
#define FLAG_INTERVAL   ((uint32_t)0x8)

#define assert_valid(t) \
    if (!igloo_ctime_is_valid((t))) \
        return igloo_ERROR_FAULT
#define assert_valid_notnull(t) \
    if (!igloo_ctime_is_valid((t)) || igloo_ctime_is_null((t))) \
        return igloo_ERROR_FAULT

#if HAVE_CLOCK_GETTIME
static inline igloo_error_t resolve_system_clock(clockid_t *dst, igloo_clock_t clock)
{
    switch (clock) {
        case igloo_CLOCK_REALTIME:
            *dst = CLOCK_REALTIME;
            break;
        case igloo_CLOCK_MONOTONIC:
            *dst = CLOCK_MONOTONIC;
            break;
        default:
            return igloo_ERROR_INVAL;
            break;
    }
    return igloo_ERROR_NONE;
}
#endif

static inline igloo_clock_t get_clock(igloo_ctime_t t)
{
    return (t.flags >> 24) & 0xFF;
}

static inline void init(igloo_ctime_t *dst)
{
    memset(dst, 0, sizeof(*dst));
    dst->flags = MARKER;
    dst->ssec  = MARKER;
}

static igloo_error_t   igloo_ctime_from_time_t_and_nsecs(igloo_ctime_t *dst, time_t src, int32_t nsecs, igloo_clock_t clock)
{
    if (!dst)
        return igloo_ERROR_FAULT;

    if (sizeof(src) > sizeof(dst->fsec)) {
        if ((src > 0 && src > (time_t)UINT64_MAX) || (src < 0 && src < (-(time_t)UINT64_MAX)))
            return igloo_ERROR_RANGE;
    }

    if (nsecs >= 1000000000 || nsecs <= -1000000000)
        return igloo_ERROR_INVAL;

    if (clock > 255)
        return igloo_ERROR_INVAL;

    init(dst);

    if (src >= 0) {
        dst->fsec = src;
    } else {
        // make src = |src| - 1 so it always fits it's range
        src = -(src + 1);
        dst->fsec = src;
        dst->fsec++; // add the 1 we substracted earlier.
        dst->flags |= FLAG_NEGATIVE;
    }

    if (nsecs) {
        if (dst->flags & FLAG_NEGATIVE) {
            if (nsecs >= 0) {
                if (dst->fsec) {
                    dst->fsec--;
                    dst->ssec |= (1000000000 - nsecs) << 1;
                } else {
                    // This is invalid. Handle it anyway.
                    dst->flags -= FLAG_NEGATIVE;
                    dst->ssec |= nsecs << 1;
                }
            } else {
                dst->ssec |= (-nsecs) << 1;
            }
        } else {
            if (nsecs >= 0) {
                dst->ssec |= nsecs << 1;
            } else {
                if (dst->fsec) {
                    dst->fsec--;
                    dst->ssec |= (1000000000 + nsecs) << 1;
                } else {
                    dst->ssec |= (-nsecs) << 1;
                    dst->flags |= FLAG_NEGATIVE;
                }
            }
        }
    }

    dst->flags |= clock << 24;

    return igloo_ERROR_NONE;
}

igloo_error_t   igloo_ctime_from_null(igloo_ctime_t *dst)
{
    if (!dst)
        return igloo_ERROR_FAULT;

    init(dst);
    dst->flags |= FLAG_NULL;
    return igloo_ERROR_NONE;
}

igloo_error_t   igloo_ctime_from_time_t(igloo_ctime_t *dst, time_t src, igloo_clock_t clock)
{
    return igloo_ctime_from_time_t_and_nsecs(dst, src, 0, clock);
}

igloo_error_t   igloo_ctime_from_interval(igloo_ctime_t *dst, uint64_t sec, uint32_t nsec, igloo_clock_t clock)
{
    igloo_error_t error;

    error = igloo_ctime_from_time_t_and_nsecs(dst, sec, nsec, clock);
    if (error != igloo_ERROR_NONE)
        return error;

    dst->flags |= FLAG_INTERVAL;

    return igloo_ERROR_NONE;
}

igloo_error_t   igloo_ctime_from_now(igloo_ctime_t *dst, igloo_clock_t clock)
{
    if (!dst)
        return igloo_ERROR_FAULT;

    if (1) {
#if HAVE_CLOCK_GETTIME
        struct timespec ts;
        clockid_t clkid;
        igloo_error_t error;

        error = resolve_system_clock(&clkid, clock);
        if (error != igloo_ERROR_NONE)
            return error;

        if (clock_gettime(clkid, &ts) != 0)
            return igloo_error_from_system(NULL);

        return igloo_ctime_from_time_t_and_nsecs(dst, ts.tv_sec, ts.tv_nsec, clock);
#elif HAVE_GETTIMEOFDAY
        struct timeval tv;

        if (clock != igloo_CLOCK_REALTIME)
            return igloo_ERROR_INVAL;

        if (gettimeofday(&tv, NULL) != 0)
            return igloo_error_from_system(NULL);

        return igloo_ctime_from_time_t_and_nsecs(dst, tv.tv_sec, tv.tv_usec * 1000, clock);
#elif HAVE_FTIME
        struct timeb tb;

        if (clock != igloo_CLOCK_REALTIME)
            return igloo_ERROR_INVAL;

        if (ftime(&tb) != 0)
            return igloo_error_from_system(NULL);

        return igloo_ctime_from_time_t_and_nsecs(dst, tb.time, tb.millitm * 1000 * 1000, clock);
#else
        if (clock != igloo_CLOCK_REALTIME)
            return igloo_ERROR_INVAL;

        return igloo_ctime_from_time_t_and_nsecs(dst, time(NULL), 0, clock);
#endif
    }
}

igloo_error_t   igloo_ctime_sleep(igloo_ctime_t t)
{
    igloo_clock_t clock;

    assert_valid_notnull(t);

    if (!igloo_ctime_is_interval(t) || igloo_ctime_is_negative(t))
       return igloo_ERROR_DOM;

    clock = get_clock(t);

    if (1) {
#if HAVE_CLOCK_NANOSLEEP
        struct timespec ts = {.tv_sec = t.fsec, .tv_nsec = (t.ssec >> 1)};
        clockid_t clkid;
        igloo_error_t error;

        error = resolve_system_clock(&clkid, clock);
        if (error != igloo_ERROR_NONE)
            return error;

        if (clock_nanosleep(clkid, 0, &ts, NULL) != 0)
            return igloo_error_from_system(NULL);

        return igloo_ERROR_NONE;
#elif HAVE_NANOSLEEP
        struct timespec ts = {.tv_sec = t.fsec, .tv_nsec = (t.ssec >> 1)};

        if (clock != igloo_CLOCK_MONOTONIC)
            return igloo_ERROR_NOSYS;

        if (nanosleep(&ts, NULL) != 0)
            return igloo_error_from_system(NULL);

        return igloo_ERROR_NONE;
#elif HAVE_SELECT
        struct timeval tv = {.tv_sec = t.fsec, .tv_usec = ((t.ssec >> 1)/1000)};

        if (clock != igloo_CLOCK_MONOTONIC)
            return igloo_ERROR_NOSYS;

        if (select(0, NULL, NULL, NULL, &tv) != 0)
            return igloo_error_from_system(NULL);

        return igloo_ERROR_NONE;
#elif HAVE_USLEEP
        // Having fun here to provide a efficient sleep, still only
        // using values < 1s as some usleep()s consider this an error.

        uint64_t fsec = t.fsec;

        if (clock != igloo_CLOCK_MONOTONIC)
            return igloo_ERROR_NOSYS;

        while (fsec >= 3) {
            int i;

            for (i = 0; i < 4; i++) {
                if (usleep(750000) != 0)
                    return igloo_error_from_system(NULL);
            }

            fsec -= 3;
        }

        while (fsec) {
            int i;

            for (i = 0; i < 2; i++) {
                if (usleep(500000) != 0)
                    return igloo_error_from_system(NULL);
            }

            fsec--;
        }

        if ((t.ssec >> 1)/1000) {
            if (usleep((t.ssec >> 1)/1000) != 0)
                return igloo_error_from_system(NULL);
        }

        return igloo_ERROR_NONE;
#else
        // sleep() requires a loop. Also we do not trust it to allow values > 32767s.
        uint64_t fsec = t.fsec;
        bool subsecs = t.ssec != MARKER;

        if (clock != igloo_CLOCK_MONOTONIC)
            return igloo_ERROR_NOSYS;

        while (fsec || subsecs) {
            unsigned int to;

            if (fsec > 32767) {
                to = 32767;
            } else {
                to = fsec;
                if (subsecs) {
                    to++;
                    subsecs = false;
                }
            }

            while (to)
                to = sleep(to);

            if (fsec > 32767) {
                fsec -= 32767;
            } else {
                fsec = 0;
            }
        }

        return igloo_ERROR_NONE;
#endif
    }
}

igloo_error_t   igloo_ctime_to_time_t(time_t *dst, igloo_ctime_t src)
{
    time_t t;

    if (!dst)
        return igloo_ERROR_FAULT;

    assert_valid(src);

    if (src.flags & FLAG_NULL)
       return igloo_ERROR_DOM;

    if (!(src.flags & FLAG_NEGATIVE)) {
        t = src.fsec;
        if (t < 0 || ((uint64_t)t) != src.fsec)
            return igloo_ERROR_RANGE;
        *dst = t;
        return igloo_ERROR_NONE;
    }

    t = -src.fsec;
    if (t >= 0 || ((uint64_t)t) != -src.fsec)
        return igloo_ERROR_RANGE;

    if (src.ssec != MARKER) {
        t--;
        if (t >= 0)
            return igloo_ERROR_RANGE;
    }

    *dst = t;

    return igloo_ERROR_NONE;
}

bool            igloo_ctime_is_null(igloo_ctime_t t)
{
    return t.flags & FLAG_NULL ? true : false;
}

bool            igloo_ctime_is_valid(igloo_ctime_t t)
{
    return (t.flags & MARKER) && (t.ssec & MARKER) ? true : false;
}

bool            igloo_ctime_is_interval(igloo_ctime_t t)
{
    return igloo_ctime_is_valid(t) && !(t.flags & FLAG_NULL) && (t.flags & FLAG_INTERVAL) ? true : false;
}

bool            igloo_ctime_is_absolute(igloo_ctime_t t)
{
    return igloo_ctime_is_valid(t) && !(t.flags & FLAG_NULL) && !(t.flags & FLAG_INTERVAL) ? true : false;
}

bool            igloo_ctime_is_negative(igloo_ctime_t t)
{
    return igloo_ctime_is_interval(t) && (t.flags & FLAG_NEGATIVE) ? true : false;
}
