/* $Id: uinstall.c,v 1.21 2002/03/03 20:57:15 richdawe Exp $ */

/*
 *  uinstall.c - Uninstall routines for zippo
 *  Copyright (C) 2000-2002 by Richard Dawe
 *      
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "common.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>

/* libzippo includes */
#include <libzippo/package.h>
#include <libzippo/packlist.h>
#include <libzippo/packdep.h>
#include <libzippo/dsm.h>
#include <libzippo/mft.h>
#include <libzippo/util.h>
#include <libzippo/backup.h>

#include "zippo.h"
#include "uinstall.h"
#include "md5hash.h"

#ifdef HAVE_CONIO_H
#include <conio.h>
#else /* !HAVE_CONIO_H */
/* This hack needs testing! How do you flush input stream before read? */
#define getch getchar
#define putch putchar
#endif /* HAVE_CONIO_H */

/* ---------------------
 * - perform_uninstall -
 * --------------------- */

/* TODO: How to cope with removing empty directories? */

int
perform_uninstall (const ZIPPO_UNINSTALL *req)
{
  PACKAGE_INFO    *packages = NULL;
  PACKAGE_INFO    *package  = NULL;
  PACKAGE_INFO   **matched  = NULL;
  DSM_FILE_ERROR  *dfe      = NULL; /* List of err's in main DSM parse. */
  DSM_FILE_ERROR  *ndfe     = NULL;
  md5hash_entry  **list     = NULL;
  char db_path[PATH_MAX];

  /* For dependency checking need a list of failed deps to report to user. */
#define MAX_MATCHED_DEP MAX_PACKAGES
  PACKAGE_INFO *matched_package[MAX_MATCHED_DEP + 1]; /* Space for NULL */
  PACKAGE_DEP  *matched_dep[MAX_MATCHED_DEP + 1]; /* Space for NULL */
  int           matched_dep_max = MAX_MATCHED_DEP;

  int remove_action = REMOVE_QUERY; /* Default to querying user */
  int ret           = EXIT_SUCCESS; /* Default to successful */
  int i;

  /* Get a list of installed packages */
  packages = dsm_load_all((const char **) req->dsm_path, NULL, &dfe);
  packages = ver_load_all((const char **) req->mft_path, packages);
	
  packlist_dedupe(packages);
  packlist_xref(packages);
	
  if (packages == NULL)
    warn("No installed packages found");

  /* Display DSM errors. */
  if (dfe != NULL)
    warn("Parsing failed for some DSM(s) in installed database");
	
  for (ndfe = dfe; ndfe != NULL; ndfe = ndfe->q_forw) {
    dsm_perror(ndfe->name, ndfe->de);
  }
	
  dsm_free_file_error_list(dfe);

  /* Build the DSM database path */
  strcpy(db_path, req->root);
  addforwardslash(db_path);
  strcat(db_path, ZIPPO_DB_PREFIX);
  addforwardslash(db_path);

  /* Find the package in the package list. */  
  matched = packlist_find(packages, req->name,
			  PACKLIST_FIND_SIMPLE|PACKLIST_FIND_USER);

  if ((matched == NULL) || (*matched == NULL)) {
    /* No matches */
    warnf("No package was found matching '%s'!", req->name);

    packlist_free(packages);
    return(EXIT_FAILURE);
  }

  /* List matched packages */
  for (i = 0; matched[i] != NULL; i++) {
    printf(". Matched package: %s %s\n",
	   matched[i]->name, package_version_string(&matched[i]->version));
  }

  /* If there's more than one match, abort. */
  if (matched[1] != NULL) {
    warnf("More than one package matched '%s' - please be more specific!",
	  req->name);

    free(matched);
    packlist_free(packages);
    return(EXIT_FAILURE);
  }

  package = matched[0];

  /* --- Check dependencies --- */

  /* Initialise matched_package, matched_dep arrays */
  for (i = 0; i <= matched_dep_max; i++) {
    matched_package[i] = NULL;
    matched_dep[i] = NULL;
  }

  /* Requires */
  if (package_dep_exists(packages, package, PACKAGE_DEP_REQUIRES,
			 matched_package, matched_dep, matched_dep_max)) {
    printf("--- %s %s required by: ---\n",
	   package->name, package_version_string(&package->version));

    for (i = 0; matched_package[i] != NULL; i++) {
      printf("%s %s %s %s\n",
	     matched_package[i]->name,
	     package_version_string(&matched_package[i]->version),
	     package_dep_type_string(matched_dep[i]->dep_type),
	     package_dep_string(matched_dep[i]));
    }
    printf("\n");
    ret = EXIT_FAILURE;
  }

  /* Depends on */
  if (package_dep_exists(packages, package, PACKAGE_DEP_DEPENDS_ON,
			 matched_package, matched_dep, matched_dep_max)) {
    printf("--- %s %s depended on by: ---\n",
	   package->name, package_version_string(&package->version));

    for (i = 0; matched_package[i] != NULL; i++) {
      printf("%s %s %s %s\n",
	     matched_package[i]->name,
	     package_version_string(&matched_package[i]->version),
	     package_dep_type_string(matched_dep[i]->dep_type),
	     package_dep_string(matched_dep[i]));
    }
    printf("\n");
  }

  /* Install before */
  /* TODO: install-before => uninstall-after? */
  /*if (package_dep_exists(packages, package,
			 PACKAGE_DEP_INSTALL_BEFORE,
			 matched_package, matched_dep, matched_dep_max)) {
    printf("- Install before packages found:\n");
    for (i = 0; matched_package[i] != NULL; i++) {
      printf("%s %s %s %s\n",
	     matched_package[i]->name,
	     package_version_string(&matched_package[i]->version),
	     package_dep_type_string(matched_dep[i]->dep_type),
	     package_dep_string(matched_dep[i]));
    }
    printf("\n");
    ret = EXIT_FAILURE;
    }*/

  /* If we're in test mode, bail out now. */
  if (req->mod == UM_TEST) {    
    /* List changed files */
    uninstall_show_changed_files(package, db_path, req->prefix);

    /* Tidy up */
    free(matched);
    packlist_free(packages);

    info("Uninstall in test-mode complete - exiting\n");
    return(ret);
  }

  /* Bail out now, if dependencies would be broken. */
  if (ret != EXIT_SUCCESS)
    return(ret);

  /* Delete entries from the info directory for any info files. */
  if (!uninstall_info_entries(package, db_path, req->prefix)) {
    warnf("Error deleting info directory entries for %s %s",
	  package->name, package_version_string(&package->version));
  }

  /* Get all the MD5 hash values calculated previously. */
  list = md5hash_get(package, db_path);
  if (list != NULL) {
    /* Remove hashed files */
    /* TODO: Handle prefix */
    if (!uninstall_md5_hashed_files(package, req->verbosity,
				    req->prefix, req->backup_prefix,
				    list, remove_action)) {
      ret = EXIT_FAILURE;
    }

    /* Tidy up */
    md5hash_entries_free(list);
    free(list), list = NULL;
  } else {
    /* Remove unhashed files */
    /* TODO: Handle prefix */
    if (!uninstall_unhashed_files(package, req->verbosity,
				  req->prefix, req->backup_prefix,
				  (const char **) req->mft_path,
				  remove_action))
      ret = EXIT_FAILURE;
  }    

  /* If the previous stages succeeded, then remove MD5 hash & DSM files from
   * the package database. Otherwise, leave the database as it is, because
   * the user may be able to correct the previous reason for failure.*/
  if ((ret == EXIT_SUCCESS) && !uninstall_tidy_up(package, db_path))
    ret = EXIT_FAILURE;

  /* Tidy up */
  free(matched);
  packlist_free(packages);
  
  return(ret);
}

/* ------------------------------
 * - uninstall_md5_hashed_files -
 * ------------------------------ */

int
uninstall_md5_hashed_files (PACKAGE_INFO *package, const int verbosity,
			    const char *prefix, const char *backup_prefix,
			    md5hash_entry **list, const int remove_action)
{
  char        md5_hash[MD5_HASH_SIZE];
  const char *backup_path = NULL;
  char       *unprefixed_path = NULL;
  char        temp_file[PATH_MAX];
  int         action   = remove_action;
  int         remove   = 0;
  int         keypress = 0;
  int         ret      = 0;
  int         i;

  /* Generate name for the backup path */
  backup_path = backup_get_path(package, backup_prefix);

  /* Calculate MD5 hash of each file in the list. */
  for (i = 0; list[i] != NULL; i++) {
    assert(list[i]->filename != NULL);

    /* Build the filename */
    /*strcpy(temp_file, prefix);
      strcat(temp_file, list[i]->filename);*/
    strcpy(temp_file, list[i]->filename);

    /* Does the file still exist? */
    if (access(temp_file, R_OK) != 0) {
      warnf("File '%s' no longer exists", temp_file);
      continue;
    }

    /* If it's a directory, skip it. */
    /* TODO: Remove empty directories later? */
    ret = isdir(temp_file);

    if (ret < 0) {
      warnf("Unable to check that '%s' is a directory (errno = %d): %s",
	    temp_file, errno, strerror(errno));
      continue;
    }

    if (ret) {
      printf(". Keeping directory '%s'\n", temp_file);
      continue;
    }

    /* Calculate & compare MD5 hash */
    if (!md5hash_calculate(temp_file, md5_hash)) {
      warnf("Error calculating MD5 hash for file '%s'", temp_file);
      continue;
    }

    if (memcmp(list[i]->md5hash, md5_hash, MD5_HASH_SIZE) != 0) {      
      warnf("MD5 hash for file '%s' has changed", temp_file);      

      if (remove_action == REMOVE_QUERY) {
	/* Ask for action to be performed */
	printf("'%s' has changed\n", temp_file);
	printf("[r]emove, [b]ackup, [k]eep or [a]bort? ");

	keypress = 0;
	while (keypress == 0) {
	  keypress = getch();
	
	  switch(keypress) {
	  case 'r': case 'R': case 'b': case 'B':
	  case 'k': case 'K': case 'a': case 'A':
	    break;

	  default:
	    putch('\a');
	    keypress = 0;
	    break;
	  }
	}

	printf("%c\n", keypress);

	switch(keypress) {
	case 'r': case 'R':
	  action = REMOVE_REMOVE;
	  break;

	default:
	case 'b': case 'B':
	  action = REMOVE_BACKUP;
	  break;

	case 'k': case 'K':
	  action = REMOVE_KEEP;
	  break;

	case 'a': case 'A':
	  action = REMOVE_ABORT;
	  break;
	}
      } else {
	action = remove_action;
      }

      switch(action) {
      case REMOVE_REMOVE:
	remove = 1;
	break;

      default:
      case REMOVE_BACKUP:
	/* TODO: Sort out prefix handling */
	remove = 1;
	unprefixed_path = temp_file + strlen(prefix);

	ret = backup_file(unprefixed_path, prefix, backup_path);
	if (ret == 0) {
	  printf(". Backed up: '%s%s' -> '%s%s'\n",
		 prefix, unprefixed_path, backup_path, unprefixed_path);
	} else {
	  warnf("Unable to back up '%s%s' - ABORTING",
		prefix, unprefixed_path);
	  return(0);
	}
	break;

      case REMOVE_KEEP:	
	remove = 0;
	break;
      
      case REMOVE_ABORT:
	printf("Uninstallation of %s %s aborted by user\n",
	       package->name, package_version_string(&package->version));
	return(0);
	break;
      }

      if (!remove) {
	/* If we're keeping this file, skip onto next one. */
	printf(". Keeping '%s'\n", temp_file);
	continue;
      }
    }    

    /* Remove the file */
    if (unlink(temp_file) != 0) {
      warnf("Failed to remove file '%s'", temp_file);
      continue;
    } else {
      if (verbosity != V_NORMAL)
	printf(". Removed file '%s'\n", temp_file);
    }
  }

  return(1);
}

/* ----------------------------
 * - uninstall_unhashed_files -
 * ---------------------------- */

int
uninstall_unhashed_files (PACKAGE_INFO *package, const int verbosity,
			  const char *prefix, const char *backup_prefix,
			  const char **mft_path, const int remove_action)
{
  char       **files    = NULL;
  const char  *backup_path = NULL;
  char         temp_file[PATH_MAX];
  int          remove   = 0;
  int          action   = remove_action;
  int          keypress = 0;
  int          ret      = 0;
  int          i        = 0;

  /* Generate name for the backup path */
  backup_path = backup_get_path(package, backup_prefix);

  /* No files array => failure */
  files = mft_get_list(mft_path, package);
  if (files == NULL)
    return(0);

  for (i = 0; files[i] != NULL; i++) {
    /* Build filename */
    strcpy(temp_file, prefix);
    strcat(temp_file, files[i]);

    /* Does the file still exist? */
    if (access(temp_file, R_OK) != 0) {
      warnf("File '%s' no longer exists", temp_file);
      continue;
    }

    /* If it's a directory, skip it. */
    /* TODO: Remove empty directories later? */
    ret = isdir(temp_file);

    if (ret < 0) {
      warnf("Unable to check that '%s' is a directory (errno = %d): %s",
	    temp_file, errno, strerror(errno));
      continue;
    }

    if (ret) {
      printf(". Keeping directory '%s'\n", temp_file);
      continue;
    }

    /* Remove the file */
    if (remove_action == REMOVE_QUERY) {
      /* Ask for action to be performed */
      printf("'%s' will be removed:\n", temp_file);
      printf("[r]emove, [b]ackup, [k]eep or [a]bort? ");

      keypress = 0;
      while (keypress == 0) {
	keypress = getch();
	
	switch(keypress) {
	case 'r': case 'R': case 'b': case 'B':
	case 'k': case 'K': case 'a': case 'A':
	  break;

	default:
	  putch('\a');
	  keypress = 0;
	  break;
	}
      }

      printf("%c\n", keypress);

      switch(keypress) {
      case 'r': case 'R':
	action = REMOVE_REMOVE;
	break;

      default:
      case 'b': case 'B':
	action = REMOVE_BACKUP;
	break;

      case 'k': case 'K':
	action = REMOVE_KEEP;
	break;

      case 'a': case 'A':
	action = REMOVE_ABORT;
	break;
      }
    } else {
      action = remove_action;
    }

    switch(action) {
    case REMOVE_REMOVE:
      remove = 1;
      break;

    default:
    case REMOVE_BACKUP:
      /* TODO: Sort out prefix handling */
      remove = 1;

      ret = backup_file(files[i], prefix, backup_path);
      if (ret == 0) {
	printf(". Backed up: '%s%s' -> '%s%s'\n",
	       prefix, files[i], backup_path, files[i]);
      } else {
	warnf("Unable to back up '%s%s' - ABORTING",
	      prefix, files[i]);
	return(0);
      }
      break;

    case REMOVE_KEEP:	
      remove = 0;
      break;
      
    case REMOVE_ABORT:
      printf("Uninstallation of %s %s aborted by user\n",
	     package->name, package_version_string(&package->version));
      return(0);
      break;
    }

    if (!remove) {
      /* If we're keeping this file, skip onto next one. */
      printf(". Keeping '%s'\n", temp_file);
      continue;
    }

    if (unlink(temp_file) != 0) {
      warnf("Failed to remove file '%s'", temp_file);
      continue;
    } else {
      if (verbosity != V_NORMAL)
	printf(". Removed file '%s'\n", temp_file);
    }
  }

  return(1);
}

/* ---------------------
 * - uninstall_tidy_up -
 * --------------------- */

int
uninstall_tidy_up (PACKAGE_INFO *package, const char *db_path)
{
  char temp_file[PATH_MAX];
  int ret = 1; /* Succeed by default */

  /* No DSM => nothing to do */
  if (!package->has_dsm)
    return(ret);

  /* Remove MD5 hash & DSM files from the package database. Remove the MD5 
   * hash first, because it's less important. */
  strcpy(temp_file, db_path);
  strcat(temp_file, package->dsm_name);
  strcat(temp_file, ".md5");

  if (access(temp_file, R_OK) == 0) {
    if (unlink(temp_file) != 0) {
      warnf("Unable to remove package's MD5 hashes from database - '%s'",
	    temp_file);
      ret = 0;
    } else {
      printf(". Removed MD5 hashes for files from %s %s\n",
	     package->name, package_version_string(&package->version));
    }
  } /* MD5 file may not exist, but that's not an error. */

  strcpy(temp_file, db_path);
  strcat(temp_file, package->dsm_name);
  strcat(temp_file, ".dsm");

  if (access(temp_file, R_OK) != 0) {
    /* Warn the user that the DSM doesn't exist now. */
    warnf("Package's DSM no longer exists - '%s'", temp_file);
  } else {
    if (unlink(temp_file) != 0) {
      warnf("Unable to remove package's DSM from database - '%s'", temp_file);
      ret = 0;
    } else {
      printf(". Removed DSM '%s'\n", temp_file);
    }
  }

  return(ret);
}

/* ----------------------
 * - show_changed_files -
 * ---------------------- */

int
uninstall_show_changed_files (PACKAGE_INFO *package, const char *db_path,
			      const char *prefix)
{
  md5hash_entry **list = NULL;
  char temp_file[PATH_MAX];
  char md5_hash[MD5_HASH_SIZE];
  int i;
  int ret = 1; /* Succeed by default */

  /* Get all the MD5 hash values calculated previously. */
  list = md5hash_get(package, db_path);
  if (list == NULL) {
    /* No MD5 => failed */
    return(0);
  }
  
  /* Calculate MD5 hash of each file in the list. */
  for (i = 0; list[i] != NULL; i++) {
    assert(list[i]->filename != NULL);

    /* Build the filename */
    /*strcpy(temp_file, prefix);
      strcat(temp_file, list[i]->filename);*/
    strcpy(temp_file, list[i]->filename);

    /* Does the file still exist? */
    if (access(temp_file, R_OK) != 0) {
      warnf("File '%s' no longer exists", temp_file);
      continue;
    }

    /* Calculate & compare MD5 hash */
    if (!md5hash_calculate(temp_file, md5_hash)) {
      warnf("Error calculating MD5 hash for file '%s'", temp_file);
      continue;
    }

    if (memcmp(list[i]->md5hash, md5_hash, MD5_HASH_SIZE) != 0) {
      warnf("MD5 hash for file '%s' has changed", temp_file);
      continue;
    }
  }

  /* Tidy up */
  md5hash_entries_free(list);
  free(list), list = NULL;

  return(ret);
}
