/* $Id: e2p_unpack.c 853 2008-04-07 10:38:17Z tpgww $

Copyright (C) 2003-2008 tooar <tooar@gmx.net>

This file is part of emelFM2.
emelFM2 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 3, or (at your option)
any later version.

emelFM2 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.

You should have received a copy of the GNU General Public License
along with emelFM2; see the file GPL. If not, contact the Free Software
Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

/**
@file plugins/e2p_unpack.c
@brief plugin for interfacting with several archive managers, to unpack selected item(s)
*/

#include "emelfm2.h"
#include <string.h>
#include "e2_plugins.h"
#include "e2_dialog.h"
#include "e2_task.h"
#include "e2_filelist.h"
//same enum as in pack plugin
enum { TAR_GZ, TAR_BZ2, TAR, ZIP, Z7Z, RAR, ARJ, ZOO, MAXTYPES };

typedef struct _E2P_Unpackdata
{
	gchar *package;	//absolute path of source archive, utf8 string
	gchar *workdir;	//absolute path of dir used to unpack this archive,
					//== unpack_tmp or a tmp variant of that, utf-8, no trailer
	gchar *last_dir;	//dir to go back to after starting a repack
	glong thispid;	//id of process which is re-packing an archive
//	guint chdir_id;	//id of timer which checks for whether to ask user what to do
	guint pack_id;	//id of timer which checks for repack completion
	E2_CDType cd_completed;	//flag set when cd to temp dir is completed
	gchar *command;	//the upack command to be run
	gboolean departing;	//TRUE when the temp dir is waiting to be processed after
						//the user selects from a what-to-do dialog
						//also used to block reentrant use of the hook function
} E2P_Unpackdata;

static gboolean _e2p_unpack_delete_dir (E2P_Unpackdata *data);
static gboolean _e2p_unpack_change_dir_hook (gchar *path, E2P_Unpackdata *data);

static gchar *unpack_tmp;	//absolute path of 'base working' directory (no trailer), utf-8 string

/**
@brief cleanup plugin data
This is executed with BGL open or closed
@param data pointer to plugin data

@return
*/
static void _e2p_unpack_cleanup (E2P_Unpackdata *data)
{
	g_free (data->package);
	g_free (data->workdir);
	g_free (data->command);
	if (data->last_dir != NULL)
		g_free (data->last_dir);
	DEALLOCATE (E2P_Unpackdata, data);
}
/**
@brief timer callback to resume idle checking

@param data pointer to unpack data struct

@return FALSE always
*/
static gboolean _e2p_unpack_pause (E2P_Unpackdata *data)
{
	data->pack_id = g_idle_add_full (G_PRIORITY_LOW,
		(GSourceFunc)_e2p_unpack_delete_dir, data, NULL);
	return FALSE;
}
/**
@brief idle callback to check whether temp-dir deletion can proceed safely
We do this check at an idle time to reduce risk of conflict, but interpose a timer
between idle callbacks to reduce their frequency
@param data pointer to unpack data struct

@return FALSE always
*/
static gboolean _e2p_unpack_delete_dir (E2P_Unpackdata *data)
{
	LISTS_LOCK
	//conservative approach - no deletion while any chance of the temp-dir being used
	if (curr_view->listcontrols.cd_working ||
		curr_view->listcontrols.refresh_working || (
#ifdef E2_VFS
		curr_view->spacedata == NULL &&	//local temp dirs
#endif
		g_str_has_prefix (curr_view->dir, data->workdir)))
	{
		LISTS_UNLOCK
		//wait before checking for an idle again
		data->pack_id = g_timeout_add (500, (GSourceFunc)_e2p_unpack_pause, data);
		return FALSE;
	}
	if (other_view->listcontrols.cd_working ||
		other_view->listcontrols.refresh_working || (
#ifdef E2_VFS
		other_view->spacedata == NULL &&	//local temp dirs
#endif
		g_str_has_prefix (other_view->dir, data->workdir)))
	{
		LISTS_UNLOCK
		//wait before checking for an idle again
		data->pack_id = g_timeout_add (500, (GSourceFunc)_e2p_unpack_pause, data);
		return FALSE;
	}
	LISTS_UNLOCK
	//kill the idle now
//	if (data->pack_id > 0)
//		g_source_remove (data->pack_id);
//	else
//		while (g_source_remove_by_user_data (data)) {}
	//now we're ready for cleanup
	e2_filelist_disable_refresh ();
	gchar *local = F_FILENAME_TO_LOCALE (data->workdir);
#ifdef E2_VFS
	VPATH ddata = { local, NULL };	//local unpacking only
	if (e2_fs_access2 (&ddata E2_ERR_NONE()) == 0)
		e2_task_backend_delete (&ddata);
#else
	if (e2_fs_access2 (local E2_ERR_NONE()) == 0)
		e2_task_backend_delete (local);
#endif

	e2_filelist_enable_refresh ();
	F_FREE (local);
	_e2p_unpack_cleanup (data);
	//FIXME different refresh approach with E2_ASYNC
	//in case a pane shows parent of temp dir
#ifdef E2_FAM
	e2_filelist_request_refresh (curr_view->dir, FALSE);
	e2_filelist_request_refresh (other_view->dir, TRUE);
#else
	e2_filelist_check_dirty (GINT_TO_POINTER (1));
#endif
	return FALSE;
}
/**
@brief delete the temp dir
This is executed with BGL open or closed
@param data pointer to plugin data

@return
*/
static void _e2p_unpack_clear (E2P_Unpackdata *data)
{
	//ensure BGL is open and manage any race with refresh function
	data->pack_id = g_idle_add_full (G_PRIORITY_LOW,
		(GSourceFunc)_e2p_unpack_delete_dir, data, NULL);
}
/**
@brief timer callback to periodically check whether a repack is completed

@param data pointer to plugin data struct

@return TRUE if the repack process is still running
*/
static gboolean _e2p_unpack_clean_dir (E2P_Unpackdata *data)
{
	if (e2_command_find_process ((guint)data->thispid))
		return TRUE;	//wait some more
	//now we're ready for cleanup
//	g_source_remove (data->pack_id);
//	if (_e2p_unpack_delete_dir (data))
		//keep trying until the deletion is done
		g_idle_add_full (G_PRIORITY_LOW,
			(GSourceFunc)_e2p_unpack_delete_dir, data, NULL);
	return FALSE;
}
/**
@brief repack the temp dir
This is executed inside a callback with BGL closed
@param data pointer to plugin data

@return
*/
static void _e2p_unpack_repack (E2P_Unpackdata *data)
{
#ifdef E2_VFSTMP
	if (curr_view->spacedata != NULL)	//CHECKME fuse ok?
	  return;
#endif
	guint index;
	gchar *package = data->package;
	static gchar *cmd_str [MAXTYPES] =
	{	//these command strings are in same order as enum
		">tar cf - . | gzip - > \"%s\"",
		">tar cf - . | bzip2 - > \"%s\"",
		"tar cf \"%s\" .",
		"zip -r \"%s\" .",
		"7za a -t7z \"%s\" .",	//CHECKME
		"rar u -ol \"%s\" .",
		"arj u -al \"%s\" .",
		"zoo ahP \"%s\" ."	//FIXME replace, not add ?
	};
/*
compress ANSI files: 7za a -tzip archive.zip file1 file2 ... fileN
compress ANSI dir:   7za a -tzip archive.zip dirnametocompress\
compress UNICODE files: 7za a -t7z archive.7z file1 file2 ... fileN
compress UNICODE dir:   7za a -t7z archive.7z dirnametocompress\
*/
	//command strings are all designed to be executed from
	//the temp dir, on all its contents
	//CHECKME package is assumed to have ascii-coded extension - ok ?
	//FIXME a better way to distinguish filetypes eg magic number
	if ((strstr (package, ".tar.gz") != NULL) ||
		(strstr (package, ".tgz") != NULL))
			index = TAR_GZ;
	else if (strstr (package, ".tar.bz2") != NULL)
		index = TAR_BZ2;
	else if (strstr (package, ".tar") != NULL)
		index = TAR;
	else if (strstr (package, ".zip") != NULL)
		index = ZIP;
	else if (strstr (package, ".7z") != NULL)
		index = Z7Z;
	else if (strstr (package, ".rar") != NULL)
		index = RAR;
	else if (strstr (package, ".arj") != NULL)
		index = ARJ;
	else //if (strstr (package, ".zoo") != NULL)
		index = ZOO;

	g_free (data->command);
	data->command = g_strdup_printf (cmd_str [index], package);

	gint res = e2_command_run_at (data->command, data->workdir, E2_COMMAND_RANGE_DEFAULT);
	//FIXME race here if something else is run at a bad time, so find the pid
	//with matching command string instead
	if (res == 0)
	{
		E2_TaskRuntime *td = e2_task_find_last_running_child (TRUE);
		data->thispid = (td != NULL) ? td->pid : 0;
	}
	else
		data->thispid = 0;
	//CHECKME refreshing may move CWD away from the temp dir while the repack is underway

	//periodically check whether re-build finished, when so, cleanup the temp dir
	//CHECKME make this timer cancellable at session end
	data->pack_id = g_timeout_add (500, (GSourceFunc) _e2p_unpack_clean_dir, data);
}
/**
@brief callback for "what-to-do" dialog's "response" signal

@param dialog UNUSED the dialog where the response was triggered
@param response the response for the clicked button
@param rt pointer to data for dialog

@return
*/
static void _e2p_unpack_response_decode_cb (GtkDialog *dialog, gint response,
	E2P_Unpackdata *data)
{
	gtk_widget_destroy (GTK_WIDGET(dialog));
	//do this outside of current hook func, as we need to check both panes and data
	e2_hook_unregister (&app.pane1.hook_change_dir, _e2p_unpack_change_dir_hook,
		data, TRUE);
	e2_hook_unregister (&app.pane2.hook_change_dir, _e2p_unpack_change_dir_hook,
		data, TRUE);

	switch (response)
	{
		case E2_RESPONSE_USER1:	//repack the temp dir
			_e2p_unpack_repack (data);
			break;
		case E2_RESPONSE_USER2: //keep the unpacked archive
			_e2p_unpack_cleanup (data);
		//	case GTK_RESPONSE_CANCEL:
			break;
		//case E2_RESPONSE_REMOVE:
		default: //this will pick up GTK_RESPONSE_NONE or GTK_RESPONSE_DELETE_EVENT
			_e2p_unpack_clear (data);
			break;
	}
}
/**
@brief hook function for cd in either pane (app.paneX.hook_change_dir)
This is initiated from a cd thread, and with BGL open/off
@param path UNUSED path of an opened directory, utf-8 string
@param data pointer to operation data struct
@return TRUE always so hook remains active
*/
static gboolean _e2p_unpack_change_dir_hook (gchar *path, E2P_Unpackdata *data)
{
	if (data->departing)
		return TRUE;	//a callback has begun, ignore this other cd
	data->departing = TRUE;	//temp block on nested checking
	/* this hookfunc is called toward the end of a cd process, there's no need
	   to check for various "busy" flags before proceeding
	   the first cd will be into the temp dir, so path will be that dir with
	   trailing separator */
	if (
#ifdef E2_VFSTMP
		//FIXME dirs when not mounted local
		curr_view->spacedata == NULL //local temp dirs
		other_view->spacedata == NULL //local temp dirs
#else
		( g_str_has_prefix (curr_view->dir, data->workdir)
		|| g_str_has_prefix (other_view->dir, data->workdir))
#endif
	   )
	{
		data->departing = FALSE;	//unblock
		return TRUE;
	}

	//user changed dir, now neither pane is for anywhere in unpack-dir tree
	printd (DEBUG, "ready to cleanup unpack dir");

	//ask user what to do with the unpacked items
	GtkWidget *dialog = e2_dialog_create (GTK_STOCK_DIALOG_QUESTION,
		_("What do you want to do with the unpacked item(s) ?"), NULL,
		_e2p_unpack_response_decode_cb, data);

	e2_dialog_add_undefined_button (dialog, GTK_STOCK_CLEAR,
		_("Re_pack"), E2_RESPONSE_USER1);
	e2_dialog_add_undefined_button (dialog, GTK_STOCK_APPLY,
		_("_Retain"), E2_RESPONSE_USER2);
	GtkWidget *button = e2_dialog_add_undefined_button (dialog, GTK_STOCK_DELETE,
		_("_Delete"), E2_RESPONSE_REMOVE);

	e2_dialog_setup (dialog, app.main_window);
	gdk_threads_enter ();
	e2_dialog_run (dialog, NULL, 0);
	gtk_widget_grab_focus (button);
	gtk_window_present (GTK_WINDOW (dialog));
	gdk_threads_leave ();

	return TRUE; 	//no hook cleanup here
					//done in response cb, as we need to check both panes and data
}
/**
@brief unpack plugin action : unpack a supported archive into a temp dir

@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE if action completed successfully, else FALSE
*/
static gboolean _e2p_unpack (gpointer from, E2_ActionRuntime *art)
{
	gchar *package, *workdir, *converted;
	FileInfo *info;
	guint index;

	//these unpack-command strings are in same order as enum
	//all are executed from the temp dir (as some can only do that)
	//all are ascii (no conversion to utf8 before execution)
	static gchar *cmd_str [MAXTYPES] =
	{
		//FIXME -z option to tar is a GNU extension
		//FIXME -j option ditto
		//or -C \"%s\" for tar's
#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(darwin)
		"tar -xpzf \"%s\"",
		"tar -xpjf \"%s\"",
		"tar -xpf \"%s\"",
#else
		//note: an --atime-preserve in these tar commands prevents the
		//file-monitoring process from noticing anything in the temp dir
		"tar --overwrite -xpzf \"%s\"",
		"tar --overwrite -xpjf \"%s\"",
		"tar --overwrite -xpf \"%s\"",
#endif
		//.deb command (for what version ?) provided by Martin Zelaia
		//"mkdir ./DEBIAN ./CONTENTS;>ar -x \"%s\" | tar -xfz control.tar.gz -C ./DEBIAN | tar -xfz data.tar.gz -C ./CONTENTS; rm control.tar.gz data.tar.gz;cp ./DEBIAN/control ./INFO;rm ./debian-binary";
		"unzip -o \"%s\"",  //or "unzip -o -d \"%s\" \"%s\""
/*
decompress ANSI:    7za x archive.zip -odirname -aoa
decompress UNICODE: 7za x archive.7z -odirname -aoa
*/
		"7za x \"%s\" -aoa", //or ??
		"rar x -o+ \"%s\"", //rar will only extract to current dir
		"arj x -y \"%s\"", //or "arj x -y \"%s\" \"%s\"", //NOTE swapped order of archive & path
		"zoo xO \"%s\""	//zoo will only extract to current dir
	};

	info = e2_fileview_get_selected_first_local (curr_view, FALSE);
	if (info == NULL)
		return FALSE;	//nothing selected
	if ((strstr (info->filename, ".tar.gz") != NULL) ||
		(strstr (info->filename, ".tgz") != NULL))
			index = TAR_GZ;
	else if (strstr (info->filename, ".tar.bz2") != NULL)
		index = TAR_BZ2;
	else if (strstr (info->filename, ".tar") != NULL)
		index = TAR;
	//.deb command (for what version ?) provided by Martin Zelaia
	//  else if (strstr(info->filename, ".deb") != NULL)
	//    index = ?
	else if (strstr (info->filename, ".zip") != NULL)
		index = ZIP;
	else if (strstr (info->filename, ".7z") != NULL)
		index = Z7Z;
	else if (strstr(info->filename, ".rar") != NULL)
		index = RAR;
	else if (strstr(info->filename, ".arj") != NULL)
		index = ARJ;
	else if (strstr(info->filename, ".zoo") != NULL)
		index = ZOO;
	else
	{
		e2_output_print_error (_("Selected item is not a supported archive"), FALSE);
		return FALSE;
	}

	//CHECKME recursive unpacking may be ok now, with different temp dirs
#ifdef E2_VFSTMP
	//FIXME handle space-change too
	if (curr_view->spacedata == ? || strstr (curr_view->dir, unpack_tmp) != NULL)
#else
	if (strstr (curr_view->dir, unpack_tmp) != NULL)
#endif
	{ //the temp dir may be deleted when the plugin function is
		//completed, so it would be very bad to open an archive
		//inside the temp dir
		e2_output_print_error (_("Recursive unpack is not supported"), FALSE);
		return FALSE;
	}

	converted = F_FILENAME_TO_LOCALE (unpack_tmp);
	workdir = e2_utils_get_tempname (converted);
#ifdef E2_VFS
	VPATH ddata = { workdir, NULL };	//local unpacking only
#endif
	F_FREE (converted);
	//(re)make it
#ifdef E2_VFS
	if (e2_fs_recurse_mkdir (&ddata, 0777 E2_ERR_NONE()))
#else
	if (e2_fs_recurse_mkdir (workdir, 0777 E2_ERR_NONE()))
#endif
	{
		converted = F_DISPLAYNAME_FROM_LOCALE (workdir);
		gchar *msg = g_strdup_printf ("Could not create working directory '%s'",
			converted);
		e2_output_print_error (msg, TRUE);
		F_FREE (converted);
		g_free (workdir);
		return FALSE;
	}

	E2P_Unpackdata *data = ALLOCATE0 (E2P_Unpackdata);
	CHECKALLOCATEDWARN (data, return FALSE;)
	data->workdir = D_FILENAME_FROM_LOCALE (workdir);
	g_free (workdir);

	converted = F_FILENAME_FROM_LOCALE (info->filename);
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
	package = g_strdup_printf ("%s%s", curr_view->dir, converted);  //dir has trailing /
#endif
	data->package = package;

	F_FREE (converted);

	//no conversion of command encoding
	data->command = g_strdup_printf (cmd_str [index], package);

	e2_window_set_cursor (GDK_WATCH);
	//unpack the archive into the temp directory
	gint result = e2_command_run_at (data->command, data->workdir, E2_COMMAND_RANGE_DEFAULT);
	e2_window_set_cursor (GDK_LEFT_PTR);
	if (result != 0)
	{
		workdir = F_FILENAME_TO_LOCALE (data->workdir);
#ifdef E2_VFS
		ddata.localpath = workdir;
		e2_task_backend_delete (&ddata);
#else
		e2_task_backend_delete (workdir);
#endif
		F_FREE (workdir);
		_e2p_unpack_cleanup (data);
		return FALSE;
	}
	//now go see it
	e2_pane_change_dir (NULL, data->workdir);
	//setup to clean when temp dir not open anymore
	e2_hook_register (&app.pane1.hook_change_dir, _e2p_unpack_change_dir_hook, data);
	e2_hook_register (&app.pane2.hook_change_dir, _e2p_unpack_change_dir_hook, data);

	return TRUE;
}

//aname must be confined to this module
static gchar *aname;
/**
@brief plugin initialization function, called by main program

@param p ptr to plugin data struct

@return TRUE if the initialization succeeds, else FALSE
*/
gboolean init_plugin (Plugin *p)
{
	//once-only, setup the working dir name
	unpack_tmp = e2_utils_get_temp_path ("-unpack");
	//strip the trailing ".tmp~0" as we will append a suffix like that for each unpack
	gchar *s = strrchr (unpack_tmp, '.');
	*s = '\0';

#define ANAME "unpack"
#ifdef E2_VFS
	aname = _("unpack_with_plugin");
#else
	aname = _A(100);
#endif
	p->signature = ANAME VERSION;
	p->menu_name = _("_Unpack");
	p->description = _("Unpack archive (tar, tar.gz, tar.bz2, zip, 7z, rar, arj, zoo) into a temporary directory");
	p->icon = "plugin_"ANAME"_"E2IP".png";  //use icon file pathname if appropriate

	if (p->action == NULL)
	{
		//no need to free this
		gchar *action_name = g_strconcat (_A(5),".",aname,NULL);
		p->action = e2_plugins_action_register
			(action_name, E2_ACTION_TYPE_ITEM, _e2p_unpack, NULL, FALSE, 0, NULL);
			return TRUE;
	}
	return FALSE;
}
/**
@brief cleanup transient things for this plugin

@param p pointer to data struct for the plugin

@return TRUE if all cleanups were completed
*/
gboolean clean_plugin (Plugin *p)
{
	//FIXME prevent unload when an unpack is underway
	//clear any current hook(s)
	while (e2_hook_unregister (&app.pane1.hook_change_dir, _e2p_unpack_change_dir_hook,
		NULL, FALSE)) {}
	while (e2_hook_unregister (&app.pane2.hook_change_dir, _e2p_unpack_change_dir_hook,
		NULL, FALSE)) {}

	gchar *action_name = g_strconcat (_A(5),".",aname,NULL);
	gboolean ret = e2_plugins_action_unregister (action_name);
	g_free (action_name);
	g_free (unpack_tmp);
	return ret;
}
