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

#include <string>
#include <set>
#include <iostream>
#include <ctime>
#include <algorithm>
#include <iterator>
#include <cassert>
#include <cstring>

#include "ip/NetworkingUtils.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'.
}
#endif

/*
TODO:
    switch to using char * instead of string:: to avoid allocating new strings
    every time we execute a find()
*/


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;
}


GroupServer::GroupServer( int timeoutSeconds, int maxUsers, int maxGroups )
    : timeoutSeconds_( timeoutSeconds )
    , maxUsers_( maxUsers )
    , maxGroups_( maxGroups )
    , userCount_( 0 )
    , groupCount_( 0 )
{

}


GroupServer::~GroupServer()
{
    for( const_user_iterator i = users_begin(); i != users_end(); ++i )
        delete i->second;
    for( const_group_iterator i = groups_begin(); i != groups_end(); ++i )
        delete i->second;
}


User *GroupServer::CreateUser( const char *userName, const char *userPassword )
{
    Log() << "creating user '" << userName << "'." << std::endl;
    
    User *user = new User( userName, userPassword );
    std::pair<user_iterator,bool> r =
            users_.insert( std::make_pair( user->name, user ) );
    // we assume the caller didn't try to create a user with an existing name
    assert( r.second == true );
    ++userCount_;

    return user;
}


User *GroupServer::FindUser( const char *userName )
{
    user_iterator user = users_.find( userName );

    return ( user != users_.end() ) ? user->second : (User*) 0;
}


void GroupServer::PurgeStaleUsers()
{
    std::time_t currentTime = std::time(0);
    
    for( std::map< std::string, User* >::iterator i = users_.begin();
            i != users_.end(); /* nothing */ ){

        unsigned long inactiveSeconds =
                i->second->SecondsSinceLastAliveReceived( currentTime );
        if( inactiveSeconds >= (unsigned int)timeoutSeconds_ ){
            Log() << "purging user '" << i->second->name << "' after "
                    << inactiveSeconds << " seconds of inactivity." << std::endl;

            SeparateUserFromAllGroups( i->second );
            delete i->second;
            --userCount_;
                
            // i think this is safe, we increment i before erasing the
            // element referenced by its old value...
            std::map< std::string, User* >::iterator j = i;
            ++i;
            users_.erase( j );
        }else{
            ++i;
        }
    }
}


Group *GroupServer::CreateGroup(
        const char *groupName, const char *groupPassword )
{
    Log() << "creating group '" << groupName << "'." << std::endl;
    
    Group *group = new Group( groupName, groupPassword );
    std::pair<group_iterator, bool> r =
            groups_.insert( std::make_pair( group->name, group ) );
    // we assume the caller didn't try to create an existing group
    assert( r.second == true );
    ++groupCount_;

    return group;
}


Group *GroupServer::FindGroup( const char *groupName )
{
    group_iterator group = groups_.find( groupName );

    return ( group != groups_.end() ) ? group->second : (Group*) 0;
}


// The User <-> Group relation is managed as a bidirectional link (each User
// contains a set of groups which it is associated with, and each Group
// maintains a set of Users which it is associated with). The Associate*
// and Separate* methods below create and destroy the bidirectional link
// RemoveUserReferenceFromGroup is a helper function which only destroys
// one side of the link.

void GroupServer::AssociateUserWithGroup( User *user, Group* group )
{
    Log() << "adding user '" << user->name
            << "' to group '" << group->name << "'." << std::endl;
    
    user->groups_.insert( group );
    group->users_.insert( user );
}


void GroupServer::RemoveUserReferenceFromGroup( User *user, Group* group )
{
    Log() << "removing user '" << user->name
            << "' from group '" << group->name << "'." << std::endl;
    
    std::set< User* >::iterator i = group->users_.find( user );
    assert( i != group->users_.end() );

    group->users_.erase( i );

    if( group->users_.empty() ){
        Log() << "purging empty group '" << group->name << "'." << std::endl;
        
        groups_.erase( group->name );
        delete group;
        --groupCount_;
    }
}


void GroupServer::SeparateUserFromGroup( User *user, Group* group )
{
    user->groups_.erase( group );
    RemoveUserReferenceFromGroup( user, group );
}


void GroupServer::SeparateUserFromAllGroups( User *user )
{
    for( std::set<Group*>::iterator i = user->groups_.begin();
            i != user->groups_.end(); ++i ){

        RemoveUserReferenceFromGroup( user, *i);
    }

    user->groups_.clear();
}


static void UpdateEndpoint( IpEndpointName& dest, const IpEndpointName& src,
        const char *userName, const char *whichEndpoint )
{
    if( dest != src ){

		char endpointString[ IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ];
		src.AddressAndPortAsString( endpointString );

        Log() << "updating " << whichEndpoint << " endpoint for user '"
                << userName << "' to " << endpointString << "." << std::endl;

        dest = src;
    }
}


GroupServer::UserStatus GroupServer::UserAlive(
        const char *userName, const char *userPassword,
        const IpEndpointName& privateEndpoint,
		const IpEndpointName& publicEndpoint,
        const char **groupNamesAndPasswords,
        UserStatus *userGroupsStatus, int groupCount )
{
    // find or create the user, aborting if the password for an existing
    // user is incorrect, or if the maximum number of users has been reached

    User *user = FindUser( userName );
    if( user ){
        if( user->password.compare( userPassword ) != 0 ){
            Log() << "attempt to update user '"
                    << userName << "' with incorrect password." << std::endl;
            return USER_STATUS_WRONG_PASSWORD;
        }
    }else{
        if( userCount_ == maxUsers_ ){
            Log() << "user limit reached, user '"
                    << userName << "' not admitted." << std::endl;
            return USER_STATUS_SERVER_LIMIT_REACHED;
        }else{
            user = CreateUser( userName, userPassword );
        }
    }

    UpdateEndpoint(
            user->privateEndpoint, privateEndpoint, userName, "private" );
    UpdateEndpoint( user->publicEndpoint, publicEndpoint, userName, "public" );

    user->lastAliveMessageArrivalTime = time(0);


    // previousButNotCurrentGroups begins containing all the groups the user
    // was in before the current message was received. We remove those which
    // the user is still a member of, leaving the set that the user is no
    // longer a member of. We then use the set to remove associations with
    // non-current groups.

    std::set<Group*> previousButNotCurrentGroups( user->groups_ );

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

        Group *group = FindGroup( groupName );
        if( group ){
            if( user->IsMemberOf( group ) ){
                // check that previousButNotCurrentGroups contains group before
                // removing it. it won't if we were passed the same group
                // multiple times
                std::set<Group*>::iterator j =
                        previousButNotCurrentGroups.find( group );
                if( j != previousButNotCurrentGroups.end() )
                    previousButNotCurrentGroups.erase( j );

                userGroupsStatus[i] = USER_STATUS_OK;
            }else{
                // user isn't in group so join it
                if( group->password.compare( groupPassword ) == 0 ){
                    AssociateUserWithGroup( user, group );
                    userGroupsStatus[i] = USER_STATUS_OK;
                }else{                                   
                    userGroupsStatus[i] = USER_STATUS_WRONG_PASSWORD;
                }
            }
        }else{  // group doesn't exist
            if( groupCount_ == maxGroups_ ){
                Log() << "group limit reached, group '"
                        << groupName << "' not created." << std::endl;
                userGroupsStatus[i] = USER_STATUS_SERVER_LIMIT_REACHED;
            }else{
                group = CreateGroup( groupName, groupPassword );
                AssociateUserWithGroup( user, group );
                userGroupsStatus[i] = USER_STATUS_OK;
            }
        }
    }

    for( std::set<Group*>::iterator j = previousButNotCurrentGroups.begin();
            j != previousButNotCurrentGroups.end(); ++j ){

        SeparateUserFromGroup( user, *j );
    }

    return USER_STATUS_OK;
}

