#include <string.h>
#include <sys/types.h>
#include <limits.h>
#include <time.h>
#include <sys/stat.h>
#include <glib.h>
#include <unistd.h>

#include "edv_types.h"
#include "../edv_recycled_obj.h"
#include "../edv_recbin_index.h"
#include "../edv_recbin_stat.h"
#include "edv_notify.h"
#include "edv_recycle.h"
#include "edv_utils.h"
#include "config.h"


const gchar *EDVRecycleGetError(edv_context_struct *ctx);

static void EDVCopyRecycledObjectToStatBuf(
	struct stat *stat_buf,
	const edv_recycled_object_struct *obj
);

gint EDVRecycledObjectStat(
	edv_context_struct *ctx, const guint index,
	gchar **path_rtn,
	struct stat *stat_buf
);
gint EDVRecycledObjectStatAll(
	edv_context_struct *ctx,
	gchar ***path_rtn,
	guint **index_rtn,
	struct stat ***stat_buf,
	gint *total
);

guint EDVRecycle(
	edv_context_struct *ctx, const gchar *path,
	const gboolean notify,
	gint (*progress_cb)(
		gpointer data, const gulong i, const gulong m
	),
	gpointer data
);

gint EDVRecover(
	edv_context_struct *ctx, const guint index,
	const gchar *alternate_recovery_path,
	const gboolean notify,
	gint (*progress_cb)(
		gpointer data, const gulong i, const gulong m
	),
	gpointer data
);

gint EDVPurge(
	edv_context_struct *ctx, const guint index,
	const gboolean notify,
	gint (*progress_cb)(
		gpointer data, const gulong i, const gulong m
	),
	gpointer data
);


static const gchar *last_error = NULL;


#define ATOI(s)         (((s) != NULL) ? atoi(s) : 0)
#define ATOL(s)         (((s) != NULL) ? atol(s) : 0)
#define ATOF(s)         (((s) != NULL) ? atof(s) : 0.0f)
#define STRDUP(s)       (((s) != NULL) ? g_strdup(s) : NULL)

#define MAX(a,b)        (((a) > (b)) ? (a) : (b))
#define MIN(a,b)        (((a) < (b)) ? (a) : (b))
#define CLIP(a,l,h)     (MIN(MAX((a),(l)),(h)))
#define STRLEN(s)       (((s) != NULL) ? strlen(s) : 0)
#define STRISEMPTY(s)   (((s) != NULL) ? (*(s) == '\0') : TRUE)



/*
 *	Returns a statically allocated string describing the last
 *	error that occured when calling EDVRecycle(), EDVRecover(),
 *	or EDVPurge(), or NULL if there was no error.
 */
const gchar *EDVRecycleGetError(edv_context_struct *ctx)
{
	return((ctx != NULL) ? last_error : NULL);
}

/*
 *	Coppies values from the recycled object structure to the
 *	struct stat stat_buf structure.
 *
 *	The struct stat members st_rdev, st_blksize, and st_blocks
 *	will not be set.
 */
static void EDVCopyRecycledObjectToStatBuf(
	struct stat *stat_buf,
	const edv_recycled_object_struct *obj
)
{
	if((stat_buf == NULL) || (obj == NULL))
	    return;

	if(TRUE)
	{
#define TAR	stat_buf
#define SRC	obj
	    /* Begin setting mode */
	    TAR->st_mode = 0;

	    /* Object Type */
	    switch(SRC->type)
	    {
	      case EDV_OBJECT_TYPE_UNKNOWN:
	      case EDV_OBJECT_TYPE_ERROR:
		break;
	      case EDV_OBJECT_TYPE_FILE:
#ifdef S_IFREG
		TAR->st_mode |= S_IFREG;
#endif
		break;
	      case EDV_OBJECT_TYPE_DIRECTORY:
#ifdef S_IFDIR
		TAR->st_mode |= S_IFDIR;
#endif
		break;
	      case EDV_OBJECT_TYPE_LINK:
#ifdef S_IFLNK
		TAR->st_mode |= S_IFLNK;
#endif
		break;
	      case EDV_OBJECT_TYPE_FIFO:
#if defined(S_IFFIFO)
		TAR->st_mode |= S_IFFIFO;
#elif defined(S_IFIFO)
		TAR->st_mode |= S_IFIFO;
#endif
		break;
	      case EDV_OBJECT_TYPE_DEVICE_BLOCK:
#ifdef S_IFBLK
		TAR->st_mode |= S_IFBLK;
#endif
		break;
	      case EDV_OBJECT_TYPE_DEVICE_CHARACTER:
#ifdef S_IFCHR
		TAR->st_mode |= S_IFCHR;
#endif
		break;
	      case EDV_OBJECT_TYPE_SOCKET:
#ifdef S_IFSOCK
		TAR->st_mode |= S_IFSOCK;
#endif
		break;
	    }

	    /* Permissions */
#ifdef S_IXUSR
	    if(SRC->permissions & EDV_PERMISSION_UEXECUTE)
		TAR->st_mode |= S_IXUSR;
#endif
#ifdef S_IRUSR
	    if(SRC->permissions & EDV_PERMISSION_UREAD)
		TAR->st_mode |= S_IRUSR;
#endif
#ifdef S_IWUSR
	    if(SRC->permissions & EDV_PERMISSION_UWRITE)
		TAR->st_mode |= S_IWUSR;
#endif
#ifdef S_IXGRP
	    if(SRC->permissions & EDV_PERMISSION_GEXECUTE)
		TAR->st_mode |= S_IXGRP;
#endif
#ifdef S_IRGRP
	    if(SRC->permissions & EDV_PERMISSION_GREAD)
		TAR->st_mode |= S_IRGRP;
#endif
#ifdef S_IWGRP
	    if(SRC->permissions & EDV_PERMISSION_GWRITE)
		TAR->st_mode |= S_IWGRP;
#endif
#ifdef S_IXOTH
	    if(SRC->permissions & EDV_PERMISSION_AEXECUTE)
		TAR->st_mode |= S_IXOTH;
#endif
#ifdef S_IROTH
	    if(SRC->permissions & EDV_PERMISSION_AREAD)
		TAR->st_mode |= S_IROTH;
#endif
#ifdef S_IWOTH
	    if(SRC->permissions & EDV_PERMISSION_AWRITE)
		TAR->st_mode |= S_IWOTH;
#endif
#ifdef S_ISUID
	    if(SRC->permissions & EDV_PERMISSION_SETUID)
		TAR->st_mode |= S_ISUID;
#endif
#ifdef S_ISGID
	    if(SRC->permissions & EDV_PERMISSION_SETGID)
		TAR->st_mode |= S_ISGID;
#endif
#ifdef S_ISVTX
	    if(SRC->permissions & EDV_PERMISSION_STICKY)
		TAR->st_mode |= S_ISVTX;
#endif

	    /* Time Stamps */
	    TAR->st_atime = (time_t)SRC->access_time;
	    TAR->st_mtime = (time_t)SRC->modify_time;
	    TAR->st_ctime = (time_t)SRC->change_time;

	    /* Ownership */
	    TAR->st_uid = (uid_t)SRC->owner_id;
	    TAR->st_gid = (gid_t)SRC->group_id;

	    /* Size */
	    TAR->st_size = (time_t)SRC->size;

#undef TAR
#undef SRC
	}
}

/*
 *	Get statistics of a recycled object by its index.
 *
 *	The path_rtn specifies the pointer to the return full path
 *	describing the original location of the object. You may pass
 *	NULL if the return value is not requested.
 *
 *	The stat_buf specifies the pointer to the return struct stat
 *	describing the original object's statistics. You may pass
 *	NULL if the return value is not requested.
 *
 *	The struct stat members st_rdev, st_blksize, and st_blocks
 *	members will be undefined.
 *
 *	Returns 0 on success or non-zero on error.
 */
gint EDVRecycledObjectStat(
	edv_context_struct *ctx, const guint index,
	gchar **path_rtn,
	struct stat *stat_buf
)
{
	edv_recycled_object_struct *obj;
	const gchar *index_path = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;

	last_error = NULL;

	if(path_rtn != NULL)
	    *path_rtn = NULL;
	if(stat_buf != NULL)
	    memset(stat_buf, 0x00, sizeof(stat_buf));

	if(STRISEMPTY(index_path))
	{
	    last_error = "Bad value";
	    return(-1);
	}

	/* Get recycled object statistics */
	obj = EDVRecBinObjectStat(index_path, index);
	if(obj == NULL)
	{
	    last_error = "Unable to obtain the recycled object's statistics";
	    return(-1);
	}

	/* Transfer values from recycled object structure to path_rtn
	 * and stat_buf
	 */
	if(path_rtn != NULL)
	{
	    /* Allocate a string containing the full path to the
	     * original object. Note that original_path only contains
	     * the original location and no the original object's name
	     * so original_path and name need to be put togeather
	     */
	    *path_rtn = g_strdup_printf(
		"%s%c%s",
		obj->original_path,
		G_DIR_SEPARATOR,
		obj->name
	    );
	}
	if(stat_buf != NULL)
	{
	    /* Copy recycled object structure values to stat_buf */
	    EDVCopyRecycledObjectToStatBuf(stat_buf, obj);
	}

	EDVRecycledObjectDelete(obj);

	return(0);
}

/*
 *	Gets a listing of all recycled objects.
 *
 *	The path_rtn specifies the pointer to the return list for full
 *	paths to the original object. You may pass NULL if the return
 *	value is not requested.
 *
 *	The index_rtn specifies the pointer to the return list of
 *	recycled object indices. You may pass NULL if the return value
 *	is not requested.
 *
 *	The stat_buf specifies the pointer to the return list of
 *	recycled object stats. You may pass NULL if the return value  
 *	is not requested.
 *
 *	The returned pointer arrays and each pointer to location must
 *	be deleted by the calling function.
 *
 *	The struct stat members st_rdev, st_blksize, and st_blocks
 *	members will be undefined.
 *
 *	Returns 0 on success or non-zero on error.
 */
gint EDVRecycledObjectStatAll(
	edv_context_struct *ctx,
	gchar ***path_rtn,
	guint **index_rtn,
	struct stat ***stat_buf,
	gint *total
)
{
	gint i;
	struct stat *stat_buf_ptr;
	const edv_recycled_object_struct *obj;
	edv_recbin_index_struct *rbi_ptr;
	const gchar *index_path = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;

	if(path_rtn != NULL)
	    *path_rtn = NULL;
	if(index_rtn != NULL)
	    *index_rtn = NULL;
	if(stat_buf != NULL)
	    *stat_buf = NULL;

	if(total != NULL)
	    *total = 0;

	if(STRISEMPTY(index_path) || (total == NULL))
	    return(-1);

	/* Open recycled objects index file for subsequent reading
	 * of each recycled object's stats
	 */
	rbi_ptr = EDVRecBinIndexOpen(index_path);
	if(rbi_ptr == NULL)
	    return(-1);

	/* Read each recycled object */
	while(!EDVRecBinIndexNext(rbi_ptr))
	{
	    obj = rbi_ptr->obj;
	    if(obj != NULL)
	    {
		/* Increase total */
		i = MAX(*total, 0);
		*total = i + 1;

		/* Append this object's path to the path return list */
		if(path_rtn != NULL)
		{
		    *path_rtn = (gchar **)g_realloc(
			*path_rtn,
			(*total) * sizeof(gchar *)
		    );
		    if(*path_rtn == NULL)
		    {
			*total = 0;
			break;
		    }
		    (*path_rtn)[i] = g_strdup_printf(
			"%s%c%s",
			obj->original_path,
			G_DIR_SEPARATOR,
			obj->name
		    );
		}

		/* Append this object's index to the index return list */
		if(index_rtn != NULL)
		{
		    *index_rtn = (guint *)g_realloc(
			*index_rtn,
			(*total) * sizeof(guint)
		    );
		    if(*index_rtn == NULL)
		    {
			*total = 0;
			break;
		    }
		    (*index_rtn)[i] = obj->index;
		}

		/* Append this object's stats to the stats return list */
		if(stat_buf != NULL)
		{
		    *stat_buf = (struct stat **)g_realloc(
			*stat_buf,
			(*total) * sizeof(struct stat *)
		    );
		    if(*stat_buf == NULL)
		    {
			*total = 0;
			break;
		    }
		    (*stat_buf)[i] = stat_buf_ptr = g_malloc0(sizeof(struct stat));
		    EDVCopyRecycledObjectToStatBuf(stat_buf_ptr, obj);
		}
	    }
	}	/* Read each recycled object */

	EDVRecBinIndexClose(rbi_ptr);

	return(0);
}

/*
 *	Deletes an actual disk object and places it into the recycle
 *	bin.
 *
 *	If notify is set to TRUE then a "object_removed_notify" and
 *	"recycled_object_added_notify" will be queued on the ctx
 *	(only on success).
 *
 *	Returns the recycled object index number or 0 on error.
 */
guint EDVRecycle(
	edv_context_struct *ctx, const gchar *path,
	const gboolean notify,
	gint (*progress_cb)(
		gpointer data, const gulong i, const gulong m
	),
	gpointer data
)
{
	const gchar *s;
	gchar *cwd, *dpath;
	guint index;
	gulong deleted_time;
	mode_t m;
	edv_recycled_object_struct *obj;
	struct stat lstat_buf;
	const gchar *index_path = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;

	last_error = NULL;

	if(STRISEMPTY(index_path) || STRISEMPTY(path))
	{
	    last_error = "Bad value";
	    return(0);
	}

	cwd = EDVGetCWD();
	dpath = EDVEvaluatePath(cwd, path);
	deleted_time = (gulong)time(NULL);

	/* Get the object's local stats */
	if(lstat(dpath, &lstat_buf))
	{
	    g_free(dpath);
	    g_free(cwd);
	    last_error = "Unable to get the object's local stats";
	    return(0);
	}

	m = lstat_buf.st_mode;

#define TAR	obj
	/* Create a new recycled object and set its values based on the
	 * specified object's local stats
	 */
	TAR = EDVRecycledObjectNew();
	if(TAR == NULL)
	{                           
	    g_free(dpath);
	    g_free(cwd);
	    last_error = "Memory allocation error";
	    return(0);
	}

	/* Name */
	s = strrchr(dpath, G_DIR_SEPARATOR);
	if(s != NULL)
	    s++;
	else
	    s = dpath;
	g_free(TAR->name);
	TAR->name = STRDUP(s);

	/* Original Location (path without the object's name) */
	g_free(TAR->original_path);
	TAR->original_path = g_dirname(dpath);
	if(TAR->original_path == NULL)
	    TAR->original_path = STRDUP("/");

	/* Date Deleted */
	TAR->deleted_time = deleted_time;

	/* Type */
	if(S_ISREG(m))
	    TAR->type = EDV_OBJECT_TYPE_FILE;
#ifdef S_ISDIR
	else if(S_ISDIR(m))
	    TAR->type = EDV_OBJECT_TYPE_DIRECTORY;
#endif
#ifdef S_ISLNK
	else if(S_ISLNK(m))
	    TAR->type = EDV_OBJECT_TYPE_LINK;
#endif
#ifdef S_ISBLK
	else if(S_ISBLK(m))
	    TAR->type = EDV_OBJECT_TYPE_DEVICE_BLOCK;
#endif
#ifdef S_ISCHR
	else if(S_ISCHR(m))
	    TAR->type = EDV_OBJECT_TYPE_DEVICE_CHARACTER;
#endif
#ifdef S_ISFIFO
	else if(S_ISFIFO(m))
	    TAR->type = EDV_OBJECT_TYPE_FIFO;
#endif
#ifdef S_ISSOCK
	else if(S_ISSOCK(m))
	    TAR->type = EDV_OBJECT_TYPE_SOCKET;
#endif
	else
	    TAR->type = EDV_OBJECT_TYPE_FILE;

	/* Permissions */
	TAR->permissions = 0;
#ifdef S_IXUSR
	if(m & S_IXUSR)
	    TAR->permissions |= EDV_PERMISSION_UEXECUTE;
#endif
#ifdef S_IRUSR
	if(m & S_IRUSR)
	    TAR->permissions |= EDV_PERMISSION_UREAD;
#endif
#ifdef S_IWUSR
	if(m & S_IWUSR)
	    TAR->permissions |= EDV_PERMISSION_UWRITE;
#endif
#ifdef S_IXGRP
	if(m & S_IXGRP)
	    TAR->permissions |= EDV_PERMISSION_GEXECUTE;
#endif
#ifdef S_IRGRP
	if(m & S_IRGRP)
	    TAR->permissions |= EDV_PERMISSION_GREAD;
#endif
#ifdef S_IWGRP
	if(m & S_IWGRP)
	    TAR->permissions |= EDV_PERMISSION_GWRITE;
#endif
#ifdef S_IXOTH
	if(m & S_IXOTH)
	    TAR->permissions |= EDV_PERMISSION_AEXECUTE;
#endif
#ifdef S_IROTH
	if(m & S_IROTH)
	    TAR->permissions |= EDV_PERMISSION_AREAD;
#endif
#ifdef S_IWOTH
	if(m & S_IWOTH)
	    TAR->permissions |= EDV_PERMISSION_AWRITE;
#endif
#ifdef S_ISUID
	if(m & S_ISUID)
	    TAR->permissions |= EDV_PERMISSION_SETUID;
#endif
#ifdef permissions
	if(m & S_ISGID)
	    TAR->permissions |= EDV_PERMISSION_SETGID;
#endif
#ifdef S_ISVTX
	if(m & S_ISVTX)
	    TAR->permissions |= EDV_PERMISSION_STICKY;
#endif

	/* Time Stamps */
	TAR->access_time = (gulong)lstat_buf.st_atime;
	TAR->modify_time = (gulong)lstat_buf.st_mtime;
	TAR->change_time = (gulong)lstat_buf.st_ctime;

	/* Ownership */
	TAR->owner_id = (gint)lstat_buf.st_uid;
	TAR->group_id = (gint)lstat_buf.st_gid;

	/* Size */
	TAR->size = (gulong)lstat_buf.st_size;

#undef TAR

	/* Add this object to recycled objects index file and get
	 * the new index allocated for it
	 */
	index = EDVRecBinIndexAdd(
	     index_path, obj
	);
	if(index != 0)
	{
	    /* Recycle this object */
	    if(EDVRecBinDiskObjectDelete(
		index_path,
		index,
		dpath,
		progress_cb, data
	    ))
	    {
		/* An error occured while recycling the object */
		last_error = EDVRecBinIndexGetError();
		EDVRecBinIndexRemove(
		    index_path, index
		);
		index = 0;
	    }
	    /* Notify Endeavour about the recycled object being
	     * recycled?
	     */
	    else if(notify)
	    {
		EDVNotifyQueueObjectRemoved(ctx, dpath);
		EDVNotifyQueueRecycledObjectAdded(ctx, index);
	    }
	}
	else
	{
	    last_error = EDVRecBinIndexGetError();
	}

	EDVRecycledObjectDelete(obj);
	g_free(dpath);
	g_free(cwd);

	return(index);
}

/*
 *	Recovers an object from the recycled objects directory.
 *
 *	The index specifies the index of the recycled object to
 *	recover.
 *
 *	The alternate_recovery_path specifies the full path to the
 *	directory that is to be used as the alternate recovery location
 *	for the recycled object. If alternate_recovery_path is NULL
 *	then the recycled object's original location will be used
 *	as the recovery location.
 *
 *	If notify is set to TRUE then a "object_added_notify" and
 *	"recycled_object_removed_notify" will be queued on the ctx
 *	(only on success).
 *
 *	Returns non-zero on error.
 */
gint EDVRecover(
	edv_context_struct *ctx, const guint index,
	const gchar *alternate_recovery_path,
	const gboolean notify,
	gint (*progress_cb)(
		gpointer data, const gulong i, const gulong m
	),
	gpointer data
)
{
	gint status;
	gchar *dpath, *original_path;
	const gchar *index_path = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;

	last_error = NULL;

	if(STRISEMPTY(index_path))
	{
	    last_error = "Bad value";
	    return(-1);
	}

	if(EDVRecycledObjectStat(
	    ctx, index, &original_path, NULL
	))
	    return(-1);

	/* Alternate recovery path specified? */
	if(alternate_recovery_path != NULL)
	{
	    gchar	*cwd = EDVGetCWD(),
			*parent = EDVEvaluatePath(cwd, alternate_recovery_path);
	    if(parent == NULL)
	    {
		last_error = "Unable to evaluate the alternate recovery location";
		g_free(cwd);
		return(-1);
	    }
	    dpath = g_strconcat(
		parent,
		G_DIR_SEPARATOR_S,
		g_basename(original_path),
		NULL
	    );
	    g_free(cwd);
	    g_free(parent);
	}
	else
	{
	    /* Use the original location and name */
	    dpath = NULL;
	}

	/* Recover the recycled object */
	status = EDVRecBinDiskObjectRecover(
	    index_path,
	    index,		/* Index of the recycled object to recover */
	    dpath,		/* Full path to the recovered object */
	    progress_cb, data
	);
	if(status != 0)
	{
	    /* An error occured while recovering the object */
	    last_error = EDVRecBinIndexGetError();
	}
	else
	{
	    /* Success, now remove the recycled object entry from the
	     * recycled objects index file
	     */
	    EDVRecBinIndexRemove(
		index_path, index
	    );

	    /* Notify Endeavour about the recycled object being
	     * recovered?
	     */
	    if(notify)
	    {
		const gchar *new_path = (dpath != NULL) ? dpath : original_path;
		EDVNotifyQueueObjectAdded(ctx, new_path);
		EDVNotifyQueueRecycledObjectRemoved(ctx, index);
	    }
	}

	g_free(original_path);
	g_free(dpath);

	return(status);
}

/*
 *	Permanently removes an object from the recycled objects directory.
 *
 *	If notify is set to TRUE then a "recycled_object_removed_notify"
 *	will be queued on the ctx (only on success).
 *
 *	Returns non-zero on error.
 */
gint EDVPurge(
	edv_context_struct *ctx, const guint index,
	const gboolean notify,
	gint (*progress_cb)(
		gpointer data, const gulong i, const gulong m
	),
	gpointer data
)
{
	gint status;
	const gchar *index_path = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;

	last_error = NULL;

	if(STRISEMPTY(index_path))
	{
	    last_error = "Bad value";
	    return(-1);
	}

	/* Purge the recycled object */
	status = EDVRecBinDiskObjectPurge(
	    index_path,
	    index,
	    progress_cb, data
	);
	if(status)
	{
	    /* An error occured while purging the object */
	    last_error = EDVRecBinIndexGetError();
	}
	else
	{
	    /* Success, now remove the recycled object entry from the
	     * recycled objects index file
	     */
	    EDVRecBinIndexRemove(
		index_path, index
	    );

	    /* Notify Endeavour about the recycled object being
	     * purged?
	     */
	    if(notify)
		EDVNotifyQueueRecycledObjectRemoved(ctx, index);
	}

	return(status);
}
