// https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#t5-combine-generic-and-oo-techniques-to-amplify-their-strengths-not-their-costs
// https://www.youtube.com/watch?v=QGcVXgEVMJg
// https://www.youtube.com/watch?v=bIhUE5uUFOA
// https://www.youtube.com/watch?v=4eeESJQk-mw&t=23m26s
// https://www.youtube.com/watch?v=gVGtNFg4ay0
// https://www.youtube.com/watch?v=OtU51Ytfe04
// https://www.youtube.com/watch?v=8c6BAQcYF_E
// https://wiki.c2.com/?ExternalPolymorphism
// https://www.dre.vanderbilt.edu/~schmidt/PDF/C++-EP.pdf


#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <algorithm>


using namespace std::string_literals;


// String

std::ostream & operator<<(std::ostream & ostream, std::string const & string)
{
    ostream << std::quoted(string);
    return ostream;
}


// Value

class Value
{
public:

    template<typename Data>
    Value(Data data) : concept_{new Model<Data>(std::move(data))} {}
    Value(Value const & other) : concept_(other.concept_->clone()) {}
    Value & operator=(Value const & other)
    {
        concept_.reset(other.concept_->clone());
        return *this;
    }
    Value(Value && other) = default;
    Value & operator=(Value && other) = default;

    template<typename Data>
    Data const * get() const
    {
        if(auto model = dynamic_cast<Model<Data> *>(concept_.get()))
            return &model->data;
        return nullptr;
    }
    template<typename Data>
    Data * get()
    {
        // return const_cast<Data *>(
        //     const_cast<Value const *>(this)->get<Data>()
        // );
        return std::as_const(*this).get<Data>();
    }

    friend std::ostream & operator<<(
        std::ostream & ostream, Value const & value
    )
    {
        value.concept_->print(ostream);
        return ostream;
    }

private:

    struct Concept
    {
        virtual ~Concept() = default;
        virtual Concept * clone() const = 0;
        virtual void print(std::ostream & ostream) const = 0;
    };

    template<typename Data>
    struct Model : Concept
    {
        Model(Data data) : data{std::move(data)} {}
        Concept * clone() const override
        {
            return new Model<Data>(*this);
        }
        void print(std::ostream & ostream) const override
        {
            ostream << data;
        }
        Data data;
    };

    std::unique_ptr<Concept> concept_;
};


// List

using List = std::vector<Value>;


std::ostream & operator<<(std::ostream & ostream, List const & list)
{
    ostream << "[ ";
    for (auto const & elem : list)
        ostream << elem << " ";
    ostream << "]";
    return ostream;
}


// Operation

class Operation
{
public:

    virtual ~Operation() = default;
    virtual void operator()(List & value) = 0;
};

class DuplicateOperation : public Operation
{
public:

    void operator()(List & list) override
    {
        list.reserve(2 * list.size());
        std::copy(list.begin(), list.end(), std::back_inserter(list));
    }
};

template<typename Predicate>
class FilterOperation : Operation
{
public:

    FilterOperation(Predicate predicate) : predicate_(std::move(predicate)) {}

    void operator()(List & list) override
    {
        list.erase(
            std::remove_if(
                list.begin(),
                list.end(),
                [this](Value const & value)
                {
                    return !predicate_(value);
                }
            ),
            list.end()
        );
    }

private:

    Predicate predicate_;
};

class ReplaceOperation : Operation
{
public:

    using Map = std::unordered_map<std::string, std::string>;

    ReplaceOperation(Map map) : map_(std::move(map)) {}

    void operator()(List & list) override
    {
        for (auto & elem : list)
            if (auto * string = elem.get<std::string>())
                if (auto it = map_.find(*string); it != map_.end())
                    *string = it->second;
    }

private:

     Map map_;
};


/*

Testcase 1:
[ 0 "Hello" 1 "World!" ]
After filtering out only strings:
[ "Hello" "World!" ]
After replace text:
[ "Howdy" "World!" ]

Testcase 2:
[ 1 -5 2 "-3" ]
After duplication:
[ 1 -5 2 "-3" 1 -5 2 "-3" ]
After filtering out positive integers:
[ 1 2 "-3" 1 2 "-3" ]

Testcase 3:
[ "A" [ "B" "C" ] "D" ]
After duplication:
[ "A" [ "B" "C" ] "D" "A" [ "B" "C" ] "D" ]

Testcase 4:
[ 3.14 ]
After assignment:
[ "Something else" ]

*/

bool is_string(Value const & value)
{
    return value.get<std::string>();
}

bool is_nonint_or_nonnegative(Value const & value)
{
    if (auto * i = value.get<int>())
        return *i >= 0;
    return true;
}

int main()
{
    {
        auto list = List{0, "Hello"s, 1, "World!"s};

        std::cout << "Testcase 1:" << std::endl;
        std::cout << list << std::endl;

        std::cout << "After filtering out only strings:" << std::endl;
        auto op1 = FilterOperation{is_string};
        op1(list);
        std::cout << list << std::endl;

        std::cout << "After replace text:" << std::endl;
        auto op2 = ReplaceOperation{{
            {"Hello", "Howdy"},
        }};
        op2(list);
        std::cout << list << std::endl;
    }
    std::cout << std::endl;
    {
        auto list = List{1, -5, 2, "-3"s};

        std::cout << "Testcase 2:" << std::endl;
        std::cout << list << std::endl;

        std::cout << "After duplication:" << std::endl;
        auto op1 = DuplicateOperation{};
        op1(list);
        std::cout << list << std::endl;

        std::cout << "After filtering out positive integers:" << std::endl;
        auto op2 = FilterOperation{is_nonint_or_nonnegative};
        op2(list);
        std::cout << list << std::endl;
    }
    std::cout << std::endl;
    {
        auto list = List{"A"s, List{"B"s, "C"s}, "D"s};

        std::cout << "Testcase 3:" << std::endl;
        std::cout << list << std::endl;

        std::cout << "After duplication:" << std::endl;
        auto op1 = DuplicateOperation{};
        op1(list);
        std::cout << list << std::endl;
    }
    std::cout << std::endl;
    {
        auto list = List{3.14};

        std::cout << "Testcase 4:" << std::endl;
        std::cout << list << std::endl;

        std::cout << "After assignment:" << std::endl;
        list[0] = "And now for something completely different"s;
        std::cout << list << std::endl;
    }
}