#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <atomic>

std::mutex m;
std::condition_variable cv;
std::string data;
std::atomic<bool> ready( false );
//bool ready = false;
bool processed = false;

void worker_thread1()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk( m );
    cv.wait( lk, [] {return ready==true;} );
    // after the wait, we own the lock.
    std::cout << "Worker 1 thread is processing data\n";
    data += " after processing";
    // Send data back to main()
    processed = true;
    ready = false;
    std::cout << "Worker thread signals data processing completed\n";
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}

void worker_thread2()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk( m );
    //cv.wait(lk, []{return ready==true;});
    if( !cv.wait_for( lk, std::chrono::milliseconds( 100 ), [] {return ready==true;} ) ) {
        std::cout << "Worker 2 thread gave up\n";
        return;
    }
    // after the wait, we own the lock.
    std::cout << "Worker 2 thread is processing data\n";
    data += " after processing";
    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}

int main()
{
    std::thread worker1( worker_thread1 );
    std::thread worker2( worker_thread2 );
    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk( m );
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
    // wait for the worker
    {
        std::unique_lock<std::mutex> lk( m );
        cv.wait( lk, [] {return processed;} );
    }
    std::cout << "Back in main(), data = " << data << '\n';
    worker1.join();
    worker2.join();
}

// with notify_one:
// depending which thread passes through the lock first result will be
//
// main() signals data ready for processing
// Worker 1 thread is processing data
// Worker thread signals data processing completed
// Back in main(), data = Example data after processing
// Worker 2 thread gave up
// 
// main() signals data ready for processing
// Worker 2 thread is processing data
// Worker thread signals data processing completed
// Worker 1 thread is processing data
// Worker thread signals data processing completed
// Back in main(), data = Example data after processing after processing
// 
// Actualy there is a third outcome possible (but rare) - deadlock
// Exercise: find conditions that lead to a deadlock