/********************************************************************
 * Copyright (C) 2018 by UCBL                                       *
 * Initial author: Matthieu Moy                                     *
 ********************************************************************/

#include <thread>
#include <mutex>
#include <iostream>
#include <vector>
#include <condition_variable>
#include <unistd.h>
using namespace std;

#define NB_ROUNDS 10
#define NB_CARS   10

/* Pick a version here (Miralonde_Starve or Miralonde_Starve_Free) */
#define Miralonde Miralonde_Starve_Free

/**
 * Simple version, but one direction may starve if cars come often
 * enough in the other direction.
 */
class Miralonde_Starve {
public:
	void EntreeNS() {
		m.lock();
		cout << "EntreeNS: request" << endl;
		while (nb_cars_SN != 0) {
			c.wait(m);
		}
		nb_cars_NS++;
		cout << "EntreeNS: pass" << endl;
		m.unlock();
	}
	void EntreeSN() {
		m.lock();
		cout << "                    EntreeSN: request" << endl;
		while (nb_cars_NS != 0) {
			c.wait(m);
		}
		nb_cars_SN++;
		cout << "                    EntreeSN: pass" << endl;
		m.unlock();
	}
	void SortieNS() {
		m.lock();
		cout << "SortieNS" << endl;
		nb_cars_NS--;
		c.notify_all();
		m.unlock();
	}
	void SortieSN() {
		m.lock();
		cout << "                    SortieSN" << endl;
		nb_cars_SN--;
		c.notify_all();
		m.unlock();
	}
private:
	int nb_cars_NS = 0;
	int nb_cars_SN = 0;
	mutex m;
	condition_variable_any c;
};

/**
 * Starvation-free version of Miralonde.
 *
 * Cars can go if:
 * - nobody takes the other direction
 * - and :
 *     - nobody's waiting on the other side, or
 *     - previous car went the oposite way
 */
class Miralonde_Starve_Free {
public:
	void EntreeNS() {
		m.lock();
		cout << "EntreeNS: request" << endl;
		nb_cars_waiting_N++;
		while (not(nb_cars_SN == 0 &&
			   (nb_cars_waiting_S == 0 ||
			    current_direction == SN))) {
			c.wait(m);
		}
		nb_cars_waiting_N--;
		nb_cars_NS++;
		current_direction = NS;
		cout << "EntreeNS: pass" << endl;
		m.unlock();
	}
	void EntreeSN() {
		m.lock();
		cout << "                    EntreeSN: request" << endl;
		nb_cars_waiting_S++;
		while (not(nb_cars_NS == 0 &&
			   (nb_cars_waiting_N == 0 ||
			    current_direction == NS))) {
			c.wait(m);
		}
		nb_cars_waiting_S--;
		nb_cars_SN++;
		cout << "                    EntreeSN: pass" << endl;
		current_direction = SN;
		m.unlock();
	}
	void SortieNS() {
		m.lock();
		cout << "SortieNS" << endl;
		nb_cars_NS--;
		c.notify_all();
		m.unlock();
	}
	void SortieSN() {
		m.lock();
		cout << "                    SortieSN" << endl;
		nb_cars_SN--;
		c.notify_all();
		m.unlock();
	}
private:
	enum direction {
		NS,
		SN,
	};
	direction current_direction = NS;
	int nb_cars_waiting_N = 0;
	int nb_cars_waiting_S = 0;
	int nb_cars_NS = 0;
	int nb_cars_SN = 0;
	mutex m;
	condition_variable_any c;
};


/***** Tests *****/

void car_thread(Miralonde *m) {
	for (int i = 0; i < NB_ROUNDS; i++) {
		if (rand() % 2 == 0) usleep(100);
		m->EntreeNS();
		if (rand() % 2 == 0) usleep(100);
		m->SortieNS();
		if (rand() % 2 == 0) usleep(100);
		m->EntreeSN();
		if (rand() % 2 == 0) usleep(100);
		m->SortieSN();
	}
}

void car_unfair_NS(Miralonde *m) {
	m->EntreeNS();
	sleep(1);
	m->SortieNS();
	m->EntreeSN();
	sleep(1);
	m->SortieSN();
}

void car_unfair_SN(Miralonde *m) {
	m->EntreeSN();
	sleep(1);
	m->SortieSN();
	m->EntreeNS();
	sleep(1);
	m->SortieNS();
}

int main() {
	vector<thread> v;
	Miralonde m;

	v.push_back(thread(car_unfair_SN, &m));
	v.push_back(thread(car_unfair_NS, &m));
	usleep(10);
	for (int i = 0; i < NB_CARS; i++) {
		v.push_back(thread(car_thread, &m));
	}
	for (thread &t : v) {
		t.join();
	}
}
