#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include <math.h>
#include "fifo.h"

typedef struct __safe_random_generator {
    double x;
    double a;
} safe_random_generator_t;

typedef struct __thread_data {
    safe_random_generator_t srg;
    int nb_elems;
} thread_data_t;

/* The FIFO will be global as all threads need to have access to it */
fifo_t* global_fifo = NULL;

/**
 * Generate a thread safe random number with a period of 2^44.
 * Good accuracy, speed.
 */
double thread_safe_random( safe_random_generator_t* srg )
{
    long long Lx, La, a1, a2, x1, x2, xa;
    const double d2m46 = pow(0.5, 46);

    Lx = srg->x;
    La = srg->a;
    a1 = (La >> 23) & ((1ULL << 24) - 1);
    a2 = La & ((1ULL << 24) - 1);
    x1 = (Lx >> 23) & ((1ULL << 24) - 1);
    x2 = Lx & ((1ULL << 24) - 1);
    xa = a1 * x2 + a2 * x1;
    xa = (xa & ((1ULL << 24) - 1)) + a2 * x2;
    Lx = xa & ((1ULL << 47) - 1);
    srg->x = (double)Lx;
    return d2m46 * (srg->x);
}

/**
 * The main thread function. Each thread will execute this
 * function based on a different set of arguments.
 */
void* thread_function( void* args )
{
    thread_data_t* td = (thread_data_t*)args;
    int nb_elems = td->nb_elems;
    int nb_push = nb_elems, nb_pop = nb_elems;
    long rnd;
    fifo_elem_t* elem;

    while( 0 != (nb_push + nb_pop) ) {
        /* get a thread safe random number to base our action on */
        rnd = (long)thread_safe_random( &(td->srg) );
        
        /* Do a push if there is still room ... */
        if( 1 == (rnd % 3) ) {
        force_push:
            if( nb_push > 0 ) {
                nb_push--;
                elem = malloc( sizeof(fifo_elem_t) );
                fifo_push( global_fifo, elem );
                continue;
            }
        }
        /* otherwise do a pop */
        if( nb_pop > 0 ) {
            nb_pop--;
            elem = fifo_pop( global_fifo );
            free( elem );
        } else {
            /* force the push if no pop possible */
            goto force_push;
        }
    }
    return NULL;
}

#define DELAY(t1, t2) ((t2.tv_sec - t1.tv_sec)*1000000 + (t2.tv_usec - t1.tv_usec))

int main( int argc, char* argv[] )
{
    int i, num_threads = 1, rc, num_elems = 100000;
    pthread_t* thread_ids;
    struct timeval start_time, thread_creation_time, complete_time;
    thread_data_t* td;

    /* Create the global fifo */
    global_fifo = fifo_create();

    thread_ids = (pthread_t*)malloc( sizeof(pthread_t) * num_threads );
    td = (thread_data_t*)malloc( sizeof(thread_data_t) * num_threads );
    gettimeofday( &start_time, NULL );
    for( i = 0; i < num_threads; i++ ) {
        td[i].srg.x = (double)random();
        td[i].srg.a = (double)random();
        td[i].nb_elems = num_elems;

        rc = pthread_create( &thread_ids[i], NULL, thread_function,
                             (void*)&(td[i]) );
        if( 0 != rc  ) {
            printf( "Failed to create a thread... Prepare to give up !\n" );
            break;
        }
    }

    gettimeofday( &thread_creation_time, NULL );
    for( i--; i >= 0; i-- ) {
        void* return_value;
        pthread_join( thread_ids[i], &return_value );
    }
    gettimeofday( &complete_time, NULL );
    free( thread_ids );

    /* Destroy the global fifo */
    fifo_destroy( &global_fifo );

    printf( "Threads created in %ld micro-seconds\n",
            DELAY( start_time, thread_creation_time ) );
    printf( "Total execution time %ld micro-seconds\n",
            DELAY( start_time, complete_time ) );

    return 0;
}


