// ZPR - zadanie dodatkowe
// 
// Autor:           Michał Pałka
// Nr. indeksu:     300336
// 
// Opis oraz przykład użycia wzorca klasy std::scoped_lock dostępnej od standardu C++17
// 
// Klasa std::scoped_lock wprowadzona w standardzie języka C++17 pozwala na blokowanie blokad (np. std::mutex) z wykorzystaniem mechanizmu RAII. 
// Opisywany wzorzec klasy na pierwszy rzut oka jest bardzo podobny do wzorca std::lock_guard znanego z C++11. W rzeczywistości jest on jednak
// od niego znacznie potężniejszy i bardziej użyteczny. Główną różnicą pomiędzy wspomnianymi wzorcami klas jest to, że std::lock_guard może obsługiwać
// tylko jedną blokadę, natomiast std::scoped_lock jest skonstruowany w taki sposób, że może przyjąć dowolną liczbę blokad w swojej liście argumentów
// (z dokładnością do wielkości stosu), i może działać dla różnych typów blokad. Cechą wspólną obu wzorców klas jest natomiast blokowanie przekazanych
// blokad przy konstrukcji obiektu oraz ich zwalnianie przy jego destrukcji.
// Bardzo ważną, a zarazem użyteczną, właściwością std::scoped_lock jest implementacja mechanizmu unikania impasu (deadlock avoidance). Oznacza to,
// że używając wzorca klasy std::scoped_lock programista nie doprowadzi do sytuacji, w której wątki zablokują się, czekając na dostęp do wspólnych
// zasobów, niezależnie od kolejności przekazanych do std::scoped_lock blokad. Sytuacja taka może zdarzyć się zarówno przy ręcznym blokowaniu
// i zwalnianiu blokad, jak i przy wykorzystaniu std::lock_guard, i może być trudna do wykrycia (program może się zawiesić np. raz na 1000000 razy).
//
// Kompilacja i uruchomienie przykładu:
// 
// W celu kompilacji przykładu należy w terminalu uruchomić podane poniżej polecenie:
//      `g++ -std=c++17 scoped_lock_example.cpp -pthread -o scoped_lock_example`
// Następnie należy uruchomić program podajac jako argument `DEAD_LOCK' w celu uruchomienia wariantu bez użycia std::scoped_lock (program się zawiesi,
// konieczna będzie terminacja procesu), bądź podając `SCOPED_LOCK` w celu uruchomienia wariantu używającej std::scoped_lock (program zadziała poprawnie).
// Aby uruchomić program, należy w terminalu uruchomić jedno z podanych poniżej poleceń:
//      `./scoped_lock_example DEAD_LOCK`   - bez użycia std::scoped_lock 
//      `./scoped_lock_example SCOPED_LOCK` - z użyciem std::scoped_lock 

#include <mutex>
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <cstring>


using namespace std::chrono_literals;

// Własna implementacja blokady. Przechowuje obiekt typu std::mutex i daje możliwość jego obsługi dodając opóźnienie czasowe przy jego blokowaniu, aby 
// deadlock występował za każdym razem przy niewłaściwej kolejności blokowania blokad.
class LazyMutex
{
    std::mutex mutex_{};    // Obsługiwana blokada 

  public: 
    LazyMutex() noexcept = default;
    ~LazyMutex() = default;

    LazyMutex(const LazyMutex&) = delete;
    LazyMutex& operator=(const LazyMutex&) = delete;

    // Blokuje mutex_ dodając 10ms opóźnienia czasowego.
    void lock()
    {
        std::this_thread::sleep_for(10ms);
        mutex_.lock();
    }

    // Próbuje zablokować mutex_ dodając 10ms opóźnienia czasowego.
    bool try_lock()
    {
        std::this_thread::sleep_for(10ms);
        return mutex_.try_lock();
    }

    // Odblokowuje mutex_.
    void unlock()
    {
        mutex_.unlock();
    }
};

// Warianty działania aplikacji
enum class APPLICATION_VARIANT{
    kDeadLock,      // wariant bez użycia std::scoped_lock, wystąpi deadlock 
    kScopedLock     // wariant z użyciem std::scoped_lock, nie wystąpi deadlock 
};

// Funkcja pomocnicza do pisania na terminal przez różne wątki.
void thread_safe_logger(const char* message)
{
    static std::mutex cout_lock{};
    std::scoped_lock lock(cout_lock);

    std::cout << message << std::endl;
}

// Funkcja pomocnicza do wyświetlenia komunikatu w przypadku braku argumentów wejściowych.
void NoInputArgument()
{
    thread_safe_logger("No input argument!");
    thread_safe_logger("Input argument should be one of the following:");
    thread_safe_logger(" - DEAD_LOCK");
    thread_safe_logger(" - SCOPED_LOCK");
}

// Funkcja pomocnicza do wyświetlenia komunikatu w przypadku zbyt wielu argumentów wejściowych.
void TooManyInputArguments()
{
    thread_safe_logger("Too many input argument!");
    thread_safe_logger("Input argument should be one of the following:");
    thread_safe_logger(" - DEAD_LOCK");
    thread_safe_logger(" - SCOPED_LOCK");
};

// Funkcja pomocnicza do wyświetlenia komunikatu w przypadku nieznanego argumentu wejściowego.
void NotKnownInputArguments()
{
    thread_safe_logger("Input argument not known!");
    thread_safe_logger("Input argument should be one of the following:");
    thread_safe_logger(" - DEAD_LOCK");
    thread_safe_logger(" - SCOPED_LOCK");
};

// Rozmiar vektorów symulujących dane współdzielone przez wątki
constexpr size_t vec_size = 10;

// Funkcja główna programu.
// 
// Twórca programu zdaje sobie sprawę z tego, że w program w zaprezentowanej formie działa synchronicznie i nie potrzebowałby wcale wątków
// do poprawnego działania. Demonstruje on jednak działanie wzorca klasy std::scoped_lock, ze szczególnym naciskiem na mechanizm deadlock avoidance,
// co jest jego głównym celem
int main(int argc, char** argv)
{
    if (argc == 1){
        NoInputArgument();
        exit(-1);
    } 
    else if (argc > 2)
    {
        TooManyInputArguments();
        exit(-1);
    }

    APPLICATION_VARIANT application_variant;

    if (strcmp(argv[1], "DEAD_LOCK") == 0)
    {
        application_variant = APPLICATION_VARIANT::kDeadLock;
    }
    else if (strcmp(argv[1], "SCOPED_LOCK") == 0)
    {
        application_variant = APPLICATION_VARIANT::kScopedLock;
    }
    else 
    {
        NotKnownInputArguments();
        exit(-1);
    }
    
    // Blokady dla danych współdzielonych przez wątki
    LazyMutex m1, m2;

    // Dane współdzielone przez wątki
    std::vector<int> vec1(vec_size), vec2(vec_size);


    auto dead_lock_thread_1 = [&](){ 
        thread_safe_logger("thread_1 entry");

        // Blokowanie blokat w kolejności m1 -> m2
        std::lock_guard<LazyMutex> lock1(m1);
        std::lock_guard<LazyMutex> lock2(m2);
        
        thread_safe_logger("thread_1 mutexes locked");

        // Operacje na współdzielonych danych 
        for (size_t i = 0; i < vec_size; ++i)
        {
            thread_safe_logger("thread_1 operation");

            vec1.push_back(i);
            vec2.push_back(vec_size - i);

            std::this_thread::sleep_for(100ms);
        }

        vec1.clear();
        vec2.clear();
            
        thread_safe_logger("thread_1 exit");
        thread_safe_logger("");
    };

    auto  dead_lock_thread_2 = [&](){ 
        thread_safe_logger("thread_2 entry");

        // Blokowanie blokat w kolejności m2 -> m1
        std::lock_guard<LazyMutex> lock2(m2);
        std::lock_guard<LazyMutex> lock1(m1);
        
        thread_safe_logger("thread_2 mutexes locked");

        // Operacje na współdzielonych danych 
        for (size_t i = 0; i < vec_size; ++i)
        {
            thread_safe_logger("thread_2 operation");

            vec1.push_back(vec_size - i);
            vec2.push_back(i);

            std::this_thread::sleep_for(100ms);
        }

        vec1.clear();
        vec2.clear();
            
        thread_safe_logger("thread_2 exit");
        thread_safe_logger("");
    };

    auto scoped_lock_thread_1 = [&](){
        thread_safe_logger("thread_1 entry");

        // Przekazanie blokat w kolejności m1 -> m2
        std::scoped_lock lock(m1, m2);
        
        thread_safe_logger("thread_1 mutexes locked");

        // Operacje na współdzielonych danych 
        for (size_t i = 0; i < vec_size; ++i)
        {
            thread_safe_logger("thread_1 operation");

            vec1.push_back(i);
            vec2.push_back(vec_size - i);

            std::this_thread::sleep_for(100ms);
        }

        vec1.clear();
        vec2.clear();
            
        thread_safe_logger("thread_1 exit");
        thread_safe_logger("");
    };

    auto scoped_lock_thread_2 = [&](){
        thread_safe_logger("thread_2 entry");

        // Przekazanie blokat w kolejności m2 -> m1
        std::scoped_lock lock(m2, m1);
        
        thread_safe_logger("thread_2 mutexes locked");

        // Operacje na współdzielonych danych 
        for (size_t i = 0; i < vec_size; ++i)
        {
            thread_safe_logger("thread_2 operation");

            vec1.push_back(vec_size - i);
            vec2.push_back(i);

            std::this_thread::sleep_for(100ms);
        }

        vec1.clear();
        vec2.clear();
            
        thread_safe_logger("thread_2 exit");
        thread_safe_logger("");
    };

    std::unique_ptr<std::thread> t1, t2;
    
    // Tworzenie odpowiednich wątków w zależności od wariantu aplikacji
    switch (application_variant)
    {
    case APPLICATION_VARIANT::kDeadLock:
        t1 = std::make_unique<std::thread>(dead_lock_thread_1);
        t2 = std::make_unique<std::thread>(dead_lock_thread_2);
        break;
    
    case APPLICATION_VARIANT::kScopedLock:
        t1 = std::make_unique<std::thread>(scoped_lock_thread_1);
        t2 = std::make_unique<std::thread>(scoped_lock_thread_2);
        break;
    
    default:
        break;
    }

    t1->join();
    t2->join();

    return 0;
}
