/*
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 "OscGroupServer.h"

#include <cassert>
#include <cstring>
#include <ctime>
#include <iostream>
#include <fstream>
#include <cstdlib>

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

#include "ip/UdpSocket.h"
#include "osc/OscPacketListener.h"
#include "ip/TimerListener.h"

#include "GroupServer.h"

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


static std::ostream& Log()
{
    std::time_t t;
    std::time( &t );

    // ctime() returns a constant width 26 char string including trailing \0
    // the fields are all constant width.
    char s[26];
    std::strcpy( s, std::ctime( &t ) );
    s[24] = '\0'; // remove trailing null

    std::cout << s << ": ";
    return std::cout;
}


static const char *UserStatusToString( GroupServer::UserStatus userStatus )
{
    switch( userStatus ){
        case GroupServer::USER_STATUS_OK:
            return "ok";
        case GroupServer::USER_STATUS_WRONG_PASSWORD:
            return "wrong password";
        case GroupServer::USER_STATUS_SERVER_LIMIT_REACHED:
            return "user limit reached";
        case GroupServer::USER_STATUS_UNKNOWN:
            /* FALLTHROUGH */ ;
    }

    return "unknown";
}


class OscGroupServerListener
    : public osc::OscPacketListener
    , public TimerListener
{

    GroupServer groupServer_;
    UdpSocket& externalSocket_;

    #define IP_MTU_SIZE 1536
    char buffer_[IP_MTU_SIZE];
    osc::OutboundPacketStream resultStream_;
    std::size_t emptyResultSize_;

    void user_alive( const osc::ReceivedMessage& m, 
				const IpEndpointName& remoteEndpoint )
    {
        // /groupserver/user_alive
        //      userName password
        //      privateIpAddress privatePort
        //      groupName0 groupPassword0 groupName1 groupPassword1 ...

        osc::ReceivedMessageArgumentStream args = m.ArgumentStream();
        const char *userName, *userPassword;
        osc::int32 privateAddress, privatePort;

        args >> userName >> userPassword >> privateAddress >> privatePort;

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

		IpEndpointName privateEndpoint( privateAddress, privatePort );

        int groupCount = (m.ArgumentCount() - 4) / 2;
        const char **groupNamesAndPasswords = 0;
        GroupServer::UserStatus *userGroupsStatus = 0;
        if( groupCount > 0 ){
            groupNamesAndPasswords = new const char*[ groupCount * 2 ];
            int i = 0;
            while( !args.Eos() ){
                args >> groupNamesAndPasswords[i++];    // group name
                args >> groupNamesAndPasswords[i++];    // group password
            }

            userGroupsStatus = new GroupServer::UserStatus[ groupCount ];
            for( int j=0; j < groupCount; ++j )
                userGroupsStatus[j] = GroupServer::USER_STATUS_UNKNOWN;
        }

        GroupServer::UserStatus userStatus =
                groupServer_.UserAlive( userName, userPassword,
                        privateEndpoint, remoteEndpoint,
                        groupNamesAndPasswords, userGroupsStatus, groupCount );

        resultStream_ << osc::BeginMessage( "/groupclient/user_alive_status" )
            << userName
            << userPassword
            << UserStatusToString( userStatus )                
            << osc::EndMessage;

        if( userStatus == GroupServer::USER_STATUS_OK ){
            for( int i=0; i < groupCount; ++i ){
                const char *groupName = groupNamesAndPasswords[i*2];
                const char *groupPassword = groupNamesAndPasswords[i*2 + 1];

                resultStream_
                    << osc::BeginMessage( "/groupclient/user_group_status" )
                    << userName
                    << userPassword
                    << groupName
                    << groupPassword
                    << UserStatusToString( userGroupsStatus[i] )
                    << osc::EndMessage;
            }
        }

        delete [] groupNamesAndPasswords;
        delete [] userGroupsStatus;
    }

    void MakeUserInfoMessage( osc::OutboundPacketStream& p,
            User *user, std::time_t currentTime )
    {
        // addresses are transmitted as ones complement (bit inverse)
        // to avoid problems with buggy NATs trying to re-write addresses
        
        p << osc::BeginMessage( "/groupclient/user_info" )
            << user->name.c_str()
            << ~((osc::int32)user->privateEndpoint.address)
            << (osc::int32)user->privateEndpoint.port
            << ~((osc::int32)user->publicEndpoint.address)
            << (osc::int32)user->publicEndpoint.port
            << (osc::int32)user->SecondsSinceLastAliveReceived( currentTime );

        for( User::const_group_iterator i = user->groups_begin();
                i != user->groups_end(); ++i )
            p << (*i)->name.c_str();
                
        p << osc::EndMessage;
    }

    void get_group_users_info( const osc::ReceivedMessage& m,
            const IpEndpointName& remoteEndpoint )
    {
        (void) remoteEndpoint; // suppress unused parameter warning
        
        // /groupserver/get_group_users_info group-name group-password

        osc::ReceivedMessageArgumentStream args = m.ArgumentStream();
        const char *groupName, *groupPassword;

        args >> groupName >> groupPassword >> osc::EndMessage;

        Group *group = groupServer_.FindGroup( groupName );
        if( group && group->password.compare( groupPassword ) == 0 ){
            std::time_t currentTime = time(0);
            
            for( Group::const_user_iterator i = group->users_begin();
                    i != group->users_end(); ++i )
                MakeUserInfoMessage( resultStream_, *i, currentTime );
        }

        // we don't return a result to the client in the case where the group
        // doesn't exist, or the password is wrong because legitimate clients
        // will have already successfully joined the group, or they will have
        // received an error message when they tried to join.
    }

protected:
    
    virtual void ProcessMessage( const osc::ReceivedMessage& m, 
			const IpEndpointName& remoteEndpoint )
    {
        try{

            if( std::strcmp( m.AddressPattern(), "/groupserver/user_alive" ) == 0 ){
                user_alive( m, remoteEndpoint );
            }else if( std::strcmp( m.AddressPattern(), "/groupserver/get_group_users_info" ) == 0 ){
                get_group_users_info( m, remoteEndpoint );
            }

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

public:
    OscGroupServerListener( int timeoutSeconds, int maxUsers, int maxGroups,
                UdpSocket& externalSocket )
        : groupServer_( timeoutSeconds, maxUsers, maxGroups )
        , externalSocket_( externalSocket )
        , resultStream_( buffer_, IP_MTU_SIZE )
    {
        resultStream_ << osc::BeginBundle();
        resultStream_ << osc::EndBundle;
        emptyResultSize_ = resultStream_.Size();
    }

    virtual void ProcessPacket( const char *data, int size, 
			const IpEndpointName& remoteEndpoint )
    {
        resultStream_.Clear();
        resultStream_ << osc::BeginBundle();

        osc::OscPacketListener::ProcessPacket( data, size, remoteEndpoint );

        resultStream_ << osc::EndBundle;

        assert( resultStream_.IsReady() );

        if( resultStream_.Size() != emptyResultSize_ ){
            char addressString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
            remoteEndpoint.AddressAndPortAsString( addressString );
		    Log() << "responding to request from " << addressString << "." << std::endl;
            
		    externalSocket_.SendTo(
                    remoteEndpoint, resultStream_.Data(), resultStream_.Size() );
        }
    }

    virtual void TimerExpired()
    {
        groupServer_.PurgeStaleUsers();
    }
};


void RunOscGroupServerUntilSigInt(
        int port, int timeoutSeconds, int maxUsers, int maxGroups, const char *logFile )
{
    std::ofstream *logStream = 0;
    std::streambuf *oldCoutRdbuf = std::cout.rdbuf();

    try{
        if( logFile ){
            std::cout << "oscgroupserver redirecting output to " << logFile << std::endl;
            logStream = new std::ofstream( logFile, std::ios_base::app );
            std::cout.rdbuf( logStream->rdbuf() );
        }
    
        UdpReceiveSocket externalSocket(
                IpEndpointName( IpEndpointName::ANY_ADDRESS, port ) );
        OscGroupServerListener externalSocketListener(
                timeoutSeconds, maxUsers, maxGroups, externalSocket );

        SocketReceiveMultiplexer mux;
        mux.AttachSocketListener( &externalSocket, &externalSocketListener );

        // timer is used for purging inactive users
        mux.AttachPeriodicTimerListener( (timeoutSeconds * 1000) / 10, &externalSocketListener );
    
        Log() << "oscgroupserver listening on port " << port << "...\n"
                << "timeoutSeconds=" << timeoutSeconds << ", "
                << "maxUsers=" << maxUsers << ", "
                << "maxGroups=" << maxGroups << ", "
                << "logFile=" << ((logFile) ? logFile : "stdout") << "\n"
                << "press ctrl-c to end" << std::endl;

        mux.RunUntilSigInt();
        
        Log() << "finishing." << std::endl;

    }catch( std::exception& e ){
        Log() << e.what() << std::endl;
        Log() << "finishing." << std::endl;
    }

    std::cout.rdbuf( oldCoutRdbuf );
    delete logStream;
}


static void usage()
{
    std::cerr << "usage: oscgroupserver [-p port] [-t timeoutSeconds] [-u maxUsers] [-g maxGroups] [-l logfile]\n";
}


int oscgroupserver_main(int argc, char* argv[])
{
    if( argc % 2 != 1 ){
        usage();
        return 0;
    }

    int port = 22242;
    int timeoutSeconds = 60;
    int maxUsers = 100;
    int maxGroups = 50;
    const char *logFile = 0;

    int argIndex = 1;
    while( argIndex < argc ){
        const char *selector = argv[argIndex];
        const char *value = argv[argIndex + 1];
        if( selector[0] == '-' && selector[1] != '\0' && selector[2] == '\0' ){
            switch( selector[1] ){
                case 'p':
                    port = std::atoi( value );
                    break;
                case 't':
                    timeoutSeconds = std::atoi( value );
                    break;
                case 'u':
                    maxUsers = std::atoi( value );
                    break;
                case 'g':
                    maxGroups = std::atoi( value );
                    break;
                case 'l':
                    logFile = value;
                    break;
                default:
                    usage();
                    return 0;
            }
        }else{
            usage();
            return 0;
        }
        argIndex += 2;
    }
        
    RunOscGroupServerUntilSigInt(
            port, timeoutSeconds, maxUsers, maxGroups, logFile );

    return 0;
}


#ifndef NO_MAIN

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

#endif /* NO_MAIN */

