/*
 * Miranda-IM Vypress Chat/quickChat plugins
 * Copyright (C) Saulius Menkevicius
 *
 * 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
 *
 * $Id: chatroom.c,v 1.44 2005/03/17 11:02:43 bobas Exp $
 */

#include <time.h>
#include "miranda.h"
#include "contrib/hashtable.h"
#include "contrib/strhashfunc.h"
#include "libvqproto/vqproto.h"

#include "main.h"
#include "user.h"
#include "userlist.h"
#include "chatroom.h"
#include "msgloop.h"
#include "chanlist.h"
#include "util.h"
#include "contacts.h"
#include "resource.h"

/* struct defs
 */

#define VQCHAT_WM_INITCS	(WM_USER + 1)

#define CHATROOM_NORMAL_STATUS	"Normal"

struct chatroom_status_entry {
	char	* topic;
};

/* static data
 */
static BOOL	s_fHaveChat;	/* TRUE, if we have chat.dll in miranda */
static HANDLE	s_hGcUserEvent, s_hGcMenuBuild;
static struct hashtable
		* s_channelHash;
static HWND	s_hwndChannelSettingsDlg;
static vqp_link_t s_vqpLink;
static UINT_PTR s_chatroom_connected_join_timer;

/* static routines
 */

static void chatroom_free_channelhash_value(void * value)
{
	struct chatroom_status_entry * entry = (struct chatroom_status_entry *)value;
	if(entry->topic)
		free(entry->topic);
	free(entry);
}

static char * chatroom_make_name(const char * channel)
{
	int len;
	char * channel_name;

	ASSERT_RETURNVALIFFAIL(VALIDPTR(channel), NULL);

	len = strlen(channel);
	channel_name = malloc(len + 2);
	channel_name[0] = '#';
	memcpy(channel_name + 1, channel, len + 1);

	return channel_name;
}

static void chatroom_send_net_join(const char * channel)
{
	char * r_channel, * r_nickname;

	ASSERT_RETURNIFFAIL(VALIDPTR(channel));
	
	r_channel = util_utf2vqp(user_codepage(), channel);
	r_nickname = util_utf2vqp(user_codepage(), user_nickname());

	/* send a join msg */
	msgloop_send(
		vqp_msg_channel_join(
			s_vqpLink, r_channel, r_nickname, user_vqp_status(), user_gender(),
			VQCHAT_VQP_SWVERSION, user_p_uuid(), user_codepage(), VQCHAT_VQP_DEF_COLOR),
		0);

	/* send request who-is-here-on-this-channel? */
	msgloop_send(vqp_msg_channel_whohere_req(s_vqpLink, r_channel, r_nickname), 0);

	free(r_channel);
	free(r_nickname);
}

static void chatroom_send_net_part(const char * channel)
{
	char * r_channel = util_utf2vqp(user_codepage(), channel),
		* r_nickname = util_utf2vqp(user_codepage(), user_nickname());

	msgloop_send(vqp_msg_channel_leave(s_vqpLink, r_channel, r_nickname, user_gender()), 1);
	free(r_channel);
	free(r_nickname);
}

static void chatroom_svc_gc_event_user_message(
	const char * channel, const char * message)
{
	/* chatroom_svc_gc_event_user_message:
	 *	handles a message the user has typed
	 *	and emits it onto the channel window and onto the
	 *	network
	 */
	char ** msg_lines;
	int i;

	ASSERT_RETURNIFFAIL(VALIDPTR(channel) && VALIDPTR(message));
	
	/* split the message into multiple lines (and skip the empty ones) */
	msg_lines = util_split_multiline(message, 0);

	/* emit message lines to the network */
	for(i = 0; msg_lines[i]; i++) {
		char * r_channel, * r_nickname, * r_text;
		enum vqp_codepage cp = user_codepage();
			
		/* emit this text on the channel window */
		chatroom_channel_user_text(channel, user_nickname(), msg_lines[i], 0);

		/* send the message to the network */
		msgloop_send(
			vqp_msg_channel_text(
				s_vqpLink, r_channel = util_utf2vqp(cp, channel),
				r_nickname = util_utf2vqp(cp, user_nickname()),
				r_text = util_utf2vqp(cp, msg_lines[i]), 0),
			0);
		free(r_channel);
		free(r_nickname);
		free(r_text);
	}

	/* free the string list */
	util_free_str_list(msg_lines);
}

static void chatroom_svc_gc_event_user_nicklistmenu(GCHOOK * pHook)
{
	PROTOSEARCHRESULT psr;
	ADDCONTACTSTRUCT acs;
	char * dst;
	
	switch(pHook->dwData) {
	case 1: /* "Beep" */
		dst = util_loc2utf(pHook->pszUID);
		if(!user_is_my_nickname(dst)) {
			char * r_dst, * r_nickname;
			enum vqp_codepage cp = userlist_user_codepage(dst);
			r_nickname = util_utf2vqp(cp, user_nickname());
			r_dst = util_utf2vqp(cp, dst);

			msgloop_send_to(
				vqp_msg_beep_signal(s_vqpLink, r_nickname, r_dst),
				0, userlist_user_addr(dst));
			free(r_nickname);
			free(r_dst);
		}
		free(dst);
		break;
		
	case 2: /* "Add User" */
		memset(&psr, 0, sizeof(psr));
		psr.cbSize = sizeof(psr);
		psr.nick = pHook->pszUID;
		memset(&acs, 0, sizeof(acs));
		acs.handleType = HANDLE_SEARCHRESULT;
		acs.szProto = VQCHAT_PROTO;
		acs.psr = &psr;

		CallService(MS_ADDCONTACT_SHOW, (WPARAM)NULL, (LPARAM)&acs);	
		break;
	}
}

static BOOL CALLBACK chatroom_channel_settings_wndproc(
	HWND hdlg, UINT nMsg,
	WPARAM wParam, LPARAM lParam)
{
	char * channel, * topic;
	
	switch(nMsg) {
	case WM_INITDIALOG:
		/* set dialog icon */
		SendMessage(
			hdlg, WM_SETICON, ICON_SMALL,
			(LPARAM)LoadIcon(g_hDllInstance,
					 MAKEINTRESOURCE(IDI_CHATROOM)));
		SendMessage(
			hdlg, WM_SETICON, ICON_BIG,
			(LPARAM)(HICON)LoadImage(
				g_hDllInstance, MAKEINTRESOURCE(IDI_VQCHAT_PROTO_LARGE),
				IMAGE_ICON, GetSystemMetrics(SM_CXICON),
				GetSystemMetrics(SM_CYICON), LR_SHARED)
		);
		break;
	
	case VQCHAT_WM_INITCS:
		channel = (char *)lParam;

		util_SetDlgItemTextUtf(hdlg, IDC_CS_EDIT_NAME, channel);
		util_SetDlgItemTextUtf(
			hdlg, IDC_CS_EDIT_TOPIC, chatroom_channel_get_topic(channel));

		SendDlgItemMessageW(hdlg, IDC_CS_EDIT_TOPIC, EM_SETSEL, (WPARAM)0, (LPARAM)-1);

		/* make dialog title */
		topic = malloc(strlen(channel) + 64);
		if(topic) {
			sprintf(topic, "#%s channel settings", channel);
			util_SetWindowTextUtf(hdlg, topic);
			free(topic);
		}

		/* disable the 'set-topic' button */
		EnableWindow(GetDlgItem(hdlg, IDC_CS_BTN_SET), FALSE);
		break;

	case WM_COMMAND:
		switch(HIWORD(wParam)) {
		case BN_CLICKED:
			switch(LOWORD(wParam)) {
			case IDC_CS_BTN_SET:
			case IDOK:
				/* set channel topic */
				channel = util_GetDlgItemTextUtf(hdlg, IDC_CS_EDIT_NAME);
				topic = util_GetDlgItemTextUtf(hdlg, IDC_CS_EDIT_TOPIC);
				if(channel && topic) {
					if(db_byte_get(NULL, VQCHAT_PROTO, "NicknameOnTopic", 0)) {
						char * new_topic = malloc(strlen(topic)
								+ strlen(user_nickname()) + 8);
						sprintf(new_topic, "%s (%s)",
								topic, user_nickname());
						chatroom_channel_topic_change(
								channel, new_topic, 1, 1);
						free(new_topic);
					} else {
						chatroom_channel_topic_change(channel, topic, 1, 1);
					}
				}
				if(topic) free(topic);
				if(channel) free(channel);
				
				/* disable the 'set-topic' button */
				EnableWindow(GetDlgItem(hdlg, IDC_CS_BTN_SET), FALSE);

				/* close the window (if pressed OK btn) */
				if(LOWORD(wParam) == IDOK)
					SendMessage(hdlg, WM_CLOSE, 0,0);
				break;
			}
			break;
			
		case EN_CHANGE:
			switch(LOWORD(wParam)) {
			case IDC_CS_EDIT_TOPIC:
				/* enable the set-topic button on topic
				 * entry change */
				EnableWindow(GetDlgItem(hdlg, IDC_CS_BTN_SET), TRUE);
				break;
			}
			break;
		}
		break;

	case WM_CLOSE:
		DestroyWindow(hdlg);
		s_hwndChannelSettingsDlg = NULL;
		break;
	}

	return (FALSE);
}

static void chatroom_svc_gc_event_user_logmenu(GCHOOK * pHook)
{
	char * channel;
	
	switch(pHook->dwData) {
	case 1:
		channel = util_loc2utf(pHook->pDest->pszID);
		chatroom_channel_show_settings_dlg(channel);
		free(channel);
		break;
	}
}

static int chatroom_svc_gc_event(WPARAM wParam, LPARAM lParam)
{
	GCHOOK * pHook = (GCHOOK *)lParam;
	HANDLE hContact;
	char * channel, * text, * dst;

	ASSERT_RETURNVALIFFAIL(VALIDPTR(pHook) && VALIDPTR(pHook->pDest), 0);

	if(strcmp(pHook->pDest->pszModule, VQCHAT_PROTO))
		return 0;

	switch(pHook->pDest->iType) {
	case GC_USER_MESSAGE:
		channel = util_loc2utf(pHook->pDest->pszID);
		text = util_loc2utf(pHook->pszText);
		
		chatroom_svc_gc_event_user_message(channel, text);
		free(channel);
		free(text);
		break;

	case GC_USER_CHANMGR:
		channel = util_loc2utf(pHook->pDest->pszID);
		chatroom_channel_show_settings_dlg(channel);
		free(channel);
		break;

	case GC_USER_PRIVMESS:
		dst = util_loc2utf(pHook->pszUID);
		if(!user_is_my_nickname(dst)) {
			hContact = contacts_add_contact(dst, 0);
			if(hContact)
				CallService(MS_MSG_SENDMESSAGE, (WPARAM)hContact, 0);
		}
		free(dst);
		break;

	case GC_USER_NICKLISTMENU:
		chatroom_svc_gc_event_user_nicklistmenu(pHook);
		break;

	case GC_USER_LOGMENU:
		chatroom_svc_gc_event_user_logmenu(pHook);
		break;
	}
	
	return 0;
}

static int chatroom_svc_gc_buildmenu(WPARAM wParam, LPARAM lParam)
{
	static struct gc_item usermenu_items[] = {
		{ "Alert Beep", 1, MENU_ITEM, FALSE },
		{ "", 0, MENU_POPUPSEPARATOR, FALSE },
		{ "&Add User", 2, MENU_ITEM, FALSE }
	};
	static struct gc_item chansettings_items[] = {
		{ "Channel &settings", 1, MENU_ITEM, FALSE }
	};

	GCMENUITEMS * gcmi = (GCMENUITEMS *)lParam;

	if(gcmi) {
		if(!lstrcmpi(gcmi->pszModule, VQCHAT_PROTO)) {
			char * dst = util_loc2utf(gcmi->pszUID);
			
			switch(gcmi->Type) {
			case MENU_ON_NICKLIST:
				gcmi->nItems = 3;
				gcmi->Item = usermenu_items;

				/* disable 'Add User' if already added */
				gcmi->Item[2].bDisabled =
					user_is_my_nickname(dst)
					|| contacts_find_contact(dst)!=NULL;
				break;

			case MENU_ON_LOG:
				gcmi->nItems = 1;
				gcmi->Item = chansettings_items;
				break;
			}
			free(dst);
		}
	}
	
	return 0;
}

static __inline void chatroom_setup_event(
	GCDEST * pDest, GCEVENT * pEvent, const char * channel, int event_type)
{
	pDest->pszModule = VQCHAT_PROTO;
	pDest->pszID = (char *)channel;
	pDest->iType = event_type;

	memset(pEvent, 0, sizeof(GCEVENT));
	pEvent->cbSize = sizeof(GCEVENT);
	pEvent->pDest = pDest;
	pEvent->time = time(NULL);
}
static __inline char * chatroom_escape_message(const char * message)
{
	int msg_len = strlen(message);
	char * escaped = malloc(msg_len * 2 + 1);	/* to be safe */
	char * p_escaped = escaped;

	do {
		if(*message == '%')
			*(p_escaped++) = '%';
		
		*(p_escaped++) = *message;
	} while(*(message++));

	return escaped;
}

static void chatroom_flush_channel_hash()
{
	if(s_channelHash)
		hashtable_destroy(s_channelHash, 1);

	s_channelHash = create_hashtable(
		32, hashtable_strhashfunc, hashtable_strequalfunc,
		chatroom_free_channelhash_value);
}

/* exported routines
 */
void chatroom_init()/*{{{*/
{
	s_fHaveChat = FALSE;
	s_channelHash = NULL;
	s_hwndChannelSettingsDlg = NULL;
	s_chatroom_connected_join_timer = 0;
}/*}}}*/

void chatroom_uninit()/*{{{*/
{
	if(s_hwndChannelSettingsDlg) {
		SendMessage(s_hwndChannelSettingsDlg, WM_CLOSE, 0, 0);
		s_hwndChannelSettingsDlg = NULL;
	}

	if(s_channelHash) {
		hashtable_destroy(s_channelHash, 1);
		s_channelHash = NULL;
	}

	if(s_fHaveChat) {
		/* unhook all the hooks */
		UnhookEvent(s_hGcUserEvent);
		UnhookEvent(s_hGcMenuBuild);
	}

	if(s_chatroom_connected_join_timer) {
		KillTimer(NULL, s_chatroom_connected_join_timer);
		s_chatroom_connected_join_timer = 0;
	}
}/*}}}*/

void chatroom_hook_modules_loaded()/*{{{*/
{
	/* check if we have m3x's Chat module in miranda */
	s_fHaveChat = ServiceExists(MS_GC_REGISTER);

	if(s_fHaveChat) {
		/* register with the Chat module */
		GCREGISTER gcr;
		gcr.cbSize = sizeof(gcr);
		gcr.dwFlags = GC_CHANMGR;
		gcr.pszModule = VQCHAT_PROTO;
		gcr.pszModuleDispName = VQCHAT_PROTO_NAME;
		gcr.iMaxText = VQP_MAX_PACKET_SIZE; /* approximation */
		gcr.nColors = 0;
		gcr.pColors = NULL;	/* hope this works */
		CallService(MS_GC_REGISTER, 0, (LPARAM)&gcr);

		/* hook to hooks */
		s_hGcUserEvent = HookEvent(ME_GC_EVENT, chatroom_svc_gc_event);
		s_hGcMenuBuild = HookEvent(ME_GC_BUILDMENU,
						chatroom_svc_gc_buildmenu);
	} else {
		/* ask the user to get the Chat module */
		if(IDYES == MessageBox(NULL,
			"The " VQCHAT_PROTO_NAME " depends on another plugin"
			" \"Chat\".\nDo you want to download it from the"
			" Miranda IM web site now?",
			"Information",
			MB_YESNO|MB_ICONINFORMATION)
			)
		{
			CallService(MS_UTILS_OPENURL, 1,
				(LPARAM)"http://www.miranda-im.org/download/"
					"details.php?action=viewfile&id=1309");
		}
			
	}
}/*}}}*/

/* chatroom_module_installed:
 *	returns TRUE if the "Chat" module is installed
 */
int chatroom_module_installed()/*{{{*/
{
	return s_fHaveChat;
}/*}}}*/

static int chatroom_connected_join_enum(const char * channel, void * data)/*{{{*/
{
	chatroom_channel_join(channel, 1);
	return 1; /* keep enumerating */
}/*}}}*/

static void CALLBACK chatroom_connected_join_timer_cb(/*{{{*/
	HWND hWnd, UINT nMsg, UINT_PTR nEvent, DWORD dwTime)
{
	/* join all of user's channels */
	chanlist_enum(*user_p_chanlist(), chatroom_connected_join_enum, NULL);

	/* this is a single-shot timer */
	KillTimer(0, s_chatroom_connected_join_timer);
	s_chatroom_connected_join_timer = 0;
}/*}}}*/

void chatroom_connected(vqp_link_t link)/*{{{*/
{
	char * r_src;
	
	s_vqpLink = link;
	
	/* create/flush channel status entry hash */
	chatroom_flush_channel_hash();

	/* join all of user's channels after a second
	 */
	s_chatroom_connected_join_timer
		= SetTimer(NULL, 0, 500, chatroom_connected_join_timer_cb);

	/* join the Main channel (not-really, just for the effect)
	 * (in case the user doesn't have it on the his chanlist)
	 */
	chatroom_send_net_join(VQCHAT_MAIN_CHANNEL);

	/* set active status update */
	r_src = util_utf2vqp(user_codepage(), user_nickname());
	msgloop_send(vqp_msg_active_change(s_vqpLink, r_src, VQCHAT_UNDEF_ACTIVE), 0);
	free(r_src);
}/*}}}*/

static int chatroom_disconnected_part_enum(const char * channel, void * data)
{
	chatroom_channel_part(channel, 1);
	return 1; /* keep enumerating */
}

void chatroom_disconnected()
{
	/* part the #Main channel, - this will leave the network */
	chatroom_send_net_part(VQCHAT_MAIN_CHANNEL);

	/* leave all the channels in user's chanlist */
	chanlist_enum(*user_p_chanlist(), chatroom_disconnected_part_enum, NULL);

	s_vqpLink = NULL;
}

/* chatroom_is_valid_channel:
 *	validates that channel name is valid
 * returns:
 *	0 if invalid,
 *	non-0 if valid
 */
int chatroom_is_valid_channel(const char * channel)
{
	ASSERT_RETURNVALIFFAIL(VALIDPTR(channel), 0);

	/* channel name must be of non-0 length
	 * and channel name must not contain a '#' char
	 */
	return strlen(channel)!=0 && strchr(channel, '#')==NULL;
}

/* chatroom_channel_join:
 *	  Join the specified channel, and add it to the user's chanlist
 *	(if not there already).
 *	  The connected_event param is set to non-0 only when joining the
 *	channels after connecting to the network.
 */
static int chatroom_channel_join_userlist_enum_fn(
	const char * user_name, void * channel)
{
	/* join the channel, if the user is there */
	if(chanlist_contains(userlist_user_chanlist(user_name), (const char *)channel)) {
		GCDEST gcd;
		GCEVENT gce;
		char * l_user_name = util_utf2loc(user_name);

		chatroom_setup_event(&gcd, &gce, (const char *)channel, GC_EVENT_JOIN);
		gce.pszStatus = CHATROOM_NORMAL_STATUS;
		gce.bIsMe = FALSE;
		gce.bAddToLog = FALSE;
		gce.pszNick = l_user_name;
		gce.pszUID = l_user_name;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		free(l_user_name);
	}

	return 1; /* keep enumerating */
}

/* chatroom_channel_join:
 *	joins the specified channel and updates channel list
 *	(if @connecting_to_net != 0)
 * params:
 *	@connecting_to_net - non-0 if the chatroom_channel_join() was called
 *		due to connecting-to-the-net event
 */
void chatroom_channel_join(
	const char * channel, int connecting_to_net)
{
	char * chanlist;
	struct chatroom_status_entry * entry;
	
	ASSERT_RETURNIFFAIL(!user_offline());

	/* validate channel name */
	if(!chatroom_is_valid_channel(channel))
		return;

	/* add channel entry to hashtable
	 */
	entry = (struct chatroom_status_entry*)hashtable_search(s_channelHash, (void*)channel);
	if(entry == NULL) {
		entry = malloc(sizeof(struct chatroom_status_entry));
		entry->topic = strdup("");
		hashtable_insert(s_channelHash, strdup(channel), entry);
	}

	/* update user's channel list
	 */
	chanlist = *user_p_chanlist();
	if(!connecting_to_net) {
		/* don't do anything if we've joined the channel already */
		if(chanlist_contains(chanlist, channel))
			return;

		/* add this channel to chanlist and store it in db */
		user_set_chanlist(*user_p_chanlist() = chanlist_add(chanlist, channel), TRUE);
	}

	/* send join msg to the network:
	 *	dont do this for the MAIN channel, as it is considered
	 *	"special"
	 */
	if(strcmp(channel, VQCHAT_MAIN_CHANNEL))
		chatroom_send_net_join(channel);

	/* do stuff with Chat module */
	if(s_fHaveChat) {
		GCWINDOW gcw;
		char * channel_name = chatroom_make_name(channel),
			* l_channel_name = util_utf2loc(channel_name),
			* l_current_topic = util_utf2loc(entry->topic);
		free(channel_name);

		/* create window */
		gcw.cbSize = sizeof(gcw);
		gcw.iType = GCW_CHATROOM;
		gcw.pszModule = VQCHAT_PROTO;
		gcw.pszName = l_channel_name;
		gcw.pszID = channel;
		gcw.pszStatusbarText = NULL;
		gcw.bDisableNickList = FALSE;
		gcw.dwItemData = 0;

		if(!CallService(MS_GC_NEWCHAT, 0, (LPARAM)&gcw)) {
			/* setup channel's window */
			GCDEST gcd;
			GCEVENT gce;
			char * l_user_nickname = util_utf2loc(user_nickname());

			/* register the "Normal" status */
			chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_ADDGROUP);
			gce.pszStatus = CHATROOM_NORMAL_STATUS;
			CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

			/* fill in the nick list */
			userlist_enum(chatroom_channel_join_userlist_enum_fn, (void*) channel);

			/* add me to the list */
			chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_JOIN);
			gce.pszNick = l_user_nickname;
			gce.pszUID = l_user_nickname;
			gce.pszStatus = CHATROOM_NORMAL_STATUS;
			gce.bIsMe = TRUE;
			CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

			/* set current topic */
			chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_TOPIC);
			gce.pszText = l_current_topic;
			gce.bAddToLog = FALSE;
			CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

			chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_SETSBTEXT);
			/* this is a workaround for a bug in Chat implementation
			 * where it will segfault if strlen(pszText) == "" */
			gce.pszText = strlen(l_current_topic) ? l_current_topic: " ";
			CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

			/* init done */
			chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_CONTROL);
			CallService(MS_GC_EVENT, (WPARAM)WINDOW_INITDONE, (LPARAM)&gce);
			CallService(MS_GC_EVENT, (WPARAM)WINDOW_ONLINE, (LPARAM)&gce);

			free(l_user_nickname);
		}

		/* free channel name '#chan' we've alloced */
		free(l_channel_name);
		free(l_current_topic);
	}
}

void chatroom_channel_part(const char * channel, int disconnected_event)
{
	char * chanlist;
	ASSERT_RETURNIFFAIL(!user_offline());

	chanlist = *user_p_chanlist();
	if(!disconnected_event) {
		/* check if we've parted this channel already */
		if(!chanlist_contains(chanlist, channel))
			return;

		/* remove channel from chanlist and store it in db */
		user_set_chanlist(*user_p_chanlist() = chanlist_remove(chanlist, channel), TRUE);
	}

	/* remove channel entry from hashtable */
	hashtable_remove(s_channelHash, (void*)channel, 1);

	/* send part message to the network, except for the VQCHAT_MAIN_CHANNEL */
	if(strcmp(channel, VQCHAT_MAIN_CHANNEL))
		chatroom_send_net_part(channel);

	/* do stuff with the chat module */
	if(s_fHaveChat) {
		GCDEST gcd;
		GCEVENT gce;
		
		/* close chat window */
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_CONTROL);
		CallService(MS_GC_EVENT, WINDOW_TERMINATE, (LPARAM)&gce);
	}
}

void chatroom_channel_topic_change(
	const char * channel, const char * new_topic,
	int notify_network, int add_to_log)
{
	struct chatroom_status_entry * entry;
	int topic_changed;

	ASSERT_RETURNIFFAIL(VALIDPTR(channel) && VALIDPTR(new_topic));

	/* get current channel topic (if there is one)
	 * or alloc new one to store channel (as this might not be open already
	 * and we will need this topic if we join the channel)
	 */
	entry = (struct chatroom_status_entry*) hashtable_search(s_channelHash, (void*)channel);
	if(entry == NULL) {
		topic_changed = 1;
		
		entry = malloc(sizeof(struct chatroom_status_entry));
		entry->topic = strdup(new_topic);
		hashtable_insert(s_channelHash, strdup(channel), entry);
	} else {
		/* we have previous topic here: check if the new one is different */
		topic_changed = strcmp(entry->topic, new_topic);

		/* store the new topic for the channel */
		if(topic_changed) {
			free(entry->topic);
			entry->topic = strdup(new_topic);
		}
	}

	/* the topic has changed and we have this channel's topic
	 * on the list: change the topic 
	 */
	if(s_fHaveChat && topic_changed
			&& chanlist_contains(*user_p_chanlist(), channel)) {
		GCDEST gcd;
		GCEVENT gce;
		char * l_new_topic = util_utf2loc(new_topic);

		/* update chatroom topic */
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_TOPIC);
		gce.pszText = l_new_topic;
		gce.bAddToLog = add_to_log;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		/* set status bar to topic text */
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_SETSBTEXT);
		/* this is a workaround for a bug in Chat implementation
		 * where it will segfault if strlen(pszText) == "" */
		gce.pszText = strlen(l_new_topic) ? l_new_topic: " ";
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		/* emit topic change message to the network */
		if(notify_network) {
			char * r_channel = util_utf2vqp(user_codepage(), channel),
				* r_new_topic = util_utf2vqp(user_codepage(), new_topic);

			msgloop_send(
				vqp_msg_channel_topic_change(s_vqpLink, r_channel, r_new_topic),
				0);
			free(r_channel);
			free(r_new_topic);
		}

		free(l_new_topic);
	}
}

/* chatroom_channel_show_settings_dlg:
 *	Shows channel settings dialog for specified channel.
 *	Focuses on current channel settings dialog, if one already active.
 */
void chatroom_channel_show_settings_dlg(const char * channel)
{
	ASSERT_RETURNIFFAIL(VALIDPTR(channel)
			&& chanlist_contains(*user_p_chanlist(), channel));

	/* show channel settings dialog
	 */
	if(!s_hwndChannelSettingsDlg) {
		/* create and init the dialog */
		s_hwndChannelSettingsDlg = CreateDialog(
			g_hDllInstance, MAKEINTRESOURCE(IDD_CS),
			NULL, chatroom_channel_settings_wndproc);

		SendMessage(s_hwndChannelSettingsDlg, VQCHAT_WM_INITCS, 0, (LPARAM)channel);
	}

	/* activate window */
	SetActiveWindow(s_hwndChannelSettingsDlg);
}

const char * chatroom_channel_get_topic(const char * channel)
{
	struct chatroom_status_entry * entry;

	ASSERT_RETURNVALIFFAIL(VALIDPTR(channel), "");

	entry = (struct chatroom_status_entry*)hashtable_search(s_channelHash, (void*)channel);
	return (entry && entry->topic) ? entry->topic: "";
}

static int chatroom_user_name_change_enum_fn(
	const char * channel, void * enum_fn_data)
{
	GCDEST gcd;
	GCEVENT gce;
	const char * user_name = ((const char **)enum_fn_data)[0],
	      * new_user_name = ((const char **)enum_fn_data)[1];
	
	char	* l_user_name = util_utf2loc(user_name),
		* l_new_user_name = util_utf2loc(new_user_name);
	
	ASSERT_RETURNVALIFFAIL(VALIDPTR(channel) && VALIDPTR(enum_fn_data), 1);
	ASSERT_RETURNVALIFFAIL(VALIDPTR(user_name) && VALIDPTR(new_user_name), 1);

	/* update contact's nickname */
	chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_NICK);
	gce.pszUID = l_user_name;
	gce.pszNick = l_user_name;
	gce.pszText = l_new_user_name;
	gce.bAddToLog = TRUE;
	gce.bIsMe = TRUE;
	CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

	/* update contact's UID */
	chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_CHID);
	gce.pszUID = l_user_name;
	gce.pszText = l_new_user_name;
	gce.pszStatus = CHATROOM_NORMAL_STATUS;
	gce.bIsMe = TRUE;
	CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

	free(l_user_name);
	free(l_new_user_name);

	return 1;
}

void chatroom_user_name_change(const char * user_name, const char * new_user_name)
{
	const char * enum_fn_data[2];
	ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(new_user_name));

	enum_fn_data[0] = user_name;
	enum_fn_data[1] = new_user_name;
	
	chanlist_enum(
		*user_p_chanlist(),
		chatroom_user_name_change_enum_fn,
		enum_fn_data);
}

static int chatroom_user_status_change_enum_fn(
	const char * channel, void * notice_text)
{
	GCDEST gcd;
	GCEVENT gce;
	char * l_notice_text = util_utf2loc(notice_text);
	
	chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_INFORMATION);
	gce.pszText = (const char *)l_notice_text;
	gce.bAddToLog = TRUE;
	CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

	free(l_notice_text);

	return 1;
}

void chatroom_user_status_change(int new_status)
{
	char * notice_text;

	notice_text = malloc(sizeof(user_nickname()) + 64);
	ASSERT_RETURNIFFAIL(VALIDPTR(notice_text));

	sprintf(notice_text, "%s changed status to \"%s\"",
		user_nickname(), user_status_name(new_status));
	
	chanlist_enum(
		*user_p_chanlist(),
		chatroom_user_status_change_enum_fn,
		(void*)notice_text);

	free(notice_text);
}

void chatroom_channel_user_join(
	const char * channel, const char * user_name,
	int explicit_join)
{
	ASSERT_RETURNIFFAIL(VALIDPTR(channel) && VALIDPTR(user_name));

	if(s_fHaveChat) {
		GCDEST gcd;
		GCEVENT gce;
		char * l_user_name = util_utf2loc(user_name);
		
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_JOIN);
		gce.pszNick = l_user_name;
		gce.pszUID = l_user_name;
		gce.pszStatus = CHATROOM_NORMAL_STATUS;
		gce.bIsMe = FALSE;
		gce.bAddToLog = explicit_join;

		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		free(l_user_name);
	}
}

void chatroom_channel_user_part(
	const char * channel, const char * user_name,
	int explicit_part)
{
	ASSERT_RETURNIFFAIL(VALIDPTR(channel) && VALIDPTR(user_name));

	if(s_fHaveChat) {
		GCDEST gcd;
		GCEVENT gce;
		char * l_nickname = util_utf2loc(user_name);
		
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_PART);
		gce.pszUID = l_nickname;
		gce.pszNick = l_nickname;
		gce.pszText = explicit_part ? NULL: Translate("ping timeout");
		gce.bAddToLog = TRUE;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		free(l_nickname);
	}
}

static int chatroom_channel_user_name_change_enum_event_fn(
	const char * channel, void * fn_data)
{
	const char * user_name = ((const char **)fn_data)[0],
	      * new_user_name = ((const char **)fn_data)[1];
	int add_to_log = (int)((const char **)fn_data)[2];

	/* send notice text to this channel, only
	 * if the user is there too
	 */
	if(!chanlist_contains(userlist_user_chanlist(user_name), channel)) {
		GCDEST gcd;
		GCEVENT gce;
		char * l_user_name = util_utf2loc(user_name),
			* l_new_user_name = util_utf2loc(new_user_name);

		/* change nickname */
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_NICK);
		gce.pszUID = l_user_name;
		gce.pszText = l_new_user_name;
		gce.pszNick = l_new_user_name;
		gce.bAddToLog = add_to_log;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		/* change user UID */
		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_CHID);
		gce.pszUID = l_user_name;
		gce.pszText = l_new_user_name;
		gce.pszStatus = CHATROOM_NORMAL_STATUS;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		free(l_user_name);
		free(l_new_user_name);
	}

	return 1; /* keep enumerating */
}

void chatroom_channel_user_name_change(
	const char * user_name, const char * new_user_name,
	int add_to_log)
{
	ASSERT_RETURNIFFAIL(VALIDPTR(user_name) && VALIDPTR(new_user_name));

	if(s_fHaveChat) {
		const char * enum_fn_data[3];

		enum_fn_data[0] = user_name;
		enum_fn_data[1] = new_user_name;
		enum_fn_data[2] = (const char *) add_to_log;
		chanlist_enum(
			*user_p_chanlist(),
			chatroom_channel_user_name_change_enum_event_fn,
			(void*)enum_fn_data);
	}
}

static int chatroom_channel_user_status_change_enum_event_fn(
	const char * channel, void * enum_fn_data)
{
	const char * user_name = ((const char **)enum_fn_data)[0],
	      * notice_text = ((const char **)enum_fn_data)[1];
	int add_to_log = (int)((const char **)enum_fn_data)[2];

	ASSERT_RETURNVALIFFAIL(VALIDPTR(notice_text) && VALIDPTR(user_name), 1);
	ASSERT_RETURNVALIFFAIL(VALIDPTR(channel) && VALIDPTR(notice_text), 1);

	/* send notice text to this channel, only
	 * if the user is there too
	 */
	if(chanlist_contains(userlist_user_chanlist(user_name), channel)) {
		GCDEST gcd;
		GCEVENT gce;
		char * l_notice_text = util_utf2loc(notice_text);

		chatroom_setup_event(&gcd, &gce, channel, GC_EVENT_INFORMATION);
		gce.pszText = (const char *)l_notice_text;
		gce.bAddToLog = add_to_log;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		free(l_notice_text);
	}

	return 1; /* keep enumerating */
}

/* chatroom_channel_user_status_change:
 *	notifies user status change to all of registered channel
 */
void chatroom_channel_user_status_change(
	const char * user_name, int new_status, const char * new_status_text,
	int add_to_log)
{
	ASSERT_RETURNIFFAIL(VALIDPTR(user_name));

	if(s_fHaveChat) {
		const char * enum_fn_data[3];
		char * notice_text;
		int status_len = strlen(new_status_text);

		/* make notice text */
		notice_text = malloc(64 + strlen(user_name) + status_len);
		sprintf(notice_text,
			status_len
				? "%s changed status to \"%s\": %s"
				: "%s changed status to \"%s\"",
			user_name,
			user_status_name(new_status),
			new_status_text
		);

		/* send notice text through all the channels */
		enum_fn_data[0] = user_name;
		enum_fn_data[1] = notice_text;
		enum_fn_data[2] = (const char *)add_to_log;
		chanlist_enum(
			*user_p_chanlist(),
			chatroom_channel_user_status_change_enum_event_fn,
			(void*)enum_fn_data);

		/* free notice text */
		free(notice_text);
	}
}

void chatroom_channel_user_text(
	const char * channel, const char * user_name,
	const char * text, int action_text)
{
	ASSERT_RETURNIFFAIL(VALIDPTR(channel) && VALIDPTR(user_name));
	ASSERT_RETURNIFFAIL(VALIDPTR(text));
	
	if(s_fHaveChat && chanlist_contains(*user_p_chanlist(), channel)) {
		GCDEST gcd;
		GCEVENT gce;
		char * escaped_text = chatroom_escape_message(text),
			* l_user_name = util_utf2loc(user_name),
			* l_escaped_text = util_utf2loc(escaped_text);

		chatroom_setup_event(
			&gcd, &gce, channel,
			action_text ? GC_EVENT_ACTION: GC_EVENT_MESSAGE);
		gce.pszUID = l_user_name;
		gce.pszNick = l_user_name;
		gce.pszText = l_escaped_text;
		gce.bAddToLog = TRUE;
		CallService(MS_GC_EVENT, 0, (LPARAM)&gce);

		free(l_user_name);
		free(l_escaped_text);
		free(escaped_text);
	}
}