/*
 * Simulation of bounded-buffer problem.
 *
 * Program requires the following command-line arguments:
 *   size of buffer
 *   number of producers
 *   number of consumers
 *   number of items for each producer to produce
 *   maximum time to delay in producers (milliseconds)
 *   maximum time to delay in consumers (milliseconds)
 */
#define _XOPEN_SOURCE 500 /* may be needed to get usleep() in unistd.h */
#define _SVID_SOURCE || _BSD_SOURCE || _XOPEN_SOURCE /* similarly for *rand48_r() */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <pthread.h>
#include <semaphore.h>
#include "timer.h"
#include "getint.h"
#include "bbuffer.h"

void * producer_fcn(void *thread_arg);
void * consumer_fcn(void *thread_arg);

/* global variables */

int buffer_size;
int num_producers;
int num_consumers;
int num_items_per_producer;
int max_producer_delay;
int max_consumer_delay;
int seed;

bbuffer_t buffer;

sem_t buffer_mutex;
sem_t empty;
sem_t full;

double start_time;
long elapsed_time(void) {
    return (get_time() - start_time) * 1000;
}

/* ---- main program ---- */

int main(int argc, char * argv[]) {

    char *usage_msg = "parameters:\n"
            "\tbuffsize\n"
            "\tproducers\n"
            "\tconsumers\n"
            "\titems per producer\n"
            "\tproducer_delay (msec)\n"
            "\tconsumer_delay (msec)\n"
            "\tseed for random delays (optional)\n";

    if (argc < 7) {
        fprintf(stderr, usage_msg);
        exit(EXIT_FAILURE);
    }
    get_positive_int_or_exit(usage_msg, argv[1], &buffer_size);
    get_positive_int_or_exit(usage_msg, argv[2], &num_producers);
    get_positive_int_or_exit(usage_msg, argv[3], &num_consumers);
    get_positive_int_or_exit(usage_msg, argv[4], &num_items_per_producer);
    get_positive_int_or_exit(usage_msg, argv[5], &max_producer_delay);
    get_positive_int_or_exit(usage_msg, argv[6], &max_consumer_delay);
    if (argc > 7) {
        get_positive_int_or_exit(usage_msg, argv[7], &seed);
    }

    /* initialize timer */
    start_time = get_time();

    /* initialize buffer */
    if (!create_bbuffer(&buffer, buffer_size)) {
        fprintf(stderr, "could not create buffer\n");
        return EXIT_FAILURE;
    }

    /* initialize for synchronization */
    sem_init(&buffer_mutex, 0, 1);
    sem_init(&empty, 0, buffer_size);
    sem_init(&full, 0, 0);

    /* set up IDs for threads */
    int producerIDs[num_producers];
    for (int i = 0; i < num_producers; ++i)
        producerIDs[i] = i;
    int consumerIDs[num_consumers];
    for (int i = 0; i < num_consumers; ++i)
        consumerIDs[i] = i;

    /* start threads for producers and consumers */
    pthread_t producers[num_producers];
    for (int i = 0; i < num_producers; ++i) 
        pthread_create(&producers[i], NULL, producer_fcn, (void *) &producerIDs[i]);
    pthread_t consumers[num_consumers];
    for (int i = 0; i < num_consumers; ++i) 
        pthread_create(&consumers[i], NULL, consumer_fcn, (void *) &consumerIDs[i]);

    /* wait for all producer threads to complete */
    for (int i = 0; i < num_producers; ++i) 
        pthread_join(producers[i], NULL);
    /* 
     * wait for user input to exit
     * ugly hack but keeping track of when all generated items have been consumed adds
     *   significant complexity
     */
    printf("enter anything other than blank line to exit\n");
    getchar();

    /* clean up and exit */
    sem_destroy(&buffer_mutex);
    sem_destroy(&empty);
    sem_destroy(&full);
    free_bbuffer(&buffer);

    return EXIT_SUCCESS;
}

/* ---- code to be executed by each producer ---- */

void * producer_fcn(void * thread_arg) {
    int myID = * (int *) thread_arg;

    /* use *rand48_r because they're thread-safe and rand() is not */
    struct drand48_data rand_local_save;
    /* unique sequence in each thread */
    srand48_r(seed + myID, &rand_local_save);

    long int rand_out;

    for (int i = 0; i < num_items_per_producer; ++i) {

        /* simulate producing item */
        lrand48_r(&rand_local_save, &rand_out);
        int delay = (rand_out % max_producer_delay) + 1;
        usleep(delay * 1000);
        bbuffer_item_t item = { myID, i };
        printf("at time %ld producer %d produced item (%d %d)\n", 
                elapsed_time(), myID, item.producer, item.number);

        /* put with synchronization */
        sem_wait(&empty);
        sem_wait(&buffer_mutex);
        insert_bbuffer(&buffer, item);
        printf("at time %ld producer %d added item, count now %d\n", 
                elapsed_time(), myID, buffer.count);
        sem_post(&buffer_mutex);
        sem_post(&full);
    }

    pthread_exit((void* ) NULL);
}

/* ---- code to be executed by each consumer ---- */

void * consumer_fcn(void * thread_arg) {
    int myID = * (int *) thread_arg;

    /* use *rand48_r because they're thread-safe and rand() is not */
    struct drand48_data rand_local_save;
    /* unique sequence in each thread */
    srand48_r(seed + myID + num_producers, &rand_local_save);

    long int rand_out;

    /* delay to make things more interesting -- give producers a head start */
    usleep(max_consumer_delay * 1000);

    /* run until program is stopped -- ugly hack but see comment in main() */
    while (true) {

        /* get with_synchronization */
        sem_wait(&full);
        sem_wait(&buffer_mutex);
        bbuffer_item_t item = remove_bbuffer(&buffer);
        printf("at time %ld consumer %d removed item, count now %d\n", 
                elapsed_time(), myID, buffer.count);
        sem_post(&buffer_mutex);
        sem_post(&empty);

        /* simulate consuming item */
        lrand48_r(&rand_local_save, &rand_out);
        int delay = (rand_out % max_consumer_delay) + 1;
        usleep(delay * 1000);
        printf("at time %ld consumer %d consumed item (%d %d)\n", 
                elapsed_time(), myID, item.producer, item.number);
    }

    pthread_exit((void* ) NULL);
}