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

#include <iostream>
#include <memory>
#include <string>

#include "bridge_abstract.h"
#include "../pipe.h"
#include "../threads.h"
#include "../serialize.h"
#include "../sockets.h"
#include "../sockstreambuf.h"
#include "../logger.h"
#include "../algs.h"


namespace dlib
{

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

    struct connect_to_ip_and_port
    {
        connect_to_ip_and_port (
            const std::string& ip_,
            unsigned short port_
        ): ip(ip_), port(port_)
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(is_ip_address(ip) && port != 0,
                "\t connect_to_ip_and_port()"
                << "\n\t Invalid inputs were given to this function"
                << "\n\t ip:   " << ip 
                << "\n\t port: " << port
                << "\n\t this: " << this
                );
        }

    private:
        friend class bridge;
        const std::string ip;
        const unsigned short port;
    };

    inline connect_to_ip_and_port connect_to (
        const network_address& addr
    )
    {
        // make sure requires clause is not broken
        DLIB_ASSERT(addr.port != 0,
            "\t connect_to_ip_and_port()"
            << "\n\t The TCP port to connect to can't be 0."
            << "\n\t addr.port: " << addr.port
            );

        if (is_ip_address(addr.host_address))
        {
            return connect_to_ip_and_port(addr.host_address, addr.port);
        }
        else
        {
            std::string ip;
            if(hostname_to_ip(addr.host_address,ip))
                throw socket_error(ERESOLVE,"unable to resolve '" + addr.host_address + "' in connect_to()");

            return connect_to_ip_and_port(ip, addr.port);
        }
    }

    struct listen_on_port
    {
        listen_on_port(
            unsigned short port_
        ) : port(port_) 
        {
            // make sure requires clause is not broken
            DLIB_ASSERT( port != 0,
                "\t listen_on_port()"
                << "\n\t Invalid inputs were given to this function"
                << "\n\t port: " << port
                << "\n\t this: " << this
                );
        }

    private:
        friend class bridge;
        const unsigned short port;
    };

    template <typename pipe_type>
    struct bridge_transmit_decoration
    {
        bridge_transmit_decoration ( 
            pipe_type& p_
        ) : p(p_) {}

    private:
        friend class bridge;
        pipe_type& p;
    };

    template <typename pipe_type>
    bridge_transmit_decoration<pipe_type> transmit ( pipe_type& p) { return bridge_transmit_decoration<pipe_type>(p); }

    template <typename pipe_type>
    struct bridge_receive_decoration
    {
        bridge_receive_decoration ( 
            pipe_type& p_
        ) : p(p_) {}

    private:
        friend class bridge;
        pipe_type& p;
    };

    template <typename pipe_type>
    bridge_receive_decoration<pipe_type> receive ( pipe_type& p) { return bridge_receive_decoration<pipe_type>(p); }

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

    struct bridge_status
    {
        bridge_status() : is_connected(false), foreign_port(0){}

        bool is_connected;
        unsigned short foreign_port;
        std::string foreign_ip;
    };

    inline void serialize ( const bridge_status& , std::ostream& )
    {
        throw serialization_error("It is illegal to serialize bridge_status objects.");
    }

    inline void deserialize ( bridge_status& , std::istream& )
    {
        throw serialization_error("It is illegal to serialize bridge_status objects.");
    }

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

    namespace impl_brns
    {
        class impl_bridge_base
        {
        public:

            virtual ~impl_bridge_base() {}

            virtual bridge_status get_bridge_status (
            ) const = 0;
        };

        template <
            typename transmit_pipe_type,
            typename receive_pipe_type
            >
        class impl_bridge : public impl_bridge_base, private noncopyable, private multithreaded_object
        {
            /*!
                CONVENTION
                    - if (list) then
                        - this object is supposed to be listening on the list object for incoming
                          connections when not connected.
                    - else
                        - this object is supposed to be attempting to connect to ip:port when
                          not connected.

                    - get_bridge_status() == current_bs
            !*/
        public:

            impl_bridge (
                unsigned short listen_port,
                transmit_pipe_type* transmit_pipe_,
                receive_pipe_type* receive_pipe_
            ) :
                s(m),
                receive_thread_active(false),
                transmit_thread_active(false),
                port(0),
                transmit_pipe(transmit_pipe_),
                receive_pipe(receive_pipe_),
                dlog("dlib.bridge"),
                keepalive_code(0),
                message_code(1)
            {
                int status = create_listener(list, listen_port);
                if (status == PORTINUSE)
                {
                    std::ostringstream sout;
                    sout << "Error, the port " << listen_port << " is already in use.";
                    throw socket_error(EPORT_IN_USE, sout.str());
                }
                else if (status == OTHER_ERROR)
                {
                    throw socket_error("Unable to create listening socket for an unknown reason.");
                }

                register_thread(*this, &impl_bridge::transmit_thread);
                register_thread(*this, &impl_bridge::receive_thread);
                register_thread(*this, &impl_bridge::connect_thread);

                start();
            }

            impl_bridge (
                const std::string ip_,
                unsigned short port_,
                transmit_pipe_type* transmit_pipe_,
                receive_pipe_type* receive_pipe_
            ) :
                s(m),
                receive_thread_active(false),
                transmit_thread_active(false),
                port(port_),
                ip(ip_),
                transmit_pipe(transmit_pipe_),
                receive_pipe(receive_pipe_),
                dlog("dlib.bridge"),
                keepalive_code(0),
                message_code(1)
            {
                register_thread(*this, &impl_bridge::transmit_thread);
                register_thread(*this, &impl_bridge::receive_thread);
                register_thread(*this, &impl_bridge::connect_thread);

                start();
            }

            ~impl_bridge()
            {
                // tell the threads to terminate
                stop();

                // save current pipe enabled status so we can restore it to however
                // it was before this destructor ran.
                bool transmit_enabled = true;
                bool receive_enabled = true;

                // make any calls blocked on a pipe return immediately.
                if (transmit_pipe)
                {
                    transmit_enabled = transmit_pipe->is_dequeue_enabled();
                    transmit_pipe->disable_dequeue();
                }
                if (receive_pipe)
                {
                    receive_enabled = receive_pipe->is_enqueue_enabled();
                    receive_pipe->disable_enqueue();
                }

                {
                    auto_mutex lock(m);
                    s.broadcast();
                    // Shutdown the connection if we have one.  This will cause
                    // all blocked I/O calls to return an error.
                    if (con)
                        con->shutdown();
                }

                // wait for all the threads to terminate.
                wait();

                if (transmit_pipe && transmit_enabled)
                    transmit_pipe->enable_dequeue();
                if (receive_pipe && receive_enabled)
                    receive_pipe->enable_enqueue();
            }

            bridge_status get_bridge_status (
            ) const
            {
                auto_mutex lock(current_bs_mutex);
                return current_bs;
            }

        private:


            template <typename pipe_type>
            std::enable_if_t<std::is_convertible<bridge_status, typename pipe_type::type>::value>  enqueue_bridge_status (
                pipe_type* p,
                const bridge_status& status
            )
            {
                if (p)
                {
                    typename pipe_type::type temp(status);
                    p->enqueue(temp);
                }
            }

            template <typename pipe_type>
            std::enable_if_t<!std::is_convertible<bridge_status, typename pipe_type::type>::value>  enqueue_bridge_status (
                pipe_type* ,
                const bridge_status& 
            )
            {
            }

            void connect_thread (
            )
            {
                while (!should_stop())
                {
                    auto_mutex lock(m);
                    int status = OTHER_ERROR;
                    if (list)
                    {
                        do
                        {
                            status = list->accept(con, 1000);
                        } while (status == TIMEOUT && !should_stop());
                    }
                    else
                    {
                        status = create_connection(con, port, ip);
                    }
                    
                    if (should_stop())
                        break;

                    if (status != 0)
                    {
                        // The last connection attempt failed.  So pause for a little bit before making another attempt.
                        s.wait_or_timeout(2000);
                        continue;
                    }

                    dlog << LINFO << "Established new connection to " << con->get_foreign_ip() << ":" << con->get_foreign_port() << ".";

                    bridge_status temp_bs;
                    {   auto_mutex lock(current_bs_mutex);
                        current_bs.is_connected = true;
                        current_bs.foreign_port = con->get_foreign_port();
                        current_bs.foreign_ip = con->get_foreign_ip();
                        temp_bs = current_bs;
                    }
                    enqueue_bridge_status(receive_pipe, temp_bs);


                    receive_thread_active = true;
                    transmit_thread_active = true;

                    s.broadcast();

                    // Wait for the transmit and receive threads to end before we continue.
                    // This way we don't invalidate the con pointer while it is in use.
                    while (receive_thread_active || transmit_thread_active)
                        s.wait();


                    dlog << LINFO << "Closed connection to " << con->get_foreign_ip() << ":" << con->get_foreign_port() << ".";
                    {   auto_mutex lock(current_bs_mutex);
                        current_bs.is_connected = false;
                        current_bs.foreign_port = con->get_foreign_port();
                        current_bs.foreign_ip = con->get_foreign_ip();
                        temp_bs = current_bs;
                    }
                    enqueue_bridge_status(receive_pipe, temp_bs);
                }

            }


            void receive_thread (
            )
            {
                while (true)
                {
                    // wait until we have a connection
                    {   auto_mutex lock(m);
                        while (!receive_thread_active && !should_stop())
                        {
                            s.wait();
                        }

                        if (should_stop())
                            break;
                    }



                    try
                    {
                        if (receive_pipe)
                        {
                            sockstreambuf buf(con);
                            std::istream in(&buf);
                            typename receive_pipe_type::type item;
                            // This isn't necessary but doing it avoids a warning about
                            // item being uninitialized sometimes.
                            assign_zero_if_built_in_scalar_type(item);

                            while (in.peek() != EOF)
                            {
                                unsigned char code;
                                in.read((char*)&code, sizeof(code));
                                if (code == message_code)
                                {
                                    deserialize(item, in);
                                    receive_pipe->enqueue(item);
                                }
                            }
                        }
                        else
                        {
                            // Since we don't have a receive pipe to put messages into we will
                            // just read the bytes from the connection and ignore them.
                            char buf[1000];
                            while (con->read(buf, sizeof(buf)) > 0) ;
                        }
                    }
                    catch (std::bad_alloc& )
                    {
                        dlog << LERROR << "std::bad_alloc thrown while deserializing message from " 
                            << con->get_foreign_ip() << ":" << con->get_foreign_port();
                    }
                    catch (dlib::serialization_error& e)
                    {
                        dlog << LERROR << "dlib::serialization_error thrown while deserializing message from " 
                            << con->get_foreign_ip() << ":" << con->get_foreign_port() 
                            << ".\nThe exception error message is: \n" << e.what();
                    }
                    catch (std::exception& e)
                    {
                        dlog << LERROR << "std::exception thrown while deserializing message from " 
                            << con->get_foreign_ip() << ":" << con->get_foreign_port() 
                            << ".\nThe exception error message is: \n" << e.what();
                    }




                    con->shutdown();
                    auto_mutex lock(m);
                    receive_thread_active = false;
                    s.broadcast();
                }

                auto_mutex lock(m);
                receive_thread_active = false;
                s.broadcast();
            }

            void transmit_thread (
            )
            {
                while (true)
                {
                    // wait until we have a connection
                    {   auto_mutex lock(m);
                        while (!transmit_thread_active && !should_stop())
                        {
                            s.wait();
                        }

                        if (should_stop())
                            break;
                    }



                    try
                    {
                        sockstreambuf buf(con);
                        std::ostream out(&buf);
                        typename transmit_pipe_type::type item;
                        // This isn't necessary but doing it avoids a warning about
                        // item being uninitialized sometimes.
                        assign_zero_if_built_in_scalar_type(item);


                        while (out)
                        {
                            bool dequeue_timed_out = false;
                            if (transmit_pipe )
                            {
                                if (transmit_pipe->dequeue_or_timeout(item,1000))
                                {
                                    out.write((char*)&message_code, sizeof(message_code));
                                    serialize(item, out);
                                    if (transmit_pipe->size() == 0)
                                        out.flush();

                                    continue;
                                }

                                dequeue_timed_out = (transmit_pipe->is_enabled() && transmit_pipe->is_dequeue_enabled());
                            }

                            // Pause for about a second.  Note that we use a wait_or_timeout() call rather 
                            // than sleep() here because we want to wake up immediately if this object is 
                            // being destructed rather than hang for a second.
                            if (!dequeue_timed_out)
                            {
                                auto_mutex lock(m);
                                if (should_stop())
                                    break;

                                s.wait_or_timeout(1000);
                            }
                            // Just send the keepalive byte periodically so we can
                            // tell if the connection is alive. 
                            out.write((char*)&keepalive_code, sizeof(keepalive_code));
                            out.flush();
                        }
                    }
                    catch (std::bad_alloc& )
                    {
                        dlog << LERROR << "std::bad_alloc thrown while serializing message to " 
                            << con->get_foreign_ip() << ":" << con->get_foreign_port();
                    }
                    catch (dlib::serialization_error& e)
                    {
                        dlog << LERROR << "dlib::serialization_error thrown while serializing message to " 
                            << con->get_foreign_ip() << ":" << con->get_foreign_port() 
                            << ".\nThe exception error message is: \n" << e.what();
                    }
                    catch (std::exception& e)
                    {
                        dlog << LERROR << "std::exception thrown while serializing message to " 
                            << con->get_foreign_ip() << ":" << con->get_foreign_port() 
                            << ".\nThe exception error message is: \n" << e.what();
                    }




                    con->shutdown();
                    auto_mutex lock(m);
                    transmit_thread_active = false;
                    s.broadcast();
                }

                auto_mutex lock(m);
                transmit_thread_active = false;
                s.broadcast();
            }

            mutex m;
            signaler s;
            bool receive_thread_active;
            bool transmit_thread_active;
            std::unique_ptr<connection> con;
            std::unique_ptr<listener> list;
            const unsigned short port;
            const std::string ip;
            transmit_pipe_type* const transmit_pipe;
            receive_pipe_type* const receive_pipe;
            logger dlog;
            const unsigned char keepalive_code;
            const unsigned char message_code;

            mutex current_bs_mutex;
            bridge_status current_bs;
        };
    }


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

    class bridge : noncopyable
    {
    public:

        bridge () {}

        template < typename T, typename U, typename V >
        bridge (
            T network_parameters,
            U pipe1,
            V pipe2 
        ) { reconfigure(network_parameters,pipe1,pipe2); }

        template < typename T, typename U>
        bridge (
            T network_parameters,
            U pipe 
        ) { reconfigure(network_parameters,pipe); }


        void clear (
        )
        {
            pimpl.reset();
        }

        template < typename T, typename R >
        void reconfigure (
            listen_on_port network_parameters,
            bridge_transmit_decoration<T> transmit_pipe,
            bridge_receive_decoration<R> receive_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<T,R>(network_parameters.port, &transmit_pipe.p, &receive_pipe.p)); }

        template < typename T, typename R >
        void reconfigure (
            listen_on_port network_parameters,
            bridge_receive_decoration<R> receive_pipe,
            bridge_transmit_decoration<T> transmit_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<T,R>(network_parameters.port, &transmit_pipe.p, &receive_pipe.p)); }

        template < typename T >
        void reconfigure (
            listen_on_port network_parameters,
            bridge_transmit_decoration<T> transmit_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<T,T>(network_parameters.port, &transmit_pipe.p, 0)); }

        template < typename R >
        void reconfigure (
            listen_on_port network_parameters,
            bridge_receive_decoration<R> receive_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<R,R>(network_parameters.port, 0, &receive_pipe.p)); }




        template < typename T, typename R >
        void reconfigure (
            connect_to_ip_and_port network_parameters,
            bridge_transmit_decoration<T> transmit_pipe,
            bridge_receive_decoration<R> receive_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<T,R>(network_parameters.ip, network_parameters.port, &transmit_pipe.p, &receive_pipe.p)); }

        template < typename T, typename R >
        void reconfigure (
            connect_to_ip_and_port network_parameters,
            bridge_receive_decoration<R> receive_pipe,
            bridge_transmit_decoration<T> transmit_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<T,R>(network_parameters.ip, network_parameters.port, &transmit_pipe.p, &receive_pipe.p)); }

        template < typename R >
        void reconfigure (
            connect_to_ip_and_port network_parameters,
            bridge_receive_decoration<R> receive_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<R,R>(network_parameters.ip, network_parameters.port, 0, &receive_pipe.p)); }

        template < typename T >
        void reconfigure (
            connect_to_ip_and_port network_parameters,
            bridge_transmit_decoration<T> transmit_pipe
        ) { pimpl.reset(); pimpl.reset(new impl_brns::impl_bridge<T,T>(network_parameters.ip, network_parameters.port, &transmit_pipe.p, 0)); }


        bridge_status get_bridge_status (
        ) const
        {
            if (pimpl)
                return pimpl->get_bridge_status();
            else
                return bridge_status();
        }

    private:

        std::unique_ptr<impl_brns::impl_bridge_base> pimpl;
    };

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

}

#endif // DLIB_BRIDGe_Hh_

