#include <concepts>
#include <stdexcept>
#include <iostream>
#include <numeric>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
// Katsyaryna Anikevich

// Introduction:
// C++20 introduced concepts to simplify template programming by allowing type constraints.
// They improve code safety and readability, simplify error messages by removing complex techniques like std::enable_if,
// and enhance code quality and flexibility by enabling custom constraints and optimization opportunities for the compiler.


// 1. Core language concepts

// Concepts like `std::integral`, `std::floating_point`, `std::move_constructible`, and others
// allow you to set specific type requirements, improving code safety and readability.
// For example, `std::integral` ensures that a type is integral, while `std::floating_point` ensures a type is a floating-point type.

// Example: A function that adds two integral types (e.g., int, long) using the `std::integral` concept.
template<std::integral T>
T add(T a, T b) {
    return a + b;
}

// 2. Comparison concepts

// Concepts for comparison operations make it easier to use comparison operators and ensure type safety.
// For example, the `std::three_way_comparable` concept ensures that a type supports the three-way comparison operator (<=>),
// while `std::equality_comparable` and `std::totally_ordered` ensure that types support equality and total ordering comparison operators.

// Example: A function that checks ordering using the `std::totally_ordered` concept.
template <std::totally_ordered T>
bool is_less(const T& a, const T& b) {
    return a < b;
}

struct NoOrder { int x; };

// 3. Object concepts

// Concepts for working with objects specify what operations can be performed on types.
// For example, `std::movable` ensures that an object of a type can be moved, while `std::copyable` ensures that an object can be copied, moved, and swapped.
// The `std::semiregular` and `std::regular` concepts extend this by requiring default construction and equality comparison.

// Example: A function that moves an object using the `std::movable` concept.
template <std::movable T>
T move_object(T obj) {
    return std::move(obj);
}

struct Good {
    int x;
};

struct Bad {
    Bad() = default;
    Bad(Bad&&) = delete;
};


// 4. Callable concepts

// Concepts for callable objects check whether they can be invoked with a given set of argument types.
// For example, `std::predicate` ensures that the callable can be invoked and the result is boolean-testable
// (convertible to boolean context, not necessarily exactly `bool`).
// while `std::invocable` checks whether the object can be invoked with the given argument types. There are also concepts for binary relations
// and strict weak ordering, such as `std::relation` and `std::strict_weak_order`.

// Example: A function that uses a predicate to check if a number is even.
template<typename F, typename T>
requires std::predicate<F, T>
bool check(F f, T value) {
    return f(value);
}

bool is_even(int x) {
    return x % 2 == 0;
}

struct NotBool {};
NotBool bad_func(int) { return {}; }


// 5. Ranges

// Concepts and functionality for working with ranges ensure that a type is a range, meaning it supports operations like beginning and ending.
// This simplifies working with containers, allowing standard algorithms and functions to be used on any type that can be interpreted as a range.

// Example: A function that returns the first element from a range using the `std::ranges::range` concept.
template<std::ranges::range R>
auto get_first_element(const R& r){
    auto it = std::ranges::begin(r);
    if (it == std::ranges::end(r)) throw std::out_of_range("empty range");
    return *it;
}

struct NotARange { int x; };

// 6. Combining constraints (logical OR / AND )

// The ability to use logical operations (such as AND and OR) in concepts
// allows for more flexible and complex constraints for templates. With logical operations,
// multiple requirements for a type can be combined.

// We can use `requires` separately from the template parameter, which allows for more precise and convenient
// combination of constraints.

// Example: A function that calculates the average of elements in a vector, where the type T must be either integral or floating-point.
template <typename T>
requires std::integral<T> || std::floating_point<T>
double Average(const std::vector<T>& vec) {
    if (vec.empty()) return 0.0;
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
    return sum / static_cast<double>(vec.size());
}


int main() {
    std::cout << "add(10, 20): " << add(10, 20) << std::endl;
    // std::cout << "add(10.5, 20.5): " << add(10.5, 20.5) << std::endl;

    std::cout << "is_less(5, 10): " << std::boolalpha << is_less(5, 10) << std::endl;
    //std::cout << is_less(NoOrder{1}, NoOrder{2});


    Good g{10};
    Good moved = move_object(std::move(g));
    // Bad b;
    // Bad moved_b = move_object(std::move(b));

    std::cout << "check(is_even, 4): " << check(is_even, 4) << std::endl;
    // std::cout << "check(bad_func, 4): " << check(bad_func, 4) << std::endl;

    std::vector<int> integers = {1, 2, 3, 4, 5};
    std::cout << "get_first_element(integers): " << get_first_element(integers) << std::endl;
    // NotARange nr{5};
    // std::cout << get_first_element(nr) << std::endl;

    std::cout << "Average of integers: " << Average(integers) << std::endl;

    std::vector<std::string> strings = {"a", "b", "c"};
    // std::cout << "Average of strings: " << Average(strings) << std::endl;
}

// Sources:
// 1. https://en.cppreference.com/w/cpp/concepts.html
// 2. https://www.cppstories.com/2021/concepts-intro/
// 3. https://www.geeksforgeeks.org/cpp/constraints-and-concepts-in-cpp-20/