/**
 * @file irc.c
 * @brief IRC client implementation for UFO:AI
 */

/*
All original materal Copyright (C) 2002-2007 UFO: Alien Invasion team.

Most of this stuff comes from Warsow

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 "client.h"
#include "cl_irc.h"

#ifdef _WIN32
#	include <winerror.h>
#else
#	include <netinet/in.h>
#	include <arpa/inet.h>
#	include <netdb.h>
#	include <fcntl.h>
#endif

#define STRINGIFY(x) #x
#define DOUBLEQUOTE(x) STRINGIFY(x)
#define IRC_MAX_INPUTLENGTH 128

static cvar_t *irc_server = NULL;
static cvar_t *irc_port = NULL;
static cvar_t *irc_channel = NULL;
static cvar_t *irc_nick = NULL;
static cvar_t *irc_user = NULL;
static cvar_t *irc_password = NULL;
static cvar_t *irc_topic = NULL;
static cvar_t *irc_defaultChannel = NULL;
static cvar_t *irc_logConsole = NULL;
static cvar_t *irc_showIfNotInMenu = NULL;
/* menu cvar */
static cvar_t *irc_send_buffer = NULL;

static int inputLengthBackup;

static qboolean irc_connected = qfalse;

static struct net_stream *irc_stream = NULL;

static const qboolean IRC_INVISIBLE = qtrue;
static const char IRC_QUIT_MSG[] = "ufoai.sf.net";

static irc_channel_t ircChan;
static irc_channel_t *chan = NULL;

static char irc_buffer[4048];
static char irc_names_buffer[1024];

static void Irc_Logic_RemoveChannelName(irc_channel_t *channel, const char *nick);
static void Irc_Logic_AddChannelName(irc_channel_t *channel, irc_nick_prefix_t prefix, const char *nick);
static void Irc_Client_Names_f(void);
static void Irc_Logic_Disconnect(const char *reason);

/*
===============================================================
Common functions
===============================================================
*/

static qboolean Irc_IsChannel (const char *target)
{
	assert(target);
	return (*target == '#' || *target == '&');
}

static void Irc_ParseName (const char *mask, char *nick, irc_nick_prefix_t *prefix)
{
	const char *emph;
	if (*mask == IRC_NICK_PREFIX_OP || *mask == IRC_NICK_PREFIX_VOICE) {
		*prefix = (irc_nick_prefix_t) *mask;	/* read prefix */
		++mask;									/* crop prefix from mask */
	} else
		*prefix = IRC_NICK_PREFIX_NONE;
	emph = strchr(mask, '!');
	if (emph) {
		/* complete hostmask, crop anything after ! */
		memcpy(nick, mask, emph - mask);
		nick[emph - mask] = '\0';
	} else
		/* just the nickname, use as is */
		strcpy(nick, mask);
}

/*
===============================================================
Protokoll functions
===============================================================
*/

static cvar_t *irc_messageBucketSize = NULL;
static cvar_t *irc_messageBucketBurst = NULL;
static cvar_t *irc_characterBucketSize = NULL;
static cvar_t *irc_characterBucketBurst = NULL;
static cvar_t *irc_characterBucketRate = NULL;

typedef struct irc_bucket_message_s {
	char *msg;
	size_t msg_len;
	struct irc_bucket_message_s *next;
} irc_bucket_message_t;

typedef struct irc_bucket_s {
	irc_bucket_message_t *first_msg;	/**< pointer to first message in queue */
	unsigned int message_size;			/**< number of messages in bucket */
	unsigned int character_size;		/**< number of characters in bucket */
	int last_refill;				/**< last refill timestamp */
	double character_token;
} irc_bucket_t;

static qboolean Irc_Proto_ParseServerMsg(const char *txt, size_t txt_len, irc_server_msg_t *msg);

static qboolean Irc_Proto_Enqueue(const char *msg, size_t msg_len);
static void Irc_Proto_RefillBucket(void);
static void Irc_Proto_DrainBucket(void);

static irc_bucket_t irc_bucket;

/**
 * @sa Irc_Proto_Disconnect
 */
static qboolean Irc_Proto_Connect (const char *host, const char *port)
{
	const qboolean status = Irc_Net_Connect(host, port);
	if (!status) {
		if (!irc_messageBucketSize) {
			irc_messageBucketSize = Cvar_Get("irc_messageBucketSize", DOUBLEQUOTE(IRC_DEFAULT_MESSAGE_BUCKET_SIZE), CVAR_ARCHIVE, NULL);
			irc_messageBucketBurst = Cvar_Get("irc_messageBucketBurst", DOUBLEQUOTE(IRC_DEFAULT_MESSAGE_BUCKET_BURST), CVAR_ARCHIVE, NULL);
			irc_characterBucketSize = Cvar_Get("irc_characterBucketSize", DOUBLEQUOTE(IRC_DEFAULT_CHARACTER_BUCKET_SIZE), CVAR_ARCHIVE, NULL);
			irc_characterBucketBurst = Cvar_Get("irc_characterBucketBurst", DOUBLEQUOTE(IRC_DEFAULT_CHARACTER_BUCKET_BURST), CVAR_ARCHIVE, NULL);
			irc_characterBucketRate = Cvar_Get("irc_characterBucketRate", DOUBLEQUOTE(IRC_DEFAULT_CHARACTER_BUCKET_RATE), CVAR_ARCHIVE, NULL);
		}
		irc_bucket.first_msg = NULL;
		irc_bucket.message_size = 0;
		irc_bucket.character_size = 0;
		irc_bucket.last_refill = cls.realtime;
		irc_bucket.character_token = (double)irc_characterBucketBurst->value;
	}
	return status;
}

/**
 * @sa Irc_Proto_Connect
 */
static qboolean Irc_Proto_Disconnect (void)
{
	const qboolean status = Irc_Net_Disconnect();
	if (!status) {
		irc_bucket_message_t *msg = irc_bucket.first_msg;
		irc_bucket_message_t *prev;
		while (msg) {
			prev = msg;
			msg = msg->next;
			Mem_Free(prev->msg);
			Mem_Free(prev);
		}
		irc_bucket.first_msg = NULL;
		irc_bucket.message_size = 0;
		irc_bucket.character_size = 0;
	}
	return status;
}

/**
 * @sa Irc_Net_Send
 */
static qboolean Irc_Proto_Quit (const char *quitmsg)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "QUIT %s\r\n", quitmsg);
	msg[sizeof(msg) - 1] = '\0';
	Irc_Net_Send(msg, msg_len);	/* send immediately */
	return qfalse;
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Nick (const char *nick)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "NICK %s\r\n", nick);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_User (const char *user, qboolean invisible, const char *name)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "USER %s %c * :%s\r\n", user, invisible ? '8' : '0', name);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Password (const char *password)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "PASS %s\r\n", password);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Join (const char *channel, const char *password)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = password
		? snprintf(msg, sizeof(msg) - 1, "JOIN %s %s\r\n", channel, password)
		: snprintf(msg, sizeof(msg) - 1, "JOIN %s\r\n", channel);
	msg[sizeof(msg) - 1] = '\0';

	/* only one channel allowed */
	if (chan)
		return qfalse;

	chan = &ircChan;
	memset(chan, 0, sizeof(irc_channel_t));
	Q_strncpyz(chan->name, channel, sizeof(chan->name));
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Part (const char *channel)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "PART %s\r\n", channel);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Mode (const char *target, const char *modes, const char *params)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = params
		? snprintf(msg, sizeof(msg) - 1, "MODE %s %s %s\r\n", target, modes, params)
		: snprintf(msg, sizeof(msg) - 1, "MODE %s %s\r\n", target, modes);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Topic (const char *channel, const char *topic)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = topic
		? snprintf(msg, sizeof(msg) - 1, "TOPIC %s :%s\r\n", channel, topic)
		: snprintf(msg, sizeof(msg) - 1, "TOPIC %s\r\n", channel);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 * @return qtrue on failure
 */
static qboolean Irc_Proto_Msg (const char *target, const char *text)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "PRIVMSG %s :%s\r\n", target, text);
	if (*text == '/') {
		Com_DPrintf(DEBUG_CLIENT, "Don't send irc commands as PRIVMSG\n");
		Cbuf_AddText(va("%s\n", &text[1]));
		return qtrue;
	}
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Notice (const char *target, const char *text)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "NOTICE %s :%s\r\n", target, text);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

#if 0
/**
 * @sa Irc_Net_Send
 */
static qboolean Irc_Proto_Pong (const char *nick, const char *server, const char *cookie)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = cookie
		? snprintf(msg, sizeof(msg) - 1, "PONG %s %s :%s\r\n", nick, server, cookie)
		: snprintf(msg, sizeof(msg) - 1, "PONG %s %s\r\n", nick, server);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Net_Send(msg, msg_len);	/* send immediately */
}
#endif

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Kick (const char *channel, const char *nick, const char *reason)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = reason
		? snprintf(msg, sizeof(msg) - 1, "KICK %s %s :%s\r\n", channel, nick, reason)
		: snprintf(msg, sizeof(msg) - 1, "KICK %s %s :%s\r\n", channel, nick, nick);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Who (const char *nick)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "WHO %s\r\n", nick);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Whois (const char *nick)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "WHOIS %s\r\n", nick);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Proto_Enqueue
 */
static qboolean Irc_Proto_Whowas (const char *nick)
{
	char msg[IRC_SEND_BUF_SIZE];
	const int msg_len = snprintf(msg, sizeof(msg) - 1, "WHOWAS %s\r\n", nick);
	msg[sizeof(msg) - 1] = '\0';
	return Irc_Proto_Enqueue(msg, msg_len);
}

/**
 * @sa Irc_Logic_ReadMessages
 */
static qboolean Irc_Proto_PollServerMsg (irc_server_msg_t *msg, qboolean *msg_complete)
{
	static char buf[IRC_RECV_BUF_SIZE];
	static char *last = buf;
	int recvd;
	*msg_complete = qfalse;
	/* recv packet */
	recvd = stream_dequeue(irc_stream, last, sizeof(buf) - (last - buf) - 1);
	if (recvd >= 0) {
		/* terminate buf string */
		const char * const begin = buf;
		last += recvd;
		*last = '\0';
		if (last != begin) {
			/* buffer not empty; */
			const char * const end = strstr(begin, "\r\n");
			if (end) {
				/* complete command in buffer, parse */
				const size_t cmd_len = end + 2 - begin;
				if (!Irc_Proto_ParseServerMsg(begin, cmd_len, msg)) {
					/* parsing successful */
					/* move succeeding commands to begin of buffer */
					memmove(buf, end + 2, sizeof(buf) - cmd_len);
					last -= cmd_len;
					*msg_complete = qtrue;
				} else {
					/* parsing failure, fatal */
					Com_Printf("Received invalid packet from server\n");
					return qtrue;
				}
			}
		} else
			*msg_complete = qfalse;
		return qfalse;
	}
	return qtrue;
}

/**
 * @brief Append the irc message to the buffer
 * @note the irc_buffer is linked to menuText array to display in the menu
 * @param[in] msg the complete irc message (without \n)
 * @return true if the message was added to the chatbuffer, too
 */
static qboolean Irc_AppendToBuffer (const char* const msg)
{
	char buf[IRC_RECV_BUF_SIZE];
	menu_t* menu;

	while (strlen(irc_buffer) + strlen(msg) + 1 >= sizeof(irc_buffer) ) {
		char *n;
		if (!(n = strchr(irc_buffer, '\n'))) {
			irc_buffer[0] = '\0';
			break;
		}
		memmove(irc_buffer, n + 1, strlen(n));
	}

	Com_sprintf(buf, sizeof(buf), "%s\n", msg);
	Q_strcat(irc_buffer, buf, sizeof(irc_buffer));
	if (irc_logConsole->integer)
		Com_Printf("IRC: %s\n", msg);

	MN_TextScrollBottom("irc_data");
	menu = MN_GetMenu(NULL); /* get current menu from stack */
	if (irc_showIfNotInMenu->integer && Q_strncmp(menu->name, "irc", 4)) {
		S_StartLocalSound("misc/talk");
		MN_AddChatMessage(msg);
		return qtrue;
	}
	return qfalse;
}

static void Irc_Client_CmdRplWhowasuser (const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	const char *nick = "", *user = "", *host = "", *real_name = trailing;
	char *p;
	unsigned int i = 0;

	/* parse params "<nick> <user> <host> * :<real name>" */
	Q_strncpyz(buf, params, sizeof(buf));
	for (p = strtok(buf, " "); p; p = strtok(NULL, " "), ++i) {
		switch (i) {
		case 1:
			nick = p;
			break;
		case 2:
			user = p;
			break;
		case 3:
			host = p;
			break;
		}
	}
	Irc_AppendToBuffer(va("^B%s was %s@%s : %s", nick, user, host, real_name));
}

static inline void Irc_Client_CmdTopic (const char *prefix, const char *trailing)
{
	Cvar_ForceSet("irc_topic", trailing);
}

static void Irc_Client_CmdRplTopic (const char *params, const char *trailing)
{
	const char *channel = strchr(params, ' ');
	if (channel) {
		++channel;
		Irc_Client_CmdTopic(params, trailing);
	}
}

static void Irc_Client_CmdRplWhoisuser (const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	const char *nick = "", *user = "", *host = "", *real_name = trailing;
	char *p;
	unsigned int i = 0;

	/* parse params "<nick> <user> <host> * :<real name>" */
	strcpy(buf, params);
	for (p = strtok(buf, " "); p; p = strtok(NULL, " "), ++i) {
		switch (i) {
		case 1:
			nick = p;
			break;
		case 2:
			user = p;
			break;
		case 3:
			host = p;
			break;
		}
	}
	Irc_AppendToBuffer(va("^B%s is %s@%s : %s", nick, user, host, real_name));
}

static void Irc_Client_CmdRplWhoisserver (const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	const char *nick = "", *server = "", *server_info = trailing;
	char *p;
	unsigned int i = 0;

	/* parse params "<nick> <server> :<server info>" */
	strcpy(buf, params);
	for (p = strtok(buf, " "); p; p = strtok(NULL, " "), ++i) {
		switch (i) {
		case 1:
			nick = p;
			break;
		case 2:
			server = p;
			break;
		}
	}
	Irc_AppendToBuffer(va("^B%s using %s : %s", nick, server, server_info));
}

static void Irc_Client_CmdRplWhoisaccount (const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	const char *nick = "", *account = "";
	char *p;
	unsigned int i = 0;

	/* parse params "<nick> <account> :is logged in as" */
	strcpy(buf, params);
	for (p = strtok(buf, " "); p; p = strtok(NULL, " "), ++i) {
		switch (i) {
		case 1:
			nick = p;
			break;
		case 2:
			account = p;
			break;
		}
	}
	Irc_AppendToBuffer(va("^B%s %s %s", nick, trailing, account));
}

static void Irc_Client_CmdRplWhoisidle (const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	const char *nick = "", *idle = "";
	char *p;
	unsigned int i = 0;

	/* parse params "<nick> <integer> :seconds idle" */
	strcpy(buf, params);
	for (p = strtok(buf, " "); p; p = strtok(NULL, " "), ++i) {
		switch (i) {
		case 1:
			nick = p;
			break;
		case 2:
			idle = p;
			break;
		}
	}
	Irc_AppendToBuffer(va("^B%s is %s %s", nick, idle, trailing));
}

static void Irc_Client_CmdRplWhoreply (const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	const char *channel = "", *user = "", *host = "", *server = "", *nick = "", *hg = "";
	char *p;
	unsigned int i = 0;

	/* parse params "<channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real name>" */
	strcpy(buf, params);
	for (p = strtok(buf, " "); p; p = strtok(NULL, " "), ++i) {
		switch (i) {
		case 0:
			channel = p;
			break;
		case 1:
			user = p;
			break;
		case 2:
			host = p;
			break;
		case 3:
			server = p;
			break;
		case 4:
			nick = p;
			break;
		case 5:
			hg = p;
			break;
		}
	}
	Irc_AppendToBuffer(va("%s %s %s %s %s %s : %s", channel, user, host, server, nick, hg, trailing));
}

static void Irc_Client_CmdMode (const char *prefix, const char *params, const char *trailing)
{
	char nick[MAX_VAR];
	irc_nick_prefix_t p;
	Irc_ParseName(prefix, nick, &p);
	Irc_AppendToBuffer(va("^B%s sets mode %s", nick, params));
}

static void Irc_Client_CmdJoin (const char *prefix, const char *params, const char *trailing)
{
	char nick[MAX_VAR];
	irc_nick_prefix_t p;
	Irc_ParseName(prefix, nick, &p);
	Irc_AppendToBuffer(va("^BJoined: %s", nick));
	Irc_Logic_AddChannelName(chan, p, nick);
}

static void Irc_Client_CmdPart (const char *prefix, const char *trailing)
{
	char nick[MAX_VAR];
	irc_nick_prefix_t p;
	Irc_ParseName(prefix, nick, &p);
	Irc_AppendToBuffer(va("^BLeft: %s (%s)", nick, prefix));
	Irc_Logic_RemoveChannelName(chan, nick);
}

static void Irc_Client_CmdQuit (const char *prefix, const char *params, const char *trailing)
{
	char nick[MAX_VAR];
	irc_nick_prefix_t p;
	Irc_ParseName(prefix, nick, &p);
	Irc_AppendToBuffer(va("^BQuits: %s (%s)", nick, trailing));
	Irc_Logic_RemoveChannelName(chan, nick);
}

static void Irc_Client_CmdKill (const char *prefix, const char *params, const char *trailing)
{
	char nick[MAX_VAR];
	irc_nick_prefix_t p;
	Irc_ParseName(prefix, nick, &p);
	Irc_AppendToBuffer(va("^BKilled: %s (%s)", nick, trailing));
	Irc_Logic_RemoveChannelName(chan, nick);
}

static void Irc_Client_CmdKick (const char *prefix, const char *params, const char *trailing)
{
	char buf[IRC_SEND_BUF_SIZE];
	char nick[MAX_VAR];
	irc_nick_prefix_t p;
	const char *channel, *victim;
	Irc_ParseName(prefix, nick, &p);
	strcpy(buf, params);
	channel = strtok(buf, " ");
	victim = strtok(NULL, " ");
	if (!strcmp(victim, irc_nick->string)) {
		/* we have been kicked */
		Irc_AppendToBuffer(va("^BYou were kicked from %s by %s (%s)", channel, nick, trailing));
	} else {
		/* someone else was kicked */
		Irc_AppendToBuffer(va("^B%s kicked %s (%s)", nick, victim, trailing));
	}
	Irc_Logic_RemoveChannelName(chan, nick);
}

/**
 * @brief Changes the cvar 'name' with the new name you set
 */
static void Irc_Client_CmdNick (const char *prefix, const char *params, const char *trailing)
{
	char nick[MAX_VAR];
	irc_nick_prefix_t p;

	/* not connected */
	if (!chan)
		return;

	Irc_ParseName(prefix, nick, &p);
	if (!strcmp(irc_nick->string, nick))
		Cvar_ForceSet("name", trailing);
	Irc_AppendToBuffer(va("%s is now known as %s", nick, trailing));
	Irc_Logic_RemoveChannelName(chan, nick);
	Irc_Logic_AddChannelName(chan, p, trailing);
}

#define IRC_CTCP_MARKER_CHR '\001'
#define IRC_CTCP_MARKER_STR "\001"

static void Irc_Client_CmdPrivmsg (const char *prefix, const char *params, const char *trailing)
{
	char nick[MAX_VAR];
	char * const emph = strchr(prefix, '!');
	char * ctcp = strchr(trailing, IRC_CTCP_MARKER_CHR);
	menu_t* menu;
	memset(nick, 0, sizeof(nick));
	if (emph)
		memcpy(nick, prefix, emph - prefix);
	else
		strcpy(nick, prefix);

	if (ctcp) {
		if (!Q_strcmp(trailing + 1, "VERSION" IRC_CTCP_MARKER_STR)) {
			/* response with the game version */
			Irc_Proto_Msg(irc_defaultChannel->string, Cvar_VariableString("version"));
			/*Irc_Proto_Notice(nick, IRC_CTCP_MARKER_STR "VERSION " UFO_VERSION " " CPUSTRING " " __DATE__ " " BUILDSTRING);*/
			Com_DPrintf(DEBUG_CLIENT, "Irc_Client_CmdPrivmsg: Response to version query\n");
		} else if (!Q_strncmp(trailing + 1, "PING", 4)) {
			char response[IRC_SEND_BUF_SIZE];
			strcpy(response, trailing);
			response[2] = 'O'; /* PING => PONG */
			Irc_Proto_Notice(nick, response);
		} else if (!Q_strcmp(trailing + 1, "TIME" IRC_CTCP_MARKER_STR)) {
			const time_t t = time(NULL);
			char response[IRC_SEND_BUF_SIZE];
			const size_t response_len = sprintf(response, IRC_CTCP_MARKER_STR "TIME :%s" IRC_CTCP_MARKER_STR, ctime(&t));
			response[response_len - 1] = '\0';	/* remove trailing \n */
			Irc_Proto_Notice(nick, response);
		} else {
			Com_Printf("Irc_Client_CmdPrivmsg: Unknown ctcp command: '%s'\n", trailing);
		}
	} else {
		menu = MN_GetMenu(NULL);

		if (!Irc_AppendToBuffer(va("<%s> %s", nick, trailing))) {
			/* check whether this is no message to the channel - but to the user */
			if (params && Q_strcmp(params, irc_defaultChannel->string)) {
				S_StartLocalSound("misc/lobbyprivmsg");
				MN_AddChatMessage(va("<%s> %s\n", nick, trailing));
			} else if (strstr(trailing, irc_nick->string)) {
				S_StartLocalSound("misc/lobbyprivmsg");
				MN_AddChatMessage(va("<%s> %s\n", nick, trailing));
				if (Q_strcmp(menu->name, "irc") && Q_strcmp(menu->name, mn_hud->string)) {
					/* we are not in hud mode, nor in the lobby menu, use a popup */
					MN_PushMenu("chat_popup");
				}
			}
		}

		if (menu && Q_strcmp(menu->name, "irc")) {
			Com_Printf("%c<%s@lobby> %s\n", 1, nick, trailing);
		}
	}
}

/**
 * @todo: Implement me
 */
static void Irc_Client_CmdRplNamreply (const char *params, const char *trailing)
{
	char *parseBuf, *pos;
	char *space;
	char nick[MAX_VAR];
	size_t len = strlen(trailing);
	irc_nick_prefix_t p;

	if (!chan)
		return;

	parseBuf = Mem_PoolAlloc((len + 1) * sizeof(char), cl_ircSysPool, 0);
	if (!parseBuf)
		return;

	strncpy(parseBuf, trailing, len);
	parseBuf[len] = '\0';

	pos = parseBuf;

	do {
		/* name are space seperated */
		space = strstr(pos, " ");
		if (space)
			*space = '\0';
		Irc_ParseName(pos, nick, &p);
		Irc_Logic_AddChannelName(chan, p, nick);
		if (space)
			pos = space + 1;
	} while (space && *pos);

	Mem_Free(parseBuf);
}

/**
 * @todo: Implement me
 */
static void Irc_Client_CmdRplEndofnames (const char *params, const char *trailing)
{
}

/**
 * @sa Irc_Logic_ReadMessages
 */
static qboolean Irc_Proto_ProcessServerMsg (const irc_server_msg_t *msg)
{
	irc_command_t cmd;
	const char *p = NULL;
	cmd.type = msg->type;

	/* some debug output */
	Com_DPrintf(DEBUG_CLIENT, "pre: '%s', param: '%s', trail: '%s'\n", msg->prefix, msg->params, msg->trailing);

	/* @todo: Skip non printable chars here */

	switch (cmd.type) {
	case IRC_COMMAND_NUMERIC:
		cmd.id.numeric = msg->id.numeric;
#ifdef DEBUG
		Com_DPrintf(DEBUG_CLIENT, "<%s> [%03d] %s : %s", msg->prefix, cmd.id.numeric, msg->params, msg->trailing);
#endif
		switch (cmd.id.numeric) {
		case RPL_HELLO:
		case RPL_WELCOME:
		case RPL_YOURHOST:
		case RPL_CREATED:
		case RPL_MYINFO:
		case RPL_MOTDSTART:
		case RPL_MOTD:
		case RPL_LOCALUSERS:
		case RPL_GLOBALUSERS:
		case RPL_ISUPPORT:
		case RPL_LUSEROP:
		case RPL_LUSERUNKNOWN:
		case RPL_LUSERCHANNELS:
		case RPL_LUSERCLIENT:
		case RPL_LUSERME:
			return qtrue;

		/* read our own motd */
		case RPL_ENDOFMOTD:
			{
				char *fbuf;
				int size;
				size = FS_LoadFile("irc_motd.txt", (byte **) &fbuf);
				if (size)
					Irc_AppendToBuffer(fbuf);
			}
			return qtrue;

		case RPL_NAMREPLY:
			Irc_Client_CmdRplNamreply(msg->params, msg->trailing);
			return qtrue;
		case RPL_ENDOFNAMES:
			Irc_Client_CmdRplEndofnames(msg->params, msg->trailing);
			return qtrue;
		case RPL_TOPIC:
			Irc_Client_CmdRplTopic(msg->params, msg->trailing);
			return qtrue;
		case RPL_NOTOPIC:
			return qtrue;
		case RPL_WHOISUSER:
			Irc_Client_CmdRplWhoisuser(msg->params, msg->trailing);
			return qtrue;
		case RPL_WHOISSERVER:
			Irc_Client_CmdRplWhoisserver(msg->params, msg->trailing);
			return qtrue;
		case RPL_WHOISIDLE:
			Irc_Client_CmdRplWhoisidle(msg->params, msg->trailing);
			return qtrue;
		case RPL_WHOISACCOUNT:
			Irc_Client_CmdRplWhoisaccount(msg->params, msg->trailing);
			return qtrue;
		case RPL_WHOREPLY:
			Irc_Client_CmdRplWhoreply(msg->params, msg->trailing);
			return qtrue;
		case RPL_WHOWASUSER:
			Irc_Client_CmdRplWhowasuser(msg->params, msg->trailing);
			return qtrue;
		case RPL_ENDOFWHO:
		case RPL_WHOISCHANNELS:
		case RPL_WHOISOPERATOR:
		case RPL_ENDOFWHOIS:
		case RPL_ENDOFWHOWAS:
			p = strchr(msg->params, ' ');
			if (p) {
				++p;
				Irc_AppendToBuffer(va("%s %s", p, msg->trailing));
			}
			return qtrue;

		/* error codes */
		case ERR_NOSUCHNICK:
		case ERR_NOSUCHSERVER:
		case ERR_NOSUCHCHANNEL:
		case ERR_CANNOTSENDTOCHAN:
		case ERR_TOOMANYCHANNELS:
		case ERR_WASNOSUCHNICK:
		case ERR_TOOMANYTARGETS:
		case ERR_NOORIGIN:
		case ERR_NORECIPIENT:
		case ERR_NOTEXTTOSEND:
		case ERR_NOTOPLEVEL:
		case ERR_WILDTOPLEVEL:
		case ERR_UNKNOWNCOMMAND:
		case ERR_NOMOTD:
		case ERR_NOADMININFO:
		case ERR_FILEERROR:
		case ERR_NONICKNAMEGIVEN:
		case ERR_ERRONEUSNICKNAME:
		case ERR_NICKNAMEINUSE:
		case ERR_NICKCOLLISION:
		case ERR_BANNICKCHANGE:
		case ERR_NCHANGETOOFAST:
		case ERR_USERNOTINCHANNEL:
		case ERR_NOTONCHANNEL:
		case ERR_USERONCHANNEL:
		case ERR_NOLOGIN:
		case ERR_SUMMONDISABLED:
		case ERR_USERSDISABLED:
		case ERR_NOTREGISTERED:
		case ERR_NEEDMOREPARAMS:
		case ERR_ALREADYREGISTRED:
		case ERR_NOPERMFORHOST:
		case ERR_PASSWDMISMATCH:
		case ERR_YOUREBANNEDCREEP:
		case ERR_BADNAME:
		case ERR_KEYSET:
		case ERR_CHANNELISFULL:
		case ERR_UNKNOWNMODE:
		case ERR_INVITEONLYCHAN:
		case ERR_BANNEDFROMCHAN:
		case ERR_BADCHANNELKEY:
		case ERR_NOPRIVILEGES:
		case ERR_CHANOPRIVSNEEDED:
		case ERR_CANTKILLSERVER:
		case ERR_NOOPERHOST:
		case ERR_UMODEUNKNOWNFLAG:
		case ERR_USERSDONTMATCH:
		case ERR_GHOSTEDCLIENT:
		case ERR_LAST_ERR_MSG:
		case ERR_SILELISTFULL:
		case ERR_NOSUCHGLINE:
		case ERR_BADPING:
		case ERR_TOOMANYDCC:
		case ERR_LISTSYNTAX:
		case ERR_WHOSYNTAX:
		case ERR_WHOLIMEXCEED:
			Irc_AppendToBuffer(va("%s : %s", msg->params, msg->trailing));
			return qtrue;
		default:
			Com_DPrintf(DEBUG_CLIENT, "<%s> [%s] %s : %s\n", msg->prefix, cmd.id.string, msg->params, msg->trailing);
			return qtrue;
		} /* switch */
		break;
	case IRC_COMMAND_STRING:
		cmd.id.string = msg->id.string;
#ifdef DEBUG
		Com_DPrintf(DEBUG_CLIENT, "<%s> [%s] %s : %s\n", msg->prefix, cmd.id.string, msg->params, msg->trailing);
#endif
		if (!Q_strncmp(cmd.id.string, "NICK", 4))
			Irc_Client_CmdNick(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "QUIT", 4))
			Irc_Client_CmdQuit(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "NOTICE", 6)) {
			if (irc_logConsole->integer)
				Com_Printf(va("%s\n", msg->trailing));
		} else if (!Q_strncmp(cmd.id.string, "PRIVMSG", 7))
			Irc_Client_CmdPrivmsg(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "MODE", 4))
			Irc_Client_CmdMode(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "JOIN", 4))
			Irc_Client_CmdJoin(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "PART", 4))
			Irc_Client_CmdPart(msg->prefix, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "TOPIC", 5))
			Irc_Client_CmdTopic(msg->prefix, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "KILL", 4))
			Irc_Client_CmdKill(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "KICK", 4))
			Irc_Client_CmdKick(msg->prefix, msg->params, msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "PING", 4))
			Com_DPrintf(DEBUG_CLIENT, "IRC: <PING> %s\n", msg->trailing);
		else if (!Q_strncmp(cmd.id.string, "ERROR", 5))
			Irc_Logic_Disconnect(msg->trailing);
		else
			Irc_AppendToBuffer(msg->trailing);
		break;
	} /* switch */
	return qfalse;
}

static qboolean Irc_Proto_ParseServerMsg (const char *txt, size_t txt_len, irc_server_msg_t *msg)
{
	const char *c = txt;
	const char *end = txt + txt_len;
	*(msg->prefix) = '\0';
	*(msg->params) = '\0';
	*(msg->trailing) = '\0';
	if (c < end && *c == ':') {
		/* parse prefix */
		char *prefix = msg->prefix;
		++c;
		while (c < end && *c != '\r' && *c != ' ') {
			*prefix = *c;
			++prefix;
			++c;
		}
		*prefix = '\0';
		++c;
	}
	if (c < end && *c != '\r') {
		/* parse command */
		if (*c >= '0' && *c <= '9') {
			/* numeric command */
			char command[4];
			int i;
			for (i = 0; i < 3; ++i) {
				if (c < end && *c >= '0' && *c <= '9') {
					command[i] = *c;
					++c;
				} else
					return qtrue;
			}
			command[3] = '\0';
			msg->type = IRC_COMMAND_NUMERIC;
			msg->id.numeric = atoi(command);
		} else { /* != \r */
			/* string command */
			char *command = msg->id.string;
			while (c < end && *c != '\r' && *c != ' ') {
				*command = *c;
				++command;
				++c;
			}
			*command = '\0';
			msg->type = IRC_COMMAND_STRING;
		}
		if (c < end && *c == ' ') {
			/* parse params and trailing */
			char *params = msg->params;
			++c;
			while (c < end && *c != '\r' && *c != ':') {
				/* parse params */
				while (c < end && *c != '\r' && *c != ' ') {
					/* parse single param */
					*params = *c;
					++params;
					++c;
				}
				if (c + 1 < end && *c == ' ' && *(c+1) != ':') {
					/* more params */
					*params = ' ';
					++params;
				}
				if (*c == ' ')
					++c;
			}
			*params = '\0';
			if (c < end && *c == ':') {
				/* parse trailing */
				char *trailing = msg->trailing;
				++c;
				while (c < end && *c != '\r') {
					*trailing = *c;
					++trailing;
					++c;
				}
				*trailing = '\0';
			}
		}
	}
	return qfalse;
}

/**
 * @sa Irc_Proto_DrainBucket
 * @sa Irc_Proto_RefillBucket
 */
static qboolean Irc_Proto_Enqueue (const char *msg, size_t msg_len)
{
	int messageBucketSize;
	int characterBucketSize;
	irc_bucket_message_t *m, *n;

	/* connection failed if this is null */
	if (!irc_messageBucketSize) {
		Com_DPrintf(DEBUG_CLIENT, "Irc_Proto_Enqueue: irc_messageBucketSize is zero\n");
		return qtrue;
	}

	messageBucketSize = irc_messageBucketSize->integer;
	characterBucketSize = irc_characterBucketSize->integer;

	/* create message node */
	m = (irc_bucket_message_t*) Mem_Alloc(sizeof(irc_bucket_message_t));
	n = irc_bucket.first_msg;
	if (irc_bucket.message_size + 1 <= messageBucketSize && irc_bucket.character_size + msg_len <= characterBucketSize) {
		/* @todo: strip high bits - or unprintable chars */
		m->msg = (char*) Mem_Alloc(msg_len);
		memcpy(m->msg, msg, msg_len);
		m->msg_len = msg_len;
		m->next = NULL;
		/* append message node */
		if (n) {
			while (n->next)
				n = n->next;
			n->next = m;
		} else {
			irc_bucket.first_msg = m;
		}
		/* update bucket sizes */
		++irc_bucket.message_size;
		irc_bucket.character_size += msg_len;
		return qfalse;
	} else {
		Com_Printf("Bucket(s) full. Could not enqueue message\n");
		return qtrue;
	}
}

/**
 * @sa Irc_Proto_Enqueue
 * @sa Irc_Proto_DrainBucket
 */
static void Irc_Proto_RefillBucket (void)
{
	/* calculate token refill */
	const double characterBucketSize = irc_characterBucketSize->value;
	const double characterBucketRate = irc_characterBucketRate->value;
	const int ms = cls.realtime;
	const int ms_delta = ms - irc_bucket.last_refill;
	const double char_delta = (ms_delta * characterBucketRate) / 1000;
	const double char_new = irc_bucket.character_token + char_delta;
	/* refill token (but do not exceed maximum) */
	irc_bucket.character_token = min(char_new, characterBucketSize);
	/* set timestamp so next refill can calculate delta */
	irc_bucket.last_refill = ms;
}

/**
 * @brief Send all enqueued packets
 * @sa Irc_Proto_Enqueue
 * @sa Irc_Proto_RefillBucket
 */
static void Irc_Proto_DrainBucket (void)
{
	const double characterBucketBurst = irc_characterBucketBurst->value;
	irc_bucket_message_t *msg;
#if 0
	Com_DPrintf(DEBUG_CLIENT, "Irc_Proto_DrainBucket: Queue send\n");
#endif
	/* remove messages whose size exceed our burst size (we can not send them) */
	for (msg = irc_bucket.first_msg; msg && msg->msg_len > characterBucketBurst; msg = irc_bucket.first_msg) {
		irc_bucket_message_t * const next = msg->next;
		/* update bucket sizes */
		--irc_bucket.message_size;
		irc_bucket.character_size -= msg->msg_len;
		/* free message */
		Mem_Free(msg->msg);
		/* dequeue message */
		irc_bucket.first_msg = next;
	}
	/* send burst of remaining messages */
	for (msg = irc_bucket.first_msg; msg; msg = irc_bucket.first_msg) {
		/* send message */
		Irc_Net_Send(msg->msg, msg->msg_len);
		irc_bucket.character_token -= msg->msg_len;
		/* dequeue message */
		irc_bucket.first_msg = msg->next;
		/* update bucket sizes */
		--irc_bucket.message_size;
		irc_bucket.character_size -= msg->msg_len;
		/* free message */
		Mem_Free(msg->msg);
		Mem_Free(msg);
	}
}

/*
===============================================================
Logic functions
===============================================================
*/

/**
 * @sa Irc_Logic_Frame
 */
static void Irc_Logic_SendMessages (void)
{
	Irc_Proto_RefillBucket();		/* first refill token */
	Irc_Proto_DrainBucket();	/* then send messages (if allowed) */
}

/**
 * @sa Irc_Logic_Frame
 * @sa Irc_Logic_Disconnect
 * @sa Irc_Proto_ProcessServerMsg
 * @sa Irc_Proto_PollServerMsg
 */
static void Irc_Logic_ReadMessages (void)
{
	qboolean msg_complete;
	do {
		irc_server_msg_t msg;
		if (!Irc_Proto_PollServerMsg(&msg, &msg_complete)) {
			/* success */
			if (msg_complete)
				Irc_Proto_ProcessServerMsg(&msg);
		} else
			/* failure */
			Irc_Logic_Disconnect("Server closed connection");
	} while (msg_complete);
}

static void Irc_Logic_Connect (const char *server, const char *port)
{
	if (!Irc_Proto_Connect(server, port)) {
		/* connected to server, send NICK and USER commands */
		const char * const pass = irc_password->string;
		const char * const user = irc_user->string;
		if (strlen(pass))
			Irc_Proto_Password(pass);
		Irc_Proto_Nick(irc_nick->string);
		Irc_Proto_User(user, IRC_INVISIBLE, user);
		irc_connected = qtrue;
	}
}

static void Irc_Logic_Disconnect (const char *reason)
{
	if (irc_connected) {
		Com_Printf("Irc_Disconnect: %s\n", reason);
		Irc_Proto_Quit(reason);
		Irc_Proto_Disconnect();
		irc_connected = qfalse;
		chan = NULL;
		Cvar_ForceSet("irc_defaultChannel", "");
		Cvar_ForceSet("irc_topic", "Connecting (please wait)...");
		Irc_Input_Deactivate();
	} else
		Com_Printf("Irc_Disconnect: not connected\n");
}

/**
 * @sa Irc_Logic_ReadMessages
 * @sa Irc_Logic_SendMessages
 */
void Irc_Logic_Frame (int now, void *data)
{
	if (irc_connected && now > 0) {
		if (irc_channel->modified) {
			/* FIXME: do this without disconnect, connect */
			Irc_Logic_Disconnect("Switched to another channel");
			Irc_Logic_Connect(irc_server->string, irc_port->string);
			Cbuf_AddText(va("irc_join %s\n", irc_channel->string));
		}
		Irc_Logic_SendMessages();
		Irc_Logic_ReadMessages();
	}
	irc_channel->modified = qfalse;
}

static const char *Irc_Logic_GetChannelTopic (const irc_channel_t *channel)
{
	assert(channel);
	return channel->topic;
}

/**
 * @brief Adds a new username to the channel username list
 * @sa Irc_Logic_RemoveChannelName
 */
static void Irc_Logic_AddChannelName (irc_channel_t *channel, irc_nick_prefix_t prefix, const char *nick)
{
	int i;
	/* first one */
	irc_user_t* user = channel->user;
	for (i = 0; user && i < channel->users; i++, user = user->next) {
		if (!Q_strncmp(&(user->key[1]), nick, MAX_VAR - 1))
			return;
	}
	user = Mem_PoolAlloc(sizeof(irc_user_t), cl_ircSysPool, 0);
	user->next = channel->user;
	channel->user = user;

	Com_sprintf(user->key, sizeof(user->key), "%c%s", prefix, nick);
	channel->users++;
	Com_DPrintf(DEBUG_CLIENT, "Add user '%s' to userlist at pos %i\n", user->key, channel->users);
	Irc_Client_Names_f();
}

/**
 * @brief Removes a username from the channel username list
 * @sa Irc_Logic_AddChannelName
 */
static void Irc_Logic_RemoveChannelName (irc_channel_t *channel, const char *nick)
{
	int i;
	/* first one */
	irc_user_t* user = channel->user;
	irc_user_t* predecessor = NULL;
	for (i = 0; user && i < channel->users; i++, user = user->next) {
		if (!Q_strncmp(&(user->key[1]), nick, sizeof(user->key))) {
			/* delete the first user from the list */
			if (!predecessor)
				channel->user = user->next;
			/* point to the descendant */
			else
				predecessor->next = user->next;
			Mem_Free(user);
			chan->users--;
			Irc_Client_Names_f();
			return;
		}
		predecessor = user;
	}
}

/*
===============================================================
Network functions
===============================================================
*/

/**
 * @return qtrue if successful - qfalse otherwise
 * @sa Irc_Net_Disconnect
 */
static qboolean Irc_Net_Connect (const char *host, const char *port)
{
	if (irc_stream)
		free_stream(irc_stream);
	irc_stream = NET_Connect(host, port);
	return irc_stream ? qfalse : qtrue;
}

/**
 * @sa Irc_Net_Connect
 */
static qboolean Irc_Net_Disconnect (void)
{
	free_stream(irc_stream);
	return qtrue;
}

static void Irc_Net_Send (const char *msg, size_t msg_len)
{
	assert(msg);
	stream_enqueue(irc_stream, msg, msg_len);
}

/*
===============================================================
Bindings
===============================================================
*/

static void Irc_Connect_f (void)
{
	const int argc = Cmd_Argc();
	if (!irc_port || !irc_server)
		return;
	if (argc >= 2 && argc <= 4) {
#if 0
		if (irc_connected)
			Irc_Logic_Disconnect("reconnect");
#endif
		if (!irc_connected) {
			/* not connected yet */
			if (argc >= 2)
				Cvar_Set("irc_server", Cmd_Argv(1));
			if (argc >= 3)
				Cvar_Set("irc_port", Cmd_Argv(2));
			Com_Printf("Connect to %s:%s\n", irc_server->string, irc_port->string);
			Irc_Logic_Connect(irc_server->string, irc_port->string);
			if (argc >= 4)
				Cbuf_AddText(va("irc_join %s\n", Cmd_Argv(3)));
		} else
			Com_Printf("Already connected.\n");
	} else if (*irc_server->string && irc_port->value) {
		if (!irc_connected)
			Cbuf_AddText(va("irc_connect %s %s %s\n", irc_server->string, irc_port->string, irc_channel->string));
		else
			Com_Printf("Already connected.\n");
	} else
			Com_Printf("Usage: %s [<server>] [<port>] [<channel>]\n", Cmd_Argv(0));
}

static void Irc_Disconnect_f (void)
{
	Irc_Logic_Disconnect("normal exit");
}

static void Irc_Client_Join_f (void)
{
	const int argc = Cmd_Argc();
	if (argc >= 2 && argc <= 3) {
		const char * const channel = Cmd_Argv(1);
		const char * const channel_pass = argc == 3	/* password is optional */
			? Cmd_Argv(2)
			: NULL;
		if (!Irc_IsChannel(channel)) {
			Com_Printf("No valid channel name\n");
			return;
		}
		Irc_Proto_Join(channel, channel_pass);	/* join desired channel */
		Cvar_ForceSet("irc_defaultChannel", channel);
	} else
		Com_Printf("Usage: %s <channel> [<password>]\n", Cmd_Argv(0));
}

static void Irc_Client_Part_f (void)
{
	const int argc = Cmd_Argc();
	if (argc == 2) {
		const char * const channel = Cmd_Argv(1);
		Irc_Proto_Part(channel);
	} else
		Com_Printf("Usage: %s <channel>\n", Cmd_Argv(0));
}

/**
 * @brief Send a message from menu or commandline
 * @note This function uses the irc_send_buffer cvar to handle the menu input for irc messages
 * See menu_irc.ufo for more information
 */
static void Irc_Client_Msg_f (void)
{
	char cropped_msg[IRC_SEND_BUF_SIZE];
	const char *msg = NULL;
	if (Cmd_Argc() >= 2)
		msg = Cmd_Args();
	else if (*irc_send_buffer->string)
		msg = irc_send_buffer->string;

	if (msg && *msg) {
		if (*irc_defaultChannel->string) {
			if (*msg == '"') {
				size_t msg_len = strlen(msg);
				memcpy(cropped_msg, msg + 1, msg_len - 2);
				cropped_msg[msg_len - 2] = '\0';
				msg = cropped_msg;
			}
			if (!Irc_Proto_Msg(irc_defaultChannel->string, msg)) {
				/* local echo */
				Irc_AppendToBuffer(va("<%s> %s", irc_nick->string, msg));
			}
			Cvar_ForceSet("irc_send_buffer", "");
		} else
			Com_Printf("Join a channel first.\n");
	}
}

static void Irc_Client_PrivMsg_f (void)
{
	if (Cmd_Argc() >= 3) {
		char cropped_msg[IRC_SEND_BUF_SIZE];
		const char * const target = Cmd_Argv(1);
		const char *msg = Cmd_Args() + strlen(target) + 1;
		if (*msg == '"') {
			size_t msg_len = strlen(msg);
			memcpy(cropped_msg, msg + 1, msg_len - 2);
			cropped_msg[msg_len - 2] = '\0';
			msg = cropped_msg;
		}
		Irc_Proto_Msg(target, msg);
	} else
		Com_Printf("Usage: %s <target> {<msg>}\n", Cmd_Argv(0));
}

static void Irc_Client_Mode_f (void)
{
	const int argc = Cmd_Argc();
	if (argc >= 3) {
		const char * const target = Cmd_Argv(1);
		const char * const modes = Cmd_Argv(2);
		const char * const params = argc >= 4
			? Cmd_Args() + strlen(target) + strlen(modes) + 2
			: NULL;
		Irc_Proto_Mode(target, modes, params);
	} else
		Com_Printf("Usage: %s <target> <modes> {<param>}\n", Cmd_Argv(0));
}

static void Irc_Client_Topic_f (void)
{
	const int argc = Cmd_Argc();
	if (argc >= 2) {
		const char * const channel = Cmd_Argv(1);
		if (chan) {
			if (argc >= 3) {
				char buf[1024];
				const char *in = Cmd_Args();
				char *out = buf;
				if (*in == '"')
					in += 2;
				in += strlen(channel) + 1;
				Q_strncpyz(out, in, sizeof(out));
				if (*out == '"') {
					size_t out_len;
					++out;
					out_len = strlen(out);
					assert(out_len >= 1);
					out[out_len - 1] = '\0';
				}
				Irc_Proto_Topic(channel, out);
			} else
				Com_Printf("%s topic: \"%s\"\n", channel, Irc_Logic_GetChannelTopic(chan));
		} else
			Com_Printf("Not joined: %s\n", channel);
	} else
		Com_Printf("Usage: %s <channel> [<topic>]\n", Cmd_Argv(0));
}

#define IRC_MAX_USERLIST 512
static char irc_userListOrdered[IRC_MAX_USERLIST][MAX_VAR];

static void Irc_Client_Names_f (void)
{
	int i;

	irc_user_t* user;
	if (chan) {
		irc_names_buffer[0] = '\0';
		user = chan->user;
		for (i = 0; i < chan->users; i++) {
			if (i >= IRC_MAX_USERLIST)
				break;
			Q_strncpyz(irc_userListOrdered[i], user->key, MAX_VAR);
			user = user->next;
		}
		if (i > 0) {
			qsort((void *)irc_userListOrdered, i, MAX_VAR, Q_StringSort);
			while (i--)
				Q_strcat(irc_names_buffer, va("%s\n", irc_userListOrdered[i]), sizeof(irc_names_buffer));
		}
	} else
		Com_Printf("Not joined\n");
}

static void Irc_Client_Kick_f (void)
{
	const int argc = Cmd_Argc();
	if (argc >= 3) {
		const char *channel = Cmd_Argv(1);
		if (chan) {
			const char * const nick = Cmd_Argv(2);
			const char *reason;
			if (argc >= 4)
				reason = Cmd_Args() + strlen(nick) + strlen(channel) + 2;
			else
				reason = NULL;
			Irc_Proto_Kick(channel, nick, reason);
		} else
			Com_Printf("Not joined: %s.\n", channel);
	} else
		Com_Printf("Usage: %s <channel> <nick> [<reason>]\n", Cmd_Argv(0));
}

static void Irc_Client_Who_f (void)
{
	if (Cmd_Argc() == 2) {
		Irc_Proto_Who(Cmd_Argv(1));
	} else
		Com_Printf("Usage: %s <usermask>\n", Cmd_Argv(0));
}

static void Irc_Client_Whois_f (void)
{
	if (Cmd_Argc() == 2) {
		Irc_Proto_Whois(Cmd_Argv(1));
	} else
		Com_Printf("Usage: %s <nick>\n", Cmd_Argv(0));
}

static void Irc_Client_Whowas_f (void)
{
	if (Cmd_Argc() == 2) {
		Irc_Proto_Whowas(Cmd_Argv(1));
	} else
		Com_Printf("Usage: %s <nick>\n", Cmd_Argv(0));
}

/*
===============================================================
Menu functions
===============================================================
*/

/**
 * @brief Adds the username you clicked to your input buffer
 * @sa Irc_UserRightClick_f
 */
static void Irc_UserClick_f (void)
{
	const char *name;
	int num, cnt;

	if (Cmd_Argc() != 2)
		return;

	if (!chan || irc_names_buffer[0] == '\0')
		return;

	num = atoi(Cmd_Argv(1));
	if (num < 0 || num >= chan->users || num >= IRC_MAX_USERLIST)
		return;

	cnt = min(chan->users, IRC_MAX_USERLIST);
	cnt -= num + 1;

	name = irc_userListOrdered[cnt];
	Cvar_Set("irc_send_buffer", va("%s%s: ", irc_send_buffer->string, &name[1]));
	Cmd_ExecuteString("irc_send_buffer_clicked");
}

/**
 * @brief Performs a whois query for the username you clicked
 * @sa Irc_UserClick_f
 */
static void Irc_UserRightClick_f (void)
{
	const char *name;
	int num, cnt;

	if (Cmd_Argc() != 2)
		return;

	if (!chan || irc_names_buffer[0] == '\0')
		return;

	num = atoi(Cmd_Argv(1));
	if (num < 0 || num >= chan->users || num >= IRC_MAX_USERLIST)
		return;

	cnt = min(chan->users, IRC_MAX_USERLIST);
	cnt -= num + 1;

	name = irc_userListOrdered[cnt];
	Irc_Proto_Whois(&name[1]);
	Cmd_ExecuteString("irc_send_buffer_clicked");
}

/*
===============================================================
Init and Shutdown functions
===============================================================
*/

void Irc_Init (void)
{
	/* commands */
	Cmd_AddCommand("irc_join", Irc_Client_Join_f, "Join an irc channel");
	Cmd_AddCommand("irc_connect", Irc_Connect_f, "Connect to the irc network");
	Cmd_AddCommand("irc_disconnect", Irc_Disconnect_f, "Disconnect from the irc network");
	Cmd_AddCommand("irc_part", Irc_Client_Part_f, NULL);
	Cmd_AddCommand("irc_privmsg", Irc_Client_PrivMsg_f, NULL);
	Cmd_AddCommand("irc_mode", Irc_Client_Mode_f, NULL);
	Cmd_AddCommand("irc_who", Irc_Client_Who_f, NULL);
	Cmd_AddCommand("irc_whois", Irc_Client_Whois_f, NULL);
	Cmd_AddCommand("irc_whowas", Irc_Client_Whowas_f, NULL);
	Cmd_AddCommand("irc_chanmsg", Irc_Client_Msg_f, NULL);
	Cmd_AddCommand("irc_topic", Irc_Client_Topic_f, NULL);
	Cmd_AddCommand("irc_names", Irc_Client_Names_f, NULL);
	Cmd_AddCommand("irc_kick", Irc_Client_Kick_f, NULL);

	Cmd_AddCommand("irc_userlist_click", Irc_UserClick_f, "Menu function for clicking a user from the list");
	Cmd_AddCommand("irc_userlist_rclick", Irc_UserRightClick_f, "Menu function for clicking a user from the list");

	Cmd_AddCommand("irc_activate", Irc_Input_Activate, "IRC init when entering the menu");
	Cmd_AddCommand("irc_deactivate", Irc_Input_Deactivate, "IRC deactivate when leaving the irc menu");

	/* cvars */
	irc_server = Cvar_Get("irc_server", "irc.freenode.org", CVAR_ARCHIVE, "IRC server to connect to");
	irc_channel = Cvar_Get("irc_channel", "#ufo:ai", CVAR_ARCHIVE, "IRC channel to join into");
	irc_channel->modified = qfalse;
	irc_port = Cvar_Get("irc_port", "6667", CVAR_ARCHIVE, "IRC port to connect to");
	irc_user = Cvar_Get("irc_user", "UFOAIPlayer", CVAR_ARCHIVE, NULL);
	irc_password = Cvar_Get("irc_password", "", CVAR_ARCHIVE, NULL);
	irc_topic = Cvar_Get("irc_topic", "Connecting (please wait)...", CVAR_NOSET, NULL);
	irc_defaultChannel = Cvar_Get("irc_defaultChannel", "", CVAR_NOSET, NULL);
	irc_logConsole = Cvar_Get("irc_logConsole", "0", CVAR_ARCHIVE, "Log all irc conversations to game console, too");
	irc_showIfNotInMenu = Cvar_Get("irc_showIfNotInMenu", "0", CVAR_ARCHIVE, "Show chat messages on top of the menu stack if we are not in the irc menu");
	irc_send_buffer = Cvar_Get("irc_send_buffer", "", 0, NULL);
	irc_nick = Cvar_Get("name", "", 0, NULL);

	/* reset buffer */
	memset(irc_buffer, 0, sizeof(irc_buffer));
}

void Irc_Shutdown (void)
{
	if (irc_connected)
		Irc_Logic_Disconnect("shutdown");
}

/**
 * @sa Irc_Input_Deactivate
 */
void Irc_Input_Activate (void)
{
	/* in case of a failure we need this in MN_PopMenu */
	msg_mode = MSG_IRC;
	if (irc_connected && *irc_defaultChannel->string) {
		menuText[TEXT_STANDARD] = irc_buffer;
		menuText[TEXT_LIST] = irc_names_buffer;
		Key_SetDest(key_input);
	} else {
		Com_DPrintf(DEBUG_CLIENT, "Irc_Input_Activate: Warning - IRC not connected\n");
		MN_PopMenu(qfalse);
		MN_PushMenu("irc_popup");
		/* cancel any active editing */
		return;
	}
	/* store this value to be able to reset it in Irc_Input_Deactivate */
	inputLengthBackup = Cvar_VariableValue("mn_inputlength");
	Cvar_SetValue("mn_inputlength", IRC_MAX_INPUTLENGTH);
}

/**
 * @sa Irc_Input_Activate
 */
void Irc_Input_Deactivate (void)
{
	irc_send_buffer->modified = qfalse;

	/* allow setting to other modes in next messagemode call */
	msg_mode = MSG_MENU;

	/* if this is set - the command is called after Irc_Input_Activate call */
	if (inputLengthBackup) {
		Cvar_SetValue("mn_inputlength", inputLengthBackup);
		inputLengthBackup = 0;
		MN_MenuTextReset(TEXT_STANDARD);
		MN_MenuTextReset(TEXT_LIST);
	}
}
