/*
 * Copyright (c) 2008 Telappliant Ltd.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 */

#include "cleaner.h"
#include "debug.h"

#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <assert.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <poll.h>
#include <dirent.h>

/**
 * Add all existing files in the directory to our list
 */
static void
cleaner_addexisting( struct sescle_cleaner* cleaner )
{
	DIR*	dir;
	struct	dirent*	entry;
	struct	stat	entry_stat;

	dir = opendir(cleaner->cfg->save_path);
	assert( dir != NULL );

	entry = readdir(dir);
	while( entry != NULL )
	{
		if( strncmp(entry->d_name, "sess_", 5) != 0 )
		{
			entry = readdir(dir);
			continue;
		}

		stat( entry->d_name, &entry_stat );
		tol_add( cleaner->list, entry->d_name, entry_stat.st_mtime + cleaner->cfg->gc_maxlifetime );
		inotify_add_watch( cleaner->inotify_fd, entry->d_name, IN_MODIFY | IN_CLOSE_WRITE | IN_DELETE );

		entry = readdir(dir);
	}

	closedir(dir);
}

struct sescle_cleaner*
cleaner_new( struct sescle_config* cfg )
{
	struct sescle_cleaner* cleaner;
	char	dir_tmp[PATH_MAX+1];
	char*	dir;
	int	notify_fd;
	int	directory_wd;

	assert( cfg != NULL );
	assert( cfg->save_path != NULL );

	// Initialize all safe stuff before allocating memory
	dir = realpath(cfg->save_path, dir_tmp);
	assert( dir != NULL );
	if( dir == NULL )
	{
		return NULL;
	}

	notify_fd = inotify_init();
	assert( notify_fd != -1 );
	if( notify_fd == -1 )
	{
		return NULL;
	}

	directory_wd = inotify_add_watch( notify_fd, dir, IN_MODIFY | IN_CREATE | IN_DELETE );
	assert( directory_wd  != -1 );
	if( directory_wd == -1 )
	{
		close(notify_fd);
		return NULL;
	}

	// Finally :)
	cleaner = malloc(sizeof(struct sescle_cleaner));
	cleaner->cfg = cfg;
	cleaner->directory = strdup(dir);
	cleaner->directory_wd = directory_wd;
	cleaner->inotify_fd = notify_fd;
	cleaner->list = tol_new();

	assert( cleaner->list != NULL );

	// Add existing items
	cleaner_addexisting( cleaner );

	return cleaner;
}

void
cleaner_delete( struct sescle_cleaner* cleaner )
{
	struct tol_entry* entry;
	struct tol_entry* next;
	
	// Clean up all remaining entries
	entry = tol_delete( cleaner->list );
	while( entry != NULL )
	{
		next = entry->next;
		tol_delete_entry(entry);
		entry = next;
	}

	assert( close( cleaner->inotify_fd ) == 0 );
	free( cleaner->directory );
	free( cleaner );

}

/**
 * Handle a single event
 *
 * @param cleaner Cleaner instance
 * @param evt inotify event
 * @param current_time Current time
 *
 * @return -1 on error (must shutdown), > 0 on success
 */
static int
cleaner_handle( struct sescle_cleaner* cleaner, struct inotify_event* evt, time_t current_time )
{
	struct tol_entry* entry;

	// Ignore events that come from files not beginning with "sess_"
	if( strncmp(evt->name, "sess_", 5) != 0 )
	{
		return 0;
	}

	// New session file created - add it for tracking
	if( evt->mask & IN_CREATE )
	{
		if( evt->wd == cleaner->directory_wd )
		{
			tol_add( cleaner->list, evt->name, current_time + cleaner->cfg->gc_maxlifetime);
			inotify_add_watch(cleaner->inotify_fd, evt->name, IN_MODIFY | IN_CLOSE_WRITE | IN_DELETE);
			return 1;
		}
		return 0;
	}

	if( evt->mask & IN_MODIFY || evt->mask & IN_CLOSE_WRITE )
	{
		if( evt->wd != cleaner->directory_wd )
		{
			tol_modify( cleaner->list, evt->name, current_time + cleaner->cfg->gc_maxlifetime);
			return 1;
		}
		return 0;
	}

	// Session file deleted - remove from list
	if( evt->mask & IN_DELETE )
	{
		if( evt->wd != cleaner->directory_wd )
		{
			entry = tol_search( cleaner->list, evt->name );
			if( entry != NULL )
			{
				tol_remove_entry( cleaner->list, entry );
				tol_delete_entry( entry );
				return 1;
			}
		}
	}

	if( evt->mask & IN_DELETE_SELF )
	{
		return -1;
	}

	return 0;
}

/**
 * Expire entries after the current time
 */
static void
cleaner_expire( struct sescle_cleaner* cleaner, time_t current_time )
{
	struct	tol_entry* entry;
	struct	tol_entry* entry_next;

	assert( cleaner != NULL );

	// Finally clean up expired events
	entry = tol_expire( cleaner->list, current_time );
	while( entry != NULL )
	{
		assert( unlink(entry->name) == 0 );

		entry_next = entry->next;
		tol_delete_entry( entry );
		entry = entry_next;
	}
}

/**
 * Read a single event from the inotify socket and pass it to the handler
 */
static void
cleaner_readevent( struct sescle_cleaner* cleaner, time_t current_time )
{
	char	buffer[PATH_MAX+sizeof(struct inotify_event*)];
	struct	inotify_event* event;
	ssize_t	tmp;

	assert( cleaner != NULL );

	tmp = read(cleaner->inotify_fd, buffer, sizeof(buffer));
	assert( tmp != -1 );

	event = (struct inotify_event*)&buffer[0];
	assert( event != NULL );

	cleaner_handle(cleaner, event, current_time);
}

int
cleaner_run( struct sescle_cleaner* cleaner, time_t wait )
{
	time_t	current_time;
	struct	pollfd inotify_pollfd;
	int	poll_status;
	int	ret_value;

	assert( cleaner != NULL );

	// Setup poll
	inotify_pollfd.fd = cleaner->inotify_fd;
	inotify_pollfd.events = POLLIN;
	inotify_pollfd.revents = 0;

	// Poll the FD waiting for an event
	poll_status = poll( &inotify_pollfd, 1, 1000 );
	current_time = time(NULL);

	if( poll_status == 1 )
	{
		cleaner_readevent( cleaner, current_time );
		ret_value = 0;
	} else if( poll_status == -1 )
	{
		return -1;
	}
	
	cleaner_expire( cleaner, current_time );
	
	return ret_value;
}


