/*
 channels-query.c : irssi

    Copyright (C) 1999-2000 Timo Sirainen

    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
*/

/*

 How the thing works:

 - After channel is joined and NAMES list is got, send "channel joined" signal
 - "channel joined" : add channel to server->queries lists

loop:
 - Wait for NAMES list from all channels before doing anything else..
 - After got the last NAMES list, start sending the queries ..
 - find the query to send, check where server->queries list isn't NULL
   (mode, who, banlist, ban exceptions, invite list)
 - if not found anything -> all channels are synced
 - send "command #chan1,#chan2,#chan3,.." command to server
 - wait for reply from server, then check if it was last query to be sent to
   server. If it was, send "channel sync" signal
 - check if the reply was for last channel in the command list. If so,
   goto loop
*/

#include "module.h"
#include "misc.h"
#include "signals.h"

#include "channels.h"
#include "irc.h"
#include "modes.h"
#include "mode-lists.h"
#include "nicklist.h"
#include "irc-servers.h"
#include "servers-redirect.h"

enum {
	CHANNEL_QUERY_MODE,
	CHANNEL_QUERY_WHO,
	CHANNEL_QUERY_BMODE,
	CHANNEL_QUERY_EMODE,
	CHANNEL_QUERY_IMODE,

	CHANNEL_QUERIES
};

#define CHANNEL_IS_MODE_QUERY(a) ((a) != CHANNEL_QUERY_WHO)

typedef struct {
	int last_query;
	char *last_query_chan;
        GSList *queries[CHANNEL_QUERIES];
} SERVER_QUERY_REC;

static void sig_connected(IRC_SERVER_REC *server)
{
	SERVER_QUERY_REC *rec;

	g_return_if_fail(server != NULL);
	if (!IS_IRC_SERVER(server))
		return;

	rec = g_new0(SERVER_QUERY_REC, 1);
        server->chanqueries = rec;
}

static void sig_disconnected(IRC_SERVER_REC *server)
{
	SERVER_QUERY_REC *rec;
	int n;

	g_return_if_fail(server != NULL);
	if (!IS_IRC_SERVER(server))
		return;

	rec = server->chanqueries;
	g_return_if_fail(rec != NULL);

	for (n = 0; n < CHANNEL_QUERIES; n++)
		g_slist_free(rec->queries[n]);
	g_free_not_null(rec->last_query_chan);
	g_free(rec);
}

/* Add channel to query list */
static void channel_query_add(IRC_CHANNEL_REC *channel, int query)
{
	SERVER_QUERY_REC *rec;

	g_return_if_fail(channel != NULL);

	rec = channel->server->chanqueries;
	g_return_if_fail(rec != NULL);

	rec->queries[query] = g_slist_append(rec->queries[query], channel);
}

static void channel_query_remove_all(IRC_CHANNEL_REC *channel)
{
	SERVER_QUERY_REC *rec;
	int n;

	rec = channel->server->chanqueries;
	g_return_if_fail(rec != NULL);

	/* remove channel from query lists */
	for (n = 0; n < CHANNEL_QUERIES; n++)
		rec->queries[n] = g_slist_remove(rec->queries[n], channel);
}


static void sig_channel_destroyed(IRC_CHANNEL_REC *channel)
{
	g_return_if_fail(channel != NULL);

	if (IS_IRC_CHANNEL(channel) && channel->server != NULL &&
	    !channel->synced)
		channel_query_remove_all(channel);
}

static int channels_have_all_names(IRC_SERVER_REC *server)
{
	GSList *tmp;

	for (tmp = server->channels; tmp != NULL; tmp = tmp->next) {
		IRC_CHANNEL_REC *rec = tmp->data;

		if (IS_IRC_CHANNEL(rec) && !rec->names_got)
			return 0;
	}

	return 1;
}

static int find_next_query(SERVER_QUERY_REC *server)
{
	int n;

	for (n = 0; n < CHANNEL_QUERIES; n++) {
		if (server->queries[n] != NULL)
			return n;
	}

	return -1;
}

static void channel_send_query(IRC_SERVER_REC *server, int query)
{
	SERVER_QUERY_REC *rec;
	IRC_CHANNEL_REC *chanrec;
	GSList *tmp, *chans, *newchans;
	char *cmd, *chanstr_commas, *chanstr;
	int onlyone;

	rec = server->chanqueries;
	g_return_if_fail(rec != NULL);

	onlyone = (server->no_multi_who && query == CHANNEL_QUERY_WHO) ||
		(server->no_multi_mode && CHANNEL_IS_MODE_QUERY(query));

        newchans = NULL;
	if (onlyone) {
		chanrec = rec->queries[query]->data;
		chans = g_slist_append(NULL, chanrec);
		chanstr_commas = g_strdup(chanrec->name);
		chanstr = g_strdup(chanrec->name);
	} else {
		char *chanstr_spaces;

		chans = rec->queries[query];

		if ((int)g_slist_length(rec->queries[query]) > server->max_query_chans) {
			GSList *lastchan;

			lastchan = g_slist_nth(rec->queries[query], server->max_query_chans-1);
			newchans = lastchan->next;
                        lastchan->next = NULL;
		}

		chanstr_commas = gslistptr_to_string(rec->queries[query], G_STRUCT_OFFSET(IRC_CHANNEL_REC, name), ",");
		chanstr_spaces = gslistptr_to_string(rec->queries[query], G_STRUCT_OFFSET(IRC_CHANNEL_REC, name), " ");

		chanstr = g_strconcat(chanstr_commas, " ", chanstr_spaces, NULL);
		g_free(chanstr_spaces);
	}

	switch (query) {
	case CHANNEL_QUERY_MODE:
		cmd = g_strdup_printf("MODE %s", chanstr_commas);
		for (tmp = chans; tmp != NULL; tmp = tmp->next) {
			chanrec = tmp->data;

			server_redirect_event((SERVER_REC *) server, chanstr, 4,
					      "event 403", "chanquery mode abort", 1,
					      "event 442", "chanquery mode abort", 1, /* "you're not on that channel" */
					      "event 479", "chanquery mode abort", 1, /* "Cannot join channel (illegal name)" IMHO this is not a logical reply from server. */
					      "event 324", "chanquery mode", 1, NULL);
		}
		break;

	case CHANNEL_QUERY_WHO:
		cmd = g_strdup_printf("WHO %s", chanstr_commas);

		for (tmp = chans; tmp != NULL; tmp = tmp->next) {
			chanrec = tmp->data;

			server_redirect_event((SERVER_REC *) server, chanstr, 3,
					      "event 401", "chanquery who abort", 1,
					      "event 403", "chanquery who abort", 1,
					      "event 315", "chanquery who end", 1,
					      "event 352", "silent event who", 1, NULL);
		}
		break;

	case CHANNEL_QUERY_BMODE:
		cmd = g_strdup_printf("MODE %s b", chanstr_commas);
		for (tmp = chans; tmp != NULL; tmp = tmp->next) {
			chanrec = tmp->data;

			/* check all the multichannel problems with all
			   mode requests - if channels are joined manually
			   irssi could ask modes separately but afterwards
			   join the two b/e/I modes together */
			server_redirect_event((SERVER_REC *) server, chanstr, 4,
					      "event 403", "chanquery mode abort", 1,
					      "event 442", "chanquery mode abort", 1, /* "you're not on that channel" */
					      "event 479", "chanquery mode abort", 1, /* "Cannot join channel (illegal name)" IMHO this is not a logical reply from server. */
					      "event 368", "chanquery ban end", 1,
					      "event 367", "chanquery ban", 1, NULL);
		}
		break;

	case CHANNEL_QUERY_EMODE:
		cmd = g_strdup_printf("MODE %s e", chanstr_commas);
		for (tmp = chans; tmp != NULL; tmp = tmp->next) {
			chanrec = tmp->data;

			server_redirect_event((SERVER_REC *) server, chanstr, 4,
					      "event 403", "chanquery mode abort", 1,
					      "event 442", "chanquery mode abort", 1, /* "you're not on that channel" */
					      "event 479", "chanquery mode abort", 1, /* "Cannot join channel (illegal name)" IMHO this is not a logical reply from server. */
					      "event 349", "chanquery eban end", 1,
					      "event 348", "chanquery eban", 1, NULL);
		}
		break;

	case CHANNEL_QUERY_IMODE:
		cmd = g_strdup_printf("MODE %s I", chanstr_commas);
		for (tmp = chans; tmp != NULL; tmp = tmp->next) {
			chanrec = tmp->data;

			server_redirect_event((SERVER_REC *) server, chanstr, 4,
					      "event 403", "chanquery mode abort", 1,
					      "event 442", "chanquery mode abort", 1, /* "you're not on that channel" */
					      "event 479", "chanquery mode abort", 1, /* "Cannot join channel (illegal name)" IMHO this is not a logical reply from server. */
					      "event 347", "chanquery ilist end", 1,
					      "event 346", "chanquery ilist", 1, NULL);
		}
		break;

	default:
                cmd = NULL;
	}

	g_free(chanstr);
	g_free(chanstr_commas);

	/* Get the channel of last query */
	chanrec = g_slist_last(chans)->data;
	rec->last_query_chan = g_strdup(chanrec->name);
	rec->last_query = query;

	if (!onlyone) {
		/* all channels queried, set to newchans which contains
		   the rest of the channels for the same query (usually NULL
		   unless query count exceeded max_query_chans) */
		g_slist_free(rec->queries[query]);
		rec->queries[query] = newchans;
	} else {
		/* remove the first channel from list */
		rec->queries[query] = g_slist_remove(rec->queries[query], chans->data);
	}

	/* send the command */
	irc_send_cmd(server, cmd);
	g_free(cmd);
}

static void channels_query_check(IRC_SERVER_REC *server)
{
	SERVER_QUERY_REC *rec;
        int query;

	g_return_if_fail(server != NULL);

	rec = server->chanqueries;
	g_return_if_fail(rec != NULL);

	g_free_and_null(rec->last_query_chan);
	if (!channels_have_all_names(server)) {
		/* all channels haven't sent /NAMES list yet */
		return;
	}

	query = find_next_query(rec);
	if (query == -1) {
		/* no queries left */
		return;
	}

        channel_send_query(server, query);
}

static void sig_channel_joined(IRC_CHANNEL_REC *channel)
{
	SERVER_QUERY_REC *rec;

	if (!IS_IRC_CHANNEL(channel))
		return;

	/* Add channel to query lists */
	if (!channel->no_modes)
		channel_query_add(channel, CHANNEL_QUERY_MODE);
	channel_query_add(channel, CHANNEL_QUERY_WHO);
	if (!channel->no_modes) {
		channel_query_add(channel, CHANNEL_QUERY_BMODE);
		if (channel->server->emode_known) {
			channel_query_add(channel, CHANNEL_QUERY_EMODE);
			channel_query_add(channel, CHANNEL_QUERY_IMODE);
		}
	}

	rec = channel->server->chanqueries;
	if (rec->last_query_chan == NULL)
		channels_query_check(channel->server);
}

/* if there's no more queries in queries in buffer, send the sync signal */
static void channel_checksync(IRC_CHANNEL_REC *channel)
{
	SERVER_QUERY_REC *rec;
	int n;

	g_return_if_fail(channel != NULL);

	if (channel->synced)
		return; /* already synced */

	rec = channel->server->chanqueries;
	g_return_if_fail(rec != NULL);

	for (n = 0; n < CHANNEL_QUERIES; n++) {
		if (g_slist_find(rec->queries[n], channel))
			return;
	}

	channel->synced = TRUE;
	signal_emit("channel sync", 1, channel);
}

static void channel_got_query(IRC_SERVER_REC *server, IRC_CHANNEL_REC *chanrec,
			      const char *channel)
{
	SERVER_QUERY_REC *rec;

	g_return_if_fail(server != NULL);
	g_return_if_fail(channel != NULL);

	rec = server->chanqueries;
	g_return_if_fail(rec != NULL);
	g_return_if_fail(rec->last_query_chan != NULL);

	/* check if channel is synced */
	if (chanrec != NULL) channel_checksync(chanrec);

	/* check if we need to get another query.. */
	if (g_strcasecmp(rec->last_query_chan, channel) == 0)
		channels_query_check(server);
}

static void event_channel_mode(IRC_SERVER_REC *server, const char *data,
			       const char *nick)
{
	IRC_CHANNEL_REC *chanrec;
	char *params, *channel, *mode;

	g_return_if_fail(data != NULL);

	params = event_get_params(data, 3 | PARAM_FLAG_GETREST, NULL, &channel, &mode);
	chanrec = irc_channel_find(server, channel);
	if (chanrec != NULL)
		parse_channel_modes(chanrec, nick, mode);
	channel_got_query(server, chanrec, channel);

	g_free(params);
}

static void multi_query_remove(IRC_SERVER_REC *server, const char *event, const char *data)
{
	GSList *queue;

	while ((queue = server_redirect_getqueue((SERVER_REC *) server, event, data)) != NULL)
		server_redirect_remove_next((SERVER_REC *) server, event, queue);
}

static void event_end_of_who(IRC_SERVER_REC *server, const char *data)
{
	IRC_CHANNEL_REC *chanrec;
	char *params, *channel, **chans;
	int n, onewho;

	g_return_if_fail(data != NULL);

	params = event_get_params(data, 2, NULL, &channel);

	chans = g_strsplit(channel, ",", -1);
        onewho = strchr(channel, ',') != NULL;
	if (onewho) {
		/* instead of multiple End of WHO replies we get
		   only this one... */
		server->one_endofwho = TRUE;
		multi_query_remove(server, "event 315", data);

		/* check that the WHO actually did return something
		   (that it understood #chan1,#chan2,..) */
		chanrec = irc_channel_find(server, chans[0]);
		if (chanrec->ownnick->host == NULL)
			server->no_multi_who = TRUE;
	}

	for (n = 0; chans[n] != NULL; n++) {
		chanrec = irc_channel_find(server, chans[n]);
		if (chanrec == NULL)
			continue;

		if (onewho && server->no_multi_who) {
			channel_query_add(chanrec, CHANNEL_QUERY_WHO);
			continue;
		}

		chanrec->wholist = TRUE;
		signal_emit("channel wholist", 1, chanrec);

		/* check if we need can send another query */
		channel_got_query(server, chanrec, chans[n]);
	}

	g_strfreev(chans);
	g_free(params);

	if (onewho && server->no_multi_who) {
		/* server didn't understand multiple WHO replies,
		   send them again separately */
		channels_query_check(server);
	}
}

static void event_end_of_banlist(IRC_SERVER_REC *server, const char *data)
{
	IRC_CHANNEL_REC *chanrec;
	char *params, *channel;

	g_return_if_fail(data != NULL);

	params = event_get_params(data, 2, NULL, &channel);
	chanrec = irc_channel_find(server, channel);

	channel_got_query(server, chanrec, channel);

	g_free(params);
}

static void event_end_of_ebanlist(IRC_SERVER_REC *server, const char *data)
{
	IRC_CHANNEL_REC *chanrec;
	char *params, *channel;

	g_return_if_fail(data != NULL);

	params = event_get_params(data, 2, NULL, &channel);
	chanrec = irc_channel_find(server, channel);

	channel_got_query(server, chanrec, channel);

	g_free(params);
}

static void event_end_of_invitelist(IRC_SERVER_REC *server, const char *data)
{
	IRC_CHANNEL_REC *chanrec;
	char *params, *channel;

	g_return_if_fail(data != NULL);

	params = event_get_params(data, 2, NULL, &channel);
	chanrec = irc_channel_find(server, channel);

	channel_got_query(server, chanrec, channel);

	g_free(params);
}

static void channel_lost(IRC_SERVER_REC *server, const char *channel)
{
	IRC_CHANNEL_REC *chanrec;

	chanrec = irc_channel_find(server, channel);
	if (chanrec != NULL) {
		/* channel not found - probably created a new channel
		   and left it immediately. */
		channel_query_remove_all(chanrec);
	}

	channel_got_query(server, chanrec, channel);
}

static void multi_command_error(IRC_SERVER_REC *server, const char *data,
				int query, const char *event)
{
	IRC_CHANNEL_REC *chanrec;
	char *params, *channel, **chans;
	int n;

	multi_query_remove(server, event, data);

	params = event_get_params(data, 2, NULL, &channel);

	chans = g_strsplit(channel, ",", -1);
	for (n = 0; chans[n] != NULL; n++)
	{
		chanrec = irc_channel_find(server, chans[n]);
		if (chanrec != NULL)
			channel_query_add(chanrec, query);
	}
	g_strfreev(chans);
	g_free(params);

	channels_query_check(server);
}

static void event_mode_abort(IRC_SERVER_REC *server, const char *data)
{
	char *params, *channel;

	g_return_if_fail(data != NULL);
	params = event_get_params(data, 2, NULL, &channel);

	if (strchr(channel, ',') == NULL) {
		channel_lost(server, channel);
	} else {
		SERVER_QUERY_REC *rec = server->chanqueries;

		server->no_multi_mode = TRUE;
		multi_command_error(server, data, rec->last_query, "event 324");
	}

	g_free(params);
}

static void event_who_abort(IRC_SERVER_REC *server, const char *data)
{
	char *params, *channel;

	g_return_if_fail(data != NULL);
	params = event_get_params(data, 2, NULL, &channel);

	if (strchr(channel, ',') == NULL) {
		channel_lost(server, channel);
	} else {
		server->no_multi_who = TRUE;
		multi_command_error(server, data, CHANNEL_QUERY_WHO, "event 315");
	}

	g_free(params);
}

void channels_query_init(void)
{
	signal_add("server connected", (SIGNAL_FUNC) sig_connected);
	signal_add("server disconnected", (SIGNAL_FUNC) sig_disconnected);
	signal_add("channel joined", (SIGNAL_FUNC) sig_channel_joined);
	signal_add("channel destroyed", (SIGNAL_FUNC) sig_channel_destroyed);

	signal_add("chanquery mode", (SIGNAL_FUNC) event_channel_mode);
	signal_add("chanquery who end", (SIGNAL_FUNC) event_end_of_who);

	signal_add("chanquery eban end", (SIGNAL_FUNC) event_end_of_ebanlist);
	signal_add("chanquery ban end", (SIGNAL_FUNC) event_end_of_banlist);
	signal_add("chanquery ilist end", (SIGNAL_FUNC) event_end_of_invitelist);
	signal_add("chanquery mode abort", (SIGNAL_FUNC) event_mode_abort);
	signal_add("chanquery who abort", (SIGNAL_FUNC) event_who_abort);
}

void channels_query_deinit(void)
{
	signal_remove("server connected", (SIGNAL_FUNC) sig_connected);
	signal_remove("server disconnected", (SIGNAL_FUNC) sig_disconnected);
	signal_remove("channel joined", (SIGNAL_FUNC) sig_channel_joined);
	signal_remove("channel destroyed", (SIGNAL_FUNC) sig_channel_destroyed);

	signal_remove("chanquery mode", (SIGNAL_FUNC) event_channel_mode);
	signal_remove("chanquery who end", (SIGNAL_FUNC) event_end_of_who);

	signal_remove("chanquery eban end", (SIGNAL_FUNC) event_end_of_ebanlist);
	signal_remove("chanquery ban end", (SIGNAL_FUNC) event_end_of_banlist);
	signal_remove("chanquery ilist end", (SIGNAL_FUNC) event_end_of_invitelist);
	signal_remove("chanquery mode abort", (SIGNAL_FUNC) event_mode_abort);
	signal_remove("chanquery who abort", (SIGNAL_FUNC) event_who_abort);
}