#include <string.h>
#include <limits.h>
#include <time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <glib.h>
#include "edvtypes.h"
#include "../edvobj.h"
#include "../edvrecbin.h"
#include "../edvrecbinfio.h"
#include "edvnotify.h"
#include "edvrecycle.h"
#include "edvutils.h"
#include "config.h"


const gchar *EDVRecycleGetError(edv_context_struct *ctx);

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

gint EDVRecycledObjectStat(
	edv_context_struct *ctx, 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,
	gboolean notify,
	gint (*progress_cb)(
		gpointer client_data, gulong pos, gulong total
	),
	gpointer client_data
);

gint EDVRecover(
	edv_context_struct *ctx, guint index, const gchar *path,
	gboolean notify,
	gint (*progress_cb)(
		gpointer client_data, gulong pos, gulong total
	),
	gpointer client_data
);

gint EDVPurge(
	edv_context_struct *ctx, guint index,
	gboolean notify,
	gint (*progress_cb)(
		gpointer client_data, gulong pos, gulong total
	),
	gpointer client_data
);


#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().  Can return NULL if there was no error.
 */
const gchar *EDVRecycleGetError(edv_context_struct *ctx)
{
	return((ctx != NULL) ? EDVRecBinFIOGetError() : NULL);
}

/*
 *	Coppies values from the recycled objects structure to the
 *	struct stat stat_buf structure.
 */
static void EDVCopyRecycledObjectToStatBuf(
	struct stat *stat_buf,
	const edv_recbin_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_FILE:
		TAR->st_mode |= S_IFREG;
		break;
	      case EDV_OBJECT_TYPE_DIRECTORY:
		TAR->st_mode |= S_IFDIR;
		break;
	      case EDV_OBJECT_TYPE_LINK:
		TAR->st_mode |= S_IFLNK;
		break;
	      case EDV_OBJECT_TYPE_DEVICE_BLOCK:
		TAR->st_mode |= S_IFBLK;
		break;
	      case EDV_OBJECT_TYPE_DEVICE_CHARACTER:
		TAR->st_mode |= S_IFCHR;
		break;
	      case EDV_OBJECT_TYPE_FIFO:
		TAR->st_mode |= S_IFIFO;
		break;
	      case EDV_OBJECT_TYPE_SOCKET:
		TAR->st_mode |= S_IFSOCK;
		break;
	    }

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

	    /* 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;

	    /* Owner and group ids */
	    TAR->st_uid = (uid_t)SRC->owner_id;
	    TAR->st_gid = (gid_t)SRC->group_id;

	    TAR->st_size = (time_t)SRC->size;

#undef TAR
#undef SRC
	}
}

/*
 *	Get statistics of a recycled object by its index.
 *
 *	If the path_rtn is not NULL then an allocated string containing
 *	the full path of the original object will be returned.  The
 *	calling function must deallocate this string.
 *
 *	Inputs path_rtn and stat_buf may be NULL, indicating that the
 *	return is not wanted.
 *
 *	Returns 0 on success or -1 on error.
 */
gint EDVRecycledObjectStat(
	edv_context_struct *ctx, guint index,
	gchar **path_rtn,
	struct stat *stat_buf
)
{
	edv_recbin_object_struct *obj;
	const gchar *recycled_index_file = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;

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

	if(STRISEMPTY(recycled_index_file))
	    return(-1);

	/* Get recycled object statistics */
	obj = EDVRecBinObjectStat(recycled_index_file, index);
	if(obj == NULL)
	    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);
	}

	EDVRecBinObjectDelete(obj);

	return(0);
}

/*
 *	Same as EDVRecycledObjectStat() except that a list of all
 *	recycled objects are returned.  The returned pointer arrays
 *	and each pointed to location must be deallocated.by the calling
 *	function.
 *
 *	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_recbin_object_struct *obj;
	edv_recbin_index_struct *rbi_ptr;
	const gchar *recycled_index_file = (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(STRISEMPTY(recycled_index_file) || (total == NULL))
	    return(-1);

	/* Open recycled objects index file for subsequent reading
	 * of each recycled object's stats
	 */
	rbi_ptr = EDVRecBinIndexOpen(recycled_index_file);
	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,
	gboolean notify,
	gint (*progress_cb)(
		gpointer client_data, gulong pos, gulong total
	),
	gpointer client_data
)
{
	const gchar *s;
	gchar *dpath, cwd[PATH_MAX];
	guint index;
	gulong date_deleted;
	mode_t m;
	edv_recbin_object_struct *obj;
	struct stat lstat_buf;
	const gchar *recycled_index_file = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;
	if(STRISEMPTY(recycled_index_file) || STRISEMPTY(path))
	    return(0);

	dpath = EDVCopyEvaluateInputPath(
	    getcwd(cwd, sizeof(cwd)), path
	);
	date_deleted = (gulong)time(NULL);

	/* Check if local object exists and get the object's local
	 * stats
	 */
	if(lstat(dpath, &lstat_buf))
	{
	    g_free(dpath);
	    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 = EDVRecBinObjectNew();
	if(TAR == NULL)
	{                           
	    g_free(dpath);
	    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->date_deleted = date_deleted;

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

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

	/* 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

	/* Record this object to recycled objects index file */
	index = EDVRecBinIndexAdd(
	     recycled_index_file, obj
	);
	if(index > 0)
	{
	    /* Remove actual object and place it into the recycled
	     * objects directory
	     */
	    if(EDVRecBinDiskObjectDelete(
		recycled_index_file,
		index,
		dpath,
		progress_cb, client_data
	    ))
	    {
		/* Error recycling object */
		index = 0;
	    }
	    else if(notify)
	    {
		/* Success, notify about object being removed */
		EDVNotifyQueueObjectRemoved(ctx, dpath);
		EDVNotifyQueueRecycledObjectAdded(ctx, index);
	    }
	}

	EDVRecBinObjectDelete(obj);
	g_free(dpath);

	return(index);
}

/*
 *	Recovers an object from the recycled objects directory.
 *
 *	If path is not NULL, then path will be used as the alternate
 *	location to recover the object.  It is recommended that you
 *	always give a path to recover the object.  The object that path
 *	reffers to may not already exist.
 *
 *	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, guint index, const gchar *path,
	gboolean notify,
	gint (*progress_cb)(
		gpointer client_data, gulong pos, gulong total
	),
	gpointer client_data
)
{
	gint status;
	gchar *dpath, cwd[PATH_MAX];
	const gchar *recycled_index_file = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;
	if(STRISEMPTY(recycled_index_file))
	    return(-1);

	dpath = EDVCopyEvaluateInputPath(
	    getcwd(cwd, PATH_MAX), path
	);

	/* Recover object */
	status = EDVRecBinDiskObjectRecover(
	    recycled_index_file,
	    index,
	    dpath,
	    progress_cb, client_data
	);
	if(status)
	{
	    /* Error */
	}
	else
	{
	    /* Success, now remove the recycled object entry from the
	     * recycled objects index file
	     */
	    EDVRecBinIndexRemove(
		recycled_index_file, index
	    );

	    /* Need notify? */
	    if(notify)
	    {
		EDVNotifyQueueObjectAdded(ctx, dpath);
		EDVNotifyQueueRecycledObjectRemoved(ctx, index);
	    }
	}

	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, guint index,
	gboolean notify,
	gint (*progress_cb)(
		gpointer client_data, gulong pos, gulong total
	),
	gpointer client_data
)
{
	gint status;
	const gchar *recycled_index_file = (ctx != NULL) ?
	    ctx->recycled_index_file : NULL;
	if(STRISEMPTY(recycled_index_file))
	    return(-1);

	/* Remove recycled object from the recycled objects directory */
	status = EDVRecBinDiskObjectPurge(
	    recycled_index_file,
	    index,
	    progress_cb, client_data
	);
	if(status)
	{
	    /* Error */
	}
	else
	{
	    /* Success, now remove the recycled object entry from the
	     * recycled objects index file
	     */
	    EDVRecBinIndexRemove(
		recycled_index_file, index
	    );

	    /* Need notify? */
	    if(notify)
	    {
		EDVNotifyQueueRecycledObjectRemoved(ctx, index);
	    }
	}

	return(status);
}
