#include <thread>
#include <string>
#include <iostream>
#include <queue>
#include <random>
#include <chrono>
#include <mutex>
#include <condition_variable>

/*
 * Compile with 'g++ -pthread a.cpp'
 */

using namespace std;

std::mutex q_mutex;
std::mutex cout_mutex;
std::condition_variable q_cv;

void producer1( queue<int>& q, const int id ) {
	// Make sure our cout does not cross some other thread's cout
	cout_mutex.lock();
	cout << "======================" << endl;
	cout << " Starting Producer " << id << endl;	
	cout << "======================" << endl;
	cout_mutex.unlock();

	random_device d;
	mt19937 mt(d());
	int t_max = 3000;
	uniform_int_distribution<> distr(0., t_max);

	int n = 0;

	while (true) {
		n = distr(mt);
		this_thread::sleep_for(chrono::milliseconds(n));

		// Make sure our cout does not cross some other thread's cout
		cout_mutex.lock();
		cout << "Producer " << id << " : " << n << endl;
		cout_mutex.unlock();
		
		// Lock q so we can push a new element
		std::unique_lock<std::mutex> q_lock(q_mutex);
		q.push(n);

		// Notify one of the consumers
		q_cv.notify_one();
	}
}

void consumer( queue<int>& q, const int id ) {
	// Make sure our cout does not cross some other thread's cout
	cout_mutex.lock();
	cout << "======================" << endl;
	cout << " Starting Consumer " << id << endl;
	cout << "======================" << endl;
	cout_mutex.unlock();

	int b = 0;
	while ( true ) {
		{ // Scope the lock of the queue
			std::unique_lock<std::mutex> q_lock(q_mutex);

			while ( q.empty() ) {
				// Wait until we get a signal from the condition variable
				q_cv.wait(q_lock);
			}

			// Get a value and pop it off
			b = q.front(); 
			q.pop();
		}	

		// Make sure our cout does not cross some other thread's cout
		cout_mutex.lock();
		cout << " -- Consumer " << id << " : " << b << endl;
		cout_mutex.unlock();

		// Slow down this thread to have a race between the consumers and the producers
		// This only works after we have successfully read an int
		this_thread::sleep_for(chrono::milliseconds(2000));
	}
}

int main() {

	queue<int> q;

	
	thread cons1( consumer, std::ref(q), 1 )  ;

	// make sure the consumer must have slept at least once
	this_thread::sleep_for(chrono::seconds(2));

	thread prod( producer1, std::ref(q), 1);

	this_thread::sleep_for(chrono::seconds(5));
	thread cons2( consumer, std::ref(q), 2)  ;


	prod.join();
	cons1.join();
	cons2.join();
	cout << "Finishing up" << endl;

	return 0;
}
