/*
 *      Binding update list
 *
 *      Authors:
 *      Juha Mynttinen            <jmynttin@cc.hut.fi>
 *
 *      $Id: bul.c,v 1.20 2001/02/23 18:25:37 antti Exp $
 *
 *      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.
 */

/*
 *	Changes:
 *
 *	Nanno Langstraat	:	Timer code cleaned up
 */

#include <linux/autoconf.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/in6.h>
#include <linux/init.h>
#include <linux/spinlock.h>
#include <net/ipv6.h>
#ifdef CONFIG_PROC_FS
#include <linux/proc_fs.h>
#endif

#include "bul.h"
#include "debug.h"
#include "mempool.h"
#include "hashlist.h"
#include "mipv6.h"

#define MIPV6_BUL_HASHSIZE 32

extern int modGT65536(__u16 x, __u16 y);

struct mipv6_bul {
	struct hashlist *entries;
	struct timer_list callback_timer;
	struct tq_struct callback_task;
	struct mipv6_allocation_pool *entry_pool;
	rwlock_t lock;
};

static struct mipv6_bul *bul;

#ifdef CONFIG_PROC_FS
static int bul_proc_info(char *buffer, char **start, off_t offset,
			    int length);
#endif

static void set_timer(void);

static struct mipv6_bul_entry *mipv6_bul_get_entry(void)
{
	DEBUG_FUNC();
	return ((struct mipv6_bul_entry *) 
		mipv6_allocate_element(bul->entry_pool));
}

static void mipv6_bul_entry_free(struct mipv6_bul_entry *entry)
{
	DEBUG_FUNC();
	mipv6_free_element(bul->entry_pool, (void *) entry);
}

static void task_handler(void *dummy)
{
	unsigned long flags;
	struct mipv6_bul_entry *entry;

	DEBUG_FUNC();

	write_lock_irqsave(&bul->lock, flags);

	entry = hashlist_get_first(bul->entries);

	if (entry == NULL) {
		DEBUG((DBG_ERROR, "bul task_handler executed but found no work to do"));
		write_unlock_irqrestore(&bul->lock, flags);
		return;
	}

	while (jiffies >= entry->callback_time) {
		if ((*entry->callback)(entry) != 0) {
			hashlist_delete(bul->entries, &entry->cn_addr);
			mipv6_bul_entry_free(entry);
			DEBUG((DBG_INFO, "Entry deleted (was expired) from binding update list"));
		} else {
			/* move entry to its right place in the hashlist */
			DEBUG((DBG_INFO, "Rescheduling"));
			hashlist_reschedule(bul->entries,
					    &entry->cn_addr,
					    entry->callback_time);
		}
		if ((entry = (struct mipv6_bul_entry *)
		     hashlist_get_first(bul->entries)) == NULL)
			break;
	}

	set_timer();

	write_unlock_irqrestore(&bul->lock, flags);
}

static void timer_handler(unsigned long dummy)
{
	unsigned long flags;

	DEBUG_FUNC();

	write_lock_irqsave(&bul->lock, flags);

	INIT_LIST_HEAD(&bul->callback_task.list);
	bul->callback_task.sync = 0;
	bul->callback_task.routine = task_handler;
	bul->callback_task.data = NULL;

	queue_task(&bul->callback_task, &tq_timer);

	write_unlock_irqrestore(&bul->lock, flags);
}

static void set_timer(void)
{
	struct mipv6_bul_entry *entry;
	unsigned long callback_time;

	DEBUG_FUNC();

	/* TODO! Is the overhead of this implementation acceptable?
	 * If not, perhaps add 'first_callback_time' field to mipv6_bul.
	 */

	entry = (struct mipv6_bul_entry *)hashlist_get_first(bul->entries);
	if (entry != NULL) {
		callback_time = entry->callback_time;
		if (entry->callback_time < jiffies) {
			DEBUG((DBG_INFO, "bul.c: set_timer: bul timer "
			       "attempted to schedule a timer with a "
			       "historical jiffies count!"));
			callback_time = jiffies+TIMERDELAY;
			DEBUG((DBG_INFO, 
			       "bul.c: set_timer: setting timer to now"));

		}
		mod_timer(&bul->callback_timer, callback_time);
	} else {
		DEBUG((DBG_INFO, "bul.c: set_timer: bul empty, not "
		       "setting a new timer"));
		del_timer(&bul->callback_timer);
	}
}

static int iterate_wrapper(
	void *entry, void *args,
	struct in6_addr *hashkey,
	unsigned long *sortkey)
{
	int (*iterator_function)(struct mipv6_bul_entry *);

	DEBUG_FUNC();

	iterator_function = args;

	return iterator_function((struct mipv6_bul_entry *) entry);
}

int bul_iterate(int (*func)(struct mipv6_bul_entry *entry)) 
{
	DEBUG_FUNC();

	return hashlist_iterate(bul->entries, func, iterate_wrapper);
}

static int mipv6_bul_dump_iterator(
	void *data, void *args,
	struct in6_addr *cn_addr,
	unsigned long *expire_time)
{
	struct mipv6_bul_entry *entry = (struct mipv6_bul_entry *)data;
	struct in6_addr *home_addr;
	struct in6_addr *coa;
	unsigned long callback_seconds;

	DEBUG_FUNC();

	home_addr = &entry->home_addr;
	coa = &entry->coa;

	if (jiffies > entry->callback_time) {
		callback_seconds = 0;
	} else {
		callback_seconds = (entry->callback_time - jiffies) / HZ;
	}

	DEBUG((DBG_INFO, "%x:%x:%x:%x:%x:%x:%x:%x, %x:%x:%x:%x:%x:%x:%x:%x"
	       "%x:%x:%x:%x:%x:%x:%x:%x, %d, %d, %d, %d, %d", 
	       NIPV6ADDR(cn_addr), NIPV6ADDR(home_addr), NIPV6ADDR(coa),
	       entry->seq, entry->state, entry->delay, entry->maxdelay,
	       callback_seconds));

	return 1;
}

int mipv6_bul_exists(struct in6_addr *cn)
{
	unsigned long flags;	
	int exists;

	DEBUG_FUNC();
	
	read_lock_irqsave(&bul->lock, flags);
	exists = hashlist_exists(bul->entries, cn);
	read_unlock_irqrestore(&bul->lock, flags);

	if (exists) return 0;
	else return -1;
}

/* TODO: Check locking in get / put.
 * Now get is atomic as put is, but it sould be so that get lock
 * and put unlocks
 */

struct mipv6_bul_entry *mipv6_bul_get(struct in6_addr *cn_addr)
{
	unsigned long flags;
	struct mipv6_bul_entry *entry;
	
	DEBUG_FUNC();

	read_lock_irqsave(&bul->lock, flags);
	entry = (struct mipv6_bul_entry *) 
		hashlist_get(bul->entries, cn_addr);
	read_unlock_irqrestore(&bul->lock, flags);
		
	return entry;
}

void mipv6_bul_put(struct mipv6_bul_entry *entry)
{
	unsigned long flags;
	
	DEBUG_FUNC();

	write_lock_irqsave(&bul->lock, flags);
	hashlist_reschedule(bul->entries,
			    &entry->cn_addr,
			    entry->callback_time);

	set_timer();

	write_unlock_irqrestore(&bul->lock, flags);
}

int mipv6_bul_add(
	struct in6_addr *cn_addr, struct in6_addr *home_addr,
	struct in6_addr *coa, 
	__u32 lifetime,	__u16 seq, __u8 prefix, __u8 flags,
	int (*callback)(struct mipv6_bul_entry *entry),
	__u32 callback_time,
	__u8 state, __u32 delay, __u32 maxdelay)
{
	unsigned long _flags;
	struct mipv6_bul_entry *entry;
	int update = 0;
	
	DEBUG_FUNC();
	
	write_lock_irqsave(&bul->lock, _flags);

	if (cn_addr == NULL || home_addr == NULL || 
	    coa == NULL || lifetime < 0 ||
	    prefix > 128 || callback == NULL || 
	    callback_time < 0 || 
	    (state != NOT_SENT && state != RESEND_EXP && state != RESEND_CONST && state != SEND_NOMORE) ||
	    delay < 0 || maxdelay < 0) {
		DEBUG((DBG_WARNING, "mipv6_bul_add: invalid arguments"));
		write_unlock_irqrestore(&bul->lock, _flags);
		return -1;
	}
	/* 
	 * decide whether to add a new entry or update existing, also
	 * check if there's room for a new entry when adding a new
	 * entry (latter is handled by mipv6_bul_get_entry() 
	 */
	if ((entry = hashlist_get(bul->entries, cn_addr)) != NULL) {
		/* if an entry for this cn_addr exists (with smaller
		 * seq than the new entry's seq), update it */
		
		if (modGT65536(seq, entry->seq)) {
			DEBUG((DBG_INFO, "mipv6_bul_add: updating an existing entry"));
			update = 1;
		} else {
			DEBUG((DBG_INFO, "mipv6_bul_add: smaller seq than existing, not updating"));
			write_unlock_irqrestore(&bul->lock, _flags);
			return -1;
		}
	} else {
		entry = mipv6_bul_get_entry();
		if (entry == NULL) {
			DEBUG((DBG_INFO, "mipv6_bul_add: binding update list full, can't add!!!"));
			write_unlock_irqrestore(&bul->lock, _flags);
			return -1;
		}
	}
	
	ipv6_addr_copy(&(entry->cn_addr), cn_addr);
	ipv6_addr_copy(&(entry->home_addr), home_addr);
	ipv6_addr_copy(&(entry->coa), coa);
	entry->expire = jiffies + lifetime * HZ;
	entry->seq = seq;
	entry->prefix = prefix;
	entry->flags = flags;
	entry->lastsend = jiffies; /* current time = last use of the entry */
	entry->state = state;
	entry->delay = delay;
	entry->maxdelay = maxdelay;
	entry->callback_time = jiffies + callback_time * HZ;
	entry->callback = callback;
	
	if (update) {
		DEBUG((DBG_INFO, "updating entry: %x", entry));
		hashlist_reschedule(bul->entries, cn_addr,
				    entry->callback_time);
	} else {
		DEBUG((DBG_INFO, "adding entry: %x", entry));
		if ((hashlist_add(bul->entries, cn_addr,
				  entry->callback_time,
				  entry)) < 0) {
			mipv6_bul_entry_free(entry);
			DEBUG((DBG_ERROR, "Hash add failed"));
			write_unlock_irqrestore(&bul->lock, _flags);
			return -1;
		}
	}

	set_timer();	

	write_unlock_irqrestore(&bul->lock, _flags);

	return 0;
}

int mipv6_bul_delete(struct in6_addr *cn_addr)
{
	unsigned long flags;
	struct mipv6_bul_entry *entry;

	DEBUG_FUNC();

	if (cn_addr == NULL) {
		DEBUG((DBG_INFO, "mipv6_bul_delete: argument NULL"));
		return -1;
	}

	write_lock_irqsave(&bul->lock, flags);

	entry = (struct mipv6_bul_entry *)
		hashlist_get(bul->entries, cn_addr);
	if (entry == NULL) {
		DEBUG((DBG_INFO, "mipv6_bul_delete: No such entry"));
		write_unlock_irqrestore(&bul->lock, flags);
		return -1;
	}
	hashlist_delete(bul->entries, &entry->cn_addr);
	mipv6_bul_entry_free(entry);

	set_timer();
	write_unlock_irqrestore(&bul->lock, flags);

	DEBUG((DBG_INFO, "mipv6_bul_delete: Binding update list entry deleted"));

	return 0;
}

void mipv6_bul_dump()
{
	unsigned long flags;

	DEBUG_FUNC();

	read_lock_irqsave(&bul->lock, flags);

	DEBUG((DBG_DATADUMP, "cn_addr, home_addr, coa, seq, state, delay, "
	       "maxdelay, callback_time (total: %d)", 
	       hashlist_count(bul->entries)));
	hashlist_iterate(bul->entries, NULL, mipv6_bul_dump_iterator);

	read_unlock_irqrestore(&bul->lock, flags);
}

#ifdef CONFIG_PROC_FS

struct procinfo_iterator_args {
	int *len;
	char *buffer;
};

static int procinfo_iterator(void *data, void *args,
			     struct in6_addr *addr, 
			     unsigned long *sortkey)
{
	struct procinfo_iterator_args *state =
		(struct procinfo_iterator_args *)args;
	struct mipv6_bul_entry *entry =
		(struct mipv6_bul_entry *)data;
	char *buffer = (char *)state->buffer;
	int *len = state->len;
	unsigned long callback_seconds;

	DEBUG_FUNC();

	if (entry == NULL) return ITERATOR_ERR;

	if (time_after(jiffies, entry->expire))
		return ITERATOR_DELETE_ENTRY;

	if (time_after(jiffies, entry->callback_time))
		callback_seconds = 0;
	else
		callback_seconds = (entry->callback_time - jiffies) / HZ;

	*len += sprintf(buffer + *len,
			"cna=%04x%04x%04x%04x%04x%04x%04x%04x "
			"ha=%04x%04x%04x%04x%04x%04x%04x%04x "
			"coa=%04x%04x%04x%04x%04x%04x%04x%04x\n"
			"exp=%ld seq=%d sta=%d del=%d mdl=%d cbs=%ld\n",
			NIPV6ADDR(&entry->cn_addr), 
			NIPV6ADDR(&entry->home_addr), 
			NIPV6ADDR(&entry->coa), 
			(entry->expire - jiffies) / HZ,
			entry->seq, entry->state, entry->delay, 
			entry->maxdelay, callback_seconds);

	return ITERATOR_CONT;
}


/*
 * Callback function for proc filesystem.
 */
static int bul_proc_info(char *buffer, char **start, off_t offset,
                            int length)
{
	unsigned long flags;
	int len = 0;
	struct procinfo_iterator_args args;

	DEBUG_FUNC();

	args.len = &len;
	args.buffer = buffer;

	read_lock_irqsave(&bul->lock, flags);
	hashlist_iterate(bul->entries, &args, procinfo_iterator);
	read_unlock_irqrestore(&bul->lock, flags);

	*start = buffer + offset;
	len -= offset;
	if (len > length) len = length;

	return len;
}

#endif /* CONFIG_PROC_FS */

int __init mipv6_initialize_bul(__u32 size)
{
	DEBUG_FUNC();

	if (size <= 0) {
		DEBUG((DBG_ERROR, 
		       "mipv6_bul_init: invalid binding update list size"));
		return -1;
	}
  
	bul = (struct mipv6_bul *) 
		kmalloc(sizeof(struct mipv6_bul), GFP_KERNEL);
	if (bul == NULL) {
		DEBUG((DBG_ERROR, "Couldn't create binding update list, kmalloc error"));
		return -1;
	}

	init_timer(&bul->callback_timer);
	bul->callback_timer.data = 0;
	bul->callback_timer.function = timer_handler;
	bul->entry_pool = 
		mipv6_create_allocation_pool(size, 
					     sizeof(struct mipv6_bul_entry),
					     GFP_KERNEL);
	if (bul->entry_pool == NULL) {
		DEBUG((DBG_ERROR, "mipv6_bul: Couldn't allocate memory for %d entries when creating binding update list", size));
		kfree(bul);
		return -1;
	}

	bul->entries = hashlist_create(size, MIPV6_BUL_HASHSIZE);
	if (bul->entries == NULL) {
		DEBUG((DBG_ERROR, "mipv6_bul: Couldn't allocate memory for hashlist when creating a binding update list"));
		hashlist_destroy(bul->entries);
		kfree(bul);
		return -1;
	}
	bul->lock = RW_LOCK_UNLOCKED;
#ifdef CONFIG_PROC_FS
	proc_net_create("mip6_bul", 0, bul_proc_info);
#endif
	DEBUG((DBG_MOD_INIT, "mipv6_bul_init: Binding update list initialized"));
	return 0;
}

int __exit mipv6_shutdown_bul()
{
	unsigned long flags;
	struct mipv6_bul_entry *entry;

	DEBUG_FUNC();

	if (bul == NULL) {
		DEBUG((DBG_INFO, "mipv6_bul_destroy: bul not initialized"));
		return -1;
	}

	write_lock_irqsave(&bul->lock, flags);

	DEBUG((DBG_INFO, "mipv6_bul_destroy: Stopping the timer"));
	del_timer(&bul->callback_timer);

	while ((entry = (struct mipv6_bul_entry *) 
		hashlist_get_first(bul->entries)) != NULL) {
		hashlist_delete(bul->entries,
				&entry->cn_addr);
		mipv6_bul_entry_free(entry);
	}

	if (bul->entries != NULL) 
		hashlist_destroy(bul->entries);

	if (bul->entry_pool != NULL) 
		mipv6_free_allocation_pool(bul->entry_pool);

	write_unlock_irqrestore(&bul->lock, flags); 

	kfree(bul);

#ifdef CONFIG_PROC_FS
	proc_net_remove("mip6_bul");
#endif

	DEBUG((DBG_MOD_INIT, 
	       "mipv6_bul_destroy: binding update list destoyed"));
	return 0;
}
