#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

using namespace std;

sem_t semCout; // zorgt voor mutual excusive gebruik van cout (1 thread tegelijk).

void check_errno(int error) {
    if (error < 0) {
        perror("Error");
        exit(EXIT_FAILURE);
    }
}

void check(int error) {
    if (error != 0) {
        cerr<<"Error: "<<strerror(error)<<endl;
        exit(EXIT_FAILURE);
    }
}

template<typename T, int size>
class CircularBuffer {
public:
    CircularBuffer();            // constructor zorgt voor de initialisatiie van de datamembers
    ~CircularBuffer();
    void put(const T& t);        // schrijf element  in de buffer. WACHT als buffer vol is!
    const T get();               // lees een element uit de buffer. WACHT als buffer leeg is!
private:
    T buffer[size];              // buffer voor het opslaan van size elementen van het type T
    int indexGet;                // index waar het volgende element uit de buffer gelezen wordt
    int indexPut;                // index waar het volgende element in de buffer geschreven wordt
    sem_t semMutualExclusive;    // binaire semaphore: zorgt voor wederzijdse uitsluiting
    sem_t semEmpty;              // counting semaphore: telt aantal lege plaatsen
    sem_t semFilled;             // counting semaphore: telt aantal gevulde plaatsen
};

template<typename T, int size>
CircularBuffer<T, size>::CircularBuffer(): indexGet(0), indexPut(0) {
    check_errno( sem_init(&semMutualExclusive, 0, 1) ); // 1 thread tegelijk toelaten in critische gebied
    check_errno( sem_init(&semEmpty, 0, size) );        // er zijn size lege plaatsen
    check_errno( sem_init(&semFilled, 0, 0) );          // er zijn 0 gevulde plaatsen
}

template<typename T, int size>
CircularBuffer<T, size>::~CircularBuffer() {
    check_errno( sem_destroy(&semMutualExclusive) );
    check_errno( sem_destroy(&semEmpty) );
    check_errno( sem_destroy(&semFilled) );
}

template<typename T, int size>
void CircularBuffer<T, size>::put(const T& t) {
    check_errno( sem_wait(&semEmpty) );             // verlaag het aantal lege plaatsen. WACHT als er geen lege plaatsen zijn!
    check_errno( sem_wait(&semMutualExclusive) );   // ga critisch gebied binnen
    buffer[indexPut] = t;
    indexPut++;
    if (indexPut == size) {
        indexPut = 0;
    }
    check_errno( sem_post(&semMutualExclusive) );   // verlaat critische gebied
    check_errno( sem_post(&semFilled) );            //  verhoog het aantal gevulde plaatsen.
}

template<typename T, int size>
const T CircularBuffer<T, size>::get() {
    check_errno( sem_wait(&semFilled) );            // verlaag het aantal gevulde plaatsen. WACHT als er geen gevulde plaatsen zijn!
    check_errno( sem_wait(&semMutualExclusive) );   // ga critische gebied binnen
    T c(buffer[indexGet]);
    indexGet++;
    if (indexGet == size) {
        indexGet = 0;
    }
    check_errno( sem_post(&semMutualExclusive) );   // verlaat critische gebied
    check_errno( sem_post(&semEmpty) );             // verhoog het aantal lege plaatsen
    return c;
}

CircularBuffer<char, 16> b;

void* producer(void* arg) { // functie voor producer thread
    char c(*static_cast<char*>(arg)); 
    check_errno( sem_wait(&semCout) );
    cout << "Thread: " << pthread_self() << " met arg: " << c << " gestart" << endl;
    check_errno( sem_post(&semCout) );
    for (int i(0); i < 1000; ++i) {
        b.put(c);
    }
    check_errno( sem_wait(&semCout) );
    cout << "Thread: " << pthread_self() << " gestopt" << endl;
    check_errno( sem_post(&semCout) );
	return 0;
}

void* consumer(void*) { // functie voor consumer thread
    check_errno( sem_wait(&semCout) );
    cout << "Thread: " << pthread_self() << " gestart" << endl;
    check_errno( sem_post(&semCout) );
    for (int i(0); i < 2000; ++i) {
        char c(b.get());
        check_errno( sem_wait(&semCout) );
        cout << c;
        check_errno( sem_post(&semCout) );
    }
    check_errno( sem_wait(&semCout) );
    cout << "Thread: " << pthread_self() << " gestopt" << endl;
    check_errno( sem_post(&semCout) );
	return 0;
}

int main(int argc, char *argv[]) {
    // De main thread moet de hoogste prioriteit hebben omdat deze thread de andere threads start en 
    // we willen de onderlinge wisselwerking van deze threads bestuderen!
    sched_param sp;
    int p;
    check( pthread_getschedparam(pthread_self(), &p, &sp) );
    sp.sched_priority = 60;
    check( pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp) );

    if (argc != 4) {
        cerr<<"Usage: opdr1.out <Priority Consumer> <Priority Frikadel Producer> <Priority Kroket Producer>"<<endl;
        exit (-1);
    }

    int prioc(atoi(argv[1]));
    int priop1(atoi(argv[2]));
    int priop2(atoi(argv[3]));

    cout << "Output voor Consumer priority = " << prioc
         << " Frikadel Producer priority = " << priop1
         << " Kroket Producer priority = " << priop2
         << endl;

    check_errno( sem_init(&semCout, 0, 1) );

    pthread_attr_t ptac, ptap1, ptap2;

    check( pthread_attr_init(&ptac) );
    check( pthread_attr_init(&ptap1) );
    check( pthread_attr_init(&ptap2) );

    check( pthread_attr_setinheritsched(&ptac, PTHREAD_EXPLICIT_SCHED) );
    check( pthread_attr_setinheritsched(&ptap1, PTHREAD_EXPLICIT_SCHED) );
    check( pthread_attr_setinheritsched(&ptap2, PTHREAD_EXPLICIT_SCHED) );

    check( pthread_attr_setschedpolicy(&ptac, SCHED_FIFO) );
    check( pthread_attr_setschedpolicy(&ptap1, SCHED_FIFO) );
    check( pthread_attr_setschedpolicy(&ptap2, SCHED_FIFO) );

    sched_param spc, spp1, spp2;

    check( pthread_attr_getschedparam(&ptac, &spc) );
    check( pthread_attr_getschedparam(&ptap1, &spp1) );
    check( pthread_attr_getschedparam(&ptap2, &spp2) );

    spc.sched_priority = prioc;
    spp1.sched_priority = priop1;
    spp2.sched_priority = priop2;

    check( pthread_attr_setschedparam(&ptac, &spc) );
    check( pthread_attr_setschedparam(&ptap1, &spp1) );
    check( pthread_attr_setschedparam(&ptap2, &spp2) );

    pthread_t ptp1, ptp2, ptc;

    char frikadel('F');
    char kroket('K');

    check( pthread_create(&ptc, &ptac, consumer, 0) );
    check( pthread_create(&ptp1, &ptap1, producer, static_cast<void*>(&frikadel)) );
    check( pthread_create(&ptp2, &ptap2, producer, static_cast<void*>(&kroket)) );

    check( pthread_join(ptc, 0) );
    check( pthread_join(ptp1, 0) );
    check( pthread_join(ptp2, 0) );

    check_errno( sem_destroy(&semCout) );

    return EXIT_SUCCESS;
}
