#include <pthread.h> // for pthread_create(), pthread_join(), etc.
#include <stdio.h> // for scanf(), printf(), etc.
#include <stdlib.h> // for malloc()
#include <unistd.h> // for sleep()

#define NUMBER_OF_BOXES_PER_DWELLER 5
#define ROOM_IN_TRUCK 10
#define MIN_BOX_WEIGHT 5
#define MAX_BOX_WEIGHT 50
#define MAX_TIME_FOR_HOUSE_DWELLER 7
#define MIN_TIME_FOR_HOUSE_DWELLER 1
#define MAX_TIME_FOR_MOVER 3
#define MIN_TIME_FOR_MOVER 1
#define MIN_TIME_FOR_TRUCKER 1
#define MAX_TIME_FOR_TRUCKER 3
#define MIN_TRIP_TIME 5
#define MAX_TRIP_TIME 10
#define RANDOM_WITHIN_RANGE(a, b, seed) (a+rand_r(&seed)%(b-a))
// For pipes
#define READ_END 0
#define WRITE_END 1

// Pipe between house dwellers and movers
int houseFloor[2];
// Pipe between movers and truckers
int nextToTrucks[2];

// Arg structure, need to assign id in loop, and a reference to a seed for rand_r(), since it mutates the seed
typedef struct args {
    int id;
    unsigned int seed;
} Args;

// Routine for house dwellers
void *dweller(void *arg) {
    Args *args = (Args *) arg; // Cast void* to Args*, as pthread_create() requires void*
    printf("Hello from house dweller %d\n", args->id);

    for (int i = 0; i < NUMBER_OF_BOXES_PER_DWELLER; i++) {
        int weight = RANDOM_WITHIN_RANGE(MIN_BOX_WEIGHT, MAX_BOX_WEIGHT, args->seed); // Generate random weight
        int time = RANDOM_WITHIN_RANGE(MIN_TIME_FOR_HOUSE_DWELLER, MAX_TIME_FOR_HOUSE_DWELLER, args->seed); // Time to pack box
        sleep(time); // Simulate packing time
        write(houseFloor[WRITE_END], &weight, sizeof(int)); // Write weight to pipe
        printf("House dweller %d created a box that weights %d in %d units of time\n", args->id, weight, time);
    }
    printf("House dweller %d is done packing\n", args->id);
    free(args); // Make sure to free the args struct
    return NULL;
}

// Routine for movers
// Comments from dweller() apply here as well
void *mover(void *arg) {
    Args *args = (Args *) arg;
    int weight;

    printf("Hello from mover %d\n", args->id);
    while (read(houseFloor[READ_END], &weight, sizeof(int)) > 0) { // Read from pipe, until dwellers are all done
        int time = RANDOM_WITHIN_RANGE(MIN_TIME_FOR_MOVER, MAX_TIME_FOR_MOVER, args->seed);
        sleep(time);
        write(nextToTrucks[WRITE_END], &weight, sizeof(int));
        printf("Mover %d brought down a box of weight %d in %d units of time\n", args->id, weight, time);
    }
    printf("Mover %d is done moving\n", args->id);
    free(args);
    return NULL;
}

// Routine for truckers
void *trucker(void *arg) {
    Args *args = (Args *) arg;
    int capacity = ROOM_IN_TRUCK;
    int truckWeight = 0;
    int weight;

    printf("Hello from trucker %d\n", args->id);
    while (read(nextToTrucks[READ_END], &weight, sizeof(int)) > 0) { // Read from pipe, until movers are all done
        capacity--;
        truckWeight += weight;
        int time = RANDOM_WITHIN_RANGE(MIN_TIME_FOR_TRUCKER, MAX_TIME_FOR_TRUCKER, args->seed);
        sleep(time);
        printf("Trucker %d loaded up box of weight %d in %d units of time, room left: %d\n", args->id, weight, time, capacity);

        if (capacity == 0) { // If truck is full, go on trip, and reset
            time = RANDOM_WITHIN_RANGE(MIN_TRIP_TIME, MAX_TRIP_TIME, args->seed);
            printf("Full truck %d with load of %d units of mass departed. Round trip will take %d units of time\n", args->id, truckWeight, time);
            sleep(time);
            capacity = ROOM_IN_TRUCK;
            truckWeight = 0;
        }
    }
    // If truck is not full, go on trip, and end, as there are no more boxes
    int time = RANDOM_WITHIN_RANGE(MIN_TRIP_TIME, MAX_TRIP_TIME, args->seed);
    printf("Not full truck %d with load of %d units of mass departed. One way trip will take %d units of time\n", args->id, truckWeight, time);
    sleep(time);
    printf("Truck %d is finished\n", args->id);
    free(args);
    return NULL;
}

int main(int argc, char **argv) {
    srand(time(NULL));

    pipe(houseFloor);
    pipe(nextToTrucks);

    // Get number of house dwellers, movers, and truckers
    int numberOfHouseDwellers, numberOfMovers, numberOfTruckers;
    printf("Please input number of people living in the house, number of movers and number of truck drivers\n");
    scanf("%d %d %d", &numberOfHouseDwellers, &numberOfMovers, &numberOfTruckers);

    // Create thread pools, and pass in args
    pthread_t dwellerThreads[numberOfHouseDwellers];
    pthread_t moverThreads[numberOfMovers];
    pthread_t truckerThreads[numberOfTruckers];

    for (int i = 0; i < numberOfHouseDwellers; i++) {
        Args *args = (Args *) malloc(sizeof(Args));
        args->id = i;
        args->seed = rand();
        pthread_create(&dwellerThreads[i], NULL, &dweller, (void *) args);
    }
    for (int i = 0; i < numberOfMovers; i++) {
        Args *args = (Args *) malloc(sizeof(Args));
        args->id = i;
        args->seed = rand();
        pthread_create(&moverThreads[i], NULL, &mover, (void *) args);
    }
    for (int i = 0; i < numberOfTruckers; i++) {
        Args *args = (Args *) malloc(sizeof(Args));
        args->id = i;
        args->seed = rand();
        pthread_create(&truckerThreads[i], NULL, &trucker, (void *) args);
    }
    // Wait for dwellers to finish and then close the house floor write, so movers know to stop
    for (int i = 0; i < numberOfHouseDwellers; i++) {
        pthread_join(dwellerThreads[i], NULL);
    }
    close(houseFloor[WRITE_END]);
    // Wait for movers to finish and then close the next to trucks write, so truckers know to stop
    // Also endings the read end of the house floor pipe, as no more boxes will be written to it
    for (int i = 0; i < numberOfMovers; i++) {
        pthread_join(moverThreads[i], NULL);
    }
    close(houseFloor[READ_END]);
    close(nextToTrucks[WRITE_END]);
    // Wait for truckers to finish, and close the read end of the next to trucks pipe, as no more boxes will be written to it
    for (int i = 0; i < numberOfTruckers; i++) {
        pthread_join(truckerThreads[i], NULL);
    }
    close(nextToTrucks[READ_END]);

    printf("Moving is finished!\n");
    return EXIT_SUCCESS;
}