"""
RPython implementations of time.time(), time.clock(), time.select().
"""

import sys
import math
import time as pytime
from rpython.translator.tool.cbuild import ExternalCompilationInfo
from rpython.rtyper.tool import rffi_platform
from rpython.rtyper.lltypesystem import rffi, lltype
from rpython.rlib.objectmodel import register_replacement_for
from rpython.rlib.rarithmetic import intmask, UINT_MAX
from rpython.rlib import rposix

_WIN32 = sys.platform.startswith('win')

if _WIN32:
    TIME_H = 'time.h'
    FTIME = '_ftime64'
    STRUCT_TIMEB = 'struct __timeb64'
    includes = ['winsock2.h', 'windows.h',
                TIME_H, 'sys/types.h', 'sys/timeb.h']
    need_rusage = False
else:
    TIME_H = 'sys/time.h'
    FTIME = 'ftime'
    STRUCT_TIMEB = 'struct timeb'
    includes = [TIME_H, 'time.h', 'errno.h', 'sys/select.h',
                'sys/types.h', 'unistd.h',
                'sys/time.h', 'sys/resource.h']

    if not sys.platform.startswith("openbsd") and \
       not sys.platform.startswith("freebsd"):
        includes.append('sys/timeb.h')

    need_rusage = True


eci = ExternalCompilationInfo(includes=includes)

class CConfig:
    _compilation_info_ = eci
    TIMEVAL = rffi_platform.Struct('struct timeval', [('tv_sec', rffi.INT),
                                                      ('tv_usec', rffi.INT)])
    HAVE_GETTIMEOFDAY = rffi_platform.Has('gettimeofday')
    HAVE_FTIME = rffi_platform.Has(FTIME)
    if need_rusage:
        RUSAGE = rffi_platform.Struct('struct rusage', [('ru_utime', TIMEVAL),
                                                        ('ru_stime', TIMEVAL)])

if sys.platform.startswith('freebsd') or sys.platform.startswith('netbsd'):
    libraries = ['compat']
elif sys.platform == 'linux2':
    libraries = ['rt']
else:
    libraries = []

class CConfigForFTime:
    _compilation_info_ = ExternalCompilationInfo(
        includes=[TIME_H, 'sys/timeb.h'],
        libraries=libraries
    )
    TIMEB = rffi_platform.Struct(STRUCT_TIMEB, [('time', rffi.INT),
                                                ('millitm', rffi.INT)])

class CConfigForClockGetTime:
    _compilation_info_ = ExternalCompilationInfo(
        includes=['time.h'],
        libraries=libraries
    )
    _NO_MISSING_RT = rffi_platform.Has('printf("%d", clock_gettime(0, 0))')
    TIMESPEC = rffi_platform.Struct('struct timespec', [('tv_sec', rffi.LONG),
                                                        ('tv_nsec', rffi.LONG)])

constant_names = ['RUSAGE_SELF', 'EINTR',
                  'CLOCK_REALTIME',
                  'CLOCK_REALTIME_COARSE',
                  'CLOCK_MONOTONIC',
                  'CLOCK_MONOTONIC_COARSE',
                  'CLOCK_MONOTONIC_RAW',
                  'CLOCK_BOOTTIME',
                  'CLOCK_PROCESS_CPUTIME_ID',
                  'CLOCK_THREAD_CPUTIME_ID',
                  'CLOCK_HIGHRES',
                  'CLOCK_PROF',
]
for const in constant_names:
    setattr(CConfig, const, rffi_platform.DefinedConstantInteger(const))
defs_names = ['GETTIMEOFDAY_NO_TZ']
for const in defs_names:
    setattr(CConfig, const, rffi_platform.Defined(const))

def decode_timeval(t):
    return (float(rffi.getintfield(t, 'c_tv_sec')) +
            float(rffi.getintfield(t, 'c_tv_usec')) * 0.000001)


def external(name, args, result, compilation_info=eci, **kwds):
    return rffi.llexternal(name, args, result,
                           compilation_info=compilation_info, **kwds)

def replace_time_function(name):
    func = getattr(pytime, name, None)
    if func is None:
        return lambda f: f
    return register_replacement_for(
        func,
        sandboxed_name='ll_time.ll_time_%s' % name)

config = rffi_platform.configure(CConfig)
globals().update(config)

# Note: time.time() is used by the framework GC during collect(),
# which means that we have to be very careful about not allocating
# GC memory here.  This is the reason for the _nowrapper=True.
if HAVE_GETTIMEOFDAY:
    if GETTIMEOFDAY_NO_TZ:
        c_gettimeofday = external('gettimeofday',
                                  [lltype.Ptr(TIMEVAL)], rffi.INT,
                                  _nowrapper=True, releasegil=False)
    else:
        c_gettimeofday = external('gettimeofday',
                                  [lltype.Ptr(TIMEVAL), rffi.VOIDP], rffi.INT,
                                  _nowrapper=True, releasegil=False)
if HAVE_FTIME:
    globals().update(rffi_platform.configure(CConfigForFTime))
    c_ftime = external(FTIME, [lltype.Ptr(TIMEB)],
                         lltype.Void,
                         _nowrapper=True, releasegil=False)
c_time = external('time', [rffi.VOIDP], rffi.TIME_T,
                  _nowrapper=True, releasegil=False)


@replace_time_function('time')
def time():
    void = lltype.nullptr(rffi.VOIDP.TO)
    result = -1.0
    if HAVE_GETTIMEOFDAY:
        # NB: can't use lltype.scoped_malloc, because that will allocate the
        # with handler in the GC, but we want to use time.time from gc.collect!
        t = lltype.malloc(TIMEVAL, flavor='raw')
        try:
            errcode = -1
            if GETTIMEOFDAY_NO_TZ:
                errcode = c_gettimeofday(t)
            else:
                errcode = c_gettimeofday(t, void)

            if rffi.cast(rffi.LONG, errcode) == 0:
                result = decode_timeval(t)
        finally:
            lltype.free(t, flavor='raw')
        if result != -1:
            return result
    else: # assume using ftime(3)
        t = lltype.malloc(TIMEB, flavor='raw')
        try:
            c_ftime(t)
            result = (float(intmask(t.c_time)) +
                      float(intmask(t.c_millitm)) * 0.001)
        finally:
            lltype.free(t, flavor='raw')
        return result
    return float(c_time(void))


# _______________________________________________________________
# time.clock()

if _WIN32:
    # hacking to avoid LARGE_INTEGER which is a union...
    QueryPerformanceCounter = external(
        'QueryPerformanceCounter', [rffi.CArrayPtr(lltype.SignedLongLong)],
         lltype.Void, releasegil=False)
    QueryPerformanceFrequency = external(
        'QueryPerformanceFrequency', [rffi.CArrayPtr(lltype.SignedLongLong)],
        rffi.INT, releasegil=False)
    class State(object):
        divisor = 0.0
        counter_start = 0
    state = State()

HAS_CLOCK_GETTIME = (CLOCK_MONOTONIC is not None)
if sys.platform == 'darwin':
    HAS_CLOCK_GETTIME = False
    # ^^^ https://bitbucket.org/pypy/pypy/issues/2432 and others
    # (change it manually if you *know* you want to build and run on
    # OS/X 10.12 or later)

if HAS_CLOCK_GETTIME:
    # Linux and other POSIX systems with clock_gettime()
    # TIMESPEC:
    globals().update(rffi_platform.configure(CConfigForClockGetTime))
    # do we need to add -lrt?
    eciclock = CConfigForClockGetTime._compilation_info_
    if not _NO_MISSING_RT:
        eciclock = eciclock.merge(ExternalCompilationInfo(libraries=['rt']))
    # the functions:
    c_clock_getres = external("clock_getres",
                              [lltype.Signed, lltype.Ptr(TIMESPEC)],
                              rffi.INT, releasegil=False,
                              save_err=rffi.RFFI_SAVE_ERRNO,
                              compilation_info=eciclock)
    c_clock_gettime = external('clock_gettime',
                               [lltype.Signed, lltype.Ptr(TIMESPEC)],
                               rffi.INT, releasegil=False,
                               save_err=rffi.RFFI_SAVE_ERRNO,
                               compilation_info=eciclock)
    c_clock_settime = external('clock_settime',
                               [lltype.Signed, lltype.Ptr(TIMESPEC)],
                               rffi.INT, releasegil=False,
                               save_err=rffi.RFFI_SAVE_ERRNO,
                               compilation_info=eciclock)
    # Note: there is no higher-level functions here to access
    # clock_gettime().  The issue is that we'd need a way that keeps
    # nanosecond precision, depending on the usage, so we can't have a
    # nice function that returns the time as a float.
    ALL_DEFINED_CLOCKS = [const for const in constant_names
                          if const.startswith('CLOCK_')
                             and globals()[const] is not None]

if need_rusage:
    RUSAGE = RUSAGE
    RUSAGE_SELF = RUSAGE_SELF or 0
    c_getrusage = external('getrusage',
                           [rffi.INT, lltype.Ptr(RUSAGE)],
                           rffi.INT,
                           releasegil=False)

def win_perf_counter():
    with lltype.scoped_alloc(rffi.CArray(rffi.lltype.SignedLongLong), 1) as a:
        if state.divisor == 0.0:
            QueryPerformanceCounter(a)
            state.counter_start = a[0]
            QueryPerformanceFrequency(a)
            state.divisor = float(a[0])
        QueryPerformanceCounter(a)
        diff = a[0] - state.counter_start
    return float(diff) / state.divisor

@replace_time_function('clock')
def clock():
    if _WIN32:
        return win_perf_counter()
    elif HAS_CLOCK_GETTIME and CLOCK_PROCESS_CPUTIME_ID is not None:
        with lltype.scoped_alloc(TIMESPEC) as a:
            if c_clock_gettime(CLOCK_PROCESS_CPUTIME_ID, a) == 0:
                return (float(rffi.getintfield(a, 'c_tv_sec')) +
                        float(rffi.getintfield(a, 'c_tv_nsec')) * 0.000000001)
    with lltype.scoped_alloc(RUSAGE) as a:
        c_getrusage(RUSAGE_SELF, a)
        result = (decode_timeval(a.c_ru_utime) +
                  decode_timeval(a.c_ru_stime))
    return result

# _______________________________________________________________
# time.sleep()

if _WIN32:
    Sleep = external('Sleep', [rffi.ULONG], lltype.Void)
else:
    c_select = external('select', [rffi.INT, rffi.VOIDP,
                                   rffi.VOIDP, rffi.VOIDP,
                                   lltype.Ptr(TIMEVAL)], rffi.INT,
                        save_err=rffi.RFFI_SAVE_ERRNO)

@replace_time_function('sleep')
def sleep(secs):
    if _WIN32:
        millisecs = secs * 1000.0
        while millisecs > UINT_MAX:
            Sleep(UINT_MAX)
            millisecs -= UINT_MAX
        Sleep(rffi.cast(rffi.ULONG, int(millisecs)))
    else:
        void = lltype.nullptr(rffi.VOIDP.TO)
        with lltype.scoped_alloc(TIMEVAL) as t:
            frac = int(math.fmod(secs, 1.0) * 1000000.)
            assert frac >= 0
            rffi.setintfield(t, 'c_tv_sec', int(secs))
            rffi.setintfield(t, 'c_tv_usec', frac)

            if rffi.cast(rffi.LONG, c_select(0, void, void, void, t)) != 0:
                errno = rposix.get_saved_errno()
                if errno != EINTR:
                    raise OSError(errno, "Select failed")
