#pragma once

#include "gp/containers/vector.hpp"
#include "gp/functional/optional.hpp"
#include "gp/ipc/bottleneck.hpp"
#include "gp/utils/allocators/allocator.hpp"
#include "gp/utils/allocators/buddy.hpp"
#include "gp/utils/pointers.hpp"

namespace gp {
    /*class abstract_sender_erasor {
        virtual ~abstract_sender_erasor() = 0;
        virtual bool try_send(const gp::buffer<char> message) = 0;
    };

    class abstract_sender_erasor_c : abstract_sender_erasor {
        virtual abstract_sender_erasor_c* clone(gp::allocator&) = 0;
        virtual ~abstract_sender_erasor_c() = 0;
    };

    template<typename predicate>
    class abstract_receiver_erasor {
        [[no_unique_address]] predicate v;
        virtual abstract_receiver_erasor* clone(abstract_receiver_erasor&, gp::allocator&) = 0;
        virtual ~abstract_receiver_erasor() = 0;
        virtual gp::optional<gp::vector<char>> try_receive(allocator& allocate) = 0;
    };

    class abstract_receiver_erasor_c : abstract_sender_erasor {
        virtual abstract_receiver_erasor_c* clone(gp::allocator&) = 0;
        virtual ~abstract_receiver_erasor_c() = 0;
    };

    template<std::copyable T>
    class impl_channel_sender : abstract_sender_erasor_c {
        T& value;

        impl_channel_sender(T& cpy)
        : value(cpy)
        {};

        impl_channel_sender(T&& cpy) 
        requires std::movable<T>
        : value(gp::forward<T>(cpy))
        {};

        virtual abstract_sender_erasor_c* clone(gp::allocator& allocator) {

        }
    };*/



    /**
     * @brief 
     */
    class basic_channel {
        allocator& self_allocator; //< This is from whatever created the channel and MUST outlive the channel
        gp::unique_ptr<allocator> local_allocator_impl; //< The allocator used here
        gp::vector<gp::vector<char>> data;
        gp::fast_bottleneck lock;
        
    public:
        /**
         * @brief Construct a new channel object
         * 
         * @param memory_source Where do we allocate from for this channel
         * @param memory_available The amount of memory to allocate, default specified in the configuration
         */
        basic_channel(allocator& memory_source, size_t memory_available = gp_config::limits::channel_default_size)
        : self_allocator(memory_source)
        , local_allocator_impl(
            gp::unique_ptr<gp::buddy<math::log2(gp_config::limits::channel_max_size/16),16>>
                ::make(self_allocator, memory_available, self_allocator)
                .cast<allocator>()
        )
        , data(*local_allocator_impl)
        {}

        /**
         * @brief Sends a message on the channel
         * 
         * @param message The message to transmit through the channel, this implies two copies. Empty messages are invalid and hence not transmitted.
         * @return true if the message was successfully sent
         * @return false if the message was not sent
         */
        bool try_send(const gp::buffer<char> message) {
            if(message.size() == 0) return false;
            auto guard = lock_guard(lock);
            auto v = gp::vector<char>(*local_allocator_impl);
            if(not v.reserve(message.size())) return false;
            for(char c : message) {
                v.push_back(c);
            }
            if(not data.push_back(v)) return false;
            if((*data.rbegin()).size() == 0) return false;
            return true;
        }

        /**
         * @brief Tries to receive a message that matches a certain predicate
         * 
         * @tparam predicate represents the predicate type, you will probably never specify that by hand
         * @param allocate The allocator to allocate the response from
         * @param pred The predicate
         * @return gp::optional<gp::vector<char>> if the receive failed, this will be empty, else it will contain the message
         */
        template<typename predicate>
        gp::optional<gp::vector<char>> try_receive(
            allocator& allocate,
            predicate pred = [](gp::buffer<char>){return true;}
        ) {
            for(auto it = data.begin(); it != data.end(); ++it) {
                auto& elem = *it;
                if(predicate(elem.as_buffer())) {
                    auto v = gp::vector<char>(allocate);
                    if(not v.reserve(elem.size())) return nullopt;
                    for(char c : elem) {
                        v.push_back(c);
                    }
                    data.remove(it);
                    return gp::move(v);
                }
            }
            return nullopt;
        }
    };
}