/**
 * An example multithreaded program that simulates
 * a professor meeting with students during her office hour.
 *
 * In this simulation, 1 realtime second simulates 1 minute.
 *
 * by Ron Mak
 * Department of Computer Engineering
 * San Jose State University
 *
 * WARNING: Contains a subtle threading error which can cause a deadlock!
 *          Can you find and fix it?
 */
#include <signal.h>
#include <sys/time.h>
#include <string>
#include <vector>

#include <iostream>
#include <deque>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <chrono>
#include <time.h>

using namespace std;

const int ID_BASE = 101;

const int CHAIR_COUNT   = 3;
const int STUDENT_COUNT = 25;

const int MAX_MEETING_DURATION = 5;
const int OFFICE_HOUR_DURATION = 20;

deque<int> chairs;
vector<thread> student_threads;

mutex chair_mutex;
mutex print_mutex;
condition_variable condition_student_arrived;

struct itimerval office_hour_timer;
time_t start_time;

int meeting_id = 0;   // id of the student meeting with the professor

int arrivals_count = 0;
int wait_count = 0;
int leaves_count = 0;
int meetings_count = 0;
int parfore_count = 0;

bool first_print = true;   // becomes false after printing the first event
bool times_up    = false;  // becomes true when the office hour is over

/**
 * The professor thread.
 */
void professor();

/**
 * The professor meets a student
 * or works on her new book.
 */
void professor_meets_student();

/**
 * The student thread.
 * @param id the student's id.
 */
void student(int id);

/**
 * A student arrives.
 * @param id the student's id.
 */
void student_arrives(int id);

/**
 * Print a line for each event:
 *   elapsed time
 *   ID of student meeting with the professor
 *   IDs of students waiting in the chairs
 *   what event occurred
 * @param event the event.
 */
void print(string event);

/**
 * Timer signal handler.
 */
void timer_handler(int signal);

/**
 * The main.
 */
int main()
{
    srand(time(0));
    time(&start_time);

    // Start the professor thread.
    thread professor_thread(professor);

    // Start the student threads.
    student_threads.reserve(STUDENT_COUNT);
    for (int i = 0; i < STUDENT_COUNT; i++)
    {
        student_threads.push_back(thread(student, ID_BASE + i));
    }

    // Set the timer signal handler.
    signal(SIGALRM, timer_handler);

    // Set the timer for the office hour duration.
    office_hour_timer.it_value.tv_sec = OFFICE_HOUR_DURATION;
    setitimer(ITIMER_REAL, &office_hour_timer, NULL);

    // Wait for the professor to complete the office hour.
    professor_thread.join();

    // Remaining waiting students leave.
    meeting_id = 0;
    while (wait_count-- > 0)
    {
        int student_id = chairs.front();
        chairs.pop_front();
        leaves_count++;

        char event[80];
        sprintf(event, "Student %d leaves",  student_id);
        print(event);
    }

    // Detach from the student threads.
    for (auto& st : student_threads) st.join();

    // Final statistics.
    printf("\n");
    printf("%5d students arrived\n", arrivals_count);
    printf("%5d students met with Prof. Fore\n", meetings_count);
    printf("%5d students left without meeting\n", leaves_count);
    printf("%5d times Prof. Fore worked on her book\n", parfore_count);

    return 0;
}

void professor()
{
    print("Professor opens her door");

    // Meet students until the office hour is over.
    do {
        professor_meets_student();
    } while (!times_up);

    print("Professor closes her door");
}

void professor_meets_student()
{
    // No student waiting, so work on ParFore language.
    if (wait_count == 0)
    {
        print("Professor works on her new book");
        parfore_count++;

        // Lock the chair mutex to protect the chairs and the wait count.
        unique_lock<mutex> lock(chair_mutex);

        // Wait for a student to arrive.
        condition_student_arrived.wait(lock, [] { return chairs.size() > 0; });
    }

    if (!times_up)
    {
        // Critical region: Remove a student from a chair.
        meeting_id = chairs.front();
        chairs.pop_front();
        wait_count--;

        char event[80];
        sprintf(event, "Professor meets with student %d",  meeting_id);
        print(event);

        // Release the mutex lock.
        chair_mutex.unlock();

        // Meet with the student.
        this_thread::sleep_for(std::chrono::seconds(
                                            rand()%MAX_MEETING_DURATION + 1));
        meetings_count++;

        sprintf(event, "Professor finishes with student %d",  meeting_id);
        print(event);
    }
}

void student(int id)
{
    // Students will arrive at random times during the office hour.
    this_thread::sleep_for(std::chrono::seconds(rand()%OFFICE_HOUR_DURATION));
    student_arrives(id);
}

void student_arrives(int id)
{
    char event[80];
    arrivals_count++;

    // Lock the chair mutex to protect the chairs and the wait count.
    unique_lock<mutex> lock(chair_mutex);

    if (wait_count < CHAIR_COUNT)
    {
        // Seat a student into a chair.
        chairs.push_back(id);
        wait_count++;

        sprintf(event, "Student %d arrives and waits", id);
        print(event);

        // Notify the professor and release the mutex lock.
        condition_student_arrived.notify_one();
		lock.unlock();
    }
    else
    {
        // Release the mutex lock.
        lock.unlock();

        leaves_count++;
        sprintf(event, "Student %d arrives and leaves", id);
        print(event);
    }
}

void print(string event)
{
    time_t now;
    time(&now);
    double elapsed = difftime(now, start_time);
    int min = 0;
    int sec = (int) elapsed;

    if (sec >= 60)
    {
        min++;
        sec -= 60;
    }

    // Acquire the mutex lock to protect the printing.
    unique_lock<mutex> lock(print_mutex);

    if (first_print)
    {
        printf("TIME | MEETING | WAITING     | EVENT\n");
        first_print = false;
    }

    // Elapsed time.
    printf("%1d:%02d | ", min, sec);

    // Who's meeting with the professor.
    if (meeting_id > 0) printf("%5d   |", meeting_id);
    else                printf("        |");

    int k = 0;

    // Who's waiting in the chairs.
    for (auto it = chairs.begin(); it != chairs.end(); it++)
    {
        printf("%4d", *it);
        k++;
    }

    // What event occurred.
    while (k++ < CHAIR_COUNT) printf("    ");
    printf(" | %s\n", event.c_str());

    // Release the print mutex lock.
    lock.unlock();
}

void timer_handler(int signal)
{
    times_up = true;  // office hour is over
}