/*
 *	Recovers recycled objects from the recycle bin.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <glib.h>
#include <endeavour2.h>
#include <unistd.h>

#include "../config.h"


static void print_help(const gchar *prog_name);

static gint recover_rmkdir(edv_context_struct *ctx, const gchar *path);

static gboolean recover_query_user_recover_object(
	const gchar *name, const guint index
);

static gint recover_progress_cb(
	gpointer data, const gulong i, const gulong m
);


#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)


/*
 *      Prints the help message.
 */
static void print_help(const gchar *prog_name)
{
	g_print(
"Usage: %s <index(ies)...> [alternate_recovery_path] [options]\n",
	    prog_name
	);
	g_print("\n\
    The <index(ies)...> specifies the index(ies) of one or more\n\
    recycled object(s) to recover. You may need to enclose each\n\
    index in double quotes or prefix it with a backslash in order\n\
    for the shell to process it correctly (e.g. \"#12345\" or \\#12345).\n\
\n\
    The [alternate_recovery_path] specifies the path to the\n\
    directory to be used as the alternate recovery location. If this\n\
    is not specified then each recycled object's original location\n\
    will be used as the recovery location.\n\
\n\
    The [options] can be any of the following:\n\
\n\
	--interactive           Prompt before recovering an object.\n\
	-i                      Same as --interactive.\n\
	--quiet                 Do not print any messages to stdout.\n\
	-q                      Same as --quiet.\n\
	--help                  Prints (this) help screen and exits.\n\
	--version               Prints version information and exits.\n\
\n\
    Return values:\n\
\n\
	0       Success.\n\
	1       General error.\n\
	2       Invalid value.\n\
	3       Systems error or memory allocation error.\n\
	4       User aborted.\n\
\n\
    To list recycled objects, use \"rls\".\n\
    To recycle objects, use \"recycle\".\n\
    To purge recycled objects, use \"purge\".\n\
\n"
	);
}


/*
 *	Creates the directory and all required parent directories.
 */
static gint recover_rmkdir(edv_context_struct *ctx, const gchar *path)
{
	gboolean need_confirm = TRUE;
	const gchar *path_start;
	gchar **path_compoents;

	if(path == NULL)
	{
	    errno = EINVAL;
	    return(2);
	}

#if defined(_WIN32)
	path_start = strchr(path, G_DIR_SEPARATOR);
	if(path_start != NULL)
	    path_start++;
	else
	    path_start = path;
#else
	path_start = strchr(path, G_DIR_SEPARATOR);
	if(path_start != NULL)
	    path_start++;
	else
	    path_start = path;
#endif

	path_compoents = g_strsplit(
	    path_start, G_DIR_SEPARATOR_S, -1
	);
	if(path_compoents != NULL)
	{
	    struct stat stat_buf;
	    gint i;
	    gchar *s, *cur_path = STRDUP("");

	    for(i = 0; path_compoents[i] != NULL; i++)
	    {
		s = g_strconcat(
		    cur_path, G_DIR_SEPARATOR_S, path_compoents[i], NULL
		);
		g_free(cur_path);
		cur_path = s;

		/* Does this directory not exist? */
		if(stat(cur_path, &stat_buf))
		{
		    const gint error_code = (gint)errno;;
		    if(error_code == ENOENT)
		    {
			const mode_t m = umask(0);
			umask(m);

			/* Confirm create directory? */
			if(need_confirm)
			{
			    gint c;
			    g_print(
				"Create directory `%s'? ",
				path
			    );
			    c = (gint)fgetc(stdin);
			    while(fgetc(stdin) != '\n');
			    if((c != 'y') && (c != 'Y'))
			    {
				g_free(cur_path);
				g_strfreev(path_compoents);
				errno = EINTR;
				return(4);
			    }
			    need_confirm = FALSE;
			}
			/* Create this directory */
			if(mkdir(
			    cur_path,
			    (~m) &
			    (S_IRUSR | S_IWUSR | S_IXUSR |
			     S_IRGRP | S_IWGRP | S_IXGRP |
			     S_IROTH | S_IWOTH | S_IXOTH)
			))
 			{
			    const gint error_code = (gint)errno;
			    gchar *error_msg = STRDUP(g_strerror(error_code));
			    if(error_msg != NULL)
			    {
				*error_msg = (gchar)toupper((int)*error_msg);
				g_printerr(
"Unable to create directory `%s': %s.\n",
				    cur_path,
				    error_msg
				);
				g_free(error_msg);
			    }
			    g_free(cur_path);
			    g_strfreev(path_compoents);
			    errno = (int)error_code;
			    return(1);
			}
			else
			{
			    EDVNotifyQueueObjectAdded(ctx, cur_path);
			    EDVContextFlush(ctx);
			}
		    }
		}
	    }
	    g_free(cur_path);
	    g_strfreev(path_compoents);
	}

	return(0);
}


/*
 *	Queries the user to recover the object specified by name.
 *
 *	Returns TRUE if the user responded with yes.
 */
static gboolean recover_query_user_recover_object(
	const gchar *name, const guint index
)
{
	gint c;

	g_print(
	    "Recover `%s (#%u)'? ",
	    name, index
	);
	c = (gint)fgetc(stdin);
	while(fgetc(stdin) != '\n');
	if((c == 'y') || (c == 'Y'))
	    return(TRUE);
	else
	    return(FALSE);
}


/*
 *	Recover progress callback.
 *
 *	Prints a line stating the object being recycled and the progress
 *	bar to stdout.
 *
 *	No new line character is printed, so this prints on to the
 *	current line only.
 */
static gint recover_progress_cb(
	gpointer data, const gulong i, const gulong m
)
{
	gint _i, n, _m;
	gchar *s;
	const gchar *path = (const gchar *)data;
	gfloat coeff = (m > 0) ? ((gfloat)i / (gfloat)m) : 0.0f; 

	/* Print name */
	s = g_strdup_printf("\rRecovering %s", path);
	_i = strlen(s);                             
	printf(s);
	g_free(s);

	/* Print spacing between name and progress bar */
	n = 50;				/* Column to stop at */
	for(; _i < n; _i++)   
	    fputc(' ', stdout);

	/* Print progress bar */
	fputc('[', stdout);		/* Left start bracket */
	_m = 23;			/* Width of bar - 2 */  
	n = (int)(_m * coeff) - 1;
	for(_i = 0; _i < n; _i++) 
	    fputc('=', stdout);   
	fputc((coeff >= 1.0f) ? '=' : '>', stdout);
	_i++;
	for(; _i < _m; _i++)
	    fputc('-', stdout);
	fputc(']', stdout);		/* Right end bracket */   

	fflush(stdout);			/* Needed since no newline */

	return(0);
}


int main(int argc, char *argv[])
{
	gboolean	interactive = FALSE,
			verbose = TRUE;
	gint status = 0, recover_status;
	gchar *alternate_recovery_path = NULL;
	GList *glist, *indicies_list = NULL;
	guint index;
	gulong time_start;
	gchar *path_rtn, *parent;
	const gchar *name;
	edv_context_struct *ctx = EDVContextNew();

	EDVContextLoadConfigurationFile(ctx, NULL);

#define DO_FREE_LOCALS {		\
 g_list_free(indicies_list);		\
 indicies_list = NULL;			\
 g_free(alternate_recovery_path);	\
 alternate_recovery_path = NULL;	\
 EDVContextDelete(ctx);			\
 ctx = NULL;				\
}

	if(argc >= 2)
	{
	    gint i;
	    const gchar *arg;

	    /* Parse arguments */
	    for(i = 1; i < argc; i++)
	    {
		arg = argv[i];
		if(arg == NULL)
		    continue;

		/* Help */
		if(!g_strcasecmp(arg, "--help") ||
		   !g_strcasecmp(arg, "-help") ||
		   !g_strcasecmp(arg, "--h") ||
		   !g_strcasecmp(arg, "-h")
		)
		{
		    print_help(argv[0]);
		    DO_FREE_LOCALS
		    return(0);
		}
		/* Version */
		else if(!g_strcasecmp(arg, "--version") ||
			!g_strcasecmp(arg, "-version")
		)
		{
		    g_print(
"Endeavour Mark II Recover Version " PROG_VERSION "\n"
PROG_COPYRIGHT
		    );
		    DO_FREE_LOCALS
		    return(0);
		}
		/* Interactive */
		else if(!g_strcasecmp(arg, "--interactive") ||
			!g_strcasecmp(arg, "-i")
		)
		{
		    interactive = TRUE;
		}
		/* Quiet */
		else if(!g_strcasecmp(arg, "--quiet") ||
			!g_strcasecmp(arg, "-q")
		)
		{
		    verbose = FALSE;
		}
		/* Verbose */
		else if(!g_strcasecmp(arg, "--verbose") ||
			!g_strcasecmp(arg, "-v")
		)
		{
		    verbose = TRUE;
		}
		/* All else check if invalid option */
		else if((*arg == '-') || (*arg == '+'))
		{
		    g_printerr(
"%s: Unsupported option.\n",
			arg 
		    );     
		    DO_FREE_LOCALS
		    return(2);
		}  
	    }

	    /* Iterate through arguments and get the list of recycled
	     * object indicies and alternate recovery location
	     */
	    for(i = 1; i < argc; i++)
	    {
		arg = argv[i];
		if(arg == NULL)
		    continue;

		/* Skip options */
		if((*arg == '-') || (*arg == '+'))
		    continue;

		/* Index? */
		if(*arg == '#')
		{
		    const guint index = (guint)ATOI(arg + 1);
		    indicies_list = g_list_append(
			indicies_list, (gpointer)index
		    );
		}
		/* All else assume it is the alternate_recovery_path */
		else
		{
		    const gchar *path = arg;
		    g_free(alternate_recovery_path);
		    alternate_recovery_path = (g_path_is_absolute(path)) ?
			STRDUP(path) :
			g_strconcat(
			    g_get_current_dir(),
			    G_DIR_SEPARATOR_S,
			    path,
			    NULL
			);
		}
	    }
	}

	/* No recycled object indicies specified? */
	if(indicies_list == NULL)
	{
	    print_help(argv[0]);
	    DO_FREE_LOCALS
	    return(2);
	}


	/* Iterate through each recycled object index */
	for(glist = indicies_list;
	    glist != NULL;
	    glist = g_list_next(glist)
	)
	{
	    index = (guint)glist->data;

	    if(EDVRecycledObjectStat(
		ctx, index,
		&path_rtn,
		NULL
	    ) != 0)
	    {
		g_printerr(
"Unable to recover `#%u': No such recycled object.\n",
		    index
		);
		status = 1;
		continue;
	    }
	    if(path_rtn == NULL)
	    {
		g_printerr(
"Unable to recover `#%u': Unable to obtain the recycled object's statisics.\n",
		    index
		);
		status = 1;
		continue;
	    }

	    if(alternate_recovery_path != NULL)
	    {
		gchar *s = g_strconcat(
		    alternate_recovery_path,
		    G_DIR_SEPARATOR_S,
		    g_basename(path_rtn),
		    NULL
		);
		g_free(path_rtn);
		path_rtn = s;
	    }

	    /* Get object's name without any path prefix */
	    name = strrchr(path_rtn, G_DIR_SEPARATOR);
 	    if(name != NULL)
	    {
		name++;
		if(*name == '\0')
		    name = path_rtn;
	    }
	    else 
	    {
		name = path_rtn;
	    }

	    /* Do confirmation */
	    if(interactive ?
		!recover_query_user_recover_object(path_rtn, index) :
		FALSE
	    )
	    {
		g_free(path_rtn);
		continue;
	    }

	    /* Recreate the parent directory as needed */
	    parent = g_dirname(path_rtn);
	    status = recover_rmkdir(ctx, parent);
	    g_free(parent);
	    if(status != 0)
	    {
		g_free(path_rtn);
		continue;
	    }

	    time_start = (gulong)time(NULL);

	    /* Recover this object */
	    recover_status = EDVRecover(
		ctx, index, alternate_recovery_path,
		TRUE,			/* Notify Endeavour */
		verbose ? recover_progress_cb : NULL,
		(gpointer)path_rtn	/* Data */
	    );

	    /* Record history */
	    EDVHistoryAppend(
		ctx,
                EDV_HISTORY_RECYCLED_OBJECT_RECOVER,
                time_start, (gulong)time(NULL),
                recover_status,
                path_rtn,
		alternate_recovery_path,
		EDVRecycleGetError(ctx)
	    );
	    EDVContextSync(ctx);

	    /* Recovered successfully? */
	    if(recover_status == 0)
	    {
		if(verbose)
		    printf("\n");
		EDVContextFlush(ctx);
	    }
	    else
	    {
		/* Recover failed, print error */
		g_printerr(
		    "%s%s (#%u): %s.\n",
		    verbose ? "\n" : "",
		    path_rtn, index,
		    EDVRecycleGetError(ctx)
		);
		status = 1;
	    }

	    g_free(path_rtn);
	}

	EDVContextSync(ctx);

	DO_FREE_LOCALS

	return(status);
#undef DO_FREE_LOCALS 
}
