/*
    Raydium - CQFD Corp.
    http://raydium.org/
    Released under both BSD license and Lesser GPL library license.
    See "license.txt" file.
*/

#ifndef DONT_INCLUDE_HEADERS
#include "index.h"
#else
#include "headers/timecall.h"
#endif

#ifdef WIN32
#define __GETTIMEOFDAY_USEC 1000
#else
#define __GETTIMEOFDAY_USEC 1000000
#endif

// needed proto
int raydium_timecall_add(void *funct, GLint hz);


void raydium_timecall_raydium(GLfloat step)
{
raydium_frame_time=step;
}

#ifdef WIN32
float raydium_timecall_internal_w32_detect_modulo(int div)
{
 LARGE_INTEGER t;
 unsigned long mx;

 QueryPerformanceFrequency(&t);
 t.QuadPart >>= div;
 if (t.HighPart>0)
    {
    //Handle to high clock frequency
    //Raydium can't handle this.
    return 0.0f;
    }
 mx=(0xFFFFFFFF / t.LowPart); // Max 32 bit time period
 return mx/60.f; // Return max time count in minutes
}


int raydium_timecall_internal_w32_divmodulo_find(void)
{
float modulo_time;
int div;

// Find a correct divide to have at least RAYDIUM_TIMECALL_W32_MODULO_MIN minutes
// before an overflow occurs.
div=-1;
do{
div++;
modulo_time=raydium_timecall_internal_w32_detect_modulo(div);
}while(modulo_time<RAYDIUM_TIMECALL_W32_MODULO_MIN);

raydium_log("timecall: win32 modulo every %.2f minutes, modulodiv is 2^%i",modulo_time,div);
return div;
}
#endif


unsigned long raydium_timecall_devrtc_clock(void)
{
#ifndef WIN32
struct timeval tv={0, 0};
fd_set readfds;
int ret;
unsigned long data,missed;

FD_ZERO(&readfds);
FD_SET(raydium_timecall_devrtc_handle, &readfds);
if( (ret=select(raydium_timecall_devrtc_handle+1, &readfds, NULL, NULL, &tv)) == -1)
    {
    raydium_log("timecall: ERROR: selecting /dev/rtc failed at runtime");
    perror("system");
    }

// IRQ fired !
if(ret>0)
 {
        if( read(raydium_timecall_devrtc_handle, &data, sizeof(unsigned long)) == -1)
        {
        raydium_log("timecall: ERROR: reading /dev/rtc failed at runtime");
        perror("system");
        }
        else
        {
        // read first 3 bytes only
        missed=(data & 0xffffff00UL)>>8;
        raydium_timecall_devrtc_clocks+=missed;
//      raydium_log("%i",raydium_timecall_devrtc_clocks);
        }
 }

return raydium_timecall_devrtc_clocks;
#else
return 0;
#endif
}

unsigned long raydium_timecall_clock_internal(void)
{
#ifndef WIN32
struct timeval tv;
#endif

#ifdef DEBUG_MOVIE
    return (float)(raydium_timecall_debug_movie*100);
#endif


if(raydium_timecall_method==RAYDIUM_TIMECALL_METHOD_CLOCK)
 {
 #ifdef WIN32
  {
// return GetTickCount();
    LARGE_INTEGER t;
    if(!QueryPerformanceCounter(&t))raydium_log("TIMECALL ERROR: Your system can not provide data(high resolution timer) to QueryPerformanceCounter function. Please tell us about this in the raydium forum.Thanks");
    t.QuadPart>>=raydium_timecall_w32_divmodulo;
// Originaly return t.LowPart<<1; Why does this is multiplied by 2 ?
    return t.LowPart;
  }
 #else
 gettimeofday(&tv,NULL);
 return (tv.tv_sec*1000000 + tv.tv_usec);
 #endif
 //return clock();
 }
//else if(raydium_timecall_method==RAYDIUM_TIMECALL_METHOD_DEVRTC)
 return raydium_timecall_devrtc_clock();
}

unsigned long raydium_timecall_clock(void)
{
long long now;

if(raydium_timecall_stopped_stamp) // time is stopped
    return raydium_timecall_stopped_stamp;

now=raydium_timecall_clock_internal();
now-=raydium_timecall_offset;

return (unsigned long)now; // should deal with modulo :)
}

signed char raydium_timecall_devrtc_rate_change(unsigned long new)
{
#ifndef APPLE
#ifndef WIN32
        if(ioctl(raydium_timecall_devrtc_handle, RTC_IRQP_SET, new)==-1)
        {
                raydium_log("timecall: ERROR: changing /dev/rtc rate to %lu Hz failed !",new);
                raydium_log("timecall: is /proc/sys/dev/rtc/max-user-freq correct for this value ?");
                perror("system");
//                exit(errno);
                return 0;
        }
        raydium_log("timecall: /dev/rtc rate changed to %lu Hz",new);
        return 1;
#else
return 0;
#endif
#endif
}

void raydium_timecall_devrtc_close(void)
{
#ifndef APPLE
#ifndef WIN32
        if(ioctl(raydium_timecall_devrtc_handle, RTC_PIE_OFF, 0) == -1)
        {
                raydium_log("timecall: ERROR disabling /dev/rtc periodic interrupts");
                perror("system");
//                exit(errno);
        }
close(raydium_timecall_devrtc_handle);
#endif
#endif
}


unsigned long raydium_timecall_devrtc_init(void)
{
#ifndef APPLE
#ifndef WIN32
 unsigned long freq;
 raydium_timecall_devrtc_clocks=0;

 if((raydium_timecall_devrtc_handle = open ("/dev/rtc", O_RDONLY)) == -1 )
 {
  raydium_log("timecall: ERROR: /dev/rtc unavailable ! (chmod a+rx /dev/rtc ?)");
  perror("system");
//  exit(errno);
  return 0;
 }

 // ok, it's now open, so let's read actual rate
 if (ioctl(raydium_timecall_devrtc_handle, RTC_IRQP_READ, &freq) == -1 )
 {
  raydium_log("timecall: ERROR reading /dev/rtc rate");
  perror("system");
//  exit(errno);
  return 0;
 }
 raydium_log("timecall: /dev/rtc rate is %lu Hz",freq);

 if(freq<RAYDIUM_TIMECALL_FREQ_MIN)
  {
   raydium_log("timecall: /dev/rtc rate (%i Hz) too low (min: %i)",freq,RAYDIUM_TIMECALL_FREQ_MIN);
   if(!raydium_timecall_devrtc_rate_change(RAYDIUM_TIMECALL_FREQ_PREFERED)) return 0;
   else freq=RAYDIUM_TIMECALL_FREQ_PREFERED; // need to verify this new value ?
  }

 if(freq<RAYDIUM_TIMECALL_FREQ_PREFERED)
  {
   raydium_log("timecall: /dev/rtc rate (%i Hz) is low (prefered: %i)",freq,RAYDIUM_TIMECALL_FREQ_PREFERED);
   if(raydium_timecall_devrtc_rate_change(RAYDIUM_TIMECALL_FREQ_PREFERED))
      freq=RAYDIUM_TIMECALL_FREQ_PREFERED; // need to verify this new value ?
  }

    // Enable periodic interrupts
    if(ioctl(raydium_timecall_devrtc_handle, RTC_PIE_ON, 0) == -1)
    {
            raydium_log("timecall: ERROR enabling /dev/rtc periodic interrupts !");
            raydium_log("timecall: is /proc/sys/dev/rtc/max-user-freq allowing %lu Hz ?",freq);
            perror("system");
//            exit(errno);
            return 0;
    }

raydium_atexit(raydium_timecall_devrtc_close);
return freq;
#else
raydium_log("timecall: FAILED: /dev/rtc only available for Linux");
return 0;
#endif
#endif
}

/*
int raydium_timecall_detect_max_clock_frequency(void)
{
int i;
clock_t first,second;
float accu,max;

first=second=clock();
while(first==second)
{
i++;
second=clock();
}

accu=((second-first)/(float)CLOCKS_PER_SEC)*1000;
max=1.0/(accu/1000);
raydium_log("timecall: clock() accuracy = %.2f ms (%.2f Hz)",accu,max);
return (int)max;
}
*/

int raydium_timecall_detect_frequency(void)
{
int i=0;
unsigned long first,second;
float accu,max;

first=second=raydium_timecall_clock();
while(first==second)
{
i++;
second=raydium_timecall_clock();
}

raydium_log("timer: detection: %i iterations: diff: %li steps (%li/sec)",i,second-first,raydium_timecall_clocks_per_sec);
accu=((second-first)/(float)raydium_timecall_clocks_per_sec)*1000;
max=1.0/(accu/1000);
raydium_log("timecall: method accuracy = %.3f ms (%.2f Hz)",accu,max);
return (int)max;
}


void raydium_timecall_init(void)
{
int i;

#ifdef DEBUG_MOVIE
raydium_timecall_method=RAYDIUM_TIMECALL_METHOD_CLOCK;
raydium_timecall_debug_movie=0;
raydium_timecall_clocks_per_sec=2500;
raydium_timecall_max_frequency=2500;
raydium_log("timecall: USING 'MOVIE' TIME ! (%.2f FPS)",(float)DEBUG_MOVIE);
#else

unsigned long tmp;
#ifdef WIN32
LARGE_INTEGER t;
#endif

//default
raydium_timecall_method=RAYDIUM_TIMECALL_METHOD_CLOCK;
raydium_timecall_clocks_per_sec=__GETTIMEOFDAY_USEC;

#ifdef WIN32
raydium_timecall_w32_divmodulo=raydium_timecall_internal_w32_divmodulo_find();
QueryPerformanceFrequency(&t);
t.QuadPart>>=raydium_timecall_w32_divmodulo;
raydium_timecall_clocks_per_sec=t.LowPart;
#endif

raydium_timecall_max_frequency=raydium_timecall_detect_frequency();


if(raydium_timecall_max_frequency<RAYDIUM_TIMECALL_FREQ_PREFERED)
{
raydium_log("timecall: basic method accuracy is low , trying /dev/rtc ...");
tmp=raydium_timecall_devrtc_init();
if(tmp) {
        raydium_timecall_method=RAYDIUM_TIMECALL_METHOD_DEVRTC;
        raydium_timecall_max_frequency=tmp;
        }
}

if(raydium_timecall_method==RAYDIUM_TIMECALL_METHOD_CLOCK)
{

#ifdef WIN32
//QueryPerformanceFrequency(&t);
raydium_log("timecall: Using Win32 QueryPerformanceCounter method");
raydium_timecall_clocks_per_sec=t.LowPart;
//printf("%i %i %i\n",t.LowPart,t.HighPart,t.QuadPart);
#else
raydium_log("timecall: Using basic gettimeofday() method");
raydium_timecall_clocks_per_sec=__GETTIMEOFDAY_USEC;
#endif

}

if(raydium_timecall_method==RAYDIUM_TIMECALL_METHOD_DEVRTC)
{
raydium_log("timecall: Using /dev/rtc method");
raydium_timecall_clocks_per_sec=raydium_timecall_max_frequency;
//raydium_timecall_method_test();
}
#endif

raydium_timecall_index=0;
for(i=0;i<RAYDIUM_MAX_TIMECALLS;i++)
    {
    raydium_timecall_funct[i]=NULL;
    raydium_timecall_soft_call[i]=0;
    raydium_timecall_interval[i]=0;
    raydium_timecall_next[i]=0;
    }
raydium_timecall_offset=0;
raydium_timecall_stopped_stamp=0;
raydium_log("timecall: OK (%lu Hz)",raydium_timecall_max_frequency);
raydium_timecall_add(raydium_timecall_raydium,-1);
}


// Utility function only: No test is done here, please, secure your calls
void raydium_timecall_freq_change(int callback, GLint hz)
{
if(!hz)
 {
 raydium_log("timecall: WARNING ! 0 Hz callback (num %i)",callback);
 raydium_timecall_interval[callback]=0;
 }
else
 raydium_timecall_interval[callback]=raydium_timecall_clocks_per_sec/abs(hz);

raydium_timecall_soft_call[callback]=(hz<0);
raydium_timecall_next[callback]=raydium_timecall_clock();

if(abs(hz)>abs(raydium_timecall_max_frequency) && hz>0)
 raydium_log("timecall: WARNING ! this callback refresh rate (%i Hz) is probably too high for this system clock (detected at %i Hz)",hz,raydium_timecall_max_frequency);

if(hz>0)
 raydium_log("timecall: callback %i: %i Hz (%i clocks interval)",callback,hz,raydium_timecall_interval[callback]);

if(hz<0)
 raydium_log("timecall: softcall %i: %i Hz (%i clocks interval)",callback,-hz,raydium_timecall_interval[callback]);
}

int raydium_timecall_add(void *funct, GLint hz)
{
if(raydium_timecall_index>=RAYDIUM_MAX_TIMECALLS)
    {
    raydium_log("timecall: ERROR ! Too much timecalls, exiting.");
    exit(29);
    }

raydium_timecall_funct[raydium_timecall_index]=funct;
raydium_timecall_freq_change(raydium_timecall_index,hz);
return raydium_timecall_index++;
}



void raydium_timecall_callback(void)
{
int i,j,steps;
GLfloat stepsf;
unsigned long now,phase;
static unsigned long last;
void (*f)();
void (*ff)(GLfloat);

// workaround for time modulos
now=raydium_timecall_clock();
if(last>now)
    {
    raydium_log("timecall: warning: time modulo detected: workarounding");
    for(i=0;i<raydium_timecall_index;i++)
        if(!raydium_timecall_soft_call[i] &&
            raydium_timecall_interval[i])
            {
              // reset timecall next value
              raydium_timecall_next[i]=now+raydium_timecall_interval[i];
            }
    }
last=now;
// end of workaround


for(i=0;i<raydium_timecall_index;i++)
 {
 now=raydium_timecall_clock();

   if(!raydium_timecall_soft_call[i] && now>=raydium_timecall_next[i] && raydium_timecall_interval[i])
    {
    steps=((now-raydium_timecall_next[i])/raydium_timecall_interval[i])+1;
    phase=(now-raydium_timecall_next[i])-((steps-1)*raydium_timecall_interval[i]);
    //raydium_log("current phase overload for timecall %i: %i (total interval = %i",i,phase,raydium_timecall_interval[i]);
    raydium_timecall_next[i]=now+raydium_timecall_interval[i]-phase;

    if(steps>1000) { // DEBUG ! need to calculate this value
                    steps=100;
                    raydium_log("WARNING: timecall's too long");
                 }

//    raydium_log("debug: need %i steps",steps);
    f=raydium_timecall_funct[i];

    for(j=0;j<steps;j++)
     f();
    }
    else if(raydium_timecall_soft_call[i]) // this an "elastic-timed" callback
    {
     stepsf=(now-raydium_timecall_next[i])/(GLfloat)raydium_timecall_interval[i];
     ff=raydium_timecall_funct[i];
     raydium_timecall_next[i]=raydium_timecall_clock();
     //raydium_log("debug: soft call: step factor: %.2f",stepsf);

     ff(stepsf);
    }
 }
}

void raydium_timecall_stop(void)
{
if(raydium_timecall_stopped_stamp) 
    {
    raydium_log("timecall: time already stopped !");
    return;
    }
raydium_timecall_stopped_stamp=raydium_timecall_clock();
}

void raydium_timecall_start(void)
{
long long offset;
unsigned long stamp;

if(!raydium_timecall_stopped_stamp)
    {
    raydium_log("timecall: time was not stopped !");
    return;
    }

stamp=raydium_timecall_stopped_stamp;
raydium_timecall_stopped_stamp=0; // free timecall_clock()

offset=raydium_timecall_clock()-stamp;
if(offset<0) offset=0; // time modulo !
raydium_timecall_offset+=(unsigned long)offset;
raydium_timecall_stopped_stamp=0;
}