// Copyright (C) 2012  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_BSP_CPph_
#define DLIB_BSP_CPph_

#include "bsp.h"
#include <memory>
#include <stack>

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

namespace dlib
{

    namespace impl1
    {

        void connect_all (
            map_id_to_con& cons,
            const std::vector<network_address>& hosts,
            unsigned long node_id
        )
        {
            cons.clear();
            for (unsigned long i = 0; i < hosts.size(); ++i)
            {
                std::unique_ptr<bsp_con> con(new bsp_con(hosts[i]));
                dlib::serialize(node_id, con->stream); // tell the other end our node_id
                unsigned long id = i+1;
                cons.add(id, con);
            }
        }

        void connect_all_hostinfo (
            map_id_to_con& cons,
            const std::vector<hostinfo>& hosts,
            unsigned long node_id,
            std::string& error_string 
        )
        {
            cons.clear();
            for (unsigned long i = 0; i < hosts.size(); ++i)
            {
                try
                {
                    std::unique_ptr<bsp_con> con(new bsp_con(hosts[i].addr));
                    dlib::serialize(node_id, con->stream); // tell the other end our node_id
                    con->stream.flush();
                    unsigned long id = hosts[i].node_id;
                    cons.add(id, con);
                }
                catch (std::exception&)
                {
                    std::ostringstream sout;
                    sout << "Could not connect to " << hosts[i].addr;
                    error_string = sout.str();
                    break;
                }
            }
        }


        void send_out_connection_orders (
            map_id_to_con& cons,
            const std::vector<network_address>& hosts
        )
        {
            // tell everyone their node ids
            cons.reset();
            while (cons.move_next())
            {
                dlib::serialize(cons.element().key(), cons.element().value()->stream);
            }

            // now tell them who to connect to
            std::vector<hostinfo> targets; 
            for (unsigned long i = 0; i < hosts.size(); ++i)
            {
                hostinfo info(hosts[i], i+1);

                dlib::serialize(targets, cons[info.node_id]->stream);
                targets.push_back(info);

                // let the other host know how many incoming connections to expect
                const unsigned long num = hosts.size()-targets.size();
                dlib::serialize(num, cons[info.node_id]->stream);
                cons[info.node_id]->stream.flush();
            }
        }

    // ------------------------------------------------------------------------------------


    }

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    namespace impl2
    {
        // These control bytes are sent before each message between nodes.  Note that many
        // of these are only sent between the control node (node 0) and the other nodes.
        // This is because the controller node is responsible for handling the
        // synchronization that needs to happen when all nodes block on calls to
        // receive_data()
        // at the same time.

        // denotes a normal content message.
        const static char MESSAGE_HEADER            = 0; 

        // sent to the controller node when someone receives a message via receive_data().
        const static char GOT_MESSAGE               = 1; 

        // sent to the controller node when someone sends a message via send().
        const static char SENT_MESSAGE              = 2; 

        // sent to the controller node when someone enters a call to receive_data()
        const static char IN_WAITING_STATE          = 3; 

        // broadcast when a node terminates itself. 
        const static char NODE_TERMINATE            = 5; 

        // broadcast by the controller node when it determines that all nodes are blocked
        // on calls to receive_data() and there aren't any messages in flight.  This is also
        // what makes us go to the next epoch.
        const static char SEE_ALL_IN_WAITING_STATE  = 6; 

        // This isn't ever transmitted between nodes.  It is used internally to indicate
        // that an error occurred.
        const static char READ_ERROR                = 7;

    // ------------------------------------------------------------------------------------

        void read_thread (
            impl1::bsp_con* con,
            unsigned long node_id,
            unsigned long sender_id,
            impl1::thread_safe_message_queue& msg_buffer
        )
        {
            try
            {
                while(true)
                {
                    impl1::msg_data msg;
                    deserialize(msg.msg_type, con->stream);
                    msg.sender_id = sender_id;

                    if (msg.msg_type == MESSAGE_HEADER)
                    {
                        msg.data.reset(new std::vector<char>);
                        deserialize(msg.epoch, con->stream);
                        deserialize(*msg.data, con->stream);
                    }

                    msg_buffer.push_and_consume(msg);

                    if (msg.msg_type == NODE_TERMINATE)
                        break;
                }
            }
            catch (std::exception& e)
            {
                impl1::msg_data msg;
                msg.data.reset(new std::vector<char>);
                vectorstream sout(*msg.data);
                sout << "An exception was thrown while attempting to receive a message from processing node " << sender_id << ".\n";
                sout << "  Sending processing node address:   " << con->con->get_foreign_ip() << ":" << con->con->get_foreign_port() << std::endl;
                sout << "  Receiving processing node address: " << con->con->get_local_ip() << ":" << con->con->get_local_port() << std::endl;
                sout << "  Receiving processing node id:      " << node_id << std::endl;
                sout << "  Error message in the exception:    " << e.what() << std::endl;

                msg.sender_id = sender_id;
                msg.msg_type = READ_ERROR;

                msg_buffer.push_and_consume(msg);
            }
            catch (...)
            {
                impl1::msg_data msg;
                msg.data.reset(new std::vector<char>);
                vectorstream sout(*msg.data);
                sout << "An exception was thrown while attempting to receive a message from processing node " << sender_id << ".\n";
                sout << "  Sending processing node address:   " << con->con->get_foreign_ip() << ":" << con->con->get_foreign_port() << std::endl;
                sout << "  Receiving processing node address: " << con->con->get_local_ip() << ":" << con->con->get_local_port() << std::endl;
                sout << "  Receiving processing node id:      " << node_id << std::endl;

                msg.sender_id = sender_id;
                msg.msg_type = READ_ERROR;

                msg_buffer.push_and_consume(msg);
            }
        }

    // ------------------------------------------------------------------------------------

    }
    
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
//                          IMPLEMENTATION OF bsp_context OBJECT MEMBERS
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    void bsp_context::
    close_all_connections_gracefully(
    )
    {
        if (node_id() != 0)
        {
            _cons.reset();
            while (_cons.move_next())
            {
                // tell the other end that we are intentionally dropping the connection
                serialize(impl2::NODE_TERMINATE,_cons.element().value()->stream);
                _cons.element().value()->stream.flush();
            }
        }

        impl1::msg_data msg;
        // now wait for all the other nodes to terminate
        while (num_terminated_nodes < _cons.size() )
        {
            if (node_id() == 0 && num_waiting_nodes + num_terminated_nodes == _cons.size() && outstanding_messages == 0)
            {
                num_waiting_nodes = 0;
                broadcast_byte(impl2::SEE_ALL_IN_WAITING_STATE);
                ++current_epoch;
            }

            if (!msg_buffer.pop(msg))
                throw dlib::socket_error("Error reading from msg_buffer in dlib::bsp_context.");

            if (msg.msg_type == impl2::NODE_TERMINATE)
            {
                ++num_terminated_nodes;
                _cons[msg.sender_id]->terminated = true;
            }
            else if (msg.msg_type == impl2::READ_ERROR)
            {
                throw dlib::socket_error(msg.data_to_string());
            }
            else if (msg.msg_type == impl2::MESSAGE_HEADER)
            {
                throw dlib::socket_error("A BSP node received a message after it has terminated.");
            }
            else if (msg.msg_type == impl2::GOT_MESSAGE)
            {
                --num_waiting_nodes;
                --outstanding_messages;
            }
            else if (msg.msg_type == impl2::SENT_MESSAGE)
            {
                ++outstanding_messages;
            }
            else if (msg.msg_type == impl2::IN_WAITING_STATE)
            {
                ++num_waiting_nodes;
            }
        }

        if (node_id() == 0)
        {
            _cons.reset();
            while (_cons.move_next())
            {
                // tell the other end that we are intentionally dropping the connection
                serialize(impl2::NODE_TERMINATE,_cons.element().value()->stream);
                _cons.element().value()->stream.flush();
            }

            if (outstanding_messages != 0)
            {
                std::ostringstream sout;
                sout << "A BSP job was allowed to terminate before all sent messages have been received.\n";
                sout << "There are at least " << outstanding_messages << " messages still in flight.   Make sure all sent messages\n";
                sout << "have a corresponding call to receive().";
                throw dlib::socket_error(sout.str());
            }
        }
    }

// ----------------------------------------------------------------------------------------

    bsp_context::
    ~bsp_context()
    {
        _cons.reset();
        while (_cons.move_next())
        {
            _cons.element().value()->con->shutdown();
        }

        msg_buffer.disable();

        // this will wait for all the threads to terminate
        threads.clear();
    }

// ----------------------------------------------------------------------------------------

    bsp_context::
    bsp_context(
        unsigned long node_id_,
        impl1::map_id_to_con& cons_
    ) :
        outstanding_messages(0),
        num_waiting_nodes(0),
        num_terminated_nodes(0),
        current_epoch(1),
        _cons(cons_),
        _node_id(node_id_)
    {
        // spawn a bunch of read threads, one for each connection
        _cons.reset();
        while (_cons.move_next())
        {
            std::unique_ptr<thread_function> ptr(new thread_function(&impl2::read_thread,
                                                                _cons.element().value().get(),
                                                                _node_id,
                                                                _cons.element().key(),
                                                                ref(msg_buffer)));
            threads.push_back(ptr);
        }

    }

// ----------------------------------------------------------------------------------------

    bool bsp_context::
    receive_data (
        std::shared_ptr<std::vector<char> >& item,
        unsigned long& sending_node_id
    ) 
    {
        notify_control_node(impl2::IN_WAITING_STATE);

        while (true)
        {
            // If there aren't any nodes left to give us messages then return right now.
            // We need to check the msg_buffer size to make sure there aren't any
            // unprocessed message there.  Recall that this can happen because status
            // messages always jump to the front of the message buffer.  So we might have
            // learned about the node terminations before processing their messages for us.
            if (num_terminated_nodes == _cons.size() && msg_buffer.size() == 0)
            {
                return false;
            }

            // if all running nodes are currently blocking forever on receive_data()
            if (node_id() == 0 && outstanding_messages == 0 && num_terminated_nodes + num_waiting_nodes == _cons.size())
            {
                num_waiting_nodes = 0;
                broadcast_byte(impl2::SEE_ALL_IN_WAITING_STATE);

                // Note that the reason we have this epoch counter is so we can tell if a
                // sent message is from before or after one of these "all nodes waiting"
                // synchronization events.  If we didn't have the epoch count we would have
                // a race condition where one node gets the SEE_ALL_IN_WAITING_STATE
                // message before others and then sends out a message to another node
                // before that node got the SEE_ALL_IN_WAITING_STATE message.  Then that
                // node would think the normal message came before SEE_ALL_IN_WAITING_STATE
                // which would be bad.
                ++current_epoch;
                return false;
            }

            impl1::msg_data data;
            if (!msg_buffer.pop(data, current_epoch))
                throw dlib::socket_error("Error reading from msg_buffer in dlib::bsp_context.");


            switch(data.msg_type)
            {
                case impl2::MESSAGE_HEADER: {
                    item = data.data;
                    sending_node_id = data.sender_id;
                    notify_control_node(impl2::GOT_MESSAGE);
                    return true;
                } break;

                case impl2::IN_WAITING_STATE: {
                    ++num_waiting_nodes;
                } break;

                case impl2::GOT_MESSAGE: {
                    --outstanding_messages;
                    --num_waiting_nodes;
                } break;

                case impl2::SENT_MESSAGE: {
                    ++outstanding_messages;
                } break;

                case impl2::NODE_TERMINATE: {
                    ++num_terminated_nodes;
                    _cons[data.sender_id]->terminated = true;
                } break;

                case impl2::SEE_ALL_IN_WAITING_STATE: {
                    ++current_epoch;
                    return false;
                } break;

                case impl2::READ_ERROR: {
                    throw dlib::socket_error(data.data_to_string());
                } break;

                default: {
                    throw dlib::socket_error("Unknown message received by dlib::bsp_context");
                } break;
            } // end switch()
        } // end while (true)
    }

// ----------------------------------------------------------------------------------------

    void bsp_context::
    notify_control_node (
        char val
    )
    {
        if (node_id() == 0)
        {
            using namespace impl2;
            switch(val)
            {
                case SENT_MESSAGE: {
                    ++outstanding_messages;
                } break;

                case GOT_MESSAGE: {
                    --outstanding_messages;
                } break;

                case IN_WAITING_STATE: {
                    // nothing to do in this case
                } break;

                default:
                    DLIB_CASSERT(false,"This should never happen");
            }
        }
        else
        {
            serialize(val, _cons[0]->stream);
            _cons[0]->stream.flush();
        }
    }

// ----------------------------------------------------------------------------------------

    void bsp_context::
    broadcast_byte (
        char val
    )
    {
        for (unsigned long i = 0; i < number_of_nodes(); ++i)
        {
            // don't send to yourself or to terminated nodes
            if (i == node_id() || _cons[i]->terminated)
                continue;

            serialize(val, _cons[i]->stream);
            _cons[i]->stream.flush();
        }
    }

// ----------------------------------------------------------------------------------------

    void bsp_context::
    send_data(
        const std::vector<char>& item,
        unsigned long target_node_id
    ) 
    {
        using namespace impl2;
        if (_cons[target_node_id]->terminated)
            throw socket_error("Attempt to send a message to a node that has terminated.");

        serialize(MESSAGE_HEADER, _cons[target_node_id]->stream);
        serialize(current_epoch, _cons[target_node_id]->stream);
        serialize(item, _cons[target_node_id]->stream);
        _cons[target_node_id]->stream.flush();

        notify_control_node(SENT_MESSAGE);
    }

// ----------------------------------------------------------------------------------------

}

#endif // DLIB_BSP_CPph_

