/*
 * flood.c: handle channel flooding. 
 *
 * This attempts to give you some protection from flooding.  Basically, it keeps
 * track of how far apart (timewise) messages come in from different people.
 * If a single nickname sends more than 3 messages in a row in under a
 * second, this is considered flooding.  It then activates the ON FLOOD with
 * the nickname and type (appropriate for use with IGNORE). 
 *
 * Thanks to Tomi Ollila <f36664r@puukko.hut.fi> for this one. 
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "irc.h"

#include "hook.h"
#include "ircaux.h"
#include "ignore.h"
#include "flood.h"
#include "vars.h"
#include "output.h"
#include "list.h"
#include "misc.h"
#include "server.h"
#include "timer.h"
#include "ignore.h"
#include "status.h"
#include "hash.h"
#include "fset.h"
#include "tcommand.h"

static const char *ignore_types[] = {
    "",
    "MSG",
    "PUBLIC",
    "NOTICE",
    "WALL",
    "WALLOP",
    "CTCP",
    "INVITE",
    "ACTION"
};

#define FLOOD_HASHSIZE 31

struct hash_entry no_flood_list[FLOOD_HASHSIZE];
struct hash_entry flood_list[FLOOD_HASHSIZE];

static int remove_oldest_flood_hashlist(struct hash_entry *, time_t, int);

extern char *FromUserHost;
extern unsigned int window_display;
extern int from_server;

static double allow_flood = 0.0;
static double this_flood = 0.0;

#define NO_RESET 0
#define RESET 1

void cmd_no_flood(struct command *cmd, char *args)
{
    char *nick;
    struct list *nptr;

    if (!args || !*args) {
	int count = 0;

	for (nptr = next_namelist(no_flood_list, NULL, FLOOD_HASHSIZE); nptr;
	     nptr = next_namelist(no_flood_list, nptr, FLOOD_HASHSIZE)) {
	    if (count++ == 0)
		put_it("%s", convert_output_format("%G+-[ %CNo Flood List %G]-------------------------------------", NULL));
	    put_it("%s", convert_output_format("%G| %c$0", "%s", nptr->name));
	}
	if (count == 0)
	    put_it("%s", convert_output_format("%RNo flood list%C is empty.", NULL));
	return;
    }
    nick = next_arg(args, &args);
    while (nick && *nick) {
	if (*nick == '-') {
	    if ((nptr = find_name_in_genericlist(nick + 1, no_flood_list, FLOOD_HASHSIZE, 1))) {
		new_free(&nptr->name);
		new_free(&nptr);
		bitchsay("%s removed from no-flood list", nick);
	    } else
		bitchsay("%s is not on your no-flood list", nick);
	} else {
	    if (*nick == '+')
		++nick;
	    if (!(nptr = find_name_in_genericlist(nick, no_flood_list, FLOOD_HASHSIZE, 0))) {
		add_name_to_genericlist(nick, no_flood_list, FLOOD_HASHSIZE);
		bitchsay("%s will no longer be checked for flooding");
	    }
	}
	nick = next_arg(args, &args);
    }
}

int get_flood_rate(int type, struct channel *channel)
{
    int flood_rate = get_int_var(FLOOD_RATE_VAR);

    if (channel) {
	switch (type) {
	case JOIN_FLOOD:
	    flood_rate = get_int_var(JOINFLOOD_TIME_VAR);
	    break;
	case PUBLIC_FLOOD:
	    flood_rate = get_int_var(PUBFLOOD_TIME_VAR);
	    break;
	case NICK_FLOOD:
	    flood_rate = get_int_var(NICKFLOOD_TIME_VAR);
	    break;
	case KICK_FLOOD:
	    flood_rate = get_int_var(KICKFLOOD_TIME_VAR);
	    break;
	case DEOP_FLOOD:
	    flood_rate = get_int_var(DEOPFLOOD_TIME_VAR);
	    break;
	default:
	    break;
	}
    } else {
	switch (type) {
	case CTCP_FLOOD:
	    flood_rate = get_int_var(DCC_FLOOD_RATE_VAR);
	case CTCP_ACTION_FLOOD:
	default:
	    break;
	}
    }
    return flood_rate;
}

int get_flood_count(int type, struct channel *channel)
{
    int flood_count = get_int_var(FLOOD_AFTER_VAR);

    if (channel) {
	switch (type) {
	case JOIN_FLOOD:
	    flood_count = get_int_var(KICK_ON_JOINFLOOD_VAR);
	    break;
	case PUBLIC_FLOOD:
	    flood_count = get_int_var(KICK_ON_PUBFLOOD_VAR);
	    break;
	case NICK_FLOOD:
	    flood_count = get_int_var(KICK_ON_NICKFLOOD_VAR);
	    break;
	case KICK_FLOOD:
	    flood_count = get_int_var(KICK_ON_KICKFLOOD_VAR);
	    break;
	case DEOP_FLOOD:
	    flood_count = get_int_var(KICK_ON_DEOPFLOOD_VAR);
	    break;
	default:
	    break;
	}
    } else {
	switch (type) {
	case CTCP_FLOOD:
	    flood_count = get_int_var(DCC_FLOOD_AFTER_VAR);
	case CTCP_ACTION_FLOOD:
	default:
	    break;
	}
    }
    return flood_count;
}

int set_flood(int type, time_t flood_time, int reset, struct nick_list *tmpnick)
{
    if (!tmpnick)
	return 0;
    switch (type) {
    case JOIN_FLOOD:
	if (reset == RESET) {
	    tmpnick->joincount = 1;
	    tmpnick->jointime = flood_time;
	} else
	    tmpnick->joincount++;
	break;
    case PUBLIC_FLOOD:
	if (reset == RESET) {
	    tmpnick->floodcount = 1;
	    tmpnick->floodtime = tmpnick->idle_time = flood_time;
	} else
	    tmpnick->floodcount++;
	break;
    case NICK_FLOOD:
	if (reset == RESET) {
	    tmpnick->nickcount = 1;
	    tmpnick->nicktime = flood_time;
	} else
	    tmpnick->nickcount++;
	break;
    case KICK_FLOOD:
	if (reset == RESET) {
	    tmpnick->kickcount = 1;
	    tmpnick->kicktime = flood_time;
	} else
	    tmpnick->kickcount++;
	break;
    case DEOP_FLOOD:
	if (reset == RESET) {
	    tmpnick->dopcount = 1;
	    tmpnick->doptime = flood_time;
	} else
	    tmpnick->dopcount++;
	break;
    default:
	break;
    }
    return 1;
}

int is_other_flood(struct channel *channel, struct nick_list *tmpnick, int type, int *t_flood)
{
    time_t diff = 0, flood_time = 0;
    int doit = 0;
    int count = 0;
    int flood_rate = 0, flood_count = 0;

    flood_time = time(NULL);

    if (!channel || !tmpnick)
	return 0;
    if (!strcmp(get_server_nickname(from_server), tmpnick->nick))
	return 0;
    if (find_name_in_genericlist(tmpnick->nick, no_flood_list, FLOOD_HASHSIZE, 0))
	return 0;

    set_flood(type, flood_time, NO_RESET, tmpnick);
    switch (type) {
    case JOIN_FLOOD:
	if (!get_int_var(JOINFLOOD_VAR))
	    break;
	diff = flood_time - tmpnick->jointime;
	count = tmpnick->joincount;
	doit = 1;
	break;
    case PUBLIC_FLOOD:
	if (!get_int_var(PUBFLOOD_VAR))
	    break;
	diff = flood_time - tmpnick->floodtime;
	count = tmpnick->floodcount;
	doit = 1;
	break;
    case NICK_FLOOD:
	if (!get_int_var(NICKFLOOD_VAR))
	    break;
	diff = flood_time - tmpnick->nicktime;
	count = tmpnick->nickcount;
	doit = 1;
	break;
    case DEOP_FLOOD:
	if (!get_int_var(DEOPFLOOD_VAR))
	    break;
	diff = flood_time - tmpnick->doptime;
	count = tmpnick->dopcount;
	doit = 1;
	break;
    case KICK_FLOOD:
	if (!get_int_var(KICKFLOOD_VAR))
	    break;
	diff = flood_time - tmpnick->kicktime;
	count = tmpnick->kickcount;
	doit = 1;
	break;
    default:
	break;
    }
    if (doit) {
	flood_count = get_flood_count(type, channel);
	flood_rate = get_flood_rate(type, channel);
	if (count >= flood_count) {
	    int flooded = 0;

	    if (!diff || (flood_rate && (diff < flood_rate))) {
		*t_flood = diff;
		flooded = 1;
	    }
	    set_flood(type, flooded ? 0L : flood_time, RESET, tmpnick);
	    return flooded;
	}
    }
    return 0;
}

static const char *get_ignore_types(unsigned int type)
{
    int x = 0;

    while (type) {
	type = type >> 1;
	x++;
    }
    return ignore_types[x];
}

/*
 * check_flooding: This checks for message flooding of the type specified for
 * the given nickname.  This is described above.  This will return 0 if no
 * flooding took place, or flooding is not being monitored from a certain
 * person.  It will return 1 if flooding is being check for someone and an ON
 * FLOOD is activated. 
 */
int check_flooding(char *nick, int type, char *line, char *channel)
{
    static int users = 0, pos = 0;
    time_t flood_time = time(NULL), diff = 0;

    struct flood *tmp;
    int flood_rate, flood_count;

    if (!(users = get_int_var(FLOOD_USERS_VAR)))
	return 1;
    if (find_name_in_genericlist(nick, no_flood_list, FLOOD_HASHSIZE, 0))
	return 1;
    if (!(tmp = find_name_in_floodlist(nick, flood_list, FLOOD_HASHSIZE, 0))) {
	if (pos >= users) {
	    pos -= remove_oldest_flood_hashlist(&flood_list[0], 0, (users + 1 - pos));
	}
	tmp = add_name_to_floodlist(nick, channel, flood_list, FLOOD_HASHSIZE);
	tmp->type = type;
	tmp->cnt = 1;
	tmp->start = flood_time;
	tmp->flood = 0;
	pos++;
	return 1;
    }
    if (!(tmp->type & type)) {
	tmp->type |= type;
	return 1;
    }
    flood_count = get_flood_count(type, NULL);	/* FLOOD_AFTER_VAR */
    flood_rate = get_flood_rate(type, NULL);	/* FLOOD_RATE_VAR */
    if (!flood_count || !flood_rate)
	return 1;
    tmp->cnt++;
    if (tmp->cnt > flood_count) {
	diff = flood_time - tmp->start;
	if (diff != 0)
	    this_flood = (double) tmp->cnt / (double) diff;
	else
	    this_flood = 0;
	allow_flood = (double) flood_count / (double) flood_rate;
	if (!diff || !this_flood || (this_flood > allow_flood)) {
	    if (tmp->flood == 0) {
		tmp->flood = 1;
		switch (type) {
		case MSG_FLOOD:
		case NOTICE_FLOOD:
		case CTCP_FLOOD:
		    if (flood_prot(nick, FromUserHost, get_ignore_types(type), type, get_int_var(IGNORE_TIME_VAR), channel))
			return 0;
		    break;
		case CTCP_ACTION_FLOOD:
		    if (flood_prot(nick, FromUserHost, get_ignore_types(CTCP_FLOOD), type, get_int_var(IGNORE_TIME_VAR), channel))
			return 0;
		default:
		    break;
		}
		if (get_int_var(FLOOD_WARNING_VAR))
		    put_it("%s",
			   convert_output_format(get_fset_var(FORMAT_FLOOD_FSET), "%s %s %s %s %s", update_clock(GET_TIME),
						 get_ignore_types(type), nick, FromUserHost, channel ? channel : "unknown"));
	    }
	    return 1;
	} else {
	    tmp->flood = 0;
	    tmp->cnt = 1;
	    tmp->start = flood_time;
	}
    }
    return 1;
}

int flood_prot(char *nick, char *userhost, const char *type, int ctcp_type, int ignoretime, char *channel)
{
    struct channel *chan;
    struct nick_list *Nick;
    char tmp[BIG_BUFFER_SIZE + 1];
    char *uh;
    int old_window_display;
    int kick_on_flood = 1;

    if ((ctcp_type == CTCP_FLOOD || ctcp_type == CTCP_ACTION_FLOOD) && !get_int_var(CTCP_FLOOD_PROTECTION_VAR))
	return 0;
    else if (!get_int_var(FLOOD_PROTECTION_VAR))
	return 0;
    else if (!my_stricmp(nick, get_server_nickname(from_server)))
	return 0;
    switch (ctcp_type) {
    case MSG_FLOOD:
	break;
    case NOTICE_FLOOD:
	break;
    case PUBLIC_FLOOD:
	if (channel) {
	    if ((chan = lookup_channel(channel, from_server, 0))) {
		kick_on_flood = get_int_var(PUBFLOOD_VAR);
		if ((Nick = find_nicklist_in_channellist(nick, chan, 0)) && kick_on_flood) {
		    if (chan->chop)
			send_to_server(SERVER(from_server), "KICK %s %s :\002%s\002 flooder", chan->channel, nick, type);
		}
	    }
	}
	break;
    case CTCP_ACTION_FLOOD:
    case CTCP_FLOOD:
    default:
	if (get_int_var(FLOOD_KICK_VAR) && kick_on_flood && channel) {
	    for (chan = server_list[from_server].chan_list; chan; chan = chan->next) {
		if ((Nick = find_nicklist_in_channellist(nick, chan, 0))) {
		    if (chan->chop)
			send_to_server(SERVER(from_server), "KICK %s %s :\002%s\002 flooder", chan->channel, nick, type);
		}
	    }
	}
    }
    if (!ignoretime)
	return 0;
    uh = clear_server_flags(userhost);
    sprintf(tmp, "*!*%s", uh);
    old_window_display = window_display;
    window_display = 0;
    ignore_nickname(tmp, ignore_type(type, strlen(type)), 0);
    window_display = old_window_display;
    sprintf(tmp, "%d ^IGNORE *!*%s NONE", ignoretime, uh);
    t_parse_command("TIMER", tmp);
    bitchsay("Auto-ignoring %s for %d minutes [\002%s\002 flood]", nick, ignoretime / 60, type);
    return 1;
}

static int remove_oldest_flood_hashlist(struct hash_entry *list, time_t timet, int count)
{
    struct flood *ptr;
    register time_t t;
    int total = 0;
    register unsigned long x;

    t = time(NULL);
    if (!count) {
	for (x = 0; x < FLOOD_HASHSIZE; x++) {
	    ptr = (struct flood *) (list + x)->list;
	    if (!ptr || !*ptr->name)
		continue;
	    while (ptr) {
		if ((ptr->start + timet) <= t) {
		    ptr = find_name_in_floodlist(ptr->name, flood_list, FLOOD_HASHSIZE, 1);
		    new_free(&(ptr->channel));
		    new_free(&ptr);
		    total++;
		    ptr = (struct flood *) (list + x)->list;
		} else
		    ptr = ptr->next;
	    }
	}
    } else {
	for (x = 0; x < FLOOD_HASHSIZE; x++) {
	    struct flood *next = NULL;

	    ptr = (struct flood *) (list + x)->list;
	    if (!ptr || !*ptr->name)
		continue;
	    while (ptr && count) {
		ptr = find_name_in_floodlist(ptr->name, flood_list, FLOOD_HASHSIZE, 1);
		next = ptr->next;
		new_free(&(ptr->channel));
		new_free(&ptr);
		total++;
		count--;
		ptr = (struct flood *) (list + x)->list;
		ptr = next;
	    }
	}
    }
    return total;
}

void clean_flood_list()
{
    remove_oldest_flood_hashlist(&flood_list[0], get_int_var(FLOOD_RATE_VAR) + 1, 0);
}
