/**
 * Implements a simulation where cars cross a one-lane bridge.  Each car is
 * represented by a thread, going either westbound or eastbound.
 *
 * This version does not use the Bridge class.
 */

#include <thread>
#include <mutex>
#include <condition_variable>
#include <algorithm>
#include <iomanip>
#include <string>
#include <iostream>
#include "random-generator.h"
#include "ostreamlock.h"
#include "thread-utils.h"
using namespace std;

// The number of cars to spawn to go across the bridge
static const unsigned int NUM_CARS = 24;

// The bounds for how long it could take for a car to get to the bridge
static const unsigned int LOW_APPROACH_TIME_MS = 0;
static const unsigned int HIGH_APPROACH_TIME_MS = 3000;

// The bounds for how long it could take for a car to cross the bridge
static const unsigned int LOW_CROSS_TIME_MS = 100;
static const unsigned int HIGH_CROSS_TIME_MS = 500;

static mutex rgen_lock;
static RandomGenerator rgen;

/* Locks around the shared random generator and returns a random value
 * between low and high to sleep for (in milliseconds).
 */
static unsigned int get_sleep_duration(unsigned int low, unsigned int high) {
    unique_lock<mutex> ul(rgen_lock);
    return rgen.getNextInt(low, high);
}

// Simulates "crossing the bridge" by sleeping for a random amount of time.
static void drive_across() {
    sleep_for(get_sleep_duration(LOW_CROSS_TIME_MS, HIGH_CROSS_TIME_MS));
}

// Simulates "driving up to the bridge" by sleeping for a random amount of time.
static void approach_bridge() {
    sleep_for(get_sleep_duration(LOW_APPROACH_TIME_MS, HIGH_APPROACH_TIME_MS));
}

// Simulates flipping a fair coin, returning true or false with equal probability
static bool flip_coin() {
    unique_lock<mutex> ul(rgen_lock);
    return rgen.getNextBool(0.5);
}

static void print(size_t id, const string& message, bool isEastbound,
    size_t& n_crossing_eastbound, size_t& n_crossing_westbound) {
        // Make a single string to contain e.g. "car #XX crossing going eastbound".
        // This must be a single string to right-align the BRIDGE graphic correctly.
        ostringstream ss;
        ss << "car #" << id << " " << message << " going " 
             << (isEastbound ? "eastbound" : "westbound");
        string statusMessage = ss.str();

        // Print the update, spaced equally so that the BRIDGE prints at the same
        // position on the line, no matter the length of the message.
        cout << oslock << setw(60) << left << statusMessage;

        // Now print the text representation of the bridge
        if (n_crossing_eastbound + n_crossing_westbound == 0) {
                cout << "BRIDGE: [none crossing]" << endl << osunlock;
        } else if (n_crossing_eastbound > 0 && n_crossing_westbound == 0) {
                cout << "BRIDGE: [";
                for (size_t i = 0; i < n_crossing_eastbound; i++) cout << "🚗";
                cout << " -->>]" << endl << osunlock;
        } else if (n_crossing_westbound > 0 && n_crossing_eastbound == 0) {
                cout << "BRIDGE: [<<-- ";
                for (size_t i = 0; i < n_crossing_westbound; i++) cout << "🚙";
                cout << "]" << endl << osunlock;
        } else {
                cout << "ERROR!!  cars stuck, trying to go both directions" << endl << osunlock;
        }
}

// Arrive at, cross, and depart from the bridge going east.
static void cross_bridge_east(size_t id, size_t& n_crossing_eastbound,
    size_t& n_crossing_westbound, mutex& bridge_lock,
    condition_variable_any& none_crossing_eastbound, 
    condition_variable_any& none_crossing_westbound) {

    approach_bridge();   // sleep
    
    // Arrive eastbound
    bridge_lock.lock();
    print(id, "arrived", true, n_crossing_eastbound, n_crossing_westbound);
    while (n_crossing_westbound > 0) {
            none_crossing_westbound.wait(bridge_lock);
    }
    n_crossing_eastbound++;
    print(id, "crossing", true, n_crossing_eastbound, n_crossing_westbound);
    bridge_lock.unlock();

    drive_across();      // sleep

    // Leave eastbound
    bridge_lock.lock();
    n_crossing_eastbound--;
    if (n_crossing_eastbound == 0) {
            none_crossing_eastbound.notify_all();
    }
    print(id, "crossed", true, n_crossing_eastbound, n_crossing_westbound);
    bridge_lock.unlock();
}

// Arrive at, cross, and depart from the bridge going west.
static void cross_bridge_west(size_t id, size_t& n_crossing_eastbound,
    size_t& n_crossing_westbound, mutex& bridge_lock,
    condition_variable_any& none_crossing_eastbound, 
    condition_variable_any& none_crossing_westbound) {

    approach_bridge();   // sleep

    // Arrive westbound
    bridge_lock.lock();
    print(id, "arrived", false, n_crossing_eastbound, n_crossing_westbound);
    while (n_crossing_eastbound > 0) {
            none_crossing_eastbound.wait(bridge_lock);
    }
    n_crossing_westbound++;
    print(id, "crossing", false, n_crossing_eastbound, n_crossing_westbound);
    bridge_lock.unlock();
 
    drive_across();      // sleep

    // Leave westbound
    bridge_lock.lock();
    n_crossing_westbound--;
    if (n_crossing_westbound == 0) {
            none_crossing_westbound.notify_all();
    }
    print(id, "crossed", false, n_crossing_eastbound, n_crossing_westbound);
    bridge_lock.unlock();
}


int main(int argc, const char *argv[]) {
    size_t n_crossing_eastbound = 0;
    size_t n_crossing_westbound = 0;
    mutex bridge_lock;
    condition_variable_any none_crossing_eastbound;
    condition_variable_any none_crossing_westbound;

    thread cars[NUM_CARS];

    for (size_t i = 0; i < NUM_CARS; i++) {
        if (flip_coin()) {
            cars[i] = thread(cross_bridge_east, i, ref(n_crossing_eastbound),
                ref(n_crossing_westbound), ref(bridge_lock), 
                ref(none_crossing_eastbound), ref(none_crossing_westbound));
        } else {
            cars[i] = thread(cross_bridge_west, i, ref(n_crossing_eastbound),
                ref(n_crossing_westbound), ref(bridge_lock), 
                ref(none_crossing_eastbound), ref(none_crossing_westbound));
        }
    }

    for (thread& car : cars) {
        car.join();
    }
    return 0;
}
