#include <iostream>
#include <pthread.h>
#include <sched.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>

using namespace std;

const int maxWaiterThreadsCount = 30;
const bool releaseMutexBetweenSignals = false;
const int waitTimeSec = 15;

bool exitProgram;
int threadsCount;
int beforeWaitersCount;
int afterWaitersCount;
int signalsSentCount;
int beforeWaitersWokenCount;
int afterWaitersWokenCount;
int iterationCount;

pthread_mutex_t mutex;
pthread_cond_t testCond;

void* ThreadMain(void* data)
{
    // Wait for the main thread to create all waiter threads
    pthread_mutex_lock(&mutex);

    while (!exitProgram) {
        // 1. Remember which is the current test iteration
        int currIterationCount = iterationCount;

        // 2. Check if we are starting the wait before all signals have been sent.
        bool beforeAllSignalsSent = signalsSentCount < threadsCount;

        // 3. Count the thread towards the appropriate group of waiters (before/after
        //    all signals are sent)
        if (beforeAllSignalsSent) {
            ++beforeWaitersCount;
        } else {
            ++afterWaitersCount;
        }

        // 4. Wait on the condition variable.
        pthread_cond_wait(&testCond, &mutex);

        // 5. If the iteration count has changed, start over
        if (currIterationCount != iterationCount) {
            continue;
        }

        // 6. Count the woken thread towards the appropriate group of woken threads (the
        //    same group as step 2).
        if (beforeAllSignalsSent) {
            ++beforeWaitersWokenCount;
        } else {
            ++afterWaitersWokenCount;
        }
   }
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

int main()
{
// 1. Test setup:
    // 1.1. Initialize the mutex and the condition variable
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&testCond, NULL);

    // 1.2.

    exitProgram = false;

    // 1.3. Create some waiter threads. All waiters will imediately block on the mutex
    pthread_mutex_lock(&mutex);

    for (threadsCount = 0; threadsCount < maxWaiterThreadsCount; ++threadsCount) {
        pthread_t thread;

        if (pthread_create(&thread, NULL, ThreadMain, NULL) != 0) {
            break;
        }

        pthread_detach(thread);
    }

    cout << "Created " << threadsCount << " threads." << endl;

// 2. Test body. Count the test body iterations to help waiters detect when a
//    new iteration has started
    for (iterationCount = 0; !exitProgram; ++iterationCount) {
        // 2.1. Reset the counters
        beforeWaitersCount = 0;
        afterWaitersCount = 0;
        beforeWaitersWokenCount = 0;
        afterWaitersWokenCount = 0;
        signalsSentCount = 0;

        // 2.2. Wait for all waiters to block on the condition variable
        while (beforeWaitersCount < threadsCount) {
            pthread_mutex_unlock(&mutex);
            sched_yield();
            pthread_mutex_lock(&mutex);
        }

        // 2.3. Send as many inidividual signals as there are waiter threads
        for (; signalsSentCount < threadsCount; ++signalsSentCount) {
            pthread_cond_signal(&testCond);

            if (releaseMutexBetweenSignals) {
                pthread_mutex_unlock(&mutex);
                sched_yield();
                pthread_mutex_lock(&mutex);
            }
        }

        // 2.4. Wait for at least two threads to block again after the signals were
        //      sent. But if we were releasing the mutex between signals it is possible
        //      that too much threads woke up and reentered the wait so we need to make
        //      sure that there are enough threads that havent woken yet.
        if (signalsSentCount - beforeWaitersWokenCount > 2) {
          while (afterWaitersCount < 2) {
              pthread_mutex_unlock(&mutex);
              sched_yield();
              pthread_mutex_lock(&mutex);
          }
        }

        // 2.5. Send a single signal
        pthread_cond_signal(&testCond);


        // 2.6. Wait for a predefined time for at least signalsSentCount 'before'
        //      waiters to wake up.
        time_t endTime;
        time_t startTime = time(&endTime);
        while (beforeWaitersWokenCount < signalsSentCount) {
            pthread_mutex_unlock(&mutex);
            sched_yield();
            pthread_mutex_lock(&mutex);

            if (time(&endTime) - startTime > waitTimeSec) {
                break;
            }
        }

        // 2.7. If some threads that should have been unblocked by the first
        //      signalsSentCount signals have remained blocked, report that the
        //      race was hit
        if (beforeWaitersWokenCount < signalsSentCount) {
            cout << "Race hit." << endl
                 << "\tWaited:\t\t" << endTime - startTime << "s" << endl
                 << "\tFailed to wake\t: " << signalsSentCount - beforeWaitersWokenCount << endl
                 << "\tExtra woken:\t" << afterWaitersWokenCount - 1 << endl;
            // Notify all waiters that they should exit
            exitProgram = true;
        } else {
            cout << "Race not hit." << endl;
        }

        // 2.8. Send a broadcast to let all waiters move to the start of the next
        //      test iteration or exit
        pthread_cond_broadcast(&testCond);
    }

    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}
