/*  libarchive plugin for Tux Commander
 *   version 0.1.5, designed for libarchive 2.5.5
 *  Copyright (C) 2008 Tomas Bzatek <tbzatek@users.sourceforge.net>
 *   Check for updates on tuxcmd.sourceforge.net
 *
 *  Uses libarchive library
 *   Copyright (c) 2003-2007 Tim Kientzle
 *
 *


 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <fnmatch.h>
#include <unistd.h>
#include <glib.h>

#include "vfs_types.h"
#include "vfsutils.h"
#include "strutils.h"
#include "treepathutils.h"
#include "treepath_vfs.h"

#include <archive.h>
#include <archive_entry.h>



#define VERSION "0.1.5"
#define BUILD_DATE "2009-10-25"
#define DEFAULT_BLOCK_SIZE 65536





/******************************************************************************************************/
/**  Auxiliary classes                                                                                */
/**************                                                                        ****************/



//  Declaration of the global plugin object
struct TVFSGlobs {
  TVFSLogFunc log_func;
  char *curr_dir;
  char *archive_path;

  unsigned long block_size;

  struct PathTree *files;
  struct VfsFilelistData *vfs_filelist;

  u_int64_t total_size;

  TVFSAskQuestionCallback callback_ask_question;
  TVFSAskPasswordCallback callback_ask_password;
  TVFSProgressCallback callback_progress;
  void *callback_data;
};






////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////
//  Basic initialization functions
struct TVFSGlobs *
VFSNew (TVFSLogFunc log_func)
{
  struct TVFSGlobs * globs;

  globs = (struct TVFSGlobs *) malloc (sizeof (struct TVFSGlobs));
  memset (globs, 0, sizeof (struct TVFSGlobs));

  globs->block_size = DEFAULT_BLOCK_SIZE;

  globs->callback_data = NULL;
  globs->callback_ask_question = NULL;
  globs->callback_ask_password = NULL;
  globs->callback_progress = NULL;

  globs->log_func = log_func;
  if (globs->log_func != NULL) globs->log_func("libarchive plugin: VFSInit");

  return globs;
}

void
VFSSetCallbacks (struct TVFSGlobs *globs,
                 TVFSAskQuestionCallback ask_question_callback,
                 TVFSAskPasswordCallback ask_password_callback,
                 TVFSProgressCallback progress_func,
                 void *data)
{
  globs->callback_ask_question = ask_question_callback;
  globs->callback_ask_password = ask_password_callback;
  globs->callback_progress = progress_func;
  globs->callback_data = data;
}

void
VFSFree (struct TVFSGlobs *globs)
{
  if (globs->log_func != NULL) globs->log_func("libarchive plugin: VFSDestroy");
  free (globs);
}

int VFSVersion()
{
  return cVFSVersion;
}

struct TVFSInfo * 
VFSGetInfo()
{
  struct TVFSInfo *module_info = g_malloc0 (sizeof (struct TVFSInfo));

  module_info->ID = g_strdup ("libarchive_plugin");
  module_info->Name = g_strdup ("libarchive plugin");
  module_info->About = g_strdup_printf ("version %s, build date: %s\nusing %s\n", VERSION, BUILD_DATE, ARCHIVE_LIBRARY_VERSION);
  module_info->Copyright = g_strdup ("Plugin Copyright (C) 2008-2009 Tomáš Bžatek\nlibarchive sources Copyright (c) 2003-2007 Tim Kientzle");

  return module_info;
}

char *
VFSGetArchiveExts ()
{
  return g_strdup ("tar;tar.gz;tar.bz2;tgz;tbz2;cpio;iso;a;deb");
}


/**************************************************************************************************************************************/
/**************************************************************************************************************************************/

static TVFSResult libarchive_open(struct archive **a, const char *filename, size_t block_size)
{
  int r;

  *a = archive_read_new();

  //  Register supported formats

  archive_read_support_compression_all(*a);
  archive_read_support_format_all(*a);

/*
  archive_read_support_compression_bzip2(*a);
  archive_read_support_compression_gzip(*a);
  archive_read_support_compression_compress(*a);
  archive_read_support_compression_none(*a);
  archive_read_support_format_tar(*a);
  archive_read_support_format_cpio(*a);
  archive_read_support_format_ar(*a);
  archive_read_support_format_empty(*a);
  archive_read_support_format_gnutar(*a);
  archive_read_support_format_iso9660(*a);
  archive_read_support_format_cpio(*a);
*/

  r = archive_read_open_file(*a, filename, block_size);
  if (r) {
    fprintf(stderr, "(EE) libarchive_open: error occured when opening archive: %s\n", archive_error_string(*a));
    return cVFS_Failed;
  }
  return cVFS_OK;
}



TVFSResult VFSOpenArchive(struct TVFSGlobs *globs, char *sName)
{
  globs->files = filelist_tree_new();
  globs->vfs_filelist = vfs_filelist_new(globs->files);
  globs->curr_dir = NULL;
  globs->archive_path = strdup(sName);
  globs->total_size = 0;
  fprintf(stderr, "(--) VFSOpenArchive: trying to open archive '%s'...\n", globs->archive_path);

  TVFSResult Result;
  struct archive *a;
  struct archive_entry *entry;
  int r;

  Result = libarchive_open(&a, globs->archive_path, globs->block_size);
  if (Result == cVFS_OK)
  {
	  for (;;) {
			entry = NULL;
			r = archive_read_next_header(a, &entry);
			if (r == ARCHIVE_EOF) {
				break;
			} else
			if (r == ARCHIVE_WARN) {
				log("(WW) VFSOpenArchive: file '%s' - libarchive warning: '%s'\n", archive_entry_pathname(entry), archive_error_string(a));
			} else
			if (r != ARCHIVE_OK) {
				fprintf(stderr, "(EE) VFSOpenArchive: error occured while reading archive: '%s'\n", archive_error_string(a));
				Result = cVFS_Failed;
				break;
			}

			log("found file: %s, mode = %x\n", archive_entry_pathname(entry), archive_entry_mode(entry));

			//  Create a TVFSItem entry and fill all info
			struct TVFSItem *item = (struct TVFSItem*)malloc(sizeof(struct TVFSItem));
			memset(item, 0, sizeof(struct TVFSItem));

			item->iSize = (int64_t)archive_entry_size(entry);
			item->iPackedSize = -1;  /*  no support  */
			globs->total_size += item->iSize;

			mode_t mode = archive_entry_mode(entry);
			item->iMode = archive_entry_mode(entry);
			if (S_ISREG(mode)) item->ItemType = vRegular;
			else if (S_ISDIR(mode)) item->ItemType = vDirectory;
			else if (S_ISCHR(mode)) item->ItemType = vChardev;
			else if (S_ISBLK(mode)) item->ItemType = vBlockdev;
			else if (S_ISFIFO(mode)) item->ItemType = vFifo;
			else if (S_ISLNK(mode)) item->ItemType = vSymlink;
			else if (S_ISSOCK(mode)) item->ItemType = vSock;

			if (item->ItemType == vSymlink)
				item->sLinkTo = strdup(archive_entry_symlink(entry));

			item->iUID = geteuid();
			item->iGID = getegid();
			item->m_time = archive_entry_mtime(entry);
			item->c_time = archive_entry_ctime(entry);
			item->a_time = archive_entry_atime(entry);

			char *s;
			if (g_utf8_validate (archive_entry_pathname(entry), -1, NULL))
			  s = g_strdup (archive_entry_pathname(entry));
			else {
				if (archive_entry_pathname_w(entry))
				  s = wide_to_utf8 (archive_entry_pathname_w(entry));
				else
				  s = g_filename_display_name (archive_entry_pathname(entry));
			}
//			g_print("file = '%s', wide = '%ls'\n", archive_entry_pathname(entry), archive_entry_pathname_w(entry));

			//  Add item to the global list and continue with next file
			filelist_tree_add_item(globs->files, s, archive_entry_pathname(entry), item, 0);
			g_free (s);
		}
		archive_read_close(a);
  }
  archive_read_finish(a);
  fprintf(stderr, "(II) VFSOpenArchive: done. \n");

  /* FIXME: temporarily disabled */
//  filelist_tree_resolve_symlinks(globs->files);

  printf("\n\nList of items:\n");
  filelist_tree_print(globs->files);

  return Result;
}


TVFSResult VFSClose(struct TVFSGlobs *globs)
{
  if (globs) {
	  fprintf(stderr, "(II) VFSClose: Freeing objects...\n");
	  if (globs->vfs_filelist) vfs_filelist_free(globs->vfs_filelist);
	  if (globs->files) filelist_tree_free(globs->files);
	  if (globs->archive_path) free(globs->archive_path);
	  if (globs->curr_dir) free(globs->curr_dir);
  }
  return cVFS_OK;
}

char *
VFSGetPath (struct TVFSGlobs *globs)
{
  return include_trailing_path_sep (globs->curr_dir);
}

u_int64_t VFSGetFileSystemFree(struct TVFSGlobs *globs, char *APath)
{
  return 0;
}

u_int64_t VFSGetFileSystemSize(struct TVFSGlobs *globs, char *APath)
{
  return globs->total_size;
}




/******************************************************************************************************/

TVFSResult VFSChangeDir(struct TVFSGlobs *globs, char *NewPath)
{
  if (NewPath == NULL) {
    printf("(EE) VFSChangeDir: NewPath is NULL!\n");
    return cVFS_Failed;
  }

  globs->curr_dir = vfs_filelist_change_dir(globs->vfs_filelist, NewPath);
  if (globs->curr_dir) return cVFS_OK;
  else return cVFS_Failed;
}

int VFSGetPasswordRequired(struct TVFSGlobs *globs)
{
  return FALSE;
}


/******************************************************************************************************/

TVFSResult VFSListFirst(struct TVFSGlobs *globs, char *sDir, struct TVFSItem *Item)
{
  if (sDir == NULL) {
    printf("(EE) VFSListFirst: sDir is NULL!\n");
    return cVFS_Failed;
  }
  printf ("(--) VFSListFirst: Going to list all items in '%s'\n", sDir);

  return vfs_filelist_list_first(globs->vfs_filelist, sDir, Item);
}

TVFSResult VFSListNext(struct TVFSGlobs *globs, char *sDir, struct TVFSItem *Item)
{
  return vfs_filelist_list_next(globs->vfs_filelist, sDir, Item);
}

TVFSResult VFSListClose(struct TVFSGlobs *globs)
{
  return vfs_filelist_list_close(globs->vfs_filelist);
}


/******************************************************************************************************/
long VFSFileExists(struct TVFSGlobs *globs, const char *FileName, const long Use_lstat)
{
  if (! globs) return FALSE;
  return vfs_filelist_file_exists(globs->vfs_filelist, FileName, Use_lstat);
}

TVFSResult VFSFileInfo(struct TVFSGlobs *globs, char *AFileName, struct TVFSItem *Item)
{
  printf("(--) VFSFileInfo: requested info for object '%s'\n", AFileName);
  if (!globs) return cVFS_Failed;
  return vfs_filelist_file_info(globs->vfs_filelist, AFileName, Item);
}


/******************************************************************************************************/
/**  Recursive tree size counting                                                                     */
/**************                                                                        ****************/

u_int64_t VFSGetDirSize(struct TVFSGlobs *globs, char *APath)
{
  if (! globs) return 0;
  return vfs_filelist_get_dir_size(globs->vfs_filelist, APath);
}

void VFSBreakGetDirSize(struct TVFSGlobs *globs)
{
  printf("(WW) VFSBreakGetDirSize: calling break\n");
  if (globs) vfs_filelist_get_dir_size_break(globs->vfs_filelist);
}


/******************************************************************************************************/
/**  Methods modifying the archive                                                                    */
/**************                                                                        ****************/

TVFSResult VFSMkDir(struct TVFSGlobs *globs, const char *sDirName)
{
  printf("(WW) VFSMkDir: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}

TVFSResult VFSRemove(struct TVFSGlobs *globs, const char *APath)
{
  printf("(WW) VFSRemove: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}

TVFSResult VFSRename(struct TVFSGlobs *globs, const char *sSrcName, const char *sDstName)
{
  printf("(WW) VFSRename: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}

TVFSResult VFSMakeSymLink(struct TVFSGlobs *globs, const char *NewFileName, const char *PointTo)
{
  printf("(WW) VFSMakeSymLink: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}

TVFSResult VFSChmod(struct TVFSGlobs *globs, const char *FileName, const uint Mode)
{
  printf("(WW) VFSChmod: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}

TVFSResult VFSChown(struct TVFSGlobs *globs, const char *FileName, const uint UID, const uint GID)
{
  printf("(WW) VFSChown: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}

TVFSResult VFSChangeTimes(struct TVFSGlobs *globs, char *APath, long mtime, long atime)
{
  printf("(WW) VFSChangeTimes: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}



////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////

TVFSFileDes VFSOpenFile(struct TVFSGlobs *globs, const char *APath, int Mode, int *Error)
{
  *Error = cVFS_Not_Supported;
  return (TVFSFileDes)0;
}

TVFSResult VFSCloseFile(struct TVFSGlobs *globs, TVFSFileDes FileDescriptor)
{
  return cVFS_Not_Supported;
}

u_int64_t VFSFileSeek(struct TVFSGlobs *globs, TVFSFileDes FileDescriptor, u_int64_t AbsoluteOffset, int *Error)
{
  *Error = cVFS_Not_Supported;
  return 0;
}

int VFSReadFile(struct TVFSGlobs *globs, TVFSFileDes FileDescriptor, void *Buffer, int ABlockSize, int *Error)
{
  *Error = cVFS_Not_Supported;
  return 0;
}

int VFSWriteFile(struct TVFSGlobs *globs, TVFSFileDes FileDescriptor, void *Buffer, int BytesCount, int *Error)
{
  *Error = cVFS_Not_Supported;
  return 0;
}

void VFSSetBlockSize(struct TVFSGlobs *globs, int Value)
{
  globs->block_size = Value;
}

int VFSIsOnSameFS(struct TVFSGlobs *globs, const char *Path1, const char *Path2)
{
  printf("(WW) VFSIsOnSameFS: Not supported in libarchive plugin.\n");
  return TRUE;
}

int VFSTwoSameFiles(struct TVFSGlobs *globs, const char *Path1, const char *Path2)
{
  printf("(WW) VFSTwoSameFiles: Not supported in libarchive, comparing by paths.\n");
  return compare_two_same_files(Path1, Path2);
}


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////




/**
 *  The following code has been stolen from archive_read_data_into_fd.c (libarchive sources) and modified to allow progress callbacks
 *    Quote: "This implementation minimizes copying of data and is sparse-file aware."
 **/
static TVFSResult
my_archive_read_data_into_fd(struct TVFSGlobs *globs, struct archive *a, struct archive_entry *entry, const char *sDstName, size_t max_block, int Append)
{
	int r;
	int fd;
	const void *buff;
	size_t size, bytes_to_write;
	ssize_t bytes_written, total_written;
	off_t offset;
	off_t output_offset;
	u_int64_t file_size;
	gboolean cancel = FALSE;


	printf("(II) my_archive_read_data_into_fd: extracting to '%s', Append = %d\n", sDstName, Append);

	if (Append) fd = open(sDstName, O_APPEND | O_WRONLY);
	else fd = open(sDstName, O_CREAT | O_WRONLY | O_TRUNC,
	                         S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
	if (fd < 0) {
		fprintf(stderr, "(EE) my_archive_read_data_into_fd: error occured while extracting data: %s\n", strerror(errno));
		return cVFS_Failed;
	}

	total_written = 0;
	output_offset = 0;
	file_size = (u_int64_t)archive_entry_size(entry);

	while ((r = archive_read_data_block(a, &buff, &size, &offset)) == ARCHIVE_OK) {
		const char *p = buff;
		if (offset > output_offset) {
			lseek(fd, offset - output_offset, SEEK_CUR);
			output_offset = offset;
		}
		while (size > 0) {
			if (cancel)  break;
			bytes_to_write = size;
			if (bytes_to_write > max_block)
				bytes_to_write = max_block;
			bytes_written = write(fd, p, bytes_to_write);
			if (bytes_written < 0) {
				fprintf(stderr, "(EE) my_archive_read_data_into_fd: error occured while extracting data: %s\n", strerror(errno));
				return cVFS_Failed;
			}
			output_offset += bytes_written;
			total_written += bytes_written;
			p += bytes_written;
			size -= bytes_written;

	  		log("  (II) my_archive_read_data_into_fd: bytes_written = %zd, total_written = %zd\n", bytes_written, total_written);
	  		if (globs->callback_progress) {
	  			if (! globs->callback_progress(total_written, file_size, globs->callback_data)) {
	  				cancel = TRUE;
	  				break;
	  			}
//	  			usleep(100000);
	  		}
		}
	}

	if ((close(fd)) || ((r != ARCHIVE_OK) && (r != ARCHIVE_EOF))) {
		fprintf(stderr, "(EE) my_archive_read_data_into_fd: error closing extracted file: %s\n", strerror(errno));
		return cVFS_WriteErr;
	}
	if (cancel) {
		if (unlink(sDstName))
			fprintf(stderr, "(EE) my_archive_read_data_into_fd: error unlinking cancelled extraction: %s\n", strerror(errno));
		return cVFS_Cancelled;
	}
	printf("(II) my_archive_read_data_into_fd: done.\n");
	return cVFS_OK;
}


TVFSResult VFSCopyToLocal(struct TVFSGlobs *globs, const char *sSrcName, const char *sDstName, int Append)
{
  if ((sSrcName == NULL)  || (sDstName == NULL) || (strlen(sSrcName) < 1) || (strlen(sDstName) < 1)) {
    printf("(EE) VFSCopyToLocal: The value of 'sSrcName' or 'sDstName' is NULL or empty\n");
    return cVFS_Failed;
  }

  printf("(II) VFSCopyToLocal: copying file '%s' out to '%s'\n", sSrcName, sDstName);

  struct PathTree *node = filelist_tree_find_node_by_path(globs->files, sSrcName);
  if (! node) {
    fprintf(stderr, "(EE) VFSCopyToLocal: cannot find file '%s'\n", sSrcName);
    return cVFS_ReadErr;
  }

  const char *src = node->original_pathstr;
  if (! src) {
    fprintf(stderr, "(WW) VFSCopyToLocal: cannot determine original filename\n");
    src = sSrcName;
  }
  printf("(II) VFSCopyToLocal: new src path: '%s'\n", src);


  TVFSResult Result;
  struct archive *a;
  struct archive_entry *entry;
  int r;
  gboolean found = FALSE;

  Result = libarchive_open(&a, globs->archive_path, globs->block_size);
  if (Result == cVFS_OK)
  {
		for (;;) {
			entry = NULL;
			r = archive_read_next_header(a, &entry);
			if (r == ARCHIVE_EOF) {
				break;
			} else
			if (r == ARCHIVE_WARN) {
				log("(WW) VFSOpen: file '%s' - libarchive warning: '%s'\n", archive_entry_pathname(entry), archive_error_string(a));
			} else
			if (r != ARCHIVE_OK) {
				fprintf(stderr, "(EE) VFSCopyToLocal: error occured while reading archive: '%s'\n", archive_error_string(a));
				Result = cVFS_Failed;
				break;
			}

//			printf ("found file: %s, mode = %x\n", archive_entry_pathname(entry), archive_entry_mode(entry));
			const char *ssrc = src;
			const char *asrc = archive_entry_pathname(entry);
			if (IS_DIR_SEP(*ssrc))  ssrc++;
			if (IS_DIR_SEP(*asrc))  asrc++;

//			printf ("strcmp: '%s' vs. '%s'\n", ssrc, asrc);
			if (strcmp(ssrc, asrc) == 0) {
//				printf("--> found file, extracting\n");
				fprintf(stderr, "(II) VFSCopyToLocal: extract_file_path(sDstName) = '%s', extract_file_name(sDstName) = '%s' \n", extract_file_path(sDstName), extract_file_name(sDstName));
				found = TRUE;

				Result = my_archive_read_data_into_fd(globs, a, entry, sDstName, globs->block_size, Append);
				break;
			}
		}
		archive_read_close(a);
  }
  archive_read_finish(a);

  if ((! found) && Result == cVFS_OK) {
    fprintf(stderr, "(EE) VFSCopyToLocal: file not found in archive.\n");
    Result = cVFS_ReadErr;
  }

  fprintf(stderr, "(II) VFSCopyToLocal: finished. \n");
  return Result;
}

TVFSResult VFSCopyFromLocal(struct TVFSGlobs *globs, const char *sSrcName, const char *sDstName, int Append)
{
  printf("(WW) VFSCopyFromLocal: Not supported in libarchive plugin.\n");
  return cVFS_Not_Supported;
}




/**********
 * TODO:
 *
 *   - UTF-8, FName/FDisplayName and absolute/relative paths revision needed!
 *   - archive testing (needs new VFS API)
 *   - write support
 *   - support creating new archives (needs new VFS API)
 *
 ***/
