/* $Id: upgrade.c,v 1.19 2002/07/21 18:23:45 richdawe Exp $ */

/*
 *  upgrade.c - Upgrade routines for pakke
 *  Copyright (C) 2001, 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>

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

#include "pakke.h"
#include "upgrade.h"
#include "uinstall.h"
#include "install.h"
#include "misc.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 */

typedef struct {
  int valid;        /* Entry is valid if == 1, else invalid. */
  int broken;       /* Dependency was previously broken */
  PACKAGE_DEP *dep; /* Pointer to dependency data */
} SAVED_PACKAGE_DEP;

/* ----------------------------
 * - perform_upgrade_internal -
 * ---------------------------- */

int
perform_upgrade_internal (PACKAGE_INFO *newpackage,
			  const char *dsm,
			  PACKAGE_INFO *packages,
			  const PAKKE_UPGRADE *req)
{
  PACKAGE_INFO    *package     = NULL; /* Pointer for traversing lists.    */
  PACKAGE_INFO    *oldpackages = NULL; /* List of packages being replaced. */
  PACKAGE_INFO    *oldpackage  = NULL;
  md5hash_entry  **list        = NULL;
  char db_path[PATH_MAX];
  int replacing = 0;                  /* Any packages to replace?
				       * 1 = yes, 0 = no => not upgrade. */
  int newly_broken = 0;               /* Number of newly-broken dependencies
				       * by package upgrade. */
  int dup_action = DUP_QUERY;
  int keypress = 0;
  int remove_action = REMOVE_QUERY; /* Default to querying user */
  int ret = EXIT_SUCCESS;
  int i;

  /* Saved dependencies */
  SAVED_PACKAGE_DEP saved_deps[PACKAGE_MAX_DEP];

  /* For dependency checking need a list of failed deps to report to user. */
#define MAX_FAILED_DEP MAX_PACKAGES
  PACKAGE_DEP *failed_dep[MAX_FAILED_DEP + 1]; /* Leave space for NULL */
  int          failed_dep_max = MAX_FAILED_DEP;
	
  if (packages == NULL) {
    /* This is not an error, because upgrading a package that isn't installed
     * is equivalent to doing an install. */
    warn("No installed packages found");
  }

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

  /* TODO: Allow command-line override. */
  dup_action = newpackage->duplicate_action;

  if (isarchive(req->name))
    strcpy(newpackage->path, req->name);

  /* Currently we only support upgrading binary, source, documentation
   * and virtual packages. */
  switch(newpackage->version.type) {
  case TYPE_BINARIES:
  case TYPE_SOURCES:
  case TYPE_DOCUMENTATION:
  case TYPE_VIRTUAL:
    break;
		
  default:
    dief("Package type '%s' not supported yet for upgrades!",
	 package_type_string(&newpackage->version));
    break;
  }

  /* Build xrefs between the new package and the package list, to check
   * that it replaces some packages. */
  /* TODO: This doesn't cope with upgrading to a lower version
   * (downgrading). */
  package_xref(packages, newpackage);

  for (i = 0; newpackage->deps[i] != NULL; i++) {
    if (newpackage->deps[i]->dep_type != PACKAGE_DEP_REPLACES)
      continue;

    if (newpackage->deps[i]->dep != NULL) {
      /* Put the package on old package list. */
      oldpackage = newpackage->deps[i]->dep;

      packlist_remove(oldpackage);
      if (oldpackages == NULL)
	oldpackages = oldpackage;
      else
	packlist_add(oldpackages, oldpackage);

      replacing = 1;
    }
  }

  if (!replacing) {
    infof("No packages replaced by %s %s - installing",
	  newpackage->name, package_version_string(&newpackage->version));
  } else {
    printf("- %s %s replaces:\n",
	   newpackage->name, package_version_string(&newpackage->version));

    for (package = oldpackages; package != NULL; package = package->q_forw) {
      printf("%s %s\n",
	     package->name, package_version_string(&package->version));
    }
  }

  /* Does the package already exist in the database? */
  for (package = packages;
       package != NULL;
       package = (PACKAGE_INFO *) package->q_forw) {
    if (package_namecmp(package->name, newpackage->name) != 0)
      continue;
    
    if (package_vercmp(&package->version, &newpackage->version) != 0)
      continue;

    printf("Package %s %s is already installed!\n",
	   newpackage->name, package_version_string(&newpackage->version));
    ret = EXIT_FAILURE;
  }

  /*
   * For each package in packages, check that newpackage will satisfy
   * the dependencies that oldpackages currently does. This is tricky. Here's
   * how we'll do it:
   *
   * For each package in packages (which no longer contains the old packages),
   * save the current dependency information, so we can look for changes.
   * Then use package_xref() to generate xref's to the new package. We then
   * check for broken dependencies due to the new package using
   * package_check_deps().
   *
   * Why do we allow broken dependencies to exist in the database here? Well,
   * the dependencies may not have been satisfied when the packages were
   * installed. If the new package breaks more and/or different dependencies
   * than are currently, then we cannot install it. If the same dependencies
   * are broken, then the dependencies before and after upgrade are
   * effectively the same. 
   */

  /* TODO: This needs a good test - add a test to the test suite. */

  for (package = packages; package != NULL; package = package->q_forw) {
    /* Save status of dependencies */
    memset(&saved_deps, 0, sizeof(saved_deps));

    for (i = 0; package->deps[i] != NULL; i++) {
      saved_deps[i].valid = 1;
      saved_deps[i].dep   = package->deps[i];

      if (package->deps[i]->dep == NULL)
	saved_deps[i].broken = 1;
    }

    /* Generate cross-reference for existing package, using newpackage as
     * the package list. */
    package_xref(newpackage, package);

    for (newly_broken = i = 0; package->deps[i] != NULL; i++) {
      if ((package->deps[i]->dep == NULL) && !saved_deps[i].broken) {
	newly_broken++;
      }
    }

    /* Display newly broken dependencies */
    if (newly_broken) {
      printf("- Upgrade breaks dependencies for %s %s\n",
	     package->name, package_version_string(&package->version));

      for (newly_broken = i = 0; package->deps[i] != NULL; i++) {
	if ((package->deps[i]->dep == NULL) && saved_deps[i].broken) {
	  /* Skip previously broken deps */
	  continue;
	}

	/* Display dep */
	printf("%s %s\n",
	       package_dep_type_string(package->deps[i]->dep_type),
	       package_dep_string(package->deps[i]));
      }

      /* Fail on newly broken dependencies. */
      ret = EXIT_FAILURE;
    }
  }

  /* Check all archives are available */
  if (   !install_get_and_check_archives(newpackage,
					 req->verbosity,
					 req->interactive,
					 req->name,
					 req->download_prefix,
					 (const char **) req->zip_path)
      && (req->mod != UM_TEST)) {
    /* Abort later */
    ret = EXIT_FAILURE;
  }

  /* Show the install warning - confirmation required (see below). */
  if (newpackage->install_warning != NULL)
    printf("*** WARNING ***\n%s\n", newpackage->install_warning);

  /* Bail out now, if the archives are bad or dependencies would be broken. */
  if (ret != EXIT_SUCCESS) {
    /* Bail */
    warn("Problems found with package archives or dependencies- aborting\n");
    return(ret);
  }

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

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

  /* Get confirmation, if there was an install warning. */
  if (newpackage->install_warning != NULL) {
    printf("[c]ontinue or [a]bort? ");

    if (req->interactive) {
      /* Interactive */
      keypress = 0;
      while (keypress == 0) {
	keypress = getch();

	switch(keypress) {
	case 'c': case 'C': case 'a': case 'A':
	  break;

	default:
	  putch('\a');
	  keypress = 0;
	break;
	}
      }
    } else {
      /* Non-interactive => abort by default */
      keypress = 'a';
    }

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

    if ((keypress == 'a') || (keypress == 'A')) {
      printf("Installation of %s %s aborted by user\n",
	     newpackage->name, package_version_string(&newpackage->version));

      /* Free memory */
      packlist_free(packages);

      return(EXIT_FAILURE);
    }
  }

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

    /* Get all the MD5 hash values calculated previously. */
    list = md5hash_get(oldpackage, db_path);
    if (list != NULL) {
      /* Remove hashed files */
      /* TODO: Handle prefix */
      if (!uninstall_md5_hashed_files(oldpackage,
				      req->verbosity,
				      req->interactive,
				      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(oldpackage,
				    req->verbosity,
				    req->interactive,
				    req->prefix,
				    req->backup_prefix,
				    (const char **) req->mft_path,
				    remove_action)) {
	ret = EXIT_FAILURE;
      }
    }

    /* Remove MD5 hash & DSM files from the package database. */
    if (!uninstall_tidy_up(oldpackage, db_path))
      ret = EXIT_FAILURE;

    if (ret == EXIT_FAILURE) {
      /* TODO: How to leave package db in non-broken state? */
      
      /* Bail */
      dief("Removal of %s %s failed",
	   oldpackage->name, package_version_string(&oldpackage->version));
    }
  }

  /* - Install - */

  if (!install_extract_archive(newpackage,
			       req->verbosity,
			       req->interactive,
			       dup_action,
			       req->name,
			       req->prefix,
			       req->backup_prefix,
			       (const char **) req->zip_path)) {
    printf("Installation of %s %s failed\n",
	     newpackage->name, package_version_string(&newpackage->version));

    /* Bail */
    return(EXIT_FAILURE);
  }

  /* Write DSM into db directory. */
  if (!install_dsm(newpackage, db_path, dsm)) {
    /* TODO: Better error msg */
    /* TODO: Recover better! */
    dief("Unable to write DSM for %s %s!",
	 newpackage->name, package_version_string(&newpackage->version));
  } else {
    printf(". Created DSM for %s %s\n",
	   newpackage->name, package_version_string(&newpackage->version));
  }

  /* Generate MD5 hashes for the installed files. */
  if (md5hash(newpackage, db_path, req->prefix,
	      (const char **) req->mft_path) == -1) {
    warnf("Error generating MD5 hashes for %s %s",
	  newpackage->name, package_version_string(&newpackage->version));
    perror("install");
  } else {
    printf(". Created MD5 hashes for files from %s %s\n",
	   newpackage->name, package_version_string(&newpackage->version));
  }

  /* Create entries in the info directory for any info files. */
  if (!install_info_entries(newpackage, db_path, req->prefix)) {
    warnf("Error creating info directory entries for %s %s",
	  newpackage->name, package_version_string(&newpackage->version));
  }

  /* Done! */
  return(ret);
}

/* -------------------
 * - perform_upgrade -
 * ------------------- */

int
perform_upgrade (const PAKKE_UPGRADE *req)
{
  PACKAGE_INFO    *packages    = NULL;
  PACKAGE_INFO     newpackage;
  DSM_FILE_ERROR  *dfe         = NULL; /* List of err's in main DSM parse. */
  DSM_ERROR       *de          = NULL; /* List of err's in new DSM parse. */
  char            *dsm         = NULL; /* DSM read into memory */
  int              ret;

  /* 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);
	
  packages = packlist_dedupe(packages);
  packlist_xref(packages);

  /* Display DSM errors. */
  if (dsm_file_errors_fatal(dfe))
    warn("Parsing failed for some DSM(s) in installed database");

  show_dsm_file_errors(dfe, req->verbosity);
  dsm_free_file_error_list(dfe);
  dfe = NULL;

  /* Parse the package's DSM */
  if (dsm_get_and_parse(req->name, &newpackage, &dsm, &de) != DSM_OK) {
    /* If there were any parsing errors, display them. */
    show_dsm_errors(req->name, de, req->verbosity);

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

    dsm_free_error_list(de);
    de = NULL;

    /* Can't continue without a valid DSM. */
    die("Unable to parse DSM!");
  }

  /* Now upgrade */
  ret = perform_upgrade_internal(&newpackage, dsm, packages, req);

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

  return(ret);
}

/* -------------------------
 * - perform_upgrade_avail -
 * ------------------------- */

int
perform_upgrade_avail (const PAKKE_UPGRADE *req)
{
  PACKAGE_INFO    *packages       = NULL;
  PACKAGE_INFO    *packages_avail = NULL;
  PACKAGE_INFO    *newpackage     = NULL;
  PACKAGE_INFO   **matched        = NULL;
  DSM_FILE_ERROR  *dfe            = NULL; /* List of err's in main DSM parse */
  char             temp_file[PATH_MAX];
  char            *dsm_file;
  char            *dsm            = NULL; /* DSM read into memory */  
  int i;
  int ret;

  /* 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);
	
  packages = packlist_dedupe(packages);
  packlist_xref(packages);
	
  if (packages == NULL)
    warn("No installed packages found");

  /* Display DSM errors. */
  if (dsm_file_errors_fatal(dfe))
    warn("Parsing failed for some DSM(s) in installed database");

  show_dsm_file_errors(dfe, req->verbosity);
  dsm_free_file_error_list(dfe);
  dfe = NULL;

  /* Get a list of available packages */
  packages_avail = dsm_load_all((const char **) req->dsm_path_avail,
				NULL, &dfe);
	
  packages_avail = packlist_dedupe(packages_avail);
	
  if (packages_avail == NULL)
    warn("No available packages found");

  /* Display DSM errors. */
  if (dsm_file_errors_fatal(dfe))
    warn("Parsing failed for some DSM(s) in available database");

  show_dsm_file_errors(dfe, req->verbosity);
  dsm_free_file_error_list(dfe);
  dfe = NULL;

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

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

    /* Tidy up */
    packlist_free(packages);
    packlist_free(packages_avail);
    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);

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

  newpackage = matched[0];

  /* Get the new package's DSM */
  strcpy(temp_file, newpackage->dsm_name);
  strcat(temp_file, ".dsm");

  dsm_file = find_in_paths(temp_file, (const char **) req->dsm_path_avail, 0);

  if (dsm_file == NULL) {
    /* Tidy up */
    free(matched);
    packlist_free(packages);
    packlist_free(packages_avail);

    dief("Unable to find DSM file '%s'", temp_file);
  }

  dsm = read_text_file_to_memory(dsm_file);

  if (dsm == NULL) {
    /* Tidy up */
    free(matched);
    packlist_free(packages);
    packlist_free(packages_avail);

    dief("Unable to read DSM file '%s'", dsm_file);
  }

  /* Now install */
  ret = perform_upgrade_internal(newpackage, dsm, packages, req);

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

  return(ret);
}
