#include <iostream>
#include <array>
#include <vector>
#include <map>
#include <unordered_map>

int main () {

    /*
    Arrays
    They can store a collection of items of the same type.
    They have a fixed dimension which is defined at compile time.
    */

    std::array <int, 3> data = {1, 2, 3};

    for (int element : data) {
        std::cout << element << std::endl;
    }

    // Access first and last element
    std::cout << "First element is " << data[0] << ", or equivalently " << data.front() << std::endl;
    std::cout << "Last element is " << data.back() << std::endl;
    std::cout << "Number of elements (or dimension) is " << data.size() << std::endl;

    /*
    Vectors
    Use vectors when you do not know the dimension at compile time
    and/or you may want to change this dimension dynamically at
    run time.
    */

    std::vector<int> some_int_numbers = {1, 2, 3};
    for (int element : some_int_numbers) {
        std::cout << element << std::endl;
    }

    some_int_numbers.push_back(4);
    for (int element : some_int_numbers) {
        std::cout << element << std::endl;
    }

    std::vector<std::string> some_strings = {"Ilo"};
    for (std::string element : some_strings) {
        std::cout << element << std::endl;
    }

    some_strings.push_back("Albi");
    for (std::string element : some_strings) {
        std::cout << element << std::endl;
    }

    some_strings.clear();
    if (some_strings.empty()) {
        std::cout << "some_strings is now empty" << std::endl;
    }

    /*
    The size of a vector is different from its capacity. The size,
    as the function .size() suggests, indicates how many items are
    stored inside the vector. We can ensure that the vector has enough
    memory to store a number N of some type of items by using "reserve(N)"
    */

    std::vector<int> my_vector; // size 0, capacity 0
    my_vector.reserve(10); /* size 0, capacity 10 (a certain number of consecutive slots in memory
    has been booked so that we can add 10 items of type int) */

    /*
    The importance of this operation will become clear when
    talking about memory. Anyway the main point is that the elements
    of a vector are stored in contiguous memory cells (this is why
    vectors are accessed very fast and we should use them). Anyway,
    assume that we create a vector having a certain size and capacity,
    then we perform some other operations and we use the memory cells
    which are present after the last element of a vector. If now we
    push_back and add an element to that vector, as the vector has
    this property of having its elements in contiguous memory cells,
    the whole vector will be moved in another location in memory before
    the new element is added. This operation is clearly slow, and
    in this sense, guaranteeing a certain capacity to a vector, by
    "reserving space", can help us avoid this situation. In practice
    this means we have an estimate of the max number of elements
    a vector will have during its whole life. It is clearly a tradeoff
    as we may have a memory-critical application in which we may not
    want to decide to book memory this lightly.
    */

    // In practice any operation changing a vector's capacity may move it!

    /*
    A map is a sorted associative container, which contains key-value pairs.
    The keys are unique and stored using the < operator. This means the keys
    should be comparable (the built-in types always work, while we'll see how
    to deal with your own types), while the values can be of any type.
    (In python these are called dict - dictionaries)
    */
    std::map<int,std::string> my_map {{1,"Albi"}, {2, "Ilo"}};

    // Add item
    my_map.emplace(0,"Silvia");

    // Iterate over maps (before C++17)
    for (const auto& element : my_map) {
        const auto& key = element.first;
        const auto& value = element.second;
        std::cout << "key: " << key << ", value: " << value << std::endl;
    }

    // Modify an item: my_map[key] = new value
    my_map[0] = "Elena";

    // Iterate over maps (since C++17)
    for (const auto& [key, value] : my_map) {
        std::cout << "key: " << key << ", value: " << value << std::endl;
    }
    

    /*
    Unordered map: As the map, each element needs its unique access code,
    therefore we say the key type has to be hashable (typically int or string
    are used as a key). It has the same interface as std::map and clearly
    it is faster.
    */

    std::unordered_map<std::string,int> my_unordered_map;

    // emplace add an element in front!
    my_unordered_map.emplace("Silvia",1993); // 4th element
    my_unordered_map.emplace("Ilario",1992); // 3rd element
    my_unordered_map.emplace("Elena",1992); // 2nd element
    my_unordered_map.emplace("Alberto",1993); // 1st element

    for (const auto& [key, value] : my_unordered_map) {
        std::cout << "key: " << key << ", value: " << value << std::endl;
    }

    /*
    Iterators
    (github ssh try...)
    (try on ubuntu PC)
    */

    return 0;
}