/*
 * $Id: mpdisc_mn.c 1.20 06/05/07 21:52:43+03:00 anttit@tcs.hut.fi $
 *
 * This file is part of the MIPL Mobile IPv6 for Linux.
 * 
 * Authors:
 *  Ville Nuorvala <vnuorval@tcs.hut.fi>,
 *  Jaakko Laine <jola@tcs.hut.fi>
 *
 * Copyright 2003-2005 Go-Core Project
 * Copyright 2003-2006 Helsinki University of Technology
 *
 * MIPL Mobile IPv6 for Linux 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; version 2 of
 * the License.
 *
 * MIPL Mobile IPv6 for Linux 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 MIPL Mobile IPv6 for Linux; if not, write to the Free
 * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307 USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <time.h>
#include <netinet/in.h>
#include <netinet/icmp6.h>
#include <netinet/ip6mh.h>
#include <errno.h>
#include <syslog.h>
#include <pthread.h>

#include "debug.h"
#include "mipv6.h"
#include "util.h"
#include "icmp6.h"
#include "mpdisc_mn.h"
#include "list.h"
#include "conf.h"
#include "prefix.h"
#include "mn.h"
#include "hash.h"
#include "statistics.h"

struct mps_entry {
	struct in6_addr hoa;
	struct in6_addr ha;
	struct timespec delay;
	struct timespec lastsent;
	struct tq_elem tqe;
	uint16_t id;
};


#define MPS_BUCKETS 32
static struct hash mps_hash;
static pthread_mutex_t mps_lock;

static inline struct mps_entry *mps_get(const struct in6_addr *hoa,
					const struct in6_addr *ha)
{
	return hash_get(&mps_hash, hoa, ha);
}

#ifdef ENABLE_VT
int mpd_poll_mps(const struct in6_addr *hoa,
		 const struct in6_addr *ha,
		 struct timespec *delay,
		 struct timespec *lastsent)
{
	struct mps_entry *e;
	int err = -ENOENT;
	pthread_mutex_lock(&mps_lock);
	e = mps_get(hoa, ha);
	if (e != NULL) {
		*delay = e->delay;
		*lastsent = e->lastsent;
		err = 0;
	}
	pthread_mutex_unlock(&mps_lock);
	return err;
}
#endif
/* MN functions */

static int mpd_send_mps(struct mps_entry *e)
{
	struct iovec iov;
	struct mip_prefix_solicit *ih;

	ih = icmp6_create(&iov, MIP_PREFIX_SOLICIT, 0);

	if (ih == NULL)
		return -ENOMEM;

	/* no need to worry about network byte order since 
	   mip_ps_id only serves as an identifier */
	ih->mip_ps_id = e->id;
	clock_gettime(CLOCK_REALTIME, &e->lastsent);
	icmp6_send(0, 0, &e->hoa, &e->ha, &iov, 1);
	free_iov_data(&iov, 1);
	statistics_inc(&mipl_stat, MIPL_STATISTICS_OUT_MPS);
	return 0;
}

void mpd_cancel_mps(const struct in6_addr *hoa,
		    const struct in6_addr *ha)
{
	struct mps_entry *e;
	pthread_mutex_lock(&mps_lock);
	e = mps_get(hoa, ha);
	if (e != NULL) {
		dbg("canceling MPS\n");
		hash_delete(&mps_hash, &e->hoa, &e->ha);
		if (tsisset(e->delay))
			del_task(&e->tqe);
		free(e);
	}
	pthread_mutex_unlock(&mps_lock);
}

static void mpd_resend_mps(struct tq_elem *tqe)
{
	pthread_mutex_lock(&mps_lock);
	if (!task_interrupted()) {
		struct mps_entry *e = tq_data(tqe, struct mps_entry, tqe);
		if (tsbefore(MAX_BINDACK_TIMEOUT_TS, e->delay)) {
			mpd_send_mps(e);
			tsadd(e->delay, e->delay, e->delay);
			e->delay = tsmin(e->delay, MAX_BINDACK_TIMEOUT_TS);
			add_task_rel(&e->delay, &e->tqe, mpd_resend_mps);
		} else
			tsclear(e->delay);
	}
	pthread_mutex_unlock(&mps_lock);
}

static void mpd_send_first_mps(struct tq_elem *tqe)
{
	pthread_mutex_lock(&mps_lock);
	if (!task_interrupted()) {
		struct mps_entry *e = tq_data(tqe, struct mps_entry, tqe);
		e->id = random();
		mpd_send_mps(e);
		e->delay = conf.InitialSolicitTimer_ts;
		add_task_rel(&e->delay, &e->tqe, mpd_resend_mps);
	}
	pthread_mutex_unlock(&mps_lock);

}

#define PREFIX_LIFETIME_MAX_FINITE 0x7FFFFFFF

int mpd_schedule_first_mps(const struct in6_addr *hoa,
			   const struct in6_addr *ha,
			   const struct timespec *valid_time)
{
	struct mps_entry *e;
	int err = -ENOMEM;
	uint32_t delay;

	if (!conf.SendMobPfxSols)
		return 0;

	pthread_mutex_lock(&mps_lock);
	e = mps_get(hoa, ha);
	if (e == NULL) {
		e = malloc(sizeof(struct mps_entry));
		if (e == NULL)
			goto out;
		memset(e, 0, sizeof(struct mps_entry));
		e->hoa = *hoa;
		e->ha = *ha;
		if (hash_add(&mps_hash, e, &e->hoa, &e->ha)) {
			free(e);
			goto out;
		}
		INIT_LIST_HEAD(&e->tqe.list);
	}
	err = 0;
	delay = umin(valid_time->tv_sec, PREFIX_LIFETIME_MAX_FINITE);
	tssetsec(e->delay, delay * MPS_REFRESH_DELAY);
	add_task_rel(&e->delay, &e->tqe, mpd_send_first_mps);
	dbg("schedule MPS in %u s\n", e->delay.tv_sec);
out:
	pthread_mutex_unlock(&mps_lock);
	return err;
}

static void mpd_recv_mpa(const struct icmp6_hdr *ih, ssize_t len,
			 const struct in6_addr *src,
			 const struct in6_addr *dst,
			 __attribute__ ((unused)) int iif,
			 __attribute__ ((unused)) int hoplimit)
{
	uint8_t *opt = (uint8_t *)(ih + 1);
	struct mps_entry *e;
	struct home_addr_info *hai;
	struct timespec lastsent;
	
	pthread_mutex_lock(&mps_lock);
	e = mps_get(dst, src);
	if (e == NULL) {
		pthread_mutex_unlock(&mps_lock);
		return;
	}
	if (e->id != ih->icmp6_id) {
		pthread_mutex_unlock(&mps_lock);
		mpd_trigger_mps(dst, src);
		return;
	}
	del_task(&e->tqe);
	tsclear(e->delay);
	lastsent = e->lastsent;
	pthread_mutex_unlock(&mps_lock);

	/* todo: do something about the MO flags */

	pthread_rwlock_wrlock(&mn_lock);
	hai = mn_get_home_addr(dst);
	if (hai != NULL) {
		int optlen = len - sizeof(struct icmp6_hdr);
		while (optlen > 1) {
			uint16_t olen = opt[1] << 3;
			
			if (olen > (unsigned int)optlen || olen == 0)
				break;

			if (opt[0] == ND_OPT_PREFIX_INFORMATION) {
				struct nd_opt_prefix_info *pinfo;

				if (olen < sizeof(*pinfo))
					continue;

				pinfo = (struct nd_opt_prefix_info *) opt;
				/* internal representation host byte order */
				pinfo->nd_opt_pi_valid_time = 
					ntohl(pinfo->nd_opt_pi_valid_time);
				pinfo->nd_opt_pi_preferred_time =
					ntohl(pinfo->nd_opt_pi_preferred_time);

				if (pinfo->nd_opt_pi_valid_time <
				    pinfo->nd_opt_pi_preferred_time)
					continue;

				mn_update_home_prefix(hai, &lastsent, pinfo);
			}
			optlen -= olen;
			opt += olen;
		}
	}
	pthread_rwlock_unlock(&mn_lock);
}

static struct icmp6_handler mpd_mpa_handler = {
	.recv = mpd_recv_mpa,
};

int mpd_mn_init(void)
{
	pthread_mutexattr_t mattrs;
	pthread_mutexattr_init(&mattrs);
	pthread_mutexattr_settype(&mattrs, PTHREAD_MUTEX_FAST_NP);
	if (pthread_mutex_init(&mps_lock, &mattrs))
		return -1;
	if (hash_init(&mps_hash, DOUBLE_ADDR, MPS_BUCKETS) < 0)
		return -1;
	icmp6_handler_reg(MIP_PREFIX_ADVERT, &mpd_mpa_handler);
	return 0;
}

void mpd_mn_cleanup(void)
{
	icmp6_handler_dereg(MIP_PREFIX_ADVERT, &mpd_mpa_handler);
	pthread_mutex_lock(&mps_lock);
	hash_cleanup(&mps_hash);
	pthread_mutex_unlock(&mps_lock);
}
