#include <atomic>
#include <chrono>
#include <csignal>
#include <deque>
#include <fstream>
#include <iostream>
#include <mutex>
#include <semaphore>
#include <sstream>
#include <syncstream>
#include <thread>
#include <vector>

#define TELLER_NUM 3
#define TIME_GRAN_MSEC 200

struct CustomerInfo {
    unsigned int id;
    unsigned int arrival_time;
    unsigned int service_time;
};

struct Customer {
    unsigned int id;
    unsigned int service_time;
};

std::atomic<bool> bank_open{false};

// BEGIN_LST_CUS_Q
std::mutex customer_q_mtx;
std::deque<Customer> customer_q;
// END_LST_CUS_Q

// BEGIN_LST_COUNT_SEM
std::counting_semaphore<100> customer_num_sem{0};
// END_LST_COUNT_SEM


void signal_handler(int signum) {
    // std::cout << "\nReceived signal " << signum << ", stopping...\n";
    write(STDOUT_FILENO, "\nCaught SIGINT, shutting down...\n", 33);
    bank_open = false;
}

// BEGIN_LST_TELLER
void teller_thread(
    int id, std::chrono::time_point<std::chrono::steady_clock> start_tick) {
    std::osyncstream(std::cout)
        << "Teller id " << id << " started" << std::endl;
    std::chrono::time_point curr_tick = start_tick;
    unsigned int tick_count = 0;
    bool is_serving = false;
    std::chrono::time_point<std::chrono::steady_clock> start_serving_time;
    struct Customer curr_customer;
    while (bank_open) {
        if (is_serving) {
            if ((std::chrono::duration_cast<std::chrono::milliseconds>(
                     (curr_tick - start_serving_time) / TIME_GRAN_MSEC))
                    .count() <= curr_customer.service_time) {
                // We are still serving, do nothing
                std::osyncstream(std::cout) << "Teller id " << id << " is serving customer " << curr_customer.id << " at tick " << tick_count << std::endl;
                goto next_tick;
            } else {
                // We have just finished serving.
                std::osyncstream(std::cout) << "Teller id " << id << " finishied serving customer " << curr_customer.id << " at tick " << tick_count << std::endl;
                is_serving = false;
            }
        }

        // Try to acquire a new customer
        if (customer_num_sem.try_acquire()) {
            // Successfully acquired a customer
            std::lock_guard<std::mutex> lock(customer_q_mtx);
            if (!customer_q.empty()) {
                curr_customer = customer_q.front();
                customer_q.pop_front();

                std::osyncstream(std::cout)
                    << "Teller id " << id << " is now serving customer "
                    << curr_customer.id << " for " << curr_customer.service_time
                    << " ticks at tick " << tick_count << std::endl;

                is_serving = true;
                start_serving_time = curr_tick;
            }
        } else {
            std::osyncstream(std::cout)
                << "Teller id " << id << " is idle at tick " << tick_count << std::endl;
        }

    next_tick:
        ++tick_count;
        curr_tick += std::chrono::milliseconds(TIME_GRAN_MSEC);
        std::this_thread::sleep_until(curr_tick);
    }
    std::osyncstream(std::cout)
        << "Teller id " << id << " closing" << std::endl;
}
// END_LST_TELLER

// BEGIN_LST_CUSTOMER
void customer_thread(
    CustomerInfo info,
    std::chrono::time_point<std::chrono::steady_clock> start_tick) {

    Customer cus{.id = info.id, .service_time = info.service_time};

    std::chrono::time_point curr_tick = start_tick;
    unsigned int tick_count = 0;
    std::chrono::time_point target_tick = start_tick + std::chrono::milliseconds(TIME_GRAN_MSEC) * info.arrival_time;
    while (curr_tick < target_tick) {
        if (!bank_open) {
            return;
        }
        ++tick_count;
        curr_tick += std::chrono::milliseconds(TIME_GRAN_MSEC);
        std::this_thread::sleep_until(curr_tick);
    }
    
    // Wake up and push self into queue, then V(customer_num_sem)
    std::osyncstream(std::cout) << "Customer id " << info.id << " arrived at tick " << tick_count << std::endl;
    {
        std::lock_guard<std::mutex> lock(customer_q_mtx);
        customer_q.push_back(cus);
    }
    customer_num_sem.release();
}
// END_LST_CUSTOMER

// BEGIN_LST_MAIN
int main(int argc, char *argv[]) {
    if (argc != 2) {
        std::cout << "Invalid commandline arg, expected filename";
        return 1;
    }

    std::signal(SIGINT, signal_handler);

    // Read config from file
    std::vector<CustomerInfo> customer_infos;

    std::ifstream file(argv[1]);
    if (!file.is_open()) {
        std::cerr << "Cannot open file: " << argv[1] << std::endl;
        return 1;
    }

    std::string line;
    while (std::getline(file, line)) {
        std::istringstream iss(line);
        CustomerInfo cus;
        if (iss >> cus.id >> cus.arrival_time >> cus.service_time) {
            customer_infos.push_back(cus);
        } else {
            std::cerr << "Invalid line: " << line << std::endl;
        }
    }

    file.close();

    bank_open = true;
    std::chrono::time_point start_tick = std::chrono::steady_clock::now();

    // Set up tellers
    std::vector<std::thread> tellers;

    for (int i = 0; i < TELLER_NUM; ++i) {
        try {
            tellers.emplace_back(teller_thread, i, start_tick);
        }
        catch (const std::system_error &e) {
            std::cerr << "Failed to create teller thread, i = " << i << ": "
                      << e.what() << std::endl;
        }
    }

    // Set up customers
    std::vector<std::thread> customers;
    for (auto cu : customer_infos) {
        try {
            customers.emplace_back(customer_thread, cu, start_tick);
        } catch (const std::system_error &e) {
            std::cerr << "Failed to create customer thread, id = " << cu.id << ": "
                      << e.what() << std::endl;
        }
    }

    // Wait for all children to shutdown (by ctrl-c handler)
    for (auto &t: customers) {
        if (t.joinable()) {
            t.join();
        }
    }

    for (auto &t : tellers) {
        if (t.joinable()) {
            t.join();
        }
    }

    return 0;
}
// END_LST_MAIN