/*
 * Copyright (C) 2012 Tobias Brunner
 * Copyright (C) 2012 Giuliano Grassi
 * Copyright (C) 2012 Ralf Sager
 * HSR Hochschule fuer Technik Rapperswil
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#include "ipsec_event_relay.h"

#include <library.h>
#include <utils/debug.h>
#include <threading/rwlock.h>
#include <collections/linked_list.h>
#include <collections/blocking_queue.h>
#include <processing/jobs/callback_job.h>

typedef struct private_ipsec_event_relay_t private_ipsec_event_relay_t;

/**
 * Private additions to ipsec_event_relay_t.
 */
struct private_ipsec_event_relay_t {

	/**
	 * Public members
	 */
	ipsec_event_relay_t public;

	/**
	 * Registered listeners
	 */
	linked_list_t *listeners;

	/**
	 * Lock to safely access the list of listeners
	 */
	rwlock_t *lock;

	/**
	 * Blocking queue for events
	 */
	blocking_queue_t *queue;
};

/**
 * Helper struct used to manage events in a queue
 */
typedef struct {

	/**
	 * Type of the event
	 */
	enum {
		IPSEC_EVENT_EXPIRE,
	} type;

	/**
	 * Protocol of the SA
	 */
	uint8_t protocol;

	/**
	 * SPI of the SA, if any
	 */
	uint32_t spi;

	/**
	 * SA destination address
	 */
	host_t *dst;

	/**
	 * Additional data for specific event types
	 */
	union {

		struct {
			/** TRUE in case of a hard expire */
			bool hard;
		} expire;

	} data;

} ipsec_event_t;

/**
 * Destroy IPsec event data
 */
static void ipsec_event_destroy(ipsec_event_t *event)
{
	event->dst->destroy(event->dst);
	free(event);
}

/**
 * Dequeue events and relay them to listeners
 */
static job_requeue_t handle_events(private_ipsec_event_relay_t *this)
{
	enumerator_t *enumerator;
	ipsec_event_listener_t *current;
	ipsec_event_t *event;

	event = this->queue->dequeue(this->queue);

	this->lock->read_lock(this->lock);
	enumerator = this->listeners->create_enumerator(this->listeners);
	while (enumerator->enumerate(enumerator, (void**)&current))
	{
		switch (event->type)
		{
			case IPSEC_EVENT_EXPIRE:
				if (current->expire)
				{
					current->expire(event->protocol, event->spi, event->dst,
									event->data.expire.hard);
				}
				break;
		}
	}
	enumerator->destroy(enumerator);
	this->lock->unlock(this->lock);
	ipsec_event_destroy(event);
	return JOB_REQUEUE_DIRECT;
}

METHOD(ipsec_event_relay_t, expire, void,
	private_ipsec_event_relay_t *this, uint8_t protocol, uint32_t spi,
	host_t *dst, bool hard)
{
	ipsec_event_t *event;

	INIT(event,
		.type = IPSEC_EVENT_EXPIRE,
		.protocol = protocol,
		.spi = spi,
		.dst = dst->clone(dst),
		.data = {
			.expire = {
				.hard = hard,
			},
		},
	);
	this->queue->enqueue(this->queue, event);
}

METHOD(ipsec_event_relay_t, register_listener, void,
	private_ipsec_event_relay_t *this, ipsec_event_listener_t *listener)
{
	this->lock->write_lock(this->lock);
	this->listeners->insert_last(this->listeners, listener);
	this->lock->unlock(this->lock);
}

METHOD(ipsec_event_relay_t, unregister_listener, void,
	private_ipsec_event_relay_t *this, ipsec_event_listener_t *listener)
{
	this->lock->write_lock(this->lock);
	this->listeners->remove(this->listeners, listener, NULL);
	this->lock->unlock(this->lock);
}

METHOD(ipsec_event_relay_t, destroy, void,
	private_ipsec_event_relay_t *this)
{
	this->queue->destroy_function(this->queue, free);
	this->listeners->destroy(this->listeners);
	this->lock->destroy(this->lock);
	free(this);
}

/**
 * Described in header.
 */
ipsec_event_relay_t *ipsec_event_relay_create()
{
	private_ipsec_event_relay_t *this;

	INIT(this,
		.public = {
			.expire = _expire,
			.register_listener = _register_listener,
			.unregister_listener = _unregister_listener,
			.destroy = _destroy,
		},
		.listeners = linked_list_create(),
		.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
		.queue = blocking_queue_create(),
	);

	lib->processor->queue_job(lib->processor,
		(job_t*)callback_job_create((callback_job_cb_t)handle_events, this,
			NULL, (callback_job_cancel_t)return_false));

	return &this->public;
}
