/*
 *  deps.c
 *
 *  Copyright (c) 2002-2007 by Judd Vinet <jvinet@zeroflux.org>
 *  Copyright (c) 2005 by Aurelien Foret <orelien@chez.com>
 *  Copyright (c) 2005, 2006 by Miklos Vajna <vmiklos@frugalware.org>
 *
 *  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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* libalpm */
#include "deps.h"
#include "alpm_list.h"
#include "util.h"
#include "log.h"
#include "error.h"
#include "package.h"
#include "db.h"
#include "cache.h"
#include "handle.h"

static pmgraph_t *_alpm_graph_new(void)
{
	pmgraph_t *graph = NULL;

	MALLOC(graph, sizeof(pmgraph_t), RET_ERR(PM_ERR_MEMORY, NULL));

	if(graph) {
		graph->state = 0;
		graph->data = NULL;
		graph->parent = NULL;
		graph->children = NULL;
		graph->childptr = NULL;
	}
	return(graph);
}

static void _alpm_graph_free(void *data)
{
	pmgraph_t *graph = data;
	alpm_list_free(graph->children);
	free(graph);
}

pmdepmissing_t *_alpm_depmiss_new(const char *target, pmdepmod_t depmod,
		const char *depname, const char *depversion)
{
	pmdepmissing_t *miss;

	ALPM_LOG_FUNC;

	MALLOC(miss, sizeof(pmdepmissing_t), RET_ERR(PM_ERR_MEMORY, NULL));

	strncpy(miss->target, target, PKG_NAME_LEN);
	miss->depend.mod = depmod;
	strncpy(miss->depend.name, depname, PKG_NAME_LEN);
	if(depversion) {
		strncpy(miss->depend.version, depversion, PKG_VERSION_LEN);
	} else {
		miss->depend.version[0] = 0;
	}

	return(miss);
}

/* Convert a list of pmpkg_t * to a graph structure,
 * with a edge for each dependency.
 * Returns a list of vertices (one vertex = one package)
 * (used by alpm_sortbydeps)
 */
static alpm_list_t *_alpm_graph_init(alpm_list_t *targets)
{
	alpm_list_t *i, *j, *k;
	alpm_list_t *vertices = NULL;
	/* We create the vertices */
	for(i = targets; i; i = i->next) {
		pmgraph_t *vertex = _alpm_graph_new();
		vertex->data = (void *)i->data;
		vertices = alpm_list_add(vertices, vertex);
	}

	/* We compute the edges */
	for(i = vertices; i; i = i->next) {
		pmgraph_t *vertex_i = i->data;
		pmpkg_t *p_i = vertex_i->data;
		/* TODO this should be somehow combined with alpm_checkdeps */
		for(j = vertices; j; j = j->next) {
			pmgraph_t *vertex_j = j->data;
			pmpkg_t *p_j = vertex_j->data;
			int child = 0;
			for(k = alpm_pkg_get_depends(p_i); k && !child; k = k->next) {
				pmdepend_t *depend = k->data;
				child = alpm_depcmp(p_j, depend);
			}
			if(child) {
				vertex_i->children =
					alpm_list_add(vertex_i->children, vertex_j);
			}
		}
		vertex_i->childptr = vertex_i->children;
	}
	return(vertices);
}

/* Re-order a list of target packages with respect to their dependencies.
 *
 * Example (PM_TRANS_TYPE_ADD):
 *   A depends on C
 *   B depends on A
 *   Target order is A,B,C,D
 *
 *   Should be re-ordered to C,A,B,D
 *
 * mode should be either PM_TRANS_TYPE_ADD or PM_TRANS_TYPE_REMOVE.  This
 * affects the dependency order sortbydeps() will use.
 *
 * This function returns the new alpm_list_t* target list.
 *
 */
alpm_list_t *_alpm_sortbydeps(alpm_list_t *targets, pmtranstype_t mode)
{
	alpm_list_t *newtargs = NULL;
	alpm_list_t *vertices = NULL;
	alpm_list_t *vptr;
	pmgraph_t *vertex;

	ALPM_LOG_FUNC;

	if(targets == NULL) {
		return(NULL);
	}

	_alpm_log(PM_LOG_DEBUG, "started sorting dependencies\n");

	vertices = _alpm_graph_init(targets);

	vptr = vertices;
	vertex = vertices->data;
	while(vptr) {
		/* mark that we touched the vertex */
		vertex->state = -1;
		int found = 0;
		while(vertex->childptr && !found) {
			pmgraph_t *nextchild = (vertex->childptr)->data;
			vertex->childptr = (vertex->childptr)->next;
			if (nextchild->state == 0) {
				found = 1;
				nextchild->parent = vertex;
				vertex = nextchild;
			}
			else if(nextchild->state == -1) {
				pmpkg_t *vertexpkg = vertex->data;
				pmpkg_t *childpkg = nextchild->data;
				_alpm_log(PM_LOG_WARNING, _("dependency cycle detected:\n"));
				if(mode == PM_TRANS_TYPE_REMOVE) {
					_alpm_log(PM_LOG_WARNING, _("%s will be removed after its %s dependency\n"), vertexpkg->name, childpkg->name);
				} else {
					_alpm_log(PM_LOG_WARNING, _("%s will be installed before its %s dependency\n"), vertexpkg->name, childpkg->name);
				}
			}
		}
		if(!found) {
			newtargs = alpm_list_add(newtargs, vertex->data);
			/* mark that we've left this vertex */
			vertex->state = 1;
			vertex = vertex->parent;
			if(!vertex) {
				vptr = vptr->next;
				while(vptr) {
					vertex = vptr->data;
					if (vertex->state == 0) break;
					vptr = vptr->next;
				}
			}
		}
	}

	_alpm_log(PM_LOG_DEBUG, "sorting dependencies finished\n");

	if(mode == PM_TRANS_TYPE_REMOVE) {
		/* we're removing packages, so reverse the order */
		alpm_list_t *tmptargs = alpm_list_reverse(newtargs);
		/* free the old one */
		alpm_list_free(newtargs);
		newtargs = tmptargs;
	}

	alpm_list_free_inner(vertices, _alpm_graph_free);
	alpm_list_free(vertices);

	return(newtargs);
}

/* Little helper function for alpm_list_find */
static int satisfycmp(const void *pkg, const void *depend)
{
	return(!alpm_depcmp((pmpkg_t*) pkg, (pmdepend_t*) depend));
}

/** Checks dependencies and returns missing ones in a list.
 * Dependencies can include versions with depmod operators.
 * @param db pointer to the local package database
 * @param reversedeps handles the backward dependencies
 * @param remove an alpm_list_t* of packages to be removed
 * @param upgrade an alpm_list_t* of packages to be upgraded (remove-then-upgrade)
 * @return an alpm_list_t* of pmpkg_t* of missing_t pointers.
 */
alpm_list_t SYMEXPORT *alpm_checkdeps(pmdb_t *db, int reversedeps,
		alpm_list_t *remove, alpm_list_t *upgrade)
{
	alpm_list_t *i, *j;
	alpm_list_t *targets, *dblist = NULL, *modified = NULL;
	alpm_list_t *baddeps = NULL;
	pmdepmissing_t *miss = NULL;

	ALPM_LOG_FUNC;

	if(db == NULL) {
		return(NULL);
	}

	targets = alpm_list_join(alpm_list_copy(remove), alpm_list_copy(upgrade));
	for(i = _alpm_db_get_pkgcache(db); i; i = i->next) {
		void *pkg = i->data;
		if(alpm_list_find(targets, pkg, _alpm_pkg_cmp)) {
			modified = alpm_list_add(modified, pkg);
		} else {
			dblist = alpm_list_add(dblist, pkg);
		}
	}
	alpm_list_free(targets);

	/* look for unsatisfied dependencies of the upgrade list */
	for(i = upgrade; i; i = i->next) {
		pmpkg_t *tp = i->data;
		_alpm_log(PM_LOG_DEBUG, "checkdeps: package %s-%s\n",
				alpm_pkg_get_name(tp), alpm_pkg_get_version(tp));

		for(j = alpm_pkg_get_depends(tp); j; j = j->next) {
			pmdepend_t *depend = j->data;
			/* 1. we check the upgrade list */
			/* 2. we check database for untouched satisfying packages */
			if(!alpm_list_find(upgrade, depend, satisfycmp) &&
			   !alpm_list_find(dblist, depend, satisfycmp)) {
				/* Unsatisfied dependency in the upgrade list */
				char *missdepstring = alpm_dep_get_string(depend);
				_alpm_log(PM_LOG_DEBUG, "checkdeps: missing dependency '%s' for package '%s'\n",
						missdepstring, alpm_pkg_get_name(tp));
				free(missdepstring);
				miss = _alpm_depmiss_new(alpm_pkg_get_name(tp), depend->mod,
						depend->name, depend->version);
				baddeps = alpm_list_add(baddeps, miss);
			}
		}
	}

	if(reversedeps) {
		/* reversedeps handles the backwards dependencies, ie,
		 * the packages listed in the requiredby field. */
		for(i = dblist; i; i = i->next) {
			pmpkg_t *lp = i->data;
			for(j = alpm_pkg_get_depends(lp); j; j = j->next) {
				pmdepend_t *depend = j->data;
				/* we won't break this depend, if it is already broken, we ignore it */
				/* 1. check upgrade list for satisfiers */
				/* 2. check dblist for satisfiers */
				if(alpm_list_find(modified, depend, satisfycmp) &&
				   !alpm_list_find(upgrade, depend, satisfycmp) &&
				   !alpm_list_find(dblist, depend, satisfycmp)) {
					char *missdepstring = alpm_dep_get_string(depend);
					_alpm_log(PM_LOG_DEBUG, "checkdeps: transaction would break '%s' dependency of '%s'\n",
							missdepstring, alpm_pkg_get_name(lp));
					free(missdepstring);
					miss = _alpm_depmiss_new(lp->name, depend->mod,
							depend->name, depend->version);
					baddeps = alpm_list_add(baddeps, miss);
				}
			}
		}
	}
	alpm_list_free(modified);
	alpm_list_free(dblist);

	return(baddeps);
}

static int dep_vercmp(const char *version1, pmdepmod_t mod,
		const char *version2)
{
	int equal = 0;

	if(mod == PM_DEP_MOD_ANY) {
		equal = 1;
	} else {
		int cmp = _alpm_versioncmp(version1, version2);
		switch(mod) {
			case PM_DEP_MOD_EQ: equal = (cmp == 0); break;
			case PM_DEP_MOD_GE: equal = (cmp >= 0); break;
			case PM_DEP_MOD_LE: equal = (cmp <= 0); break;
			case PM_DEP_MOD_LT: equal = (cmp < 0); break;
			case PM_DEP_MOD_GT: equal = (cmp > 0); break;
			default: equal = 1; break;
		}
	}
	return(equal);
}

int SYMEXPORT alpm_depcmp(pmpkg_t *pkg, pmdepend_t *dep)
{
	alpm_list_t *i;

	ALPM_LOG_FUNC;

	const char *pkgname = alpm_pkg_get_name(pkg);
	const char *pkgversion = alpm_pkg_get_version(pkg);
	int satisfy = 0;

	/* check (pkg->name, pkg->version) */
	satisfy = (strcmp(pkgname, dep->name) == 0
			&& dep_vercmp(pkgversion, dep->mod, dep->version));

	/* check provisions, format : "name=version" */
	for(i = alpm_pkg_get_provides(pkg); i && !satisfy; i = i->next) {
		char *provname = strdup(i->data);
		char *provver = strchr(provname, '=');

		if(provver == NULL) { /* no provision version */
			satisfy = (dep->mod == PM_DEP_MOD_ANY
					&& strcmp(provname, dep->name) == 0);
		} else {
			/* replace the space with a NULL byte, and advance ptr the version */
			*provver = '\0';
			provver += 1;
			satisfy = (strcmp(provname, dep->name) == 0
					&& dep_vercmp(provver, dep->mod, dep->version));
		}
		free(provname);
	}

	return(satisfy);
}

pmdepend_t SYMEXPORT *alpm_splitdep(const char *depstring)
{
	pmdepend_t *depend;
	char *ptr = NULL;
	char *newstr = NULL;

	if(depstring == NULL) {
		return(NULL);
	}
	newstr = strdup(depstring);

	MALLOC(depend, sizeof(pmdepend_t), return(NULL));

	/* Find a version comparator if one exists. If it does, set the type and
	 * increment the ptr accordingly so we can copy the right strings. */
	if((ptr = strstr(newstr, ">="))) {
		depend->mod = PM_DEP_MOD_GE;
		*ptr = '\0';
		ptr += 2;
	} else if((ptr = strstr(newstr, "<="))) {
		depend->mod = PM_DEP_MOD_LE;
		*ptr = '\0';
		ptr += 2;
	} else if((ptr = strstr(newstr, "="))) { /* Note: we must do =,<,> checks after <=, >= checks */
		depend->mod = PM_DEP_MOD_EQ;
		*ptr = '\0';
		ptr += 1;
	} else if((ptr = strstr(newstr, "<"))) {
		depend->mod = PM_DEP_MOD_LT;
		*ptr = '\0';
		ptr += 1;
	} else if((ptr = strstr(newstr, ">"))) {
		depend->mod = PM_DEP_MOD_GT;
		*ptr = '\0';
		ptr += 1;

	} else {
		/* no version specified - copy in the name and return it */
		depend->mod = PM_DEP_MOD_ANY;
		strncpy(depend->name, newstr, PKG_NAME_LEN);
		depend->version[0] = '\0';
		free(newstr);
		return(depend);
	}

	/* if we get here, we have a version comparator, copy the right parts
	 * to the right places */
	strncpy(depend->name, newstr, PKG_NAME_LEN);
	strncpy(depend->version, ptr, PKG_VERSION_LEN);
	free(newstr);

	return(depend);
}

/* These parameters are messy. We check if this package, given a list of
 * targets and a db is safe to remove. We do NOT remove it if it is in the
 * target list, or if if the package was explictly installed and
 * include_explicit == 0 */
static int can_remove_package(pmdb_t *db, pmpkg_t *pkg, alpm_list_t *targets,
		int include_explicit)
{
	alpm_list_t *i, *requiredby;

	if(_alpm_pkg_find(alpm_pkg_get_name(pkg), targets)) {
		return(0);
	}

	if(!include_explicit) {
		/* see if it was explicitly installed */
		if(alpm_pkg_get_reason(pkg) == PM_PKG_REASON_EXPLICIT) {
			_alpm_log(PM_LOG_DEBUG, "excluding %s -- explicitly installed\n",
					alpm_pkg_get_name(pkg));
			return(0);
		}
	}

	/* TODO: checkdeps could be used here, it handles multiple providers
	 * better, but that also makes it slower.
	 * Also this would require to first add the package to the targets list,
	 * then call checkdeps with it, then remove the package from the targets list
	 * if checkdeps detected it would break something */

	/* see if other packages need it */
	requiredby = alpm_pkg_compute_requiredby(pkg);
	for(i = requiredby; i; i = i->next) {
		pmpkg_t *reqpkg = _alpm_db_get_pkgfromcache(db, i->data);
		if(reqpkg && !_alpm_pkg_find(alpm_pkg_get_name(reqpkg), targets)) {
			FREELIST(requiredby);
			return(0);
		}
	}
	FREELIST(requiredby);

	/* it's ok to remove */
	return(1);
}

/**
 * @brief Adds unneeded dependencies to an existing list of packages.
 * By unneeded, we mean dependencies that are only required by packages in the
 * target list, so they can be safely removed.
 * If the input list was topo sorted, the output list will be topo sorted too.
 *
 * @param db package database to do dependency tracing in
 * @param *targs pointer to a list of packages
 * @param include_explicit if 0, explicitly installed packages are not included
 */
void _alpm_recursedeps(pmdb_t *db, alpm_list_t *targs, int include_explicit)
{
	alpm_list_t *i, *j, *k;

	ALPM_LOG_FUNC;

	if(db == NULL || targs == NULL) {
		return;
	}

	for(i = targs; i; i = i->next) {
		pmpkg_t *pkg = i->data;
		for(j = alpm_pkg_get_depends(pkg); j; j = j->next) {
			pmdepend_t *depend = j->data;

			for(k = _alpm_db_get_pkgcache(db); k; k = k->next) {
				pmpkg_t *deppkg = k->data;
				if(alpm_depcmp(deppkg,depend)
						&& can_remove_package(db, deppkg, targs, include_explicit)) {
					_alpm_log(PM_LOG_DEBUG, "adding '%s' to the targets\n",
							alpm_pkg_get_name(deppkg));
						/* add it to the target list */
					targs = alpm_list_add(targs, _alpm_pkg_dup(deppkg));
				}
			}
		}
	}
}

/* populates *list with packages that need to be installed to satisfy all
 * dependencies (recursive) for syncpkg
 *
 * @param remove contains packages elected for removal
 * make sure **list is already initialized
 */
int _alpm_resolvedeps(pmdb_t *local, alpm_list_t *dbs_sync, pmpkg_t *syncpkg,
                      alpm_list_t **list, alpm_list_t *remove, pmtrans_t *trans, alpm_list_t **data)
{
	alpm_list_t *i, *j, *k;
	alpm_list_t *targ;
	alpm_list_t *deps = NULL;

	ALPM_LOG_FUNC;

	if(local == NULL || dbs_sync == NULL || syncpkg == NULL || list == NULL) {
		return(-1);
	}

	_alpm_log(PM_LOG_DEBUG, "started resolving dependencies\n");
	targ = alpm_list_add(NULL, syncpkg);
	deps = alpm_checkdeps(local, 0, remove, targ);
	alpm_list_free(targ);

	if(deps == NULL) {
		return(0);
	}

	for(i = deps; i; i = i->next) {
		int found = 0;
		pmdepmissing_t *miss = i->data;
		pmdepend_t *missdep = &(miss->depend);
		pmpkg_t *sync = NULL;

		/* check if one of the packages in *list already satisfies this dependency */
		for(j = *list; j && !found; j = j->next) {
			pmpkg_t *sp = j->data;
			if(alpm_depcmp(sp, missdep)) {
				char *missdepstring = alpm_dep_get_string(missdep);
				_alpm_log(PM_LOG_DEBUG, "%s satisfies dependency %s -- skipping\n",
				          alpm_pkg_get_name(sp), missdepstring);
				free(missdepstring);
				found = 1;
			}
		}
		if(found) {
			continue;
		}

		/* find the package in one of the repositories */
		/* check literals */
		for(j = dbs_sync; j && !found; j = j->next) {
			sync = _alpm_db_get_pkgfromcache(j->data, missdep->name);
			if(!sync) {
				continue;
			}
			found = alpm_depcmp(sync, missdep) && !_alpm_pkg_find(alpm_pkg_get_name(sync), remove);
			if(!found) {
				continue;
			}
			/* If package is in the ignorepkg list, ask before we pull it */
			if(_alpm_pkg_should_ignore(sync)) {
				pmpkg_t *dummypkg = _alpm_pkg_new(miss->target, NULL);
				QUESTION(trans, PM_TRANS_CONV_INSTALL_IGNOREPKG, dummypkg, sync, NULL, &found);
				_alpm_pkg_free(dummypkg);
			}
		}
		/*TODO this autoresolves the first 'satisfier' package... we should fix this
		 * somehow */
		/* check provides */
		/* we don't check literals again to avoid duplicated PM_TRANS_CONV_INSTALL_IGNOREPKG messages */
		for(j = dbs_sync; j && !found; j = j->next) {
			for(k = _alpm_db_get_pkgcache(j->data); k && !found; k = k->next) {
				sync = k->data;
				if(!sync) {
					continue;
				}
				found = alpm_depcmp(sync, missdep) && strcmp(sync->name, missdep->name)
					&& !_alpm_pkg_find(alpm_pkg_get_name(sync), remove);
				if(!found) {
					continue;
				}
				if(_alpm_pkg_should_ignore(sync)) {
					pmpkg_t *dummypkg = _alpm_pkg_new(miss->target, NULL);
					QUESTION(trans, PM_TRANS_CONV_INSTALL_IGNOREPKG, dummypkg, sync, NULL, &found);
					_alpm_pkg_free(dummypkg);
				}
			}
		}

		if(!found) {
			char *missdepstring = alpm_dep_get_string(missdep);
			_alpm_log(PM_LOG_ERROR, _("cannot resolve \"%s\", a dependency of \"%s\"\n"),
			          missdepstring, miss->target);
			free(missdepstring);
			if(data) {
				MALLOC(miss, sizeof(pmdepmissing_t),/*nothing*/);
				if(!miss) {
					pm_errno = PM_ERR_MEMORY;
					FREELIST(*data);
					goto error;
				}
				*miss = *(pmdepmissing_t *)i->data;
				*data = alpm_list_add(*data, miss);
			}
			pm_errno = PM_ERR_UNSATISFIED_DEPS;
			goto error;
		} else {
			_alpm_log(PM_LOG_DEBUG, "pulling dependency %s (needed by %s)\n",
					alpm_pkg_get_name(sync), alpm_pkg_get_name(syncpkg));
			*list = alpm_list_add(*list, sync);
			if(_alpm_resolvedeps(local, dbs_sync, sync, list, remove, trans, data)) {
				goto error;
			}
		}
	}

	_alpm_log(PM_LOG_DEBUG, "finished resolving dependencies\n");

	FREELIST(deps);

	return(0);

error:
	FREELIST(deps);
	return(-1);
}

const char SYMEXPORT *alpm_miss_get_target(const pmdepmissing_t *miss)
{
	ALPM_LOG_FUNC;

	/* Sanity checks */
	ASSERT(miss != NULL, return(NULL));

	return miss->target;
}

pmdepend_t SYMEXPORT *alpm_miss_get_dep(pmdepmissing_t *miss)
{
	ALPM_LOG_FUNC;

	/* Sanity checks */
	ASSERT(miss != NULL, return(NULL));

	return &(miss->depend);
}

pmdepmod_t SYMEXPORT alpm_dep_get_mod(const pmdepend_t *dep)
{
	ALPM_LOG_FUNC;

	/* Sanity checks */
	ASSERT(dep != NULL, return(-1));

	return dep->mod;
}

const char SYMEXPORT *alpm_dep_get_name(const pmdepend_t *dep)
{
	ALPM_LOG_FUNC;

	/* Sanity checks */
	ASSERT(dep != NULL, return(NULL));

	return dep->name;
}

const char SYMEXPORT *alpm_dep_get_version(const pmdepend_t *dep)
{
	ALPM_LOG_FUNC;

	/* Sanity checks */
	ASSERT(dep != NULL, return(NULL));

	return dep->version;
}

/** Reverse of splitdep; make a dep string from a pmdepend_t struct.
 * The string must be freed!
 * @param dep the depend to turn into a string
 * @return a string-formatted dependency with operator if necessary
 */
char SYMEXPORT *alpm_dep_get_string(const pmdepend_t *dep)
{
	char *opr, *str = NULL;
	size_t len;

	ALPM_LOG_FUNC;

	/* Sanity checks */
	ASSERT(dep != NULL, return(NULL));

	switch(dep->mod) {
		case PM_DEP_MOD_ANY:
			opr = "";
			break;
		case PM_DEP_MOD_GE:
			opr = ">=";
			break;
		case PM_DEP_MOD_LE:
			opr = "<=";
			break;
		case PM_DEP_MOD_EQ:
			opr = "=";
			break;
		case PM_DEP_MOD_LT:
			opr = "<";
			break;
		case PM_DEP_MOD_GT:
			opr = ">";
			break;
		default:
			opr = "";
			break;
	}

	/* we can always compute len and print the string like this because opr
	 * and ver will be empty when PM_DEP_MOD_ANY is the depend type */
	len = strlen(dep->name) + strlen(opr) + strlen(dep->version) + 1;
	MALLOC(str, len, RET_ERR(PM_ERR_MEMORY, NULL));
	snprintf(str, len, "%s%s%s", dep->name, opr, dep->version);

	return(str);
}
/* vim: set ts=2 sw=2 noet: */