/* ARISA - Ignore List
 * Copyright (C) 2004 Carl Ritson
 *
 * 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 "arisa.h"
#include <ctype.h>
#include <math.h>

/** defines **/

#define INIT_SIZE 	4
#define SHRINK_RATIO 	4
#define CHAIN_LIMIT	16
#define SIZE_CAP	8192

/** structures **/

typedef struct hh_bucket_t {
	unsigned long 	hash;
	char 		*host;
	time_t 		last_hit;
	float 		hits;
	struct hh_bucket_t *next;
} hh_bucket_t;

typedef struct hosthash_t {
//	lock_t		lock;
	hh_bucket_t 	**table;
	unsigned int 	size;
} hosthash_t;

/** hosthash functions */

static unsigned long hosthash_hash(const char *host) {
	unsigned long hash = 0;
	int i;
	
	assert(host != NULL);
	
	for(i = 0; host[i] != '\0'; ++i) {
		// irc_tolower as host maybe be a hostmask with a nick in it
		hash = hash * 37UL + (unsigned long) irc_tolower(host[i]);
	}
	
	return hash;
}

static hosthash_t *hosthash_init(void) {
	hosthash_t *ht = xalloc(sizeof(hosthash_t));
	unsigned int i;
	
//	LOCK_INIT(&(ht->lock));
	
	ht->table 	= xalloc(sizeof(hh_bucket_t *) * INIT_SIZE);
	ht->size 	= INIT_SIZE;
	for(i = 0; i < ht->size; ++i) {
		ht->table[i] = NULL;
	}

	return ht;
}

static hh_bucket_t *init_bucket(unsigned long hash, const char *host) {
	hh_bucket_t *b = xalloc(sizeof(hh_bucket_t));
	b->hash 	= hash;
	b->host 	= xstrdup(host);
	b->hits		= 0.0;
	b->last_hit	= 0;
	b->next		= NULL;
	return b;
}

static void free_bucket(hh_bucket_t *b) {
	xfree(b->host);
	xfree(b);
}

static void flush(hosthash_t *ht) {
	hh_bucket_t *b = NULL, *p = NULL;
	unsigned int i;

//	LOCK(ht);
	for(i = 0; i < ht->size; ++i) {
		for(p = ht->table[i]; p != NULL; p = b) {
			b = p->next;
			free_bucket(p);
		}
		ht->table[i] = NULL;
	}
//	UNLOCK(ht);
}

void hosthash_free(hosthash_t *ht) {
	flush(ht);
//	LOCK_FREE(&(ht->lock));
	xfree(ht->table);
	xfree(ht);
}

/* lock assumed */
static int place(hosthash_t *ht, hh_bucket_t *b, hh_bucket_t **dup) {
	hh_bucket_t *p = ht->table[b->hash % ht->size];
	int depth = 0;
	
	b->next = NULL;
	
	if(p == NULL) {
		ht->table[b->hash % ht->size] = b;
	} else {
		for(;;) {
			depth++;
			if(irc_strcasecmp(p->host,b->host) == 0) {
				if(dup != NULL)
					*dup = p;
				return -1;
			}
			if(p->next != NULL)
				p = p->next;
			else
				break;
		}
		p->next = b;
	}
	
	return depth;
}

/* lock assumed */
static void resize(hosthash_t *ht, unsigned int new_size) {
	hh_bucket_t **old_table	= ht->table;
	unsigned int old_size	= ht->size;
	unsigned int i;
	hh_bucket_t *b = NULL, *p = NULL;
	
	if(new_size > SIZE_CAP || new_size < INIT_SIZE || new_size == old_size)
		return;

	ht->size  = new_size;
	ht->table = xalloc(sizeof(hh_bucket_t *)*ht->size);
	for(i = 0; i < ht->size; ++i)
		ht->table[i] = NULL;

	for(i = 0; i < old_size; ++i) {
		if(old_table[i] == NULL)
			continue;
		for(p = old_table[i]; p != NULL; p = b) {
			b = p->next;
			if(place(ht,p,NULL) == -1)
				free_bucket(p);
		}
	}
	
	xfree(old_table);
}

static inline void decay_bucket(hh_bucket_t *bkt, 
		time_t now, int trigger, int decay) {
	if(bkt->hits > 0.0) {
		bkt->hits -= ((float)(now - bkt->last_hit) / (float) decay)
			* (float) trigger;
		if(bkt->hits < 0.0)
			bkt->hits = 0.0;
		bkt->last_hit = now;
	}
}

static int hosthash_trigger(hosthash_t *ht, const char *host, 
		time_t now, int trigger, int decay) {
	hh_bucket_t *lbkt, *bkt, *nbkt;
	unsigned long hash;
	int ret = 0, depth = 0;
	
	assert(ht != NULL);
	assert(host != NULL);
	
	hash = hosthash_hash(host);
	
//	LOCK(ht);
	
	lbkt = NULL;
	bkt = ht->table[hash % ht->size];
	while(bkt != NULL) {
		depth++;
		decay_bucket(bkt,now,trigger,decay);
		if(irc_strcasecmp(bkt->host,host) == 0)
			break;
		if(bkt->hits <= 0.0) {
			nbkt = bkt->next;
			if(lbkt != NULL)
				lbkt->next = bkt->next;
			else
				ht->table[hash % ht->size] = bkt->next;
			free_bucket(bkt);
			bkt = nbkt;
		} else {
			lbkt = bkt;
			bkt = bkt->next;
		}
	}

	if(bkt == NULL) {
		bkt = init_bucket(hash,host);
		if(lbkt != NULL)
			lbkt->next = bkt;
		else
			ht->table[hash % ht->size] = bkt;
		if((depth+1) > CHAIN_LIMIT)
			resize(ht,ht->size * 2);
	}
	
	if(ceil(bkt->hits) >= (float) trigger) {
		ret = 1;
	} else {
		bkt->hits += 1.0;
		if(ceil(bkt->hits) > (float) trigger)
			ret = 1;
	}
	bkt->last_hit = now; // this isn't really required but anyway...
	
	//D("bkt: %p, last_hit: %ld, hits: %.2f",bkt,bkt->last_hit,bkt->hits);
	
//	UNLOCK(ht);
	
	return ret;
}

static void prune(hosthash_t *ht, time_t now, int trigger, int decay) {
	hh_bucket_t *lbkt, *bkt, *nbkt;
	int i,chain_length,max_chain_length = 0;
	
	assert(ht != NULL);
	
//	LOCK(ht);
	for(i = 0; i < ht->size; ++i) {
		lbkt = NULL;
		bkt = ht->table[i];
		for(chain_length = 0; bkt != NULL; ++chain_length) {
			decay_bucket(bkt,now,trigger,decay);
			nbkt = bkt->next;
			if(bkt->hits <= 0.0) {
				free_bucket(bkt);
				if(lbkt != NULL)
					lbkt->next = nbkt;
				else
					ht->table[i] = nbkt;
				chain_length--;
			} else
				lbkt = bkt;
			bkt = nbkt;
		}
		if(chain_length > max_chain_length)
			max_chain_length = chain_length;
	}
	if(max_chain_length < (CHAIN_LIMIT / SHRINK_RATIO))
		resize(ht,ht->size / 2);
//	UNLOCK(ht);
}

static void hosthash_reset(hosthash_t *ht, const char *host) {
	hh_bucket_t *bkt;
	unsigned long hash;
	
	assert(ht != NULL);
	assert(host != NULL);
	
	hash = hosthash_hash(host);
	
//	LOCK(ht);
	
	bkt = ht->table[hash % ht->size];
	while(bkt != NULL) {
		if(irc_strcasecmp(bkt->host,host) == 0) {
			bkt->hits = 0.0;
			break;
		} else
			bkt = bkt->next;
	}

//	UNLOCK(ht);
}

/** ignore list functions **/

ignore_entry_t *alloc_ignore_entry(void) {
	ignore_entry_t *ent = xalloc(sizeof(ignore_entry_t));

	ent->flags	= 0;
	ent->mask	= NULL;
	ent->expires	= 0;
	
	return ent;
}

void free_ignore_entry(ignore_entry_t *ent) {
	if(ent->mask != NULL)
		xfree(ent->mask);
	xfree(ent);
}

ignore_list_t *alloc_ignore_list(void) {
	ignore_list_t *l = xalloc(sizeof(ignore_list_t));
	LOCK_INIT(&(l->lock));
	
	l->entries	= NULL;
	l->no_entries	= 0;
	
	l->last_prune	= 0;
	l->trigger	= 0;
	l->period	= 300;
	l->log		= 0;
	
	l->data		= hosthash_init();
	
	return l;
}

void free_ignore_list(ignore_list_t *l) {
	LOCK_FREE(&(l->lock));
	
	if(l->entries != NULL) {
		int i;
		for(i = 0; i < l->no_entries; ++i)
			free_ignore_entry(l->entries[i]);
		xfree(l->entries);
	}

	hosthash_free((hosthash_t *)l->data);
	
	xfree(l);
}

enum { IGNORE_YES = 1, IGNORE_NO = 0, IGNORE_MAYBE = -1};

static int test_list(ignore_list_t *l, time_t now, const char *hostmask) {
	char buffer[512];
	int i, expired = 0, ret = IGNORE_MAYBE;

	for(i = 0; hostmask[i] != '\0' && i < (sizeof(buffer)-1); ++i)
		buffer[i] = irc_tolower(hostmask[i]);
	buffer[i] = '\0';

	//D("hostmask: \"%s\", buffer: \"%s\"",hostmask,buffer);
	
	for(i = 0; i < l->no_entries && ret == IGNORE_MAYBE; ++i) {
		ignore_entry_t *e = l->entries[i];
		if(e->expires > now || (e->flags & IGNORE_FOREVER)) {
			if(wild_match(buffer,e->mask)) {
				//D("yes: %s",e->mask);
				ret = e->flags & IGNORE_INVERSION ? 
					IGNORE_NO : IGNORE_YES;
			} //else
				//D("no: %s",e->mask);
		} else {
			if(l->log)
				LOGP(L_INF,"Removed ignore entry for %s",
						e->mask);
			free_ignore_entry(e);
			l->entries[i] = NULL;
			expired++;
		}
	}

	if(expired)
		PTRARR_DEL(&(l->entries),&(l->no_entries),NULL);

	return ret;
}

static int add_entry(ignore_list_t *l, const char *mask, 
		int flags, time_t expires) {
	ignore_entry_t *e = alloc_ignore_entry();
	int i;

	for(i = 0; i < l->no_entries; ++i) {
		if(strcasecmp(l->entries[i]->mask,mask) == 0)
			return -1;
	}
	
	e->flags	= flags;
	e->expires	= expires;
	e->mask		= xstrdup(mask);
	for(i = 0; e->mask[i] != '\0'; ++i)
		e->mask[i] = irc_tolower(e->mask[i]);

	PTRARR_ADD(&(l->entries),&(l->no_entries),e);

	if(l->log) {
		char tbuf[32];
		LOGP(L_INF,"Added %signore entry for %s, %s%s",
			e->flags & IGNORE_INVERSION ? "un" : "",
			e->mask,
			e->flags & IGNORE_FOREVER ? "Forever" : "Till: ",
			e->flags & IGNORE_FOREVER ? "" :
				timestr(e->expires,tbuf,sizeof(tbuf)));
	}

	return 0;
}

int ignore_test(ignore_list_t *l, const char *hostmask) {
	time_t now = xtime();
	int ret;
	
	LOCK(l);
	
	if((l->last_prune + 600) < now) {
		prune((hosthash_t *)l->data,now,l->trigger,l->period);
		l->last_prune = now;
	}
	
	ret = test_list(l,now,hostmask);
	if(ret != IGNORE_MAYBE) {
		UNLOCK(l);
		return ret;
	} else
		ret = IGNORE_NO;
	
	if(l->trigger != 0 && l->period > 0) {
		if(hosthash_trigger((hosthash_t *)l->data,hostmask,now,
					l->trigger,l->period)) {
			char *ptr = strchr(hostmask,'@');
			if(ptr != NULL) {
				char buffer[512];
				snprintf(buffer,sizeof(buffer),"*%s",ptr);
				add_entry(l,buffer,0,now + l->period);
			} else {
				add_entry(l,hostmask,0,now + l->period);
			}
			hosthash_reset((hosthash_t *)l->data,hostmask);
			ret = IGNORE_YES;
		}
	}

	UNLOCK(l);

	return ret;
}

int ignore_add(ignore_list_t *l, const char *hostmask, 
		int flags, time_t expires) {
	int ret;
	LOCK(l);
	ret = add_entry(l,hostmask,flags,expires);
	UNLOCK(l);
	return 0;
}

int ignore_del(ignore_list_t *l, int pos, const char *hostmask) {
	int i,ret = -1;

	LOCK(l);
	if(hostmask == NULL && pos >= 0 && pos < l->no_entries) {
		l->entries[pos]->flags = 0;
		l->entries[pos]->expires = 0;
		ret = 0;
	} else if(hostmask != NULL) {
		for(i = 0; i < l->no_entries && ret == -1; ++i) {
			if(irc_strcasecmp(l->entries[i]->mask,hostmask) == 0) {
				l->entries[i]->flags = 0;
				l->entries[i]->expires = 0;
				ret = 0;
			}
		}
	}
	UNLOCK(l);

	return ret;
}

void ignore_wipe(ignore_list_t *l) {
	int i;
	
	LOCK(l);
	if(l->entries != NULL) {
		for(i = 0; i < l->no_entries; ++i)
			free_ignore_entry(l->entries[i]);
		xfree(l->entries);
		l->no_entries = 0;
	}
	flush((hosthash_t *)l->data);
	if(l->log)
		LOGP(L_INF,"Wiped Ignore List");
	UNLOCK(l);
}
