/*
OSCgroups -- open sound control groupcasting infrastructure
Copyright (C) 2005  Ross Bencina

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/


#include "OscGroupClient.h"

#include <cstring>
#include <iostream>
#include <vector>
#include <ctime>
#include <string>
#include <cassert>
#include <cstdlib>

#include "osc/OscReceivedElements.h"
#include "osc/OscOutboundPacketStream.h"
#include "osc/OscPacketListener.h"

#include "ip/UdpSocket.h"
#include "ip/IpEndpointName.h"
#include "ip/PacketListener.h"
#include "ip/TimerListener.h"

#include "md5.h"

#if defined(__BORLANDC__) // workaround for BCB4 release build intrinsics bug
namespace std {
using ::__strcmp__;  // avoid error: E2316 '__strcmp__' is not a member of 'std'.
}
#endif

/*
	There are three sockets:
		externalSocket is used to send and receive data from the network
        including the server and other peers
        
		localRxSocket is used to forward data to the local (client)
        application from other peers

		localTxSocket is used to recieve data from the local (client)
        application to be forwarded to other peers


    Some behavioral rules:

        the choice of which peer endpoint to forward traffic to is made based
        on which endpoints we have received pings from. if pings have been
        received from multiple endpoints preference is given to the private
        endpoint. as soon as a ping has been received we start forwarding data
        
        during the establishment phase we ping all endpoints repeatedly
        even if we have already received pings from an endpoint.

        during the establishment phase we initially send pings faster, and then
        gradually return to a slower rate.

        the establishment phase ends when:
            - we receive a ping from the private endpoint

            - we receive a ping from any endpoint, and
                ESTABLISHMENT_PING_PERIOD_COUNT ping periods have elapsed

        if the server indicates that any of the port or address information of
        the peer has changed, we restart the establishment process.

        if a ping is received from an endpoint which we haven't received from
        before we restart the establishment phase.

        pings are sent less frequently if the channel is being kept open
        by forwarded traffic. We ensure that some traffic is sent accross
        the link every IDLE_PING_PERIOD_SECONDS and that a ping is sent
        accross the link at least every ACTIVE_PING_PERIOD_SECONDS


        if the last time at which the server has heard from a peer
        AND the last time from which we received a ping from the peer
        exceeds PURGE_PEER_TIMEOUT_SECONDS then the peer is removed from
        the peers list.

        
        if we havn't received a ping from a peer in
        PEER_ESTABLISHMENT_RETRY_TIME_SECONDS then we attempt to re-establish
        the connection.



    TODO:

        o- report ping times (requires higher resolution timer i think)


    -----------

        
    
*/




#define PURGE_PEER_TIMEOUT_SECONDS          180              // time before removing peer from active list

#define PEER_ESTABLISHMENT_RETRY_TIME_SECONDS    60

#define IDLE_PING_PERIOD_SECONDS            6              // seconds between pings when the link is idle

#define ACTIVE_PING_PERIOD_SECONDS          30              // seconds between pings when the link is active

#define ESTABLISHMENT_PING_PERIOD_COUNT     5               // 5 pings are always sent to all peer endpoints



struct PeerEndpoint{
    PeerEndpoint()
        : pingReceived( false )
        , sentPingsCount( 0 )
        , forwardedPacketsCount( 0 ) {}

    IpEndpointName endpointName;
    bool pingReceived;
    std::time_t lastPingReceiveTime;

    int sentPingsCount;
    std::time_t lastPingSendTime;

    int forwardedPacketsCount;
    std::time_t lastPacketForwardTime;
};


struct Peer{
    Peer( const char *userName )
        : name( userName )
        , pingPeriodCount( 0 ) {}
        
    std::string name;

    std::time_t lastUserInfoReceiveTime;
    int secondsSinceLastAliveReceivedByServer;
    
    /*
        we maintain three addresses for each peer. the private address is the
        address the peer thinks it is, the public is the address that the
        peer appears as to the server, and the ping address is the address
        that the client appears as to us when we receive a ping from it. this
        last address is necessary for half cone (symmetric) NATs which assign
        the peer a different port to talk to us than the one they assigned to
        talk to the server.
    */

	PeerEndpoint privateEndpoint;
    PeerEndpoint publicEndpoint;
    PeerEndpoint pingEndpoint;

    int pingPeriodCount;
    std::time_t lastPingPeriodTime;

    std::time_t MostRecentActivityTime() const
    {
        // the most recent activity time is the most recent time either the
        // server heard from the peer, or we received a ping from the peer
    
        std::time_t lastUserInfoReceivedByTheServerTime =
                lastUserInfoReceiveTime - secondsSinceLastAliveReceivedByServer; // FIXME: assumes time_t is in seconds

        std::time_t result = lastUserInfoReceivedByTheServerTime;
        if( privateEndpoint.pingReceived )
            result = std::max( result, privateEndpoint.lastPingReceiveTime );
        if( publicEndpoint.pingReceived )
            result = std::max( result, publicEndpoint.lastPingReceiveTime );
        if( pingEndpoint.pingReceived )
            result = std::max( result, pingEndpoint.lastPingReceiveTime );

        return result;
    }

};


static std::vector<Peer> peers_;


class ExternalCommunicationsSender : public TimerListener {
    #define IP_MTU_SIZE 1536
    char aliveBuffer_[IP_MTU_SIZE];
    std::size_t aliveSize_;
    std::time_t lastAliveSentTime_;
    
    char pingBuffer_[IP_MTU_SIZE];
    std::size_t pingSize_;

    UdpSocket& externalSocket_;
    IpEndpointName remoteServerEndpoint_;
    IpEndpointName localToServerEndpoint_;

    std::string userName_;
    std::string userPassword_;
    std::string groupName_;
    std::string groupPassword_;

    void PrepareAliveBuffer()
    {
        osc::OutboundPacketStream p( aliveBuffer_, IP_MTU_SIZE );

        p << osc::BeginBundle();

        p << osc::BeginMessage( "/groupserver/user_alive" )
                    << userName_.c_str()
                    << userPassword_.c_str()
                    << ~((osc::int32)localToServerEndpoint_.address)
                    << (osc::int32)localToServerEndpoint_.port
                    << groupName_.c_str()
                    << groupPassword_.c_str()
                    << osc::EndMessage;

        p << osc::BeginMessage( "/groupserver/get_group_users_info" )
                    << groupName_.c_str()
                    << groupPassword_.c_str()
                    << osc::EndMessage;

        p << osc::EndBundle;

        aliveSize_ = p.Size();
    }

    void SendAlive( std::time_t currentTime )
    {
        int secondsSinceLastAliveSent = (int)std::difftime(currentTime, lastAliveSentTime_);
        if( secondsSinceLastAliveSent >= IDLE_PING_PERIOD_SECONDS ){
           
            externalSocket_.SendTo( remoteServerEndpoint_, aliveBuffer_, aliveSize_ );

            lastAliveSentTime_ = currentTime;
        }   
    }

    void PreparePingBuffer()
    {
        osc::OutboundPacketStream p( pingBuffer_, IP_MTU_SIZE );

        p << osc::BeginBundle();

        p << osc::BeginMessage( "/groupclient/ping" )
                    << userName_.c_str()
                    << osc::EndMessage;

        p << osc::EndBundle;

        pingSize_ = p.Size();
    }

    void SendPing( PeerEndpoint& to, std::time_t currentTime )
	{
		char addressString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
		to.endpointName.AddressAndPortAsString( addressString );
         
        std::cout << "sending ping to " << addressString << "\n";

        externalSocket_.SendTo( to.endpointName, pingBuffer_, pingSize_ );
        ++to.sentPingsCount;
        to.lastPingSendTime = currentTime;
	}

    
    PeerEndpoint* SelectEndpoint( Peer& peer )
    {
        if( peer.privateEndpoint.pingReceived ){

            return &peer.privateEndpoint;

        }else if( peer.publicEndpoint.pingReceived ){

            return &peer.publicEndpoint;

        }else if( peer.pingEndpoint.pingReceived ){

            return &peer.pingEndpoint;
        }

        return 0;
    }


    void PollPeerPingTimer( Peer& peer, std::time_t currentTime, bool executeNowIgnoringTimeouts=false )
    {
        bool noPingsReceivedYet =
                !peer.privateEndpoint.pingReceived
                && !peer.publicEndpoint.pingReceived
                && !peer.pingEndpoint.pingReceived;

        if( !noPingsReceivedYet ){
            // check whether we should attempt to re-establish the link
            // due to no traffic arriving for PEER_ESTABLISHMENT_RETRY_TIME_SECONDS

            std::time_t mostRecentPingTime = 0;
            if( peer.privateEndpoint.pingReceived )
                mostRecentPingTime = std::max( mostRecentPingTime, peer.privateEndpoint.lastPingReceiveTime );
            if( peer.publicEndpoint.pingReceived )
                mostRecentPingTime = std::max( mostRecentPingTime, peer.publicEndpoint.lastPingReceiveTime );
            if( peer.pingEndpoint.pingReceived )
                mostRecentPingTime = std::max( mostRecentPingTime, peer.pingEndpoint.lastPingReceiveTime );
            
            if( (int)std::difftime(currentTime, mostRecentPingTime) > PEER_ESTABLISHMENT_RETRY_TIME_SECONDS ){

                peer.pingPeriodCount = 0;
                executeNowIgnoringTimeouts = true;
            }
        }


        bool inEstablishmentPhase =
                ( ( peer.pingPeriodCount < ESTABLISHMENT_PING_PERIOD_COUNT )
                &&  ( !peer.privateEndpoint.pingReceived ) )
                || noPingsReceivedYet;

        if( inEstablishmentPhase ){
            int pingPeriod;
            if( peer.pingPeriodCount < ESTABLISHMENT_PING_PERIOD_COUNT ){
            
                pingPeriod = (int) (IDLE_PING_PERIOD_SECONDS *
                        ((double)(peer.pingPeriodCount + 1) / (double) ESTABLISHMENT_PING_PERIOD_COUNT));

            }else{
                pingPeriod = IDLE_PING_PERIOD_SECONDS;
            }

            if( currentTime >= (peer.lastPingPeriodTime + pingPeriod)
                    || executeNowIgnoringTimeouts ){
                SendPing( peer.privateEndpoint, currentTime );
                SendPing( peer.publicEndpoint, currentTime );
                if( peer.pingEndpoint.pingReceived )
                    SendPing( peer.pingEndpoint, currentTime );

                peer.lastPingPeriodTime = currentTime;
                ++peer.pingPeriodCount;
            }
        
        }else{

            PeerEndpoint *peerEndpointToUse = SelectEndpoint( peer );
            assert( peerEndpointToUse != 0 );

            bool sendPing = false;

            if( executeNowIgnoringTimeouts ){

                sendPing = true;

            }else{
            
                if( peerEndpointToUse->sentPingsCount == 0 ){

                    sendPing = true;

                }else{

                    int secondsSinceLastPing = (int)std::difftime(currentTime, peerEndpointToUse->lastPingSendTime);

                    if( peerEndpointToUse->forwardedPacketsCount == 0 ){

                        if( secondsSinceLastPing >= IDLE_PING_PERIOD_SECONDS ){
                        
                            sendPing = true;
                        }

                    }else{

                        int secondsSinceLastForwardedTraffic =
                                (int)std::difftime(currentTime, peerEndpointToUse->lastPacketForwardTime);

                        if( secondsSinceLastForwardedTraffic >= IDLE_PING_PERIOD_SECONDS ){

                            if( secondsSinceLastPing >= IDLE_PING_PERIOD_SECONDS ){
                                sendPing = true;
                            }

                        }else if( secondsSinceLastPing >= ACTIVE_PING_PERIOD_SECONDS ){
                            sendPing = true;
                        }
                    }
                }
            }

            if( sendPing ){
                SendPing( *peerEndpointToUse, currentTime );
                peer.lastPingPeriodTime = currentTime;
                ++peer.pingPeriodCount;
            }
        }
    }

    ExternalCommunicationsSender(); // no default ctor
    ExternalCommunicationsSender( const ExternalCommunicationsSender& ); // no copy ctor
    ExternalCommunicationsSender& operator=( const ExternalCommunicationsSender& ); // no assignment operator
    
public:
    ExternalCommunicationsSender( UdpSocket& externalSocket,
			IpEndpointName remoteServerEndpoint,
			int localToRemotePort,
            const char *userName, const char *userPassword,
            const char *groupName, const char *groupPassword )
        : lastAliveSentTime_( 0 )
        , externalSocket_( externalSocket )
		, remoteServerEndpoint_( remoteServerEndpoint )
		, localToServerEndpoint_( 
				externalSocket.LocalEndpointFor( remoteServerEndpoint ).address, 
				localToRemotePort )
        , userName_( userName )
        , userPassword_( userPassword )
        , groupName_( groupName )
        , groupPassword_( groupPassword )
    {
        PrepareAliveBuffer();
        PreparePingBuffer();
    }


    void RestartPeerCommunicationEstablishment( Peer& peer, std::time_t currentTime )
    {
        peer.pingPeriodCount = 0;
        PollPeerPingTimer( peer, currentTime, true );
    }


    void ForwardPacketToAllPeers( const char *data, int size )
    {
        std::time_t currentTime = std::time(0);
        
        for( std::vector<Peer>::iterator i = peers_.begin(); i != peers_.end(); ++i ){

            PeerEndpoint *peerEndpointToUse = SelectEndpoint( *i );
            if( peerEndpointToUse ){
                externalSocket_.SendTo( peerEndpointToUse->endpointName, data, size );
                ++peerEndpointToUse->forwardedPacketsCount;
                peerEndpointToUse->lastPacketForwardTime = currentTime;
            }
        }       
    }

    
    virtual void TimerExpired()
    {        
        std::time_t currentTime = std::time(0);

        SendAlive( currentTime );

        // check for peers to purge, 
        std::vector<Peer>::iterator i = peers_.begin();
        while( i != peers_.end() ){

            if( std::difftime(currentTime,i->MostRecentActivityTime()) > PURGE_PEER_TIMEOUT_SECONDS ){

                i = peers_.erase( i );

            }else{
                PollPeerPingTimer( *i, currentTime );
                ++i;
            }       
        }
    }
};


class ExternalSocketListener : public osc::OscPacketListener {

    void user_alive_status( const osc::ReceivedMessage& m, const IpEndpointName& remoteEndpoint )
    {
        // only accept user_alive_status from the server
        if( remoteEndpoint != remoteServerEndpoint_ )
            return;

        // /groupclient/user_alive_status userName userPassword status

        osc::ReceivedMessageArgumentStream args = m.ArgumentStream();

        const char *userName, *userPassword, *status;

        args >> userName >> userPassword >> status;

        if( std::strcmp( userName, userName_ ) == 0
                &&  std::strcmp( userPassword, userPassword_ ) == 0 ){
            // message really is for us

            if( std::strcmp( status, "ok" ) == 0 ){

                std::cout << "ok: user '" << userName << "' is registered with server\n";

            }else{
                std::cout << "user registration error: server returned status of '" << status
                        << "' for user '" << userName << "'\n";
            }
        }
    }

    void user_group_status( const osc::ReceivedMessage& m, const IpEndpointName& remoteEndpoint )
    {
        // only accept user_alive_status from the server
        if( remoteEndpoint != remoteServerEndpoint_ )
            return;

        // /groupclient/user_group_status userName userPassword groupName groupPassword status

        osc::ReceivedMessageArgumentStream args = m.ArgumentStream();

        const char *userName, *userPassword, *groupName, *groupPassword, *status;

        args >> userName >> userPassword >> groupName >> groupPassword >> status;

        if( std::strcmp( userName, userName_ ) == 0
                && std::strcmp( userPassword, userPassword_ ) == 0
                && std::strcmp( groupName, groupName_ ) == 0
                && std::strcmp( groupPassword, groupPassword_ ) == 0 ){
            // message really is for us

            if( std::strcmp( status, "ok" ) == 0 ){

                std::cout << "ok: user '" << userName << "' is a member of group '" << groupName << "'\n";

            }else{
                std::cout << "group membership error: server returned status of '" << status
                    << "' for user '" << userName
                    << "' membership of group '" << groupName << "'\n";
            }
        }
    }

    void user_info( const osc::ReceivedMessage& m, const IpEndpointName& remoteEndpoint )
    {
        // only accept user_info from the server
        if( remoteEndpoint != remoteServerEndpoint_ )
            return;

        // /groupclient/user_info userName privateIpAddress privatePort
        //      publicIpAddress publicPort secondsSinceLastAlive group0 group1 ...

        osc::ReceivedMessageArgumentStream args = m.ArgumentStream();

        const char *userName;
        osc::int32 privateAddress;
        osc::int32 privatePort;
        osc::int32 publicAddress;
        osc::int32 publicPort;
        osc::int32 secondsSinceLastAlive;

        args >> userName >> privateAddress >> privatePort >>
                publicAddress >> publicPort >> secondsSinceLastAlive;

        // addresses are transmitted as ones complement (bit inverse)
        // to avoid problems with buggy NATs trying to re-write addresses
        privateAddress = ~privateAddress;
        publicAddress = ~publicAddress;

		IpEndpointName privateEndpoint( privateAddress, privatePort );
		IpEndpointName publicEndpoint( publicAddress, publicPort );

		char privateAddressString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
		privateEndpoint.AddressAndPortAsString( privateAddressString );
        char publicAddressString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
		publicEndpoint.AddressAndPortAsString( publicAddressString );
        
        std::cout << "user info received for '" << userName << "', "
            << "private: " << privateAddressString
            << " public: " << publicAddressString
            << "\n";
                
        if( std::strcmp( userName, userName_ ) == 0 )
            return; // discard info referring to ourselves


        bool userIsInGroup = false;
        while( !args.Eos() ){
            const char *groupName;
            args >> groupName;
            if( std::strcmp( groupName, groupName_ ) == 0 ){
                userIsInGroup = true;
                break;
            }
        }
        

        if( userIsInGroup ){
            bool restartPeerCommunicationEstablishment = false;
            
            bool found = false;
            std::vector<Peer>::iterator peer;
            for( std::vector<Peer>::iterator i = peers_.begin(); i != peers_.end(); ++i ){

                if( i->name.compare( userName ) == 0 ){
                    peer = i;
                    found = true;
                    break;
                }
            }

            if( !found ){
                peers_.push_back( Peer( userName ) );
                peer = peers_.end() - 1;
                restartPeerCommunicationEstablishment = true;
            }

            if( peer->privateEndpoint.endpointName != privateEndpoint ){
                peer->privateEndpoint.endpointName = privateEndpoint;
                peer->privateEndpoint.pingReceived = false;
                peer->privateEndpoint.forwardedPacketsCount = 0;
                peer->pingEndpoint.pingReceived = false;
                peer->pingEndpoint.forwardedPacketsCount = 0;
                restartPeerCommunicationEstablishment = true;
            }

            if( peer->publicEndpoint.endpointName != publicEndpoint ){
                peer->publicEndpoint.endpointName = publicEndpoint;
                peer->publicEndpoint.pingReceived = false;
                peer->publicEndpoint.forwardedPacketsCount = 0;
                peer->pingEndpoint.pingReceived = false;
                peer->pingEndpoint.forwardedPacketsCount = 0;
                restartPeerCommunicationEstablishment = true;
            }


            peer->secondsSinceLastAliveReceivedByServer = secondsSinceLastAlive;

            std::time_t currentTime = std::time(0);
            peer->lastUserInfoReceiveTime = currentTime;

            if( restartPeerCommunicationEstablishment )
                externalCommunicationsSender_.RestartPeerCommunicationEstablishment( *peer, currentTime );
            
        }else{
            // fixme should remove user from peer list if it is present
        }
    }

    void ping( const osc::ReceivedMessage& m, const IpEndpointName& remoteEndpoint )
    {
        osc::ReceivedMessageArgumentStream args = m.ArgumentStream();

        const char *userName;
        // osc::TimeTag timeSent;

        // TODO:
        // support 3 variants of the ping message:
        // /ping userName (basic version, only one needed for compatibility)
        // /ping userName timeSent
        //        response -> /ping userName timeSent inResponseToUserName inResponseToTimeSent

        args >> userName >> osc::EndMessage;

		char sourceAddressString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
		remoteEndpoint.AddressAndPortAsString( sourceAddressString );
       
        std::cout << "ping recieved from '" << userName << "' at "
                << sourceAddressString  << "\n";

        for( std::vector<Peer>::iterator i = peers_.begin(); i != peers_.end(); ++i ){

            if( i->name.compare( userName ) == 0 ){
                bool restartPeerCommunicationEstablishment = false;

                std::time_t currentTime = std::time(0);

				if( remoteEndpoint == i->privateEndpoint.endpointName ){

                    restartPeerCommunicationEstablishment = !i->privateEndpoint.pingReceived;
                    i->privateEndpoint.pingReceived = true;
                    i->privateEndpoint.lastPingReceiveTime = currentTime;

                }else if( remoteEndpoint == i->publicEndpoint.endpointName ){

                    restartPeerCommunicationEstablishment = !i->publicEndpoint.pingReceived;
                    i->publicEndpoint.pingReceived = true;
                    i->publicEndpoint.lastPingReceiveTime = currentTime;
                    
                }else{
					// otherwise assume the messages is coming from the ping endpoint

                    restartPeerCommunicationEstablishment = ( !i->pingEndpoint.pingReceived
                            || i->pingEndpoint.endpointName != remoteEndpoint );
                            
                    i->pingEndpoint.endpointName = remoteEndpoint;
                    i->pingEndpoint.pingReceived = true;
                    i->pingEndpoint.lastPingReceiveTime = currentTime;
                }

                if( restartPeerCommunicationEstablishment )
                    externalCommunicationsSender_.RestartPeerCommunicationEstablishment( *i, currentTime );

                break;
            }
        }
    }
    
protected:

    virtual void ProcessMessage( const osc::ReceivedMessage& m, 
			const IpEndpointName& remoteEndpoint )
    {
        try{
    
            if( std::strcmp( m.AddressPattern(), "/groupclient/user_info" ) == 0 ){
                user_info( m, remoteEndpoint );
            }else if( std::strcmp( m.AddressPattern(), "/groupclient/ping" ) == 0 ){
                ping( m, remoteEndpoint );
            }else if( std::strcmp( m.AddressPattern(), "/groupclient/user_alive_status" ) == 0 ){
                user_alive_status( m, remoteEndpoint );
            }else if( std::strcmp( m.AddressPattern(), "/groupclient/user_group_status" ) == 0 ){
                user_group_status( m, remoteEndpoint );
            }

        }catch( osc::Exception& e ){
            std::cout << "error while parsing message: " << e.what() << "\n";
        }
    }

    IpEndpointName remoteServerEndpoint_;

	const char *userName_;
    const char *userPassword_;
    const char *groupName_;
    const char *groupPassword_;

    UdpTransmitSocket localRxSocket_;

    ExternalCommunicationsSender& externalCommunicationsSender_;

    ExternalSocketListener(); // no default ctor
    ExternalSocketListener( const ExternalSocketListener& ); // no copy ctor
    ExternalSocketListener& operator=( const ExternalSocketListener& ); // no assignment operator

public:
    ExternalSocketListener( const IpEndpointName& remoteServerEndpoint, 
			int localRxPort, const char *userName, const char *userPassword,
            const char *groupName, const char *groupPassword,
            ExternalCommunicationsSender& externalCommunicationsSender )
        : remoteServerEndpoint_( remoteServerEndpoint )
        , userName_( userName )
        , userPassword_( userPassword )
        , groupName_( groupName )
        , groupPassword_( groupPassword )
        , localRxSocket_( IpEndpointName( "localhost", localRxPort ) )
        , externalCommunicationsSender_( externalCommunicationsSender )
    {
    }

    virtual void ProcessPacket( const char *data, int size, 
			const IpEndpointName& remoteEndpoint )
    {
        // for now we parse _all_ packets, and pass all those on to clients
        // except those which come from the server. ideally we should avoid
        // parsing most packets except the ones containing pings, or perhaps
        // only process non-bundled pings.

        // in the future it could be useful to register which peer a packet
        // is coming from so that we can keep track of channel activity
        // not just by receiving pings but also by recieving other traffic
        // this would also allow us to reject packets from unknown sources

        
        osc::OscPacketListener::ProcessPacket( data, size, remoteEndpoint );
                    
        if( remoteEndpoint != remoteServerEndpoint_ ){
         
            // forward packet to local receive socket

            localRxSocket_.Send( data, size );
        }
    }
};


class LocalTxSocketListener : public PacketListener {

    ExternalCommunicationsSender& externalCommunicationsSender_;
    
    LocalTxSocketListener(); // no default ctor
    LocalTxSocketListener( const LocalTxSocketListener& ); // no copy ctor
    LocalTxSocketListener& operator=( const LocalTxSocketListener& ); // no assignment operator

public:
    LocalTxSocketListener( ExternalCommunicationsSender& externalCommunicationsSender )
        : externalCommunicationsSender_( externalCommunicationsSender )
    {
    }

	virtual void ProcessPacket( const char *data, int size, 
			const IpEndpointName& remoteEndpoint )
    {
        (void) remoteEndpoint; // suppress unused parameter warning
        externalCommunicationsSender_.ForwardPacketToAllPeers( data, size );
    }
};


char IntToHexDigit( int n )
{
    if( n < 10 )
        return (char)('0' + n);
    else
        return (char)('a' + (n-10));
}

void MakeHashString( char *dest, const char *src )
{
    MD5_CTX md5Context;
    MD5Init( &md5Context );
    MD5Update( &md5Context, (unsigned char*)src, (unsigned int)std::strlen(src) );
    unsigned char numericHash[16];
    MD5Final( numericHash, &md5Context );
    char *p = dest;
    for( int i=0; i < 16; ++i ){

        *p++ = IntToHexDigit(((unsigned char)numericHash[i] >> 4) & 0x0F);
        *p++ = IntToHexDigit((unsigned char)numericHash[i] & 0x0F);
    }
    *p = '\0';

    //printf( "src: %s dest: %s\n", src, dest );
}

void SanityCheckMd5()
{
    // if anything in this function fails there's a problem with your build configuration
    
    // check that the size of types declared in md5.h are correct
    assert( sizeof(UINT2) == 2 );
    assert( sizeof(UINT4) == 4 );

    // sanity check that the hash is working by comparing to a known good hash:
    char testHash[33];
    MakeHashString( testHash, "0123456789" );
    assert( std::strcmp( testHash, "781e5e245d69b566979b86e28d23f2c7" ) == 0 );
}

void RunOscGroupClientUntilSigInt( 
		const IpEndpointName& serverRemoteEndpoint, 
		int localToRemotePort, int localTxPort, int localRxPort, 
		const char *userName, const char *userPassword, 
		const char *groupName, const char *groupPassword )
{
    // used hashed passwords instead of the user supplied ones
    
    char userPasswordHash[33];
    MakeHashString( userPasswordHash, userPassword );

    char groupPasswordHash[33];
    MakeHashString( groupPasswordHash, groupPassword );

	UdpReceiveSocket externalSocket( 
			IpEndpointName( IpEndpointName::ANY_ADDRESS, localToRemotePort ) );

	UdpReceiveSocket localTxSocket( localTxPort );

    ExternalCommunicationsSender externalCommunicationsSender( externalSocket, 
			serverRemoteEndpoint, localToRemotePort, 
			userName, userPasswordHash, groupName, groupPasswordHash );

    ExternalSocketListener externalSocketListener( 
			serverRemoteEndpoint, localRxPort,
            userName, userPasswordHash, groupName, groupPasswordHash,
            externalCommunicationsSender );

    LocalTxSocketListener localTxSocketListener( externalCommunicationsSender );

	SocketReceiveMultiplexer mux;
    mux.AttachPeriodicTimerListener( 0, (IDLE_PING_PERIOD_SECONDS * 1000) / 10, &externalCommunicationsSender );
	mux.AttachSocketListener( &externalSocket, &externalSocketListener );
	mux.AttachSocketListener( &localTxSocket, &localTxSocketListener );    

	std::cout << "running...\n";
	std::cout << "press ctrl-c to end\n";

	mux.RunUntilSigInt();

	std::cout << "finishing.\n";	
}


int oscgroupclient_main(int argc, char* argv[])
{
    SanityCheckMd5();
    
    try{
        if( argc != 10 ){
            std::cout << "usage: oscgroupclient serveraddress serverport localtoremoteport localtxport localrxport username password groupname grouppassword\n";
            std::cout << "users should send data to localhost:localtxport and listen on localhost:localrxport\n";
            return 0;
        }

		IpEndpointName serverRemoteEndpoint( argv[1], atoi( argv[2] ) );
        int localToRemotePort = std::atoi( argv[3] );
        int localTxPort = std::atoi( argv[4] );
        int localRxPort = std::atoi( argv[5] );
        const char *userName = argv[6];
        const char *userPassword = argv[7];
        const char *groupName = argv[8];
        const char *groupPassword = argv[9];

		char serverAddressString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
		serverRemoteEndpoint.AddressAndPortAsString( serverAddressString );

        std::cout << "oscgroupclient\n";
        std::cout << "connecting to group '" << groupName << "' as user '" << userName << "'.\n";
        std::cout << "using server at " << serverAddressString
                        << " with external traffic on local port " << localToRemotePort << "\n";
        std::cout << "--> send outbound traffic to localhost port " << localTxPort << "\n";
        std::cout << "<-- listen for inbound traffic on localhost port " << localRxPort << "\n";

        RunOscGroupClientUntilSigInt( serverRemoteEndpoint, localToRemotePort,
                localTxPort, localRxPort, userName, userPassword, groupName, groupPassword );

    }catch( std::exception& e ){
        std::cout << e.what() << std::endl;
    }
    
    return 0;
}


#ifndef NO_MAIN

int main(int argc, char* argv[])
{
    return oscgroupclient_main( argc, argv );
}

#endif /* NO_MAIN */

