/* $Id: impact.c,v 1.27 2009/05/07 21:22:52 imil Exp $ */

/*
 * Copyright (c) 2009 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Emile "iMil" Heitor <imil@NetBSD.org> .
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */

/* pkg_impact() calculates "impact" of installing a package 
 * it is the heart of package install / update
 */

/* pkg_impact rationale
 *
 * pkg_impact() receive a package list as an argument.
 * Those are the packages to be installed / upgraded.
 * 
 * For every package, a dependency tree is loaded,
 * and every dependency is passed to deps_impact()
 * deps_impact() loops through local (installed) packages
 * and matches depencendy over them.
 * If the dependency exists but its version does not
 * satisfy pkg_match(), the package is marked as
 * "to upgrade", meaning it will be deleted (oldpkg) and
 * a new version will be installed (pkgname).
 *
 * Finally, the package itself is passed to deps_impact()
 * to figure out if it needs to be installed or upgraded
 */

#include "pkg_dry.h"
#include "dewey.h"

/* free impact list */
void
free_impact(Impacthead *impacthead)
{
	Pkgimpact *pimpact;

	if (impacthead == NULL)
		return;

	while (!SLIST_EMPTY(impacthead)) {
		pimpact = SLIST_FIRST(impacthead);
		SLIST_REMOVE_HEAD(impacthead, next);
		XFREE(pimpact->depname);
		XFREE(pimpact->oldpkg);
		XFREE(pimpact->pkgname);
		XFREE(pimpact);
	}
}

/* check wether or not a dependency is already recorded in the impact list */
static int
dep_present(Impacthead *impacthead, char *depname)
{
	Pkgimpact *pimpact;	
	
	SLIST_FOREACH(pimpact, impacthead, next)
		if (pimpact->pkgname != NULL && pkg_match(depname, pimpact->pkgname))
			return 1;

	return 0;
}

/* return a pkgname corresponding to a dependency */
Pkglist *
map_pkg_to_dep(Plisthead *plisthead, char *depname)
{
	Pkglist	*plist;

	SLIST_FOREACH(plist, plisthead, next)
		if (pkg_match(depname, plist->pkgname))
			return plist;

	return NULL;
}
/* similar to opattern.c's pkg_order but without pattern */
static int
version_check(char *first_pkg, char *second_pkg)
{
	char *first_ver, *second_ver;

	first_ver = strrchr(first_pkg, '-');
	second_ver = strrchr(second_pkg, '-');

	if (first_ver == NULL)
		return 2;
	if (second_ver == NULL)
		return 1;

	if (dewey_cmp(first_ver + 1, DEWEY_GT, second_ver + 1))
		return 1;
	else
		return 2;
}

/* loop through local packages and match for upgrades */
static int
deps_impact(Impacthead *impacthead,
	Plisthead *localplisthead, Plisthead *remoteplisthead, Pkgdeptree *pdp)
{
	int			matchlen, toupgrade;
	Pkgimpact	*pimpact;
	Pkglist		*plist, *mapplist;
	char		*localmatch, *remotepkg, *matchname;

	/* build match name */
	matchlen = strlen(pdp->matchname) + 2;
	XMALLOC(matchname, matchlen * sizeof(char));
	snprintf(matchname, matchlen, "%s%c", pdp->matchname, DELIMITER);

	/* record corresponding package on remote list*/
	mapplist = map_pkg_to_dep(remoteplisthead, pdp->depname);
	XSTRDUP(remotepkg, mapplist->pkgname);

	/* create initial impact entry with a DONOTHING status, permitting
	 * to check if this dependency has already been recorded 
	 */
	XMALLOC(pimpact, sizeof(Pkgimpact));
	XSTRDUP(pimpact->depname, pdp->depname);
	pimpact->action = DONOTHING;
	pimpact->oldpkg = NULL;
	pimpact->pkgname = NULL;

	SLIST_INSERT_HEAD(impacthead, pimpact, next);

	/* parse local packages to see if depedency is installed*/
	SLIST_FOREACH(plist, localplisthead, next) {
		localmatch = end_expr(plist->pkgname); /* foo| */
		
		/* match, package is installed */
		if (strncmp(localmatch, matchname, strlen(localmatch)) == 0) {
			/* default action when local package match */
			toupgrade = TOUPGRADE;
			/* installed version does not match dep requirement */
			if (!pkg_match(pdp->depname, plist->pkgname)) {
				/* local pkgname didn't match deps, remote pkg has a
				 * lesser version than local package.
				*/
				if (version_check(plist->pkgname, remotepkg) == 1) {
					printf("warning: installed package %s has a greater version than %s (to be installed)\n",
						plist->pkgname, remotepkg);
					if (!check_yesno())
						toupgrade = DONOTHING;
				}

				/* insert as an upgrade */
				/* oldpkg is used when building removal order list */
				XSTRDUP(pimpact->oldpkg, plist->pkgname);

				pimpact->action = toupgrade;

				pimpact->pkgname = remotepkg;
				/* record package dependency deepness */
				pimpact->level = pdp->level;
				/* record binary package size */
				pimpact->file_size = plist->file_size;
				/* record installed package size */
				pimpact->size_pkg = plist->size_pkg;
			}
			XFREE(localmatch);
			XFREE(matchname);

			return 1;
		} /* if installed package match */
		XFREE(localmatch);
	} /* SLIST_FOREACH plist */

	if (!dep_present(impacthead, pdp->matchname)) {
		pimpact->oldpkg = NULL;
		pimpact->action = TOINSTALL;
		
		pimpact->pkgname = remotepkg;
		/* record package dependency deepness */
		pimpact->level = pdp->level;

		pimpact->file_size = mapplist->file_size;
		pimpact->size_pkg = mapplist->size_pkg;
	}
  
	XFREE(matchname);

	return 1;
}

/* count if there's many packages with the same basename */
int
count_samepkg(Plisthead *plisthead, const char *pkgname)
{
	Pkglist	*pkglist;
	char	*plistpkg = NULL, **samepkg = NULL;
	int		count = 0, num = 0, pkglen;

	/* count if there's many packages with this name */
	SLIST_FOREACH(pkglist, plisthead, next) {

		XSTRDUP(plistpkg, pkglist->pkgname);

		pkglen = strlen(pkgname);

		if (strlen(plistpkg) < (pkglen + 1)) {
			XFREE(plistpkg);
			continue;
		}

		if (plistpkg[pkglen] != '-' && !isdigit((int)plistpkg[pkglen + 1])) {
			XFREE(plistpkg);
			continue;
		}

		trunc_str(plistpkg, '-', STR_BACKWARD);
		pkglen = max(strlen(pkgname), strlen(plistpkg));

		if (strncmp(pkgname, plistpkg, pkglen) == 0) {
			XREALLOC(samepkg, (count + 2) * sizeof(char *));
			XSTRDUP(samepkg[count], pkglist->pkgname);
			samepkg[count + 1] = NULL;
					
			count++;
		}

		XFREE(plistpkg);
	}

	if (count > 1) { /* there was more than one reference */
		printf("there's more than one version available for this package.\n");
		printf("please re-run %s with a package name matching one of the following:\n",
			getprogname());
		for (num = 0; num < count; num++)
			printf("%s\n", samepkg[num]);
	}

	free_list(samepkg);

	return count;
}

Impacthead *
pkg_impact(char **pkgargs)
{
	Plisthead	*localplisthead;
	Plisthead	*remoteplisthead;
	Deptreehead	pdphead;
	Impacthead	*impacthead;
	Pkgimpact	*pimpact, *tmpimpact;
	Pkgdeptree	*pdp;
	Pkglist		*plist;
	char		**ppkgargs, *pkgname = NULL;
	int			exists;

	/* record local package list */
	localplisthead = rec_pkglist(LOCAL_PKGS_QUERY);

	/* record remote package list */
	remoteplisthead = rec_pkglist(REMOTE_PKGS_QUERY);

	if (remoteplisthead == NULL) {
		printf("empty available package list.\n");
		return NULL;
	}

	SLIST_INIT(&pdphead);

	XMALLOC(impacthead, sizeof(Impacthead));
	SLIST_INIT(impacthead);

	/* retreive impact list for all packages listed in the command line */
	for (ppkgargs = pkgargs; *ppkgargs != NULL; ppkgargs++) {

		if (strpbrk(*ppkgargs, "*%") != NULL) /* avoid SQL jokers */
			continue;

		/* check if this is a multiple-version package (apache, ...) */
		if (count_samepkg(remoteplisthead, *ppkgargs) > 1)
			goto impactend;
		else
			/* dependencies discovery */
			full_dep_tree(*ppkgargs, DIRECT_DEPS, &pdphead, __func__, 0);

		/* parse dependencies for pkgname */
		SLIST_FOREACH(pdp, &pdphead, next) {
			exists = 0;
			/* dependency is already present in impact list */
			SLIST_FOREACH(pimpact, impacthead, next) {
				if (strncmp(pimpact->depname, pdp->depname,
						strlen(pdp->depname)) == 0)
					exists = 1;
			}
			if (exists)
				continue;
			/* compare needed deps with local packages */
			if (!deps_impact(impacthead, localplisthead,
					remoteplisthead, pdp)) {
				/* there was a versionning mismatch, proceed ? */
				if (!check_yesno()) {
					free_impact(impacthead);
					XFREE(impacthead);
					impacthead = NULL;

					goto impactend; /* avoid free's repetition */
				}
			}
		} /* SLIST_FOREACH deps */

		/* finally, insert package itself */
		SLIST_FOREACH(plist, remoteplisthead, next) {
			/* find corresponding pkg */
			XSTRDUP(pkgname, plist->pkgname);
			
			if (!exact_pkgfmt(*ppkgargs))
				/* argument is not a full pkg name, truncate plist->pkgname */
				trunc_str(pkgname, '-', STR_BACKWARD);

			if (strncmp(pkgname, *ppkgargs, strlen(pkgname)) == 0) {

				/* use a Pkgdeptree for convenience */
				XMALLOC(pdp, sizeof(Pkgdeptree));
				XSTRDUP(pdp->matchname, plist->pkgname);
				trunc_str(pdp->matchname, '-', STR_BACKWARD);

				/* passing pkgname as depname */
				XSTRDUP(pdp->depname, plist->pkgname);

				deps_impact(impacthead, localplisthead, remoteplisthead, pdp);

				XFREE(pdp->depname);
				XFREE(pdp->matchname);
				XFREE(pdp);

				XFREE(pkgname);

				break;
			}
			XFREE(pkgname);
		} /* SLIST_FOREACH remoteplisthead */

	} /* for (ppkgargs) */

impactend:
	
	free_deptree(&pdphead);
	free_pkglist(localplisthead);
	free_pkglist(remoteplisthead);

	/* remove DONOTHING entries */
	SLIST_FOREACH(pimpact, impacthead, next) {
		if (pimpact->action == DONOTHING) {

			tmpimpact = pimpact;
			SLIST_REMOVE(impacthead, pimpact, Pkgimpact, next);

			XFREE(tmpimpact->depname);
			XFREE(tmpimpact->pkgname);
			XFREE(tmpimpact->oldpkg);
			XFREE(tmpimpact);
		}
	}

	return impacthead;
}

