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

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

/*
 * TODO: Support upgrades from the available database using '-U -A'
 * like with '-i -A'.
 */

#include "common.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.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 "zippo.h"
#include "upgrade.h"
#include "uinstall.h"
#include "install.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 -
 * ------------------- */

int
perform_upgrade (const ZIPPO_UPGRADE *req)
{
  PACKAGE_INFO    *packages    = NULL;
  PACKAGE_INFO    *package     = NULL; /* Pointer for traversing lists.    */
  PACKAGE_INFO    *oldpackages = NULL; /* List of packages being replaced. */
  PACKAGE_INFO    *oldpackage  = NULL;
  PACKAGE_INFO     newpackage;
  DSM_FILE_ERROR  *dfe         = NULL; /* List of err's in main DSM parse. */
  DSM_FILE_ERROR  *ndfe        = NULL;
  DSM_ERROR       *de          = NULL; /* List of err's in new DSM parse.  */
  DSM_ERROR       *nde         = NULL;  
  md5hash_entry  **list        = NULL;
  char db_path[PATH_MAX];
  char *dsm = NULL;                   /* DSM read into memory      */
  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;

  /* 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) {
    /* This is not an error, because upgrading a package that isn't installed
     * is equivalent to doing an install. */
    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);
  dfe = NULL;

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

  if (dsm_get_and_parse(req->name, &newpackage, &dsm, &de) != DSM_OK) {
    /* If there were any parsing errors, display them. */
    for (nde = de; nde != NULL; nde = (DSM_ERROR *) nde->q_forw) {
      dsm_perror(req->name, nde);
    }

    /* Tidy up */
    dsm_free_error_list(de);
    de = NULL;

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

  /* 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;
  }

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

  /* 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 (strcmp(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;
    }
  }

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

    /* 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);

    /* Tidy up */
    free(dsm);
    packlist_free(packages);
    
    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? ");

    keypress = 0;
    while (keypress == 0) {
      keypress = getch();

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

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

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

      /* Bail */
      dief("Removal of %s %s failed",
	   oldpackage->name, package_version_string(&oldpackage->version));
    }
  }

  /* - Install - */

  if (!install_extract_archive(&newpackage, req->verbosity, 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));

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

    /* Bail */
    return(EXIT_FAILURE);
  }

  /* Write DSM into db directory. */
  if (!install_dsm(&newpackage, db_path, dsm)) {
    free(dsm);
    packlist_free(packages);
    /* TODO: Better error msg */
    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! */

  /* Free memory */
  free(dsm);
  packlist_free(packages);

  return(ret);
}
