  /********************************************************************\
  * BitlBee -- An IRC to other IM-networks gateway                     *
  *                                                                    *
  * Copyright 2002-2005 Wilmer van der Gaast and others                *
  \********************************************************************/

/* MSN module - Switchboard server callbacks and utilities              */

/*
  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 with
  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
  if not, write to the Free Software Foundation, Inc., 59 Temple Place,
  Suite 330, Boston, MA  02111-1307  USA
*/

#include <ctype.h>
#include "nogaim.h"
#include "msn.h"
#include "passport.h"
#include "md5.h"

static gboolean msn_sb_callback( gpointer data, gint source, b_input_condition cond );
static int msn_sb_command( gpointer data, char **cmd, int num_parts );
static int msn_sb_message( gpointer data, char *msg, int msglen, char **cmd, int num_parts );

int msn_sb_write( struct msn_switchboard *sb, char *s, int len )
{
	int st;
	
	st = write( sb->fd, s, len );
	if( st != len )
	{
		msn_sb_destroy( sb );
		return( 0 );
	}
	
	return( 1 );
}

int msn_sb_write_msg( struct im_connection *ic, struct msn_message *m )
{
	struct msn_data *md = ic->proto_data;
	struct msn_switchboard *sb;
	char buf[1024];

	/* FIXME: *CHECK* the reliability of using spare sb's! */
	if( ( sb = msn_sb_spare( ic ) ) )
	{
		debug( "Trying to use a spare switchboard to message %s", m->who );
		
		sb->who = g_strdup( m->who );
		g_snprintf( buf, sizeof( buf ), "CAL %d %s\r\n", ++sb->trId, m->who );
		if( msn_sb_write( sb, buf, strlen( buf ) ) )
		{
			/* He/She should join the switchboard soon, let's queue the message. */
			sb->msgq = g_slist_append( sb->msgq, m );
			return( 1 );
		}
	}
	
	debug( "Creating a new switchboard to message %s", m->who );
	
	/* If we reach this line, there was no spare switchboard, so let's make one. */
	g_snprintf( buf, sizeof( buf ), "XFR %d SB\r\n", ++md->trId );
	if( !msn_write( ic, buf, strlen( buf ) ) )
	{
		g_free( m->who );
		g_free( m->text );
		g_free( m );
		
		return( 0 );
	}
	
	/* And queue the message to md. We'll pick it up when the switchboard comes up. */
	md->msgq = g_slist_append( md->msgq, m );
	
	/* FIXME: If the switchboard creation fails, the message will not be sent. */
	
	return( 1 );
}

struct msn_switchboard *msn_sb_create( struct im_connection *ic, char *host, int port, char *key, int session )
{
	struct msn_data *md = ic->proto_data;
	struct msn_switchboard *sb = g_new0( struct msn_switchboard, 1 );
	
	sb->fd = proxy_connect( host, port, msn_sb_connected, sb );
	if( sb->fd < 0 )
	{
		g_free( sb );
		return( NULL );
	}
	
	sb->ic = ic;
	sb->key = g_strdup( key );
	sb->session = session;
	
	msn_switchboards = g_slist_append( msn_switchboards, sb );
	md->switchboards = g_slist_append( md->switchboards, sb );
	
	return( sb );
}

struct msn_switchboard *msn_sb_by_handle( struct im_connection *ic, char *handle )
{
	struct msn_data *md = ic->proto_data;
	struct msn_switchboard *sb;
	GSList *l;
	
	for( l = md->switchboards; l; l = l->next )
	{
		sb = l->data;
		if( sb->who && strcmp( sb->who, handle ) == 0 )
			return( sb );
	}
	
	return( NULL );
}

struct msn_switchboard *msn_sb_by_chat( struct groupchat *c )
{
	struct msn_data *md = c->ic->proto_data;
	struct msn_switchboard *sb;
	GSList *l;
	
	for( l = md->switchboards; l; l = l->next )
	{
		sb = l->data;
		if( sb->chat == c )
			return( sb );
	}
	
	return( NULL );
}

struct msn_switchboard *msn_sb_spare( struct im_connection *ic )
{
	struct msn_data *md = ic->proto_data;
	struct msn_switchboard *sb;
	GSList *l;
	
	for( l = md->switchboards; l; l = l->next )
	{
		sb = l->data;
		if( !sb->who && !sb->chat )
			return( sb );
	}
	
	return( NULL );
}

int msn_sb_sendmessage( struct msn_switchboard *sb, char *text )
{
	if( sb->ready )
	{
		char *packet, *buf;
		int i, j;
		
		/* Build the message. Convert LF to CR-LF for normal messages. */
		if( strcmp( text, TYPING_NOTIFICATION_MESSAGE ) == 0 )
		{
			i = strlen( MSN_TYPING_HEADERS ) + strlen( sb->ic->acc->user );
			buf = g_new0( char, i );
			i = g_snprintf( buf, i, MSN_TYPING_HEADERS, sb->ic->acc->user );
		}
		else if( strcmp( text, SB_KEEPALIVE_MESSAGE ) == 0 )
		{
			buf = g_strdup( SB_KEEPALIVE_HEADERS );
			i = strlen( buf );
		}
		else
		{
			buf = g_new0( char, sizeof( MSN_MESSAGE_HEADERS ) + strlen( text ) * 2 + 1 );
			i = strlen( MSN_MESSAGE_HEADERS );
			
			strcpy( buf, MSN_MESSAGE_HEADERS );
			for( j = 0; text[j]; j ++ )
			{
				if( text[j] == '\n' )
					buf[i++] = '\r';
				
				buf[i++] = text[j];
			}
		}
		
		/* Build the final packet (MSG command + the message). */
		packet = g_strdup_printf( "MSG %d N %d\r\n%s", ++sb->trId, i, buf );
		g_free( buf );
		if( msn_sb_write( sb, packet, strlen( packet ) ) )
		{
			g_free( packet );
			return( 1 );
		}
		else
		{
			g_free( packet );
			return( 0 );
		}
	}
	else if( sb->who )
	{
		struct msn_message *m = g_new0( struct msn_message, 1 );
		
		m->who = g_strdup( "" );
		m->text = g_strdup( text );
		sb->msgq = g_slist_append( sb->msgq, m );
		
		return( 1 );
	}
	else
	{
		return( 0 );
	}
}

struct groupchat *msn_sb_to_chat( struct msn_switchboard *sb )
{
	struct im_connection *ic = sb->ic;
	char buf[1024];
	
	/* Create the groupchat structure. */
	g_snprintf( buf, sizeof( buf ), "MSN groupchat session %d", sb->session );
	sb->chat = imcb_chat_new( ic, buf );
	
	/* Populate the channel. */
	if( sb->who ) imcb_chat_add_buddy( sb->chat, sb->who );
	imcb_chat_add_buddy( sb->chat, ic->acc->user );
	
	/* And make sure the switchboard doesn't look like a regular chat anymore. */
	if( sb->who )
	{
		g_free( sb->who );
		sb->who = NULL;
	}
	
	return sb->chat;
}

void msn_sb_destroy( struct msn_switchboard *sb )
{
	struct im_connection *ic = sb->ic;
	struct msn_data *md = ic->proto_data;
	
	debug( "Destroying switchboard: %s", sb->who ? sb->who : sb->key ? sb->key : "" );
	
	msn_msgq_purge( ic, &sb->msgq );
	msn_sb_stop_keepalives( sb );
	
	if( sb->key ) g_free( sb->key );
	if( sb->who ) g_free( sb->who );
	
	if( sb->chat )
	{
		imcb_chat_free( sb->chat );
	}
	
	if( sb->handler )
	{
		if( sb->handler->rxq ) g_free( sb->handler->rxq );
		if( sb->handler->cmd_text ) g_free( sb->handler->cmd_text );
		g_free( sb->handler );
	}
	
	if( sb->inp ) b_event_remove( sb->inp );
	closesocket( sb->fd );
	
	msn_switchboards = g_slist_remove( msn_switchboards, sb );
	md->switchboards = g_slist_remove( md->switchboards, sb );
	g_free( sb );
}

gboolean msn_sb_connected( gpointer data, gint source, b_input_condition cond )
{
	struct msn_switchboard *sb = data;
	struct im_connection *ic;
	struct msn_data *md;
	char buf[1024];
	
	/* Are we still alive? */
	if( !g_slist_find( msn_switchboards, sb ) )
		return FALSE;
	
	ic = sb->ic;
	md = ic->proto_data;
	
	if( source != sb->fd )
	{
		debug( "Error %d while connecting to switchboard server", 1 );
		msn_sb_destroy( sb );
		return FALSE;
	}
	
	/* Prepare the callback */
	sb->handler = g_new0( struct msn_handler_data, 1 );
	sb->handler->fd = sb->fd;
	sb->handler->rxq = g_new0( char, 1 );
	sb->handler->data = sb;
	sb->handler->exec_command = msn_sb_command;
	sb->handler->exec_message = msn_sb_message;
	
	if( sb->session == MSN_SB_NEW )
		g_snprintf( buf, sizeof( buf ), "USR %d %s %s\r\n", ++sb->trId, ic->acc->user, sb->key );
	else
		g_snprintf( buf, sizeof( buf ), "ANS %d %s %s %d\r\n", ++sb->trId, ic->acc->user, sb->key, sb->session );
	
	if( msn_sb_write( sb, buf, strlen( buf ) ) )
		sb->inp = b_input_add( sb->fd, GAIM_INPUT_READ, msn_sb_callback, sb );
	else
		debug( "Error %d while connecting to switchboard server", 2 );
	
	return FALSE;
}

static gboolean msn_sb_callback( gpointer data, gint source, b_input_condition cond )
{
	struct msn_switchboard *sb = data;
	struct im_connection *ic = sb->ic;
	struct msn_data *md = ic->proto_data;
	
	if( msn_handler( sb->handler ) != -1 )
		return TRUE;
	
	if( sb->msgq != NULL )
	{
		time_t now = time( NULL );
		char buf[1024];
		
		if( now - md->first_sb_failure > 600 )
		{
			/* It's not really the first one, but the start of this "series".
			   With this, the warning below will be shown only if this happens
			   at least three times in ten minutes. This algorithm isn't
			   perfect, but for this purpose it will do. */
			md->first_sb_failure = now;
			md->sb_failures = 0;
		}
		
		debug( "Error: Switchboard died" );
		if( ++ md->sb_failures >= 3 )
			imcb_log( ic, "Warning: Many switchboard failures on MSN connection. "
			              "There might be problems delivering your messages." );
		
		if( md->msgq == NULL )
		{
			md->msgq = sb->msgq;
		}
		else
		{
			GSList *l;
			
			for( l = md->msgq; l->next; l = l->next );
			l->next = sb->msgq;
		}
		sb->msgq = NULL;
		
		debug( "Moved queued messages back to the main queue, "
		       "creating a new switchboard to retry." );
		g_snprintf( buf, sizeof( buf ), "XFR %d SB\r\n", ++md->trId );
		if( !msn_write( ic, buf, strlen( buf ) ) )
			return FALSE;
	}
	
	msn_sb_destroy( sb );
	return FALSE;
}

static int msn_sb_command( gpointer data, char **cmd, int num_parts )
{
	struct msn_switchboard *sb = data;
	struct im_connection *ic = sb->ic;
	char buf[1024];
	
	if( !num_parts )
	{
		/* Hrrm... Empty command...? Ignore? */
		return( 1 );
	}
	
	if( strcmp( cmd[0], "XFR" ) == 0 )
	{
		imcb_error( ic, "Received an XFR from a switchboard server, unable to comply! This is likely to be a bug, please report it!" );
		imc_logout( ic, TRUE );
		return( 0 );
	}
	else if( strcmp( cmd[0], "USR" ) == 0 )
	{
		if( num_parts != 5 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		if( strcmp( cmd[2], "OK" ) != 0 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		if( sb->who )
		{
			g_snprintf( buf, sizeof( buf ), "CAL %d %s\r\n", ++sb->trId, sb->who );
			return( msn_sb_write( sb, buf, strlen( buf ) ) );
		}
		else
		{
			debug( "Just created a switchboard, but I don't know what to do with it." );
		}
	}
	else if( strcmp( cmd[0], "IRO" ) == 0 )
	{
		int num, tot;
		
		if( num_parts != 6 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		num = atoi( cmd[2] );
		tot = atoi( cmd[3] );
		
		if( tot <= 0 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		else if( tot > 1 )
		{
			char buf[1024];
			
			if( num == 1 )
			{
				g_snprintf( buf, sizeof( buf ), "MSN groupchat session %d", sb->session );
				sb->chat = imcb_chat_new( ic, buf );
				
				g_free( sb->who );
				sb->who = NULL;
			}
			
			imcb_chat_add_buddy( sb->chat, cmd[4] );
			
			if( num == tot )
			{
				imcb_chat_add_buddy( sb->chat, ic->acc->user );
			}
		}
	}
	else if( strcmp( cmd[0], "ANS" ) == 0 )
	{
		if( num_parts != 3 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		if( strcmp( cmd[2], "OK" ) != 0 )
		{
			debug( "Switchboard server sent a negative ANS reply" );
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		sb->ready = 1;
		
		msn_sb_start_keepalives( sb, FALSE );
	}
	else if( strcmp( cmd[0], "CAL" ) == 0 )
	{
		if( num_parts != 4 || !isdigit( cmd[3][0] ) )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		sb->session = atoi( cmd[3] );
	}
	else if( strcmp( cmd[0], "JOI" ) == 0 )
	{
		if( num_parts != 3 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		if( sb->who && g_strcasecmp( cmd[1], sb->who ) == 0 )
		{
			/* The user we wanted to talk to is finally there, let's send the queued messages then. */
			struct msn_message *m;
			GSList *l;
			int st = 1;
			
			debug( "%s arrived in the switchboard session, now sending queued message(s)", cmd[1] );
			
			/* Without this, sendmessage() will put everything back on the queue... */
			sb->ready = 1;
			
			while( ( l = sb->msgq ) )
			{
				m = l->data;
				if( st )
				{
					/* This hack is meant to convert a regular new chat into a groupchat */
					if( strcmp( m->text, GROUPCHAT_SWITCHBOARD_MESSAGE ) == 0 )
						msn_sb_to_chat( sb );
					else
						st = msn_sb_sendmessage( sb, m->text );
				}
				g_free( m->text );
				g_free( m->who );
				g_free( m );
				
				sb->msgq = g_slist_remove( sb->msgq, m );
			}
			
			msn_sb_start_keepalives( sb, FALSE );
			
			return( st );
		}
		else if( sb->who )
		{
			debug( "Converting chat with %s to a groupchat because %s joined the session.", sb->who, cmd[1] );
			
			/* This SB is a one-to-one chat right now, but someone else is joining. */
			msn_sb_to_chat( sb );
			
			imcb_chat_add_buddy( sb->chat, cmd[1] );
		}
		else if( sb->chat )
		{
			imcb_chat_add_buddy( sb->chat, cmd[1] );
			sb->ready = 1;
		}
		else
		{
			/* PANIC! */
		}
	}
	else if( strcmp( cmd[0], "MSG" ) == 0 )
	{
		if( num_parts != 4 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		sb->handler->msglen = atoi( cmd[3] );
		
		if( sb->handler->msglen <= 0 )
		{
			debug( "Received a corrupted message on the switchboard, the switchboard will be closed" );
			msn_sb_destroy( sb );
			return( 0 );
		}
	}
	else if( strcmp( cmd[0], "NAK" ) == 0 )
	{
		if( sb->who )
		{
			imcb_log( ic, "The MSN servers could not deliver one of your messages to %s.", sb->who );
		}
		else
		{
			imcb_log( ic, "The MSN servers could not deliver one of your groupchat messages to all participants." );
		}
	}
	else if( strcmp( cmd[0], "BYE" ) == 0 )
	{
		if( num_parts < 2 )
		{
			msn_sb_destroy( sb );
			return( 0 );
		}
		
		/* if( cmd[2] && *cmd[2] == '1' ) -=> Chat is being cleaned up because of idleness */
		
		if( sb->who )
		{
			msn_sb_stop_keepalives( sb );
			
			/* This is a single-person chat, and the other person is leaving. */
			g_free( sb->who );
			sb->who = NULL;
			sb->ready = 0;
			
			debug( "Person %s left the one-to-one switchboard connection. Keeping it around as a spare...", cmd[1] );
			
			/* We could clean up the switchboard now, but keeping it around
			   as a spare for a next conversation sounds more sane to me.
			   The server will clean it up when it's idle for too long. */
		}
		else if( sb->chat )
		{
			imcb_chat_remove_buddy( sb->chat, cmd[1], "" );
		}
		else
		{
			/* PANIC! */
		}
	}
	else if( isdigit( cmd[0][0] ) )
	{
		int num = atoi( cmd[0] );
		const struct msn_status_code *err = msn_status_by_number( num );
		
		imcb_error( ic, "Error reported by switchboard server: %s", err->text );
		
		if( err->flags & STATUS_SB_FATAL )
		{
			msn_sb_destroy( sb );
			return 0;
		}
		else if( err->flags & STATUS_FATAL )
		{
			imc_logout( ic, TRUE );
			return 0;
		}
		else if( err->flags & STATUS_SB_IM_SPARE )
		{
			if( sb->who )
			{
				/* Apparently some invitation failed. We might want to use this
				   board later, so keep it as a spare. */
				g_free( sb->who );
				sb->who = NULL;
				
				/* Also clear the msgq, otherwise someone else might get them. */
				msn_msgq_purge( ic, &sb->msgq );
			}
			
			/* Do NOT return 0 here, we want to keep this sb. */
		}
	}
	else
	{
		/* debug( "Received unknown command from switchboard server: %s", cmd[0] ); */
	}
	
	return( 1 );
}

static int msn_sb_message( gpointer data, char *msg, int msglen, char **cmd, int num_parts )
{
	struct msn_switchboard *sb = data;
	struct im_connection *ic = sb->ic;
	char *body;
	int blen = 0;
	
	if( !num_parts )
		return( 1 );
	
	if( ( body = strstr( msg, "\r\n\r\n" ) ) )
	{
		body += 4;
		blen = msglen - ( body - msg );
	}
	
	if( strcmp( cmd[0], "MSG" ) == 0 )
	{
		char *ct = msn_findheader( msg, "Content-Type:", msglen );
		
		if( !ct )
			return( 1 );
		
		if( g_strncasecmp( ct, "text/plain", 10 ) == 0 )
		{
			g_free( ct );
			
			if( !body )
				return( 1 );
			
			if( sb->who )
			{
				imcb_buddy_msg( ic, cmd[1], body, 0, 0 );
			}
			else if( sb->chat )
			{
				imcb_chat_msg( sb->chat, cmd[1], body, 0, 0 );
			}
			else
			{
				/* PANIC! */
			}
		}
		else if( g_strncasecmp( ct, "text/x-msmsgsinvite", 19 ) == 0 )
		{
			char *itype = msn_findheader( body, "Application-GUID:", blen );
			char buf[1024];
			
			g_free( ct );
			
			*buf = 0;
			
			if( !itype )
				return( 1 );
			
			/* File transfer. */
			if( strcmp( itype, "{5D3E02AB-6190-11d3-BBBB-00C04F795683}" ) == 0 )
			{
				char *name = msn_findheader( body, "Application-File:", blen );
				char *size = msn_findheader( body, "Application-FileSize:", blen );
				
				if( name && size )
				{
					g_snprintf( buf, sizeof( buf ), "<< \x02""BitlBee\x02"" - Filetransfer: `%s', %s bytes >>\n"
					            "Filetransfers are not supported by BitlBee for now...", name, size );
				}
				else
				{
					strcpy( buf, "<< \x02""BitlBee\x02"" - Corrupted MSN filetransfer invitation message >>" );
				}
				
				if( name ) g_free( name );
				if( size ) g_free( size );
			}
			else
			{
				char *iname = msn_findheader( body, "Application-Name:", blen );
				
				g_snprintf( buf, sizeof( buf ), "<< \x02""BitlBee\x02"" - Unknown MSN invitation - %s (%s) >>",
				                                itype, iname ? iname : "no name" );
				
				if( iname ) g_free( iname );
			}
			
			g_free( itype );
			
			if( !*buf )
				return( 1 );
			
			if( sb->who )
			{
				imcb_buddy_msg( ic, cmd[1], buf, 0, 0 );
			}
			else if( sb->chat )
			{
				imcb_chat_msg( sb->chat, cmd[1], buf, 0, 0 );
			}
			else
			{
				/* PANIC! */
			}
		}
		else if( g_strncasecmp( ct, "text/x-msmsgscontrol", 20 ) == 0 )
		{
			char *who = msn_findheader( msg, "TypingUser:", msglen );
			
			if( who )
			{
				imcb_buddy_typing( ic, who, OPT_TYPING );
				g_free( who );
			}
			
			g_free( ct );
		}
		else
		{
			g_free( ct );
		}
	}
	
	return( 1 );
}

static gboolean msn_sb_keepalive( gpointer data, gint source, b_input_condition cond )
{
	struct msn_switchboard *sb = data;
	return sb->ready && msn_sb_sendmessage( sb, SB_KEEPALIVE_MESSAGE );
}

void msn_sb_start_keepalives( struct msn_switchboard *sb, gboolean initial )
{
	struct buddy *b;
	
	if( sb && sb->who && sb->keepalive == 0 &&
	    ( b = imcb_find_buddy( sb->ic, sb->who ) ) && !b->present &&
	    set_getbool( &sb->ic->acc->set, "switchboard_keepalives" ) )
	{
		if( initial )
			msn_sb_keepalive( sb, 0, 0 );
		
		sb->keepalive = b_timeout_add( 20000, msn_sb_keepalive, sb );
	}
}

void msn_sb_stop_keepalives( struct msn_switchboard *sb )
{
	if( sb && sb->keepalive > 0 )
	{
		b_event_remove( sb->keepalive );
		sb->keepalive = 0;
	}
}
