 import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.locks.*;

/**
 * Classe représentant les objets qui sont placé dans le tampon
 * @author jdavid
 *
 */
class Consommable {

	public static final Consommable POISON = new Consommable();
	
	private boolean consomme;

	public Consommable() {
		consomme = false;
	}

	public void consomme() {
		consomme = true;
	}

	public boolean estConsomme() {
		return consomme;
	}
}

/**
 * Interface que doit implementer un tampon partagé
 * @author jdavid
 *
 */
interface Stock {
	public boolean deposer(Consommable o);

	public Consommable retirer();
}


/**
 * Implémentation avec un moniteur et donc une seule file d'attente.
 * Comme les évennements (non plein, non vide) ne sont pas dissociables,
 * la condition est vérifiée en boucle et les consommateurs et producteurs 
 * sont reveillés indistinctement.
 * @author jdavid
 *
 */
class StockAvecSynchronized implements Stock {

	private Consommable[] tampon;

	private int tete;
	private int queue;
	private int nb;

	public StockAvecSynchronized(int capacite) {
		tampon = new Consommable[capacite];
		tete = 0;
		queue = 0;
		nb = 0;
	}

	public synchronized boolean deposer(Consommable o) {
		while (nb >= tampon.length) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				return false;
			}

		}
		tampon[queue] = o;
		queue = (queue + 1) % tampon.length;
		nb += 1;
		notifyAll();
		return true;
	}

	public synchronized Consommable retirer() {
		while (nb == 0) {
			try {
				wait();
			} catch (InterruptedException e) {
				return null;
			}
		}
		Consommable res = tampon[tete];
		tampon[tete] = null;
		tete = (tete + 1) % tampon.length;
		nb -= 1;

		notifyAll();
		return res;

	}
}

/**
 * Implementation avec deux moniteurs (qui ne fonctionne pas).
 * Si seulement 1 producteur et 1 consommateur : il faut s'assurer que le retrait/ajout soit en exclusion mutuelle
 * Avec plusieurs, ca ne fonctionne pas
 * @author jdavid
 *
 */
class StockAvecDeuxMoniteurs implements Stock {

	private Consommable[] tampon;

	private int tete;
	private int queue;
	private int nb;
	private Object nonVide = new Object();
	private Object nonPlein = new Object();


	public StockAvecDeuxMoniteurs(int capacite) {
		tampon = new Consommable[capacite];
		tete = 0;
		queue = 0;
		nb = 0;
	}

	public boolean deposer(Consommable o) {
		synchronized (nonPlein) {
			while (nb >= tampon.length) {
				try {
					nonPlein.wait();
				} catch (InterruptedException e) {
					return false;
				}
			}
		}
		//
		
		synchronized (tampon) {
			tampon[queue] = o;
			queue = (queue + 1) % tampon.length;
			nb += 1;
		}
		
		synchronized (nonVide) {
			nonVide.notify();
		}
		return true;
	}

	public Consommable retirer() {
		Consommable res = null;
		synchronized (nonVide) {
			while (nb == 0) {
				try {
					nonVide.wait();
				} catch (InterruptedException e) {
					return null;
				}
			}			
		}
		 
		synchronized (tampon) {
			res = tampon[tete];
			tete = (tete + 1) % tampon.length;
			nb -= 1;
		}
		
		synchronized (nonPlein) {
			nonPlein.notify();
		}
		
		return res;

	}
}

/**
 * Implementation avec deux moniteurs (qui fonctionne).
 * @author jdavid
 *
 */
class StockAvecDeuxMoniteursBis implements Stock {

	private Consommable[] tampon;

	private int tete;
	private int queue;
	private int nb;
	private Object nonVide = new Object();
	private Object nonPlein = new Object();


	public StockAvecDeuxMoniteursBis(int capacite) {
		tampon = new Consommable[capacite];
		tete = 0;
		queue = 0;
		nb = 0;
	}

	public boolean deposer(Consommable o) {
		synchronized (nonPlein) {
			while (nb >= tampon.length) {
				try {
					nonPlein.wait();
				} catch (InterruptedException e) {
					return false;
				}
			}
			synchronized (tampon) {
				tampon[queue] = o;
				queue = (queue + 1) % tampon.length;
				nb += 1;
			}
		}
		
		
		synchronized (nonVide) {
			nonVide.notify();
		}
		return true;
	}

	public Consommable retirer() {
		Consommable res = null;
		synchronized (nonVide) {
			while (nb == 0) {
				try {
					nonVide.wait();
				} catch (InterruptedException e) {
					return null;
				}
			}
			
			synchronized (tampon) {
				res = tampon[tete];
				tete = (tete + 1) % tampon.length;
				nb -= 1;
			}
		}
		
		synchronized (nonPlein) {
			nonPlein.notify();
		}
		
		return res;

	}
	
}
/**
 * Implementation avec l'utilisation d'un Verrou Reentrant avec deux files d'attente (conditions).
 * C'est l'implementation optimale.
 * Non discuté en cours mais tutoriel dispo sur http://blog.paumard.org/cours/java-api/chap05-concurrent-locks.html
 * @author jdavid
 *
 */
class StockAvecLockEtConditions implements Stock {

	private Consommable[] tampon;

	private int tete;
	private int queue;
	private int nb;
	private Lock verrou = new ReentrantLock();
	private Condition nonVide = verrou.newCondition();
	private Condition nonPlein = verrou.newCondition();

	public StockAvecLockEtConditions(int capacite) {
		tampon = new Consommable[capacite];
		tete = 0;
		queue = 0;
		nb = 0;
	}

	public boolean deposer(Consommable o) {
		try {
			verrou.lock();
			while (nb >= tampon.length) {

				try {
					nonPlein.await();
				} catch (InterruptedException e) {
					return false;
				}

			}
			tampon[queue] = o;
			queue = (queue + 1) % tampon.length;
			nb += 1;

			nonVide.signal();
		} finally {
			verrou.unlock();
		}
		return true;
	}

	public Consommable retirer() {
		Consommable res = null;
		try {
			verrou.lock();
			while (nb == 0) {
				try {
					nonVide.await();
				} catch (InterruptedException e) {
					return null;
				}
			}
			res = tampon[tete];
			tete = (tete + 1) % tampon.length;
			nb -= 1;

			nonPlein.signal();
		} finally {
			verrou.unlock();
		}
		return res;

	}
}

class Producteur implements Runnable {

	private List<Consommable> prod;
	private Stock s;

	public Producteur(Stock s, List<Consommable> prod) {
		this.s = s;
		this.prod = prod;
	}

	public void run() {
		for (Consommable c : prod) {
			s.deposer(c);
		}
	}

}

class Consommateur implements Runnable {

	private Stock s;

	public Consommateur(Stock s) {
		this.s = s;
	}

	public void run() {
		while (!Thread.interrupted()) {
			Consommable c = s.retirer();
			// cas du consommable empoisonné -> fin
			if (c==Consommable.POISON) {
				break;
			}
			if (c.estConsomme()) {
				System.err.println("Beerk, c'est déjà consommé");
			}
			c.consomme();
		}
		System.out.println("Consommateur a fini");
	}
}

public class ExempleProducteurConsommateur {

	public static void main(String[] args) {

		// On peut ici utiliser une des implementations :
		// StockAvecSynchronized, StockAvecLockEtConditions, StockAvecDeuxMoniteurs
		Stock s = new StockAvecLockEtConditions(10);
		
		// Sans doute la meilleure solution est d'utiliser une Blocking Queue
		// pour gérer le stock. L'ArrayBlockingQueue utilise le mecanisme des Lock avec deux conditions.
		// s.put(s) -> ajouter
		// s.take() -> retirer
		//BlockingQueue<Object> s = new ArrayBlockingQueue<>(10);
		

		
		int nbProd = 45;
		int nbCons = 8;
		
		int length = 1000 * nbProd;
		List<Consommable> tab = new ArrayList<>(length);
		for (int i = 0; i < length; i++) {
			tab.add(new Consommable());
		}

		
		Thread[] consumers = new Thread[nbCons];
		for (int i = 0; i < nbCons; i++) {
			consumers[i]=new Thread(new Consommateur(s));
			consumers[i].start();
		}
		

		Thread[] producers = new Thread[nbProd];
		for (int i = 0; i < nbProd; i++) {
			producers[i] = new Thread(new Producteur(s, tab.subList(i * (length / nbProd), (i + 1) * (length / nbProd))));
			producers[i].start();
		}

		// attente de fin d'activité des producteurs
		for (Thread t : producers) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		for (int i = 0; i < nbCons; i++) {
			s.deposer(Consommable.POISON);
		}
		
		
		int nbNonConsommes=0;
		for (Consommable c : tab) {
			if (!c.estConsomme()) {
				nbNonConsommes+=1;
			}
		}
		System.out.println("Nb non consommés : "+nbNonConsommes);
		
		

	}
}
