/*
  ZPR 24Z - Zadanie dodatkowe
  Autor: Gustaw Malinowski
  Data: 04/01/2025

  Temat: Wyrażenia lambda oraz wykorszystanie Boost::hana::lazy

  Kompilacja:
    `g++ -std=c++20 -o lambda_lazy_zpr lambda_lazy_zpr.cpp -lboost_system`

  Źródła:
    1.https://www.boost.org/doc/libs/1_73_0/libs/hana/doc/html/structboost_1_1hana_1_1lazy.html
    2.https://www.nextptr.com/tutorial/ta1430524603/capture-this-in-lambda-expression-timeline-of-change
    3.https://en.cppreference.com/w/cpp/language/this
    4.https://en.cppreference.com/w/cpp/language/lambda
    5.https://medium.com/software-design/c-lambda-under-the-hood-9b5cd06e550a
    6.https://rigtorp.se/iife/
*/

#include <boost/hana/eval.hpp>
#include <boost/hana/fwd/lazy.hpp>
#include <boost/hana/lazy.hpp>
#include <chrono>
#include <functional>
#include <iostream>
#include <vector>
namespace hana = boost::hana;
// funkcje pomocnicze
void printVector(const std::vector<int> &vec) {
  for (int n : vec)
    std::cout << n << ' ';
  std::cout << '\n';
}
std::pair<int, double> getTimeToExecuteFunction(std::function<int(int)> func,
                                                int func_param,
                                                int nr_of_runs = 20) {
  int result = 0;
  auto start = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < nr_of_runs; ++i) {
    result = func(func_param);
  }
  auto end = std::chrono::high_resolution_clock::now();
  double elapsed = std::chrono::duration<double>(end - start).count();
  return {result, elapsed / nr_of_runs};
}

void lambdaIntro() {
  /*
    Wyrażenia Lambda (od C++11)

    Wyrażenia lambda pozwalają łatwo organizować kod oraz wygodnie przechwytywać
    zmienne z otoczenia.

    Składnia:
      '[<Przechwycenia>](<Parametry>) -> <Typ zwracany> {<Ciało funkcji>};'

    Lambdy można zapisywać do obiektów aby wykorzystywać je jak funkcje.
    Najczęściej odbywa się to poprzez użycie typu "auto" lub jako:
      ```
      std::function<typ_zwracany(typ_parametru) = []() -> typ_zwracany};
      ```
    Przykładowe wyrażenie Lambda:
      ```
      int a = 10;
      int b = 20;
      auto sumUsingLambda = [a, &b](int c) -> int { return a + b + c; };
      int d = sumUsingLambda(30);
      ```
      W powyższym przypadku "d" wyniesie 60.
      Przechwycone zostały zmienne "a" i "b".

    [Przechwycenia] określane są poprzez:
      - "nazwa_zmiennej" - przekazanie zmiennej przez kopię
      - "&nazwa_zmiennej" - przekazanie zmiennej przez referencję
      - "=" - przekazanie wszystkich zmiennych środowiskowych przez kopię
      - "&" - przekazanie wszystkich zmiennych środowiskowych przez referencję

    Można przekazywać wybrane parametry przez referencję a resztę przez kopię:
      `auto sumUsingLambdaA = [=, &b](int c) -> int { return a + b + c;};`
    A także odwrotnie:
      `auto sumUsingLambdaB = [&, a](int c) -> int { return a + b + c;};`

    Najpierw zawsze muszą być zapisane przechwycenia "=" i "&"
    a potem te odnoszące się do poszczególnych zmiennych.

    Wyrażenia Lambda można wykorzystywać bez określania zwracanego typu:
      '[<Przechwycenia>](<Parametry>){<Ciało funkcji>};'
    Także bez podanego pola parametrów (od C++ 23):
      '[<Przechwycenia>]{<Ciało funkcji>};'

    Trzeba określać typ zwracany gdy jest on bardziej złożony niż z pojedyńcze
    pole.
      ```
      int a = 1; int b = 2;
      // Dozwolone:
      auto x = [a,b]{ return a +b; };
      auto y = [a,b] -> int { return a +b; };


      // Zabronione
      auto z = [a,b]{ return {a,b}; };
        // error: returning initializer list
      // Dozwolone
      auto s = [a,b] -> std::pair<int,int> {return {a,b};};
      ```
  */

  // Wyrażenia Lambda to jedynie Synctactic Sugar.
  // Implementowane są jako obiekty klasy z przeciążonym operatorem().
  //  (na podstawie źródła nr. [5])

  int a = 10;
  int b = 20;
  // Wyrażenie Lambda
  std::function<int(int)> sumUsingLambda = [a, &b](int c) { return a + b + c; };

  // Realizacja
  class SumClass {
  private:
    int a_;
    int &b_;

  public:
    SumClass(int a, int &b) : a_(a), b_(b) {}
    int operator()(int c) { return a_ + b_ + c; };
  };
  SumClass sumUsingClass(a, b);

  std::cout << "Lambda expression: " << sumUsingLambda(30) << std::endl;
  std::cout << "Implementation in C++: " << sumUsingClass(30) << std::endl;
}
/*
  Gdy wyrażenie lambda ma dłuższy czas życia niż zmienne przechwytywane, powinno
  się przechwytywać je przez kopię.

  Wyrażenia lambda można również wykorzystywać w metodach klas
  i przekazywać im wskaźnik "this" przez kopię wskaźnika [this],
  czyli &(*this).

  Zasady przekazywania "this":
    - Nie da się przekazać this poprzez referencję [&this].
    - W C++17 wprowadzono możliwość przekazywania [*this], gdzie przekazywana
      jest kopia obiektu, który otacza wyrażenie lambda.
    - Do C++ 20 przy przekazaniu [=] lub [&] domyślnie
      przekazywane było [this] wartością.
    - Od C++ 20 domyślne przekazywanie przez [=] lub [&] jest deprecjonowane
      i zaleca się stosowanie [=, this] lub [&, this]
*/

// Przykład zwracania lambdy i jej wykorzystania po utracie
// żywotności obiektu otaczającego.
class MyClass {
private:
  int a_;

public:
  MyClass(int a) : a_(a){};
  auto myMethod(int b) {
    auto lambdaByCopy = [=, *this] {
      std::cout << "Lambda by copy with *this: " << this->a_ + b << std::endl;
    };

    auto lambdaByReference = [&, this] {
      std::cout << "Lambda by reference with this: " << this->a_ + b
                << std::endl;
    };
    lambdaByCopy();
    lambdaByReference();
    return lambdaByCopy;
  };
};

auto lambdaCreation() {
  MyClass mc(1);
  return mc.myMethod(10);
}

void lambdaReturning() {
  auto lambda = lambdaCreation();
  lambda();
}

/*
  Rozszerzając definicję wyrażenia lambda o nawiasy (<parametry>), można od razu
  wywołać to wyrażenie, co pozwala na uzyskanie wyniku bez potrzeby
  przypisywania lambdy do zmiennej. Jest to przykład tzw. IIFE (Immediately
  Invoked Function Expression).
*/
void lambdaImmediatelyInvokedFunctionExpression() {
  int a = 10;
  int b = [a](int value) { return a + value; }(100);
  std::cout << "IIFE example:" << b << std::endl;

  // IIFE można wykorzystywać do skomplikowanej inicjalizacji const
  // zmiennych i zabezpieczenia ich przed dalszą modyfikacją
  const int c = [a] {
    int number_to_add = 100;
    return (a + number_to_add);
  }();
}

/*
  Wyrażenia lambda można wykorzystywać rekurencyjnie, ale wymaga to użycia
  std::function, ponieważ lambda nie może odwoływać się do samej siebie
  bezpośrednio.

  Rekursywne wyrażenie lambda jest jednak znacznie wolniejsze niż rekurencyjne
  wywołanie funkcji.
*/
int computeFibonnaci(int n) {
  return n < 2 ? n : computeFibonnaci(n - 1) + computeFibonnaci(n - 2);
}

void lambdaRecursive() {
  int FIBONACCI_INDEX = 30;
  std::function<int(int)> fibonacci = [&fibonacci](int n) {
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
  };
  std::pair<int, double> lam_result =
      getTimeToExecuteFunction(fibonacci, FIBONACCI_INDEX);
  std::pair<int, double> func_result =
      getTimeToExecuteFunction(computeFibonnaci, FIBONACCI_INDEX);

  std::cout << "Fibonacci using recursive lambda:\tresult=" << lam_result.first
            << "\ttime taken=" << lam_result.second << std::endl;

  std::cout << "Fibonacci using recursive function:\tresult="
            << func_result.first << "\ttime taken=" << func_result.second
            << std::endl;
}

/*
  Wyrażenia lambda są szczególnie przydatne, gdy są używane jako parametry
  funkcji. Przykładem takiego zastosowania jest sortowanie, gdzie wyrażenie
  lambda pozwala na łatwe zdefiniowanie niestandardowego kryterium porównania
  bez potrzeby tworzenia oddzielnej funkcji.
*/

std::vector<int> selectValues(const std::vector<int> &numbers,
                              std::function<bool(int)> criterium) {
  std::vector<int> selected_numbers;
  for (int i : numbers) {
    if (criterium(i)) {
      selected_numbers.push_back(i);
    }
  }
  return selected_numbers;
}

void lambdaAsParameter() {
  std::vector<int> numbers = {1, -2, 3, -4, 5, -6, 7, -8, 9, -10};
  std::vector<int> selected_numbers =
      selectValues(numbers, [](int v) { return v > 0 ? 1 : 0; });
  std::cout << "Lambda as parameter\nBefore selection: " << std::endl;
  printVector(numbers);
  std::cout << "After selection: " << std::endl;
  printVector(selected_numbers);
}

void lambdaMain() {
  lambdaIntro();
  lambdaReturning();
  lambdaImmediatelyInvokedFunctionExpression();
  lambdaRecursive();
  lambdaAsParameter();
};

/*
  Boost::hana::lazy (od C++ 14)
  Elementy biblioteki Boost.Hana pozwalają na definiowanie wyników funkcji,
  tak, że są obliczane dopiero w momencie, gdy są rzeczywiście
  wykorzystywane.

  Wykorzystanie:
  1. Definiowana jest leniwa funkcja.
  2. Zostaje ona wywołana i zwraca "pseudo wynik".
  3. Gdy wynik obliczeń jest wymagany, "pseudo wynik" jest ewaluowany za pomocą
     `boost::hana::eval`

  Składnia:
    z funkcją
    ```
    int function_parameter = 35;
    auto lazy_function = hana::make_lazy(veryExpensiveFunction);
    auto result = lazy_function(function_parameter);
    int calculations = hana::eval(result) + 100
    ```
    z wyrażeniem lambda
    ```
    auto lazy_lambda = hana::make_lazy([]() {return veryExpensiveFunction();});
    auto result = lazy_lambda(function_parameter);
    int calculations = hana::eval(result) + 100
    ```

  Cechy rozwiązania:
  - Pozwala na operowanie na "pseudo wyniku" bez konieczności przekazywania
    parametrów przy każdym użyciu.
  - Zwiększa czytelność kodu.
  - Obliczenia są wykonywane dopiero przy wykorzystaniu wyników.
  - Może ukrywać błędy i utrudniać debugowanie przez opóźnienie obliczeń.


  Alternatywa:
  Zamiast korzystać z tej biblioteki, można użyć wyrażenia lambda z
  przechwyceniem parametrów funkcji.
*/

int computeFibonnaciUsingFunction(int index) { return computeFibonnaci(index); }

// Porównanie funkcjonalności boost::hana::lazy oraz wyrażeń lambda

void lazyComparisonToLambda() {
  const int fib_index = 40;
  auto timeNow = []() { return std::chrono::high_resolution_clock::now(); };
  auto elapsed = [timeNow](auto start) {
    return std::chrono::duration<double>(timeNow() - start).count();
  };

  // Przygotowanie lazy funkcji
  auto start = timeNow();
  auto lazyResult = hana::make_lazy(computeFibonnaciUsingFunction)(fib_index);
  double lazySetupTime = elapsed(start);

  // Przygotowanie alternatywy w postaci lambdy
  start = timeNow();
  auto lambdaResult = [fib_index] {
    return computeFibonnaciUsingFunction(fib_index);
  };
  double lambdaSetupTime = elapsed(start);

  // normalna funkcja
  start = timeNow();
  int normalResult = computeFibonnaciUsingFunction(fib_index);
  double funcSetupTime = elapsed(start);

  // Wyświetlenie rezultatów
  auto displayResult = [](const std::string &label, auto result,
                          double setupTime, double &usageTime) {
    auto start = std::chrono::high_resolution_clock::now();
    std::cout << label << "   \tresult = " << result
              << "\t time setup = " << setupTime << "[s]";
    usageTime = std::chrono::duration<double>(
                    std::chrono::high_resolution_clock::now() - start)
                    .count();
    std::cout << "\t time used = " << usageTime << "[s]" << std::endl;
  };

  double lazyUsageTime, lambdaUsageTime, funcUsageTime;
  displayResult("Lazy function", hana::eval(lazyResult), lazySetupTime,
                lazyUsageTime);
  displayResult("Lambda alternative", lambdaResult(), lambdaSetupTime,
                lambdaUsageTime);
  displayResult("Normal function", normalResult, funcSetupTime, funcUsageTime);
}
/*
Wnioski z porównania:
Wykorzystanie `boost::hana::lazy` jest wolniejsze niż wyrażenia lambda. Jednak
`hana::lazy` oferuje łatwiejsze użycie – eliminuje konieczność ręcznego
zarządzania przechwytywaniem zmiennych, choć wymaga pamiętania o zastosowaniu
`hana::eval` podczas ewaluacji wyników.
*/

int main() {
  lambdaMain();
  lazyComparisonToLambda();
  return 0;
}
