/* $Id: syncdb.c,v 1.14 2002/03/10 21:31:59 richdawe Exp $ */

/*
 *  syncdb.c - Database synchronisation routines for zippo
 *  Copyright (C) 1999-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 <limits.h>
#include <errno.h>
#include <assert.h>

/* libzippo includes */
#include <libzippo/djgpp.h>
#include <libzippo/package.h>
#include <libzippo/packlist.h>
#include <libzippo/dsm.h>
#include <libzippo/mft.h>
#include <libzippo/log.h>
#include <libzippo/util.h>

#include "zippo.h"
#include "syncdb.h"
#include "md5hash.h"
#include "base64.h"

/* We may want to install/upgrade the djgpp-dsms package, to get
 * a list of the latest available DJGPP packages. */
#include "install.h"
#include "upgrade.h"

static const char DJGPP_DSMS_PACKAGE[] = "djgpp-dsms";

/* Default DSMs for different platforms */
extern char *platform_dsm_table[];
extern char *platform_dsm_name_table[];

/* Forward declarations */
static int update_djgpp_dsms (PACKAGE_INFO *packages,
			      const ZIPPO_SYNCDB *req_syncdb);

static int sync_from_manifests (const char *db_path,
				char **mft_path,
				char **dsm_path_avail,
				PACKAGE_INFO *mft_packages,
				PACKAGE_INFO *dsm_packages,
				PACKAGE_INFO *dsm_packages_avail);

static int update_dsms (const char *db_path,
			char **mft_path,
			char **dsm_path_avail,
			PACKAGE_INFO *dsm_packages,
			PACKAGE_INFO *dsm_packages_avail);

static int update_platform_dsms (const char *db_path,
				 PACKAGE_INFO *dsm_packages);

/* ------------------
 * - perform_syncdb -
 * ------------------ */

/*
 * TODO: Should this also detect when packages have been removed?
 * I think so.
 */

int
perform_syncdb (const ZIPPO_SYNCDB *req)
{
  DSM_FILE_ERROR *dfe                = NULL;	/* DSM errors (main) */
  DSM_FILE_ERROR *dfe_avail          = NULL;	/* DSM errors (av.)  */
  DSM_FILE_ERROR *ndfe               = NULL;	/* DSM errors (av.)  */
  PACKAGE_INFO   *package            = NULL;
  PACKAGE_INFO   *mft_packages       = NULL;
  PACKAGE_INFO   *dsm_packages       = NULL;
  PACKAGE_INFO   *dsm_packages_avail = NULL;
  int             count              = 0;
  char            db_path[PATH_MAX];

  /* Get the package descriptions */
  mft_packages = ver_load_all((const char **) req->mft_path, NULL);
  dsm_packages = dsm_load_all((const char **) req->dsm_path, NULL, &dfe);
	
  /* 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;

  /* Check to see if we can install or upgrade the djgpp-dsms package
   * from one of the archive directories. */
  if (!update_djgpp_dsms(dsm_packages, req)) {
    /* Tidy up */
    packlist_free(mft_packages);
    packlist_free(dsm_packages);

    die("Unable to check for a new version of djgpp-dsms package!");
  }

  /* Regenerate package list from installed DSMs, because the DSM
   * for djgpp-dsms might have been updated by update_djgpp_dsms(). */
  packlist_free(dsm_packages);

  dsm_packages = dsm_load_all((const char **) req->dsm_path, NULL, &dfe);

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

  /* Load DSMs from the available packages database. Load also from
   * the manifest paths, to catch installed packages. */
  dsm_packages_avail = package
    = dsm_load_all((const char **) req->dsm_path_avail, NULL, &dfe_avail);

  if (package != NULL) {
    dsm_packages_avail = dsm_load_all((const char **) req->mft_path,
				      package, &dfe_avail);

    /* Tidy up original list in failure case. */
    if (dsm_packages_avail == NULL)
      packlist_free(package);

    package = NULL;
  }  

  /* Display DSM errors in available DSMs. */
  if (dfe_avail != NULL)
    warn("Parsing failed for some DSM(s) in available database");
	
  for (ndfe = dfe_avail; ndfe != NULL; ndfe = ndfe->q_forw) {
    dsm_perror(ndfe->name, ndfe->de);
  }
	
  dsm_free_file_error_list(dfe_avail);
  dfe_avail = NULL;
	
  /* NB: dsm_packages == NULL is OK, because it means there are no
   * packages in the database yet. */
  if ((mft_packages == NULL) || (dsm_packages_avail == NULL)) {
    /* Tidy up */
    if (mft_packages != NULL)
      packlist_free(mft_packages);

    if (dsm_packages != NULL)
      packlist_free(dsm_packages);
    
    if (dsm_packages_avail != NULL)
      packlist_free(dsm_packages_avail);

    /* TODO: Error message */
    return(EXIT_FAILURE);
  }  
	
  /* Build the DSM database path */
  strcpy(db_path, req->root);
  addforwardslash(db_path);
  strcat(db_path, ZIPPO_DB_PREFIX);
  addforwardslash(db_path);

  /* Are there any manifests to convert? */
  for (package = mft_packages;
       package != NULL;
       package = (PACKAGE_INFO *) package->q_forw) {
    if (strlen(package->name) > 0)
      count++;
  }

  if (count == 0) {
    info("No manifest-style packages need converting.\n");
  } else {
    /* Convert manifests */
    if (!sync_from_manifests(db_path, req->mft_path, req->dsm_path_avail,
			     mft_packages, dsm_packages, dsm_packages_avail)) {
      /* Tidy up */
      packlist_free(mft_packages);
      packlist_free(dsm_packages);
      packlist_free(dsm_packages_avail);

      die("Unable to create DSM(s) from manifest!");
    }
  }

  /* Update any installed DSMs */
  if (!update_dsms(db_path, req->mft_path, req->dsm_path_avail,
		   dsm_packages, dsm_packages_avail)) {
    /* Tidy up */
    packlist_free(mft_packages);
    packlist_free(dsm_packages);
    packlist_free(dsm_packages_avail);

    die("Unable to create/update DSM(s) for installed package(s)!");
  }

  /* Update any platform DSMs */
  if (!update_platform_dsms(db_path, dsm_packages)) {
    /* Tidy up */
    packlist_free(mft_packages);
    packlist_free(dsm_packages);
    packlist_free(dsm_packages_avail);

    die("Unable to update platform DSM(s)!");
  }

  /* Regenerate package list from installed DSMs, because it might have
   * been updated by the above code. */
  packlist_free(dsm_packages);

  dsm_packages = dsm_load_all((const char **) req->dsm_path, NULL, &dfe);

  /* 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;
	
  /* Create MD5 hashes of all files from a package, if necessary. */
  for (package = dsm_packages;
       package != NULL;
       package = (PACKAGE_INFO *) package->q_forw) {
    /* Skip virtual, group packages. */
    if (   (package->version.type == TYPE_NONE)
	|| (package->version.type == TYPE_VIRTUAL)
	|| (package->version.type == TYPE_GROUP))
      continue;

    /* TODO: This should use the package's prefix, not the global
     * root. */
    if (md5hash(package, db_path, req->root,
		(const char **) req->mft_path) == -1) {
      if (errno != EEXIST) {
	warnf("Error generating MD5 hashes for %s %s",
	      package->name, package_version_string(&package->version));
	perror("syncdb");
      }
    } else {
      printf(". Created MD5 hashes for files from %s %s\n",
	     package->name, package_version_string(&package->version));
    }
  }

  /* Free memory */
  packlist_free(dsm_packages);
  packlist_free(dsm_packages_avail);

  /* Always succeeds, unless something fatal happens. */
  return(EXIT_SUCCESS);
}

/* ---------------------
 * - update_djgpp_dsms -
 * --------------------- */

/*
 * The package 'djgpp-dsms' is a special package that is built by
 * the script 'scripts/pkgdsms.sh'. The package contains DSMs for all
 * the packages available from the DJGPP archive on Simtel.NET and
 * its mirrors. Every time a DJGPP package is uploaded to the archive,
 * the package is rebuilt.
 *
 * We can use install or upgrade to get the latest version of it. It's
 * version number contains the date, e.g. 2002.01.13.
 */

static int
update_djgpp_dsms (PACKAGE_INFO *packages, const ZIPPO_SYNCDB *req_syncdb)
{
  ZIPPO_INSTALL  req_install;
  ZIPPO_UPGRADE  req_upgrade;
  PACKAGE_INFO  *package         = NULL;
  PACKAGE_INFO  *dd_package      = NULL; /* Current djgpp-dsms package */  
  PACKAGE_INFO  *new_dd_packages = NULL;
  PACKAGE_INFO  *new_dd_package  = NULL; /* Replacement djgpp-dsms package */
  DSM_ERROR     *de              = NULL;
  DSM_ERROR     *nde             = NULL;
  glob_t         glob_local;
  int            need_upgrade    = 0;
  int            ret             = 0;
  int            i;

  for (package = packages; package != NULL; package = package->q_forw) {
    if (strcmp(package->name, DJGPP_DSMS_PACKAGE) == 0) {
      dd_package   = package;
      need_upgrade = 1;
      break;
    }
  }

  /* Find the djgpp-dsms package in the ZIP paths. */
  /* TODO: glob_in_paths with suffixes */
  if (glob_in_paths_with_suffixes("ds*.zip",
				  (const char **) req_syncdb->zip_path,
				  djgpp_archive_prefixes,
				  0, NULL, &glob_local)) {
    /* TODO: Error */
    ;
  }

  /* Parse the DSMs of the djgpp-dsm packages, so we can get the latest. */
  for (i = 0; i < glob_local.gl_pathc; i++) {
    new_dd_package = malloc(sizeof(*new_dd_package));
    if (new_dd_package == NULL) {
      /* TODO: Warning? */
      break;
    }

    if (new_dd_packages == NULL)
      new_dd_packages = new_dd_package;
    else
      packlist_add(new_dd_packages, new_dd_package);

    if (   dsm_get_and_parse(glob_local.gl_pathv[i], new_dd_package, NULL, &de)
	!= DSM_OK) {
      /* If there were any parsing errors, display them. */
      for (nde = de; nde != NULL; nde = (DSM_ERROR *) nde->q_forw) {
	dsm_perror(glob_local.gl_pathv[i], nde);
      }

      dsm_free_error_list(de);
      de = NULL;
    } else {
      /* Store the package's archive filename. */
      strcpy(new_dd_package->path, glob_local.gl_pathv[i]);
    }
  }

  globfree(&glob_local);

  /* Nothing to do, so just return. */
  if (new_dd_packages == NULL)
    return(1);

  /* Do we have the current version? If so, nothing to do. */
  if (   (dd_package != NULL)
      && (package_vercmp(&dd_package->version,
			 &new_dd_package->version) >= 0)) {
    /* TODO: Verbosity level? */
    infof("No later version of %s package found", DJGPP_DSMS_PACKAGE);

    infof("Current is %s %s",
	  dd_package->name,
	  package_version_string(&dd_package->version));

    infof("Found %s %s",
	  new_dd_package->name,
	  package_version_string(&new_dd_package->version));

    /* Tidy up */
    packlist_free(new_dd_packages);

    return(1);
  }

  /* Install/upgrade djgpp-dsms */
  if (!need_upgrade) {
    /* Install djggp-dsms */
    req_install.op             = OP_INSTALL;
    req_install.mod            = IM_NONE;
    req_install.verbosity      = req_syncdb->verbosity;
    req_install.root           = req_syncdb->root;
    req_install.prefix         = req_syncdb->prefix;
    req_install.backup_prefix  = req_syncdb->backup_prefix;
    req_install.name           = new_dd_package->path;
    req_install.dsm_path       = req_syncdb->dsm_path;
    req_install.mft_path       = req_syncdb->mft_path;
    req_install.dsm_path_avail = NULL;
    req_install.zip_path       = req_syncdb->zip_path;
    req_install.tar_gzip_path  = req_syncdb->tar_gzip_path;
    req_install.tar_bzip2_path = req_syncdb->tar_bzip2_path;

    ret = perform_install(&req_install);
  } else {
    /* Upgrade djggp-dsms */
    req_upgrade.op             = OP_UPGRADE;
    req_upgrade.mod            = UPM_NONE;
    req_upgrade.verbosity      = req_syncdb->verbosity;
    req_upgrade.root           = req_syncdb->root;
    req_upgrade.prefix         = req_syncdb->prefix;
    req_upgrade.backup_prefix  = req_syncdb->backup_prefix;
    req_upgrade.name           = new_dd_package->path;
    req_upgrade.dsm_path       = req_syncdb->dsm_path;
    req_upgrade.mft_path       = req_syncdb->mft_path;
    /*req_upgrade.dsm_path_avail = NULL;*/
    req_upgrade.zip_path       = req_syncdb->zip_path;
    req_upgrade.tar_gzip_path  = req_syncdb->tar_gzip_path;
    req_upgrade.tar_bzip2_path = req_syncdb->tar_bzip2_path;

    ret = perform_upgrade(&req_upgrade);
  }

  /* Tidy up */
  packlist_free(new_dd_packages);

  return(ret == EXIT_SUCCESS ? 1 : 0);
}

/* -----------------------
 * - sync_from_manifests -
 * ----------------------- */

static int
sync_from_manifests (const char *db_path,
		     char **mft_path,
		     char **dsm_path_avail,
		     PACKAGE_INFO *mft_packages,
		     PACKAGE_INFO *dsm_packages,
		     PACKAGE_INFO *dsm_packages_avail)
{
  PACKAGE_INFO *package  = NULL;
  PACKAGE_INFO *package2 = NULL;
  char         *dsm      = NULL; /* DSM text */
  char         *dsm_file = NULL;
  FILE         *fp       = NULL;
  char          new_dsm_file[PATH_MAX];
  char          temp_file[PATH_MAX];
  int           found    = 0;

  /* Go through manifest list and look for matching DSMs. */
  for (package = mft_packages;
       package != NULL;
       package = (PACKAGE_INFO *) package->q_forw) {
    found = 0;

    /* Skip already installed DSMs. Normally a package list would
     * not include duplicates, because we would have run
     * packlist_dedupe() on it, but here it's all under our
     * control. */
    for (package2 = dsm_packages;
	 package2 != NULL;
	 package2 = (PACKAGE_INFO *) package2->q_forw) {
      if (strlen(package2->name) == 0) continue;

      if (strcasecmp(package->manifest, package2->manifest) == 0) {
	found = 1;
	break;
      }
    }

    if (found)
      continue;

    /* Available DSMs */
    for (package2 = dsm_packages_avail;
	 package2 != NULL;
	 package2 = (PACKAGE_INFO *) package2->q_forw) {
      if (strcasecmp(package->manifest, package2->manifest) == 0) {
	found = 1;

	/* Status message */
	printf(". Matched package: %s -> %s %s\n",
	       package->name,
	       package2->name,
	       package_version_string(&package2->version));

	break;
      }
    }

    if (!found) {
      /* Failed to match - move onto next */
      warnf("Unmatched package: '%s'", package->name);
      continue;
    }

    /* For the matched manifest -> DSM mapping, copy the DSM to
     * the DSM database. Find the DSM case insensitively. */
    strcpy(temp_file, package2->dsm_name);
    strcat(temp_file, ".dsm");

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

    if (dsm_file == NULL) {
      /* Look in the manifest directory too */
      dsm_file = find_in_paths(temp_file, (const char **) mft_path, 0);
    }

    if (dsm_file == NULL) {
      warnf("Unable to find DSM file '%s'", temp_file);
      continue;
    }

    dsm = read_text_file_to_memory(dsm_file);

    if (dsm == NULL) {
      warnf("Unable to read DSM file '%s'", dsm_file);
      continue;
    }

    strcpy(new_dsm_file, db_path);
    strcat(new_dsm_file, temp_file);

    fp = fopen(new_dsm_file, "wt");
    if (fp == NULL) {
      free(dsm);
      packlist_free(mft_packages);
      packlist_free(dsm_packages);
      packlist_free(dsm_packages_avail);
      die("Unable to create DSM for installed package!");
    }

    if (fwrite(dsm, strlen(dsm), 1, fp) <= 0) {
      free(dsm);
      packlist_free(mft_packages);
      packlist_free(dsm_packages);
      packlist_free(dsm_packages_avail);
      die("Unable to write DSM for installed package!");
    }

    free(dsm);
    fclose(fp);

    printf(". Created DSM for %s %s\n",
	   package2->name, package_version_string(&package2->version));
  }

  return(1);
}

/* ---------------
 * - update_dsms -
 * --------------- */

static int
update_dsms (const char *db_path,
	     char **mft_path,
	     char **dsm_path_avail,
	     PACKAGE_INFO *dsm_packages,
	     PACKAGE_INFO *dsm_packages_avail)
{
  PACKAGE_INFO *package  = NULL;
  PACKAGE_INFO *package2 = NULL;
  char         *dsm      = NULL; /* DSM text */
  char         *dsm_file = NULL;
  FILE         *fp       = NULL;
  char new_dsm_file[PATH_MAX];
  char temp_file[PATH_MAX];
  int found = 0;

  /* Go through the installed DSM list and see if any of the DSMs have
   * been updated. */
  for (package = dsm_packages;
       package != NULL;
       package = (PACKAGE_INFO *) package->q_forw) {
    /* Compare with entried from available DSM list. */
    found = 0;
		
    for (package2 = dsm_packages_avail;
	 package2 != NULL;
	 package2 = (PACKAGE_INFO *) package2->q_forw) {
      /* String-sensitive comparison on name */
      if (strcmp(package->name, package2->name) != 0)
	continue;

      /* Version comparison */
      if (package_vercmp(&package->version,
			 &package2->version) != 0)
	continue;

      /* Type comparison */
      if (package->version.type != package2->version.type)
	continue;

      /* DSM file version */
      if (package_vercmp(&package->dsm_file_version,
			 &package2->dsm_file_version) >= 0) {
	/* We have the same or an older DSM version,
	 * so continue searching. */
	/* TODO: Should this abort here? Does the
	 * package list take dsm-file-version into
	 * account? */
	continue;
      }

      /* Set found flag, so DSM will get updated. */
      found = 1;
      break;
    }

    if (!found)
      continue;

    /* For the matched DSM, copy the new DSM to the DSM database.
     * Find the DSM case insensitively. */
		
    /* TODO: This code assumes the dsm-name has not changed between
     * versions of the dsm-file. Is this a reasonable
     * restriction? */
    strcpy(temp_file, package2->dsm_name);
    strcat(temp_file, ".dsm");
    dsm_file = find_in_paths(temp_file, (const char **) dsm_path_avail, 0);

    if (dsm_file == NULL) {
      /* Look in the manifest directory too */
      dsm_file = find_in_paths(temp_file, (const char **) mft_path, 0);
    }

    if (dsm_file == NULL) {
      warnf("Unable to find DSM file '%s'", temp_file);
      continue;
    }

    dsm = read_text_file_to_memory(dsm_file);

    if (dsm == NULL) {
      warnf("Unable to read DSM file '%s'", dsm_file);
      continue;
    }

    strcpy(new_dsm_file, db_path);
    strcat(new_dsm_file, temp_file);

    /* Write out the DSM. */
    fp = fopen(new_dsm_file, "wt");
    if (fp == NULL) {
      free(dsm);
      return(0);      
    }

    if (fwrite(dsm, strlen(dsm), 1, fp) <= 0) {
      free(dsm);
      return(0);
    }

    free(dsm);
    fclose(fp);

    printf(". Updated DSM for %s %s\n",
	   package2->name, package_version_string(&package2->version));
  }

  return(1);
}

/* ------------------------
 * - update_platform_dsms -
 * ------------------------ */

static int
update_platform_dsms (const char *db_path, PACKAGE_INFO *dsm_packages)
{
  PACKAGE_INFO *package = NULL;
  PACKAGE_INFO  plat_package; /* Parsed platform DSM */
  DSM_ERROR    *de      = NULL;
  DSM_ERROR    *nde     = NULL;
  char          path[PATH_MAX];
  FILE         *fp      = NULL;
  int           found   = 0;
  int           i;

  for (i = 0; platform_dsm_table[i] != NULL; i++) {
    assert(platform_dsm_name_table[i] != NULL);

    /* Parse the platform DSM, so we can do version comparisons with
     * the installed package database. */
    if (dsm_parse(platform_dsm_table[i], &plat_package, &de) != DSM_OK) {
      /* Display parsing errors */
      warnf("Parsing failed for platform DSM '%s'",
	    platform_dsm_name_table[i]);

      for (nde = de; nde != NULL; nde = nde->q_forw) {
	dsm_perror(platform_dsm_name_table[i], nde);
      }

      dsm_free_error_list(de);
      de = NULL;

      warnf("Platform DSM '%s' skipped", platform_dsm_name_table[i]); 
      continue;
    }

    /* Check each installed package */
    for (found = 0, package = dsm_packages;
	 package != NULL;
	 package = package->q_forw) {
      /* Match on name and version */
      if (strcmp(package->name, plat_package.name) != 0)
	continue;

      if (package_vercmp(&package->version, &plat_package.version) != 0)
	continue;

      if (package_vercmp(&package->dsm_file_version,
			 &plat_package.dsm_file_version) >= 0)
	continue;

      /* Matched */
      found = 1;
      break;
    }

    if (found) {
      /* Update the platform DSM */
      printf(". Updating platform DSM for %s %s\n",
	   plat_package.name, package_version_string(&plat_package.version));

      strcpy(path, db_path);
      strcat(path, platform_dsm_name_table[i]);
      strcat(path, ".dsm");

      fp = fopen(path, "wt");
      if (fp == NULL) {
	warnf("Unable to write DSM for platform '%s'!",
	      platform_dsm_name_table[i]);
      } else {
	/* Write new DSM */
	fputs(platform_dsm_table[i], fp);
	fclose(fp);
      }
    }
  }

  return(1);
}
