/* Channel-handling routines.
 *
 * IRC Services is copyright (c) 1996-2009 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"

/*************************************************************************/

#define HASHFUNC(key) (hashlookup[(uint8)((key)[1])]<<5 \
		       | ((key)[1] ? hashlookup[(uint8)((key)[2])] : 0))
#define HASHSIZE     1024
#include "hash.h"
void add_channel(Channel *chan);  /* to avoid "no prototype" warning */
void del_channel(Channel *chan);  /* same */
DEFINE_HASH(channel, Channel, name)

static int cb_create       = -1;
static int cb_delete       = -1;
static int cb_join         = -1;
static int cb_join_check   = -1;
static int cb_mode         = -1;
static int cb_mode_change  = -1;
static int cb_umode_change = -1;
static int cb_topic        = -1;

/*************************************************************************/
/*************************************************************************/

int channel_init(int ac, char **av)
{
    cb_create       = register_callback(NULL, "channel create");
    cb_delete       = register_callback(NULL, "channel delete");
    cb_join         = register_callback(NULL, "channel JOIN");
    cb_join_check   = register_callback(NULL, "channel JOIN check");
    cb_mode         = register_callback(NULL, "channel MODE");
    cb_mode_change  = register_callback(NULL, "channel mode change");
    cb_umode_change = register_callback(NULL, "channel umode change");
    cb_topic        = register_callback(NULL, "channel TOPIC");
    if (cb_create < 0 || cb_delete < 0 || cb_join < 0 || cb_join_check < 0
     || cb_mode < 0 || cb_mode_change < 0 || cb_umode_change < 0
     || cb_topic < 0
    ) {
	log("channel_init: register_callback() failed\n");
	return 0;
    }
    return 1;
}

/*************************************************************************/

void channel_cleanup()
{
    Channel *c;

    for (c = first_channel(); c; c = next_channel())
	del_channel(c);
    unregister_callback(NULL, cb_topic);
    unregister_callback(NULL, cb_umode_change);
    unregister_callback(NULL, cb_mode_change);
    unregister_callback(NULL, cb_mode);
    unregister_callback(NULL, cb_join_check);
    unregister_callback(NULL, cb_join);
    unregister_callback(NULL, cb_delete);
    unregister_callback(NULL, cb_create);
}

/*************************************************************************/

/* Return statistics.  Pointers are assumed to be valid. */

void get_channel_stats(long *nrec, long *memuse)
{
    long count = 0, mem = 0;
    Channel *chan;
    struct c_userlist *cu;
    int i;

    for (chan = first_channel(); chan; chan = next_channel()) {
	count++;
	mem += sizeof(*chan);
	if (chan->topic)
	    mem += strlen(chan->topic)+1;
	if (chan->key)
	    mem += strlen(chan->key)+1;
	ARRAY_FOREACH (i, chan->bans) {
	    mem += sizeof(char *);
	    if (chan->bans[i])
		mem += strlen(chan->bans[i])+1;
	}
	ARRAY_FOREACH (i, chan->excepts) {
	    mem += sizeof(char *);
	    if (chan->excepts[i])
		mem += strlen(chan->excepts[i])+1;
	}
	LIST_FOREACH (cu, chan->users)
	    mem += sizeof(*cu);
    }
    *nrec = count;
    *memuse = mem;
}

/*************************************************************************/
/*************************************************************************/

/* Add/remove a user to/from a channel, creating or deleting the channel as
 * necessary.  If creating the channel, restore mode lock and topic as
 * necessary.  Also check for auto-opping and auto-voicing.  If a mode is
 * given, it is assumed to have been set by the remote server.
 * Returns the Channel structure for the given channel, or NULL if the user
 * was refused access to the channel (by the join check callback).
 */

Channel *chan_adduser(User *user, const char *chan, int32 modes)
{
    Channel *c = get_channel(chan);
    int newchan = !c;
    struct c_userlist *u;

    if (call_callback_2(NULL, cb_join_check, chan, user) > 0)
	return NULL;
    if (newchan) {
	if (debug)
	    log("debug: Creating channel %s", chan);
	/* Allocate pre-cleared memory */
	c = scalloc(sizeof(Channel), 1);
	c->anuser = 0;
	strscpy(c->name, chan, sizeof(c->name));
	c->creation_time = time(NULL);
	add_channel(c);
	call_callback_3(NULL, cb_create, c, user, modes);
    }
    u = smalloc(sizeof(struct c_userlist));
    LIST_INSERT(u, c->users);
    u->user = user;
    u->mode = modes;
    u->flags = 0;
    call_callback_2(NULL, cb_join, c, u);
    return c;
}


void chan_deluser(User *user, Channel *c)
{
    struct c_userlist *u;
    int i;

    LIST_SEARCH_SCALAR(c->users, user, user, u);
    if (!u) {
	log("channel: BUG: chan_deluser() called for %s in %s but they "
	    "were not found on the channel's userlist.",
	    user->nick, c->name);
	return;
    }
        (c->anuser)--;
    LIST_REMOVE(u, c->users);
    free(u);

    if (!c->users) {
	if (debug)
	    log("debug: Deleting channel %s", c->name);
	call_callback_1(NULL, cb_delete, c);
	set_cmode(NULL, c);  /* make sure nothing's left buffered */
	free(c->topic);
	free(c->key);
	free(c->link);
	free(c->flood);
	for (i = 0; i < c->bans_count; i++)
	    free(c->bans[i]);
	free(c->bans);
	for (i = 0; i < c->excepts_count; i++)
	    free(c->excepts[i]);
	free(c->excepts);
	del_channel(c);
	free(c);
    }
}

/*************************************************************************/

/* Search for the given ban on the given channel, and return the index into
 * chan->bans[] if found, -1 otherwise.  Nicknames are compared using
 * irc_stricmp(), usernames and hostnames using stricmp().
 */
static int find_ban(const Channel *chan, const char *ban)
{
    char *s, *t;
    int i;

    t = strchr(ban, '!');
    i = 0;
    ARRAY_FOREACH (i, chan->bans) {
	s = strchr(chan->bans[i], '!');
	if (s && t) {
	    if (s-(chan->bans[i]) == t-ban
	     && irc_strnicmp(chan->bans[i], ban, s-(chan->bans[i])) == 0
	     && stricmp(s+1, t+1) == 0
	    ) {
		return i;
	    }
	} else if (!s && !t) {
	    /* Bans without '!' should be impossible; just
	     * do a case-insensitive compare */
	    if (stricmp(chan->bans[i], ban) == 0)
		return i;
	}
    }
    return -1;
}

/*************************************************************************/

/* Search for the given ban (case-insensitive) on the channel; return 1 if
 * it exists, 0 if not.
 */

int chan_has_ban(const char *chan, const char *ban)
{
    Channel *c = get_channel(chan);
    if (!c)
	return 0;
    return find_ban(c, ban) >= 0;
}

/*************************************************************************/

/* Handle a channel MODE command.
 * When called internally to modify channel modes, callers may assume that
 * the contents of the argument strings will not be modified.
 */

static void do_cumode(const char *source, Channel *chan, int32 flag, int add,
		      const char *nick);

void do_cmode(const char *source, int ac, char **av)
{
    Channel *chan;
    char *s;
    int add = 1;		/* 1 if adding modes, 0 if deleting */
    char *modestr;

    chan = get_channel(av[0]);
    if (!chan) {
	log("channel: MODE %s for nonexistent channel %s",
					merge_args(ac-1, av+1), av[0]);
	return;
    }
    if ((protocol_features & PF_MODETS_FIRST) && isdigit(av[1][0])) {
	ac--;
	av++;
    }
    modestr = av[1];

    if (!NoBouncyModes) {
	/* Count identical server mode changes per second (mode bounce check)*/
	/* Doesn't trigger on +/-[bov] or other multiply-settable modes */
	static char multimodes[BUFSIZE];
	if (!*multimodes) {
	    int i = 0;
	    i = snprintf(multimodes, sizeof(multimodes), "%s",
			 chanmode_multiple);
	    snprintf(multimodes+i, sizeof(multimodes)-i, "%s",
		     mode_flags_to_string(MODE_ALL,MODE_CHANUSER));
	}
	if (strchr(source, '.') && strcmp(source, ServerName) != 0
	 && !modestr[strcspn(modestr, multimodes)]
	) {
	    static char lastmodes[BUFSIZE];
	    if (time(NULL) != chan->server_modetime
	     || strcmp(modestr, lastmodes) != 0
	    ) {
		chan->server_modecount = 0;
		chan->server_modetime = time(NULL);
		strscpy(lastmodes, modestr, sizeof(lastmodes));
	    }
	    chan->server_modecount++;
	}
    }

    s = modestr;
    ac -= 2;
    av += 2;

    while (*s) {
	char modechar = *s++;
	int32 flag;
	int params;

	if (modechar == '+') {
	    add = 1;
	    continue;
	} else if (modechar == '-') {
	    add = 0;
	    continue;
	} else if (add < 0) {
	    continue;
	}

	/* Check for it as a channel user mode */

	flag = mode_char_to_flag(modechar, MODE_CHANUSER);
	if (flag) {
	    if (--ac < 0) {
		log("channel: MODE %s %s: missing parameter for %c%c",
		    chan->name, modestr, add ? '+' : '-', modechar);
		break;
	    }
	    do_cumode(source, chan, flag, add, *av++);
	    continue;
	}

	/* Nope, must be a regular channel mode */

	flag = mode_char_to_flag(modechar, MODE_CHANNEL);
	if (!flag)
	    continue;
	if (flag == MODE_INVALID)
	    flag = 0;
	params = mode_char_to_params(modechar, MODE_CHANNEL);
	params = (params >> (add*8)) & 0xFF;
	if (ac < params) {
	    log("channel: MODE %s %s: missing parameter(s) for %c%c",
		chan->name, modestr, add ? '+' : '-', modechar);
	    break;
	}

	if (call_callback_5(NULL, cb_mode, source,chan,modechar,add,av) <= 0) {

	    if (add)
		chan->mode |= flag;
	    else
		chan->mode &= ~flag;

	    switch (modechar) {
	      case 'k':
		free(chan->key);
		if (add)
		    chan->key = sstrdup(av[0]);
		else
		    chan->key = NULL;
		break;

	      case 'l':
		if (add)
		    chan->limit = atoi(av[0]);
		else
		    chan->limit = 0;
		break;

	      case 'b':
		if (add) {
		    ARRAY_EXTEND(chan->bans);
		    chan->bans[chan->bans_count-1] = sstrdup(av[0]);
		} else {
		    int i = find_ban(chan, av[0]);
		    if (i >= 0) {
			free(chan->bans[i]);
			ARRAY_REMOVE(chan->bans, i);
		    } else {
			log("channel: MODE %s -b %s: ban not found",
			    chan->name, *av);
		    }
		}
		break;

	    } /* switch (modechar) */

	} /* if (callback() <= 0) */

	ac -= params;
	av += params;

    } /* while (*s) */

    call_callback_2(NULL, cb_mode_change, source, chan);
}

/* Modify a user's CUMODE. */
static void do_cumode(const char *source, Channel *chan, int32 flag, int add,
		      const char *nick)
{
    struct c_userlist *u;
    User *user;
    int32 oldmode;

    user = get_user(nick);
    if (!user) {
	log("channel: MODE %s %c%c for nonexistent user %s",
	    chan->name, add ? '+' : '-',
	    mode_flag_to_char(flag, MODE_CHANUSER), nick);
	return;
    }
    LIST_SEARCH_SCALAR(chan->users, user, user, u);
    if (!u) {
	log("channel: MODE %s %c%c for user %s not on channel",
	    chan->name, add ? '+' : '-',
	    mode_flag_to_char(flag, MODE_CHANUSER), nick);
	return;
    }

    oldmode = u->mode;
    if (flag == MODE_INVALID)
	return;
    if (add)
	u->mode |= flag;
    else
	u->mode &= ~flag;

    call_callback_4(NULL, cb_umode_change, source, chan, u, oldmode);
}

/*************************************************************************/

/* Handle a TOPIC command. */

void do_topic(const char *source, int ac, char **av)
{
    Channel *c = get_channel(av[0]);
    const char *topic;
    char *s;

    if (!c) {
	log("channel: TOPIC %s for nonexistent channel %s",
	    merge_args(ac-1, av+1), av[0]);
	return;
    }
    s = strchr(av[1], '!');
    if (s)
	*s = 0;
    if (ac > 3)
	topic = av[3];
    else
	topic = "";
    if (call_callback_4(NULL, cb_topic, c, topic, av[1],
			strtotime(av[2],NULL)) > 0)
	return;
    strscpy(c->topic_setter, av[1], sizeof(c->topic_setter));
    if (c->topic) {
	free(c->topic);
	c->topic = NULL;
    }
    if (ac > 3 && *av[3])
	c->topic = sstrdup(av[3]);
}

/*************************************************************************/
