#include <stdio.h>

#include "SDL.h"
#include "SDL_atomic.h"
#include "SDL_assert.h"
#include "SDL_cpuinfo.h"

/*
  Absolutely basic tests just to see if we get the expected value
  after calling each function.
*/

static
char *
tf(SDL_bool tf)
{
    static char *t = "TRUE";
    static char *f = "FALSE";

    if (tf)
    {
       return t;
    }

    return f;
}

static
void RunBasicTest()
{
    int value;
    SDL_SpinLock lock = 0;

    SDL_atomic_t v;
    SDL_bool tfret = SDL_FALSE;

    printf("\nspin lock---------------------------------------\n\n");

    SDL_AtomicLock(&lock);
    printf("AtomicLock                   lock=%d\n", lock);
    SDL_AtomicUnlock(&lock);
    printf("AtomicUnlock                 lock=%d\n", lock);

    printf("\natomic -----------------------------------------\n\n");
     
    SDL_AtomicSet(&v, 0);
    tfret = SDL_AtomicSet(&v, 10) == 0;
    printf("AtomicSet(10)        tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));
    tfret = SDL_AtomicAdd(&v, 10) == 10;
    printf("AtomicAdd(10)        tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));

    SDL_AtomicSet(&v, 0);
    SDL_AtomicIncRef(&v);
    tfret = (SDL_AtomicGet(&v) == 1);
    printf("AtomicIncRef()       tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));
    SDL_AtomicIncRef(&v);
    tfret = (SDL_AtomicGet(&v) == 2);
    printf("AtomicIncRef()       tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));
    tfret = (SDL_AtomicDecRef(&v) == SDL_FALSE);
    printf("AtomicDecRef()       tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));
    tfret = (SDL_AtomicDecRef(&v) == SDL_TRUE);
    printf("AtomicDecRef()       tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));

    SDL_AtomicSet(&v, 10);
    tfret = (SDL_AtomicCAS(&v, 0, 20) == SDL_FALSE);
    printf("AtomicCAS()          tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));
    value = SDL_AtomicGet(&v);
    tfret = (SDL_AtomicCAS(&v, value, 20) == SDL_TRUE);
    printf("AtomicCAS()          tfret=%s val=%d\n", tf(tfret), SDL_AtomicGet(&v));
}

/**************************************************************************/
/* Atomic operation test
 * Adapted with permission from code by Michael Davidsaver at:
 *  http://bazaar.launchpad.net/~mdavidsaver/epics-base/atomic/revision/12105#src/libCom/test/epicsAtomicTest.c
 * Original copyright 2010 Brookhaven Science Associates as operator of Brookhaven National Lab
 * http://www.aps.anl.gov/epics/license/open.php
 */

/* Tests semantics of atomic operations.  Also a stress test
 * to see if they are really atomic.
 *
 * Serveral threads adding to the same variable.
 * at the end the value is compared with the expected
 * and with a non-atomic counter.
 */
 
/* Number of concurrent incrementers */
#define NThreads 2
#define CountInc 100
#define VALBITS (sizeof(atomicValue)*8)
 
#define atomicValue int
#define CountTo ((atomicValue)((unsigned int)(1<<(VALBITS-1))-1))
#define NInter (CountTo/CountInc/NThreads)
#define Expect (CountTo-NInter*CountInc*NThreads)
 
SDL_COMPILE_TIME_ASSERT(size, CountTo>0); /* check for rollover */
 
static SDL_atomic_t good = { 42 };
 
static atomicValue bad = 42;
 
static SDL_atomic_t threadsRunning;

static SDL_sem *threadDone;
 
static
int adder(void* junk)
{
    unsigned long N=NInter;
    printf("Thread subtracting %d %lu times\n",CountInc,N);
    while (N--) {
        SDL_AtomicAdd(&good, -CountInc);
        bad-=CountInc;
    }
    SDL_AtomicAdd(&threadsRunning, -1);
    SDL_SemPost(threadDone);
    return 0;
}
 
static
void runAdder(void)
{
    Uint32 start, end;
    int T=NThreads;
 
    start = SDL_GetTicks();
 
    threadDone = SDL_CreateSemaphore(0);

    SDL_AtomicSet(&threadsRunning, NThreads);

    while (T--)
        SDL_CreateThread(adder, NULL);
 
    while (SDL_AtomicGet(&threadsRunning) > 0)
        SDL_SemWait(threadDone);
 
    SDL_DestroySemaphore(threadDone);

    end = SDL_GetTicks();
 
    printf("Finished in %f sec\n", (end - start) / 1000.f);
}
 
static
void RunEpicTest()
{
    int b;
    atomicValue v;
 
    printf("\nepic test---------------------------------------\n\n");

    printf("Size asserted to be >= 32-bit\n");
    SDL_assert(sizeof(atomicValue)>=4);
 
    printf("Check static initializer\n");
    v=SDL_AtomicGet(&good);
    SDL_assert(v==42);
 
    SDL_assert(bad==42);
 
    printf("Test negative values\n");
    SDL_AtomicSet(&good, -5);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==-5);
 
    printf("Verify maximum value\n");
    SDL_AtomicSet(&good, CountTo);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==CountTo);
 
    printf("Test compare and exchange\n");
 
    b=SDL_AtomicCAS(&good, 500, 43);
    SDL_assert(!b); /* no swap since CountTo!=500 */
    v=SDL_AtomicGet(&good);
    SDL_assert(v==CountTo); /* ensure no swap */
 
    b=SDL_AtomicCAS(&good, CountTo, 44);
    SDL_assert(!!b); /* will swap */
    v=SDL_AtomicGet(&good);
    SDL_assert(v==44);
 
    printf("Test Add\n");
 
    v=SDL_AtomicAdd(&good, 1);
    SDL_assert(v==44);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==45);
 
    v=SDL_AtomicAdd(&good, 10);
    SDL_assert(v==45);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==55);
 
    printf("Test Add (Negative values)\n");
 
    v=SDL_AtomicAdd(&good, -20);
    SDL_assert(v==55);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==35);
 
    v=SDL_AtomicAdd(&good, -50); /* crossing zero down */
    SDL_assert(v==35);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==-15);
 
    v=SDL_AtomicAdd(&good, 30); /* crossing zero up */
    SDL_assert(v==-15);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==15);
 
    printf("Reset before count down test\n");
    SDL_AtomicSet(&good, CountTo);
    v=SDL_AtomicGet(&good);
    SDL_assert(v==CountTo);
 
    bad=CountTo;
    SDL_assert(bad==CountTo);
 
    printf("Counting down from %d, Expect %d remaining\n",CountTo,Expect);
    runAdder();
 
    v=SDL_AtomicGet(&good);
    printf("Atomic %d Non-Atomic %d\n",v,bad);
    SDL_assert(v==Expect);
    SDL_assert(bad!=Expect);
}

/* End atomic operation test */
/**************************************************************************/

/**************************************************************************/
/* Lock-free FIFO test */

/* This is useful to test the impact of another thread locking the queue
   entirely for heavy-weight manipulation.
 */
#define TEST_SPINLOCK_FIFO

#define NUM_READERS 4
#define NUM_WRITERS 4
#define EVENTS_PER_WRITER   1000000

/* The number of entries must be a power of 2 */
#define MAX_ENTRIES 256
#define WRAP_MASK   (MAX_ENTRIES-1)

typedef struct
{
    SDL_atomic_t sequence;
    SDL_Event event;
} SDL_EventQueueEntry;

typedef struct
{
    SDL_EventQueueEntry entries[MAX_ENTRIES];

    char cache_pad1[SDL_CACHELINE_SIZE-((sizeof(SDL_EventQueueEntry)*MAX_ENTRIES)%SDL_CACHELINE_SIZE)];

    SDL_atomic_t enqueue_pos;

    char cache_pad2[SDL_CACHELINE_SIZE-sizeof(SDL_atomic_t)];

    SDL_atomic_t dequeue_pos;

    char cache_pad3[SDL_CACHELINE_SIZE-sizeof(SDL_atomic_t)];

#ifdef TEST_SPINLOCK_FIFO
    SDL_SpinLock lock;
    SDL_atomic_t rwcount;
    SDL_atomic_t watcher;

    char cache_pad4[SDL_CACHELINE_SIZE-sizeof(SDL_SpinLock)-2*sizeof(SDL_atomic_t)];
#endif

    volatile SDL_bool active;

    /* Only needed for the mutex test */
    SDL_mutex *mutex;

} SDL_EventQueue;

static void InitEventQueue(SDL_EventQueue *queue)
{
    int i;

    for (i = 0; i < MAX_ENTRIES; ++i) {
        SDL_AtomicSet(&queue->entries[i].sequence, i);
    }
    SDL_AtomicSet(&queue->enqueue_pos, 0);
    SDL_AtomicSet(&queue->dequeue_pos, 0);
#ifdef TEST_SPINLOCK_FIFO
    queue->lock = 0;
    SDL_AtomicSet(&queue->rwcount, 0);
#endif
    queue->active = SDL_TRUE;
}

static SDL_bool EnqueueEvent_LockFree(SDL_EventQueue *queue, const SDL_Event *event)
{
    SDL_EventQueueEntry *entry;
    unsigned queue_pos;
    unsigned entry_seq;
    int delta;
    SDL_bool status;

#ifdef TEST_SPINLOCK_FIFO
    /* This is a gate so an external thread can lock the queue */
    SDL_AtomicLock(&queue->lock);
    SDL_assert(SDL_AtomicGet(&queue->watcher) == 0);
    SDL_AtomicIncRef(&queue->rwcount);
    SDL_AtomicUnlock(&queue->lock);
#endif

    queue_pos = (unsigned)SDL_AtomicGet(&queue->enqueue_pos);
    for ( ; ; ) {
        entry = &queue->entries[queue_pos & WRAP_MASK];
        entry_seq = (unsigned)SDL_AtomicGet(&entry->sequence);

        delta = (int)(entry_seq - queue_pos);
        if (delta == 0) {
            /* The entry and the queue position match, try to increment the queue position */
            if (SDL_AtomicCAS(&queue->enqueue_pos, (int)queue_pos, (int)(queue_pos+1))) {
                /* We own the object, fill it! */
                entry->event = *event;
                SDL_AtomicSet(&entry->sequence, (int)(queue_pos + 1));
                status = SDL_TRUE;
                break;
            }
        } else if (delta < 0) {
            /* We ran into an old queue entry, which means it still needs to be dequeued */
            status = SDL_FALSE;
            break;
        } else {
            /* We ran into a new queue entry, get the new queue position */
            queue_pos = (unsigned)SDL_AtomicGet(&queue->enqueue_pos);
        }
    }

#ifdef TEST_SPINLOCK_FIFO
    SDL_AtomicDecRef(&queue->rwcount);
#endif
    return status;
}

static SDL_bool DequeueEvent_LockFree(SDL_EventQueue *queue, SDL_Event *event)
{
    SDL_EventQueueEntry *entry;
    unsigned queue_pos;
    unsigned entry_seq;
    int delta;
    SDL_bool status;

#ifdef TEST_SPINLOCK_FIFO
    /* This is a gate so an external thread can lock the queue */
    SDL_AtomicLock(&queue->lock);
    SDL_assert(SDL_AtomicGet(&queue->watcher) == 0);
    SDL_AtomicIncRef(&queue->rwcount);
    SDL_AtomicUnlock(&queue->lock);
#endif

    queue_pos = (unsigned)SDL_AtomicGet(&queue->dequeue_pos);
    for ( ; ; ) {
        entry = &queue->entries[queue_pos & WRAP_MASK];
        entry_seq = (unsigned)SDL_AtomicGet(&entry->sequence);

        delta = (int)(entry_seq - (queue_pos + 1));
        if (delta == 0) {
            /* The entry and the queue position match, try to increment the queue position */
            if (SDL_AtomicCAS(&queue->dequeue_pos, (int)queue_pos, (int)(queue_pos+1))) {
                /* We own the object, fill it! */
                *event = entry->event;
                SDL_AtomicSet(&entry->sequence, (int)(queue_pos+MAX_ENTRIES));
                status = SDL_TRUE;
                break;
            }
        } else if (delta < 0) {
            /* We ran into an old queue entry, which means we've hit empty */
            status = SDL_FALSE;
            break;
        } else {
            /* We ran into a new queue entry, get the new queue position */
            queue_pos = (unsigned)SDL_AtomicGet(&queue->dequeue_pos);
        }
    }

#ifdef TEST_SPINLOCK_FIFO
    SDL_AtomicDecRef(&queue->rwcount);
#endif
    return status;
}

static SDL_bool EnqueueEvent_Mutex(SDL_EventQueue *queue, const SDL_Event *event)
{
    SDL_EventQueueEntry *entry;
    unsigned queue_pos;
    unsigned entry_seq;
    int delta;
    SDL_bool status = SDL_FALSE;

    SDL_mutexP(queue->mutex);

    queue_pos = (unsigned)queue->enqueue_pos.value;
    entry = &queue->entries[queue_pos & WRAP_MASK];
    entry_seq = (unsigned)entry->sequence.value;

    delta = (int)(entry_seq - queue_pos);
    if (delta == 0) {
        ++queue->enqueue_pos.value;

        /* We own the object, fill it! */
        entry->event = *event;
        entry->sequence.value = (int)(queue_pos + 1);
        status = SDL_TRUE;
    } else if (delta < 0) {
        /* We ran into an old queue entry, which means it still needs to be dequeued */
    } else {
        printf("ERROR: mutex failed!\n");
    }

    SDL_mutexV(queue->mutex);

    return status;
}

static SDL_bool DequeueEvent_Mutex(SDL_EventQueue *queue, SDL_Event *event)
{
    SDL_EventQueueEntry *entry;
    unsigned queue_pos;
    unsigned entry_seq;
    int delta;
    SDL_bool status = SDL_FALSE;

    SDL_mutexP(queue->mutex);

    queue_pos = (unsigned)queue->dequeue_pos.value;
    entry = &queue->entries[queue_pos & WRAP_MASK];
    entry_seq = (unsigned)entry->sequence.value;

    delta = (int)(entry_seq - (queue_pos + 1));
    if (delta == 0) {
        ++queue->dequeue_pos.value;

        /* We own the object, fill it! */
        *event = entry->event;
        entry->sequence.value = (int)(queue_pos + MAX_ENTRIES);
        status = SDL_TRUE;
    } else if (delta < 0) {
        /* We ran into an old queue entry, which means we've hit empty */
    } else {
        printf("ERROR: mutex failed!\n");
    }

    SDL_mutexV(queue->mutex);

    return status;
}

static SDL_sem *writersDone;
static SDL_sem *readersDone;
static SDL_atomic_t writersRunning;
static SDL_atomic_t readersRunning;

typedef struct
{
    SDL_EventQueue *queue;
    int index;
    char padding1[SDL_CACHELINE_SIZE-(sizeof(SDL_EventQueue*)+sizeof(int))%SDL_CACHELINE_SIZE];
    int waits;
    SDL_bool lock_free;
    char padding2[SDL_CACHELINE_SIZE-sizeof(int)-sizeof(SDL_bool)];
} WriterData;

typedef struct
{
    SDL_EventQueue *queue;
    int counters[NUM_WRITERS];
    int waits;
    SDL_bool lock_free;
    char padding[SDL_CACHELINE_SIZE-(sizeof(SDL_EventQueue*)+sizeof(int)*NUM_WRITERS+sizeof(int)+sizeof(SDL_bool))%SDL_CACHELINE_SIZE];
} ReaderData;

static int FIFO_Writer(void* _data)
{
    WriterData *data = (WriterData *)_data;
    SDL_EventQueue *queue = data->queue;
    int index = data->index;
    int i;
    SDL_Event event;

    event.type = SDL_USEREVENT;
    event.user.windowID = 0;
    event.user.code = 0;
    event.user.data1 = data;
    event.user.data2 = NULL;

    if (data->lock_free) {
        for (i = 0; i < EVENTS_PER_WRITER; ++i) {
            event.user.code = i;
            while (!EnqueueEvent_LockFree(queue, &event)) {
                ++data->waits;
                SDL_Delay(0);
            }
        }
    } else {
        for (i = 0; i < EVENTS_PER_WRITER; ++i) {
            event.user.code = i;
            while (!EnqueueEvent_Mutex(queue, &event)) {
                ++data->waits;
                SDL_Delay(0);
            }
        }
    }
    SDL_AtomicAdd(&writersRunning, -1);
    SDL_SemPost(writersDone);
    return 0;
}

static int FIFO_Reader(void* _data)
{
    ReaderData *data = (ReaderData *)_data;
    SDL_EventQueue *queue = data->queue;
    SDL_Event event;

    if (data->lock_free) {
        for ( ; ; ) {
            if (DequeueEvent_LockFree(queue, &event)) {
                WriterData *writer = (WriterData*)event.user.data1;
                ++data->counters[writer->index];
            } else if (queue->active) {
                ++data->waits;
                SDL_Delay(0);
            } else {
                /* We drained the queue, we're done! */
                break;
            }
        }
    } else {
        for ( ; ; ) {
            if (DequeueEvent_Mutex(queue, &event)) {
                WriterData *writer = (WriterData*)event.user.data1;
                ++data->counters[writer->index];
            } else if (queue->active) {
                ++data->waits;
                SDL_Delay(0);
            } else {
                /* We drained the queue, we're done! */
                break;
            }
        }
    }
    SDL_AtomicAdd(&readersRunning, -1);
    SDL_SemPost(readersDone);
    return 0;
}

#ifdef TEST_SPINLOCK_FIFO
/* This thread periodically locks the queue for no particular reason */
static int FIFO_Watcher(void* _data)
{
    SDL_EventQueue *queue = (SDL_EventQueue *)_data;

    while (queue->active) {
        SDL_AtomicLock(&queue->lock);
        SDL_AtomicIncRef(&queue->watcher);
        while (SDL_AtomicGet(&queue->rwcount) > 0) {
            SDL_Delay(0);
        }
        /* Do queue manipulation here... */
        SDL_AtomicDecRef(&queue->watcher);
        SDL_AtomicUnlock(&queue->lock);

        /* Wait a bit... */
        SDL_Delay(1);
    }
    return 0;
}
#endif /* TEST_SPINLOCK_FIFO */

static void RunFIFOTest(SDL_bool lock_free)
{
    SDL_EventQueue queue;
    WriterData writerData[NUM_WRITERS];
    ReaderData readerData[NUM_READERS];
    Uint32 start, end;
    int i, j;
    int grand_total;
 
    printf("\nFIFO test---------------------------------------\n\n");
    printf("Mode: %s\n", lock_free ? "LockFree" : "Mutex");

    readersDone = SDL_CreateSemaphore(0);
    writersDone = SDL_CreateSemaphore(0);

    SDL_memset(&queue, 0xff, sizeof(queue));

    InitEventQueue(&queue);
    if (!lock_free) {
        queue.mutex = SDL_CreateMutex();
    }

    start = SDL_GetTicks();
 
#ifdef TEST_SPINLOCK_FIFO
    /* Start a monitoring thread */
    if (lock_free) {
        SDL_CreateThread(FIFO_Watcher, &queue);
    }
#endif

    /* Start the readers first */
    printf("Starting %d readers\n", NUM_READERS);
    SDL_zero(readerData);
    SDL_AtomicSet(&readersRunning, NUM_READERS);
    for (i = 0; i < NUM_READERS; ++i) {
        readerData[i].queue = &queue;
        readerData[i].lock_free = lock_free;
        SDL_CreateThread(FIFO_Reader, &readerData[i]);
    }

    /* Start up the writers */
    printf("Starting %d writers\n", NUM_WRITERS);
    SDL_zero(writerData);
    SDL_AtomicSet(&writersRunning, NUM_WRITERS);
    for (i = 0; i < NUM_WRITERS; ++i) {
        writerData[i].queue = &queue;
        writerData[i].index = i;
        writerData[i].lock_free = lock_free;
        SDL_CreateThread(FIFO_Writer, &writerData[i]);
    }
 
    /* Wait for the writers */
    while (SDL_AtomicGet(&writersRunning) > 0) {
        SDL_SemWait(writersDone);
    }
 
    /* Shut down the queue so readers exit */
    queue.active = SDL_FALSE;

    /* Wait for the readers */
    while (SDL_AtomicGet(&readersRunning) > 0) {
        SDL_SemWait(readersDone);
    }

    end = SDL_GetTicks();
 
    SDL_DestroySemaphore(readersDone);
    SDL_DestroySemaphore(writersDone);

    if (!lock_free) {
        SDL_DestroyMutex(queue.mutex);
    }
 
    printf("Finished in %f sec\n", (end - start) / 1000.f);

    printf("\n");
    for (i = 0; i < NUM_WRITERS; ++i) {
        printf("Writer %d wrote %d events, had %d waits\n", i, EVENTS_PER_WRITER, writerData[i].waits);
    }
    printf("Writers wrote %d total events\n", NUM_WRITERS*EVENTS_PER_WRITER);

    /* Print a breakdown of which readers read messages from which writer */
    printf("\n");
    grand_total = 0;
    for (i = 0; i < NUM_READERS; ++i) {
        int total = 0;
        for (j = 0; j < NUM_WRITERS; ++j) {
            total += readerData[i].counters[j];
        }
        grand_total += total;
        printf("Reader %d read %d events, had %d waits\n", i, total, readerData[i].waits);
        printf("  { ");
        for (j = 0; j < NUM_WRITERS; ++j) {
            if (j > 0) {
                printf(", ");
            }
            printf("%d", readerData[i].counters[j]);
        }
        printf(" }\n");
    }
    printf("Readers read %d total events\n", grand_total);
}

/* End FIFO test */
/**************************************************************************/

int
main(int argc, char *argv[])
{
    RunBasicTest();
    RunEpicTest();
/* This test is really slow, so don't run it by default */
#if 0
    RunFIFOTest(SDL_FALSE);
#endif
    RunFIFOTest(SDL_TRUE);
    return 0;
}

/* vi: set ts=4 sw=4 expandtab: */
