/*
 * prereq.c - Determine prerequisite packages
 *
 * Written 2010 by Werner Almesberger
 * Copyright 2010 Werner Almesberger
 *
 * 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.
 */


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

#include "util.h"
#include "id.h"
#include "pkg.h"
#include "qpkg.h"
#include "prereq.h"


struct list {
	const struct ref *refs;
	struct list *next;
};

struct stack {
	struct pkg *pkg;
	struct stack *next;
};


static struct pkg **best = NULL;
static struct pkg **installs = NULL;
static int n_best;	/* undefined if best == NULL */
static int n_install = 0;
static int install_max = 0;


/* ----- List of packages considered for installation ---------------------- */


static void done(void)
{
	int size;

	if (best && n_best <= n_install)
		return;
	free(best);
	size = sizeof(*installs)*(n_install+1);
	best = alloc_size(size);
	memcpy(best, installs, sizeof(*installs)*n_install);
	n_best = n_install;
	best[n_best] = NULL;
}


static void append_install(struct pkg *pkg)
{
	if (n_install == install_max) {
		install_max = (install_max+1)*2;
		installs = realloc(installs, sizeof(*installs)*install_max);
		if (!installs) {
			perror("realloc");
			exit(1);
		}
	}
	installs[n_install++] = pkg;
	assert(!pkg->mark && !(pkg->flags & QPKG_INSTALLED));
	pkg->mark = 1;
}


static void backtrack(void)
{
	assert(n_install);
	n_install--;
	installs[n_install]->mark = 0;
}


/* ----- Check dependencies and conflicts ---------------------------------- */


static int satisfies(const struct pkg *pkg, const struct ref *ref)
{
	int cmp;

	if (pkg->id != ref->pkg)
		return 0;
	if (!ref->version)
		return 1;
	/* @@@ error in the database, not qpkg's fault */
	assert(pkg->version);
	cmp = comp_versions(pkg->version, ref->version);
//fprintf(stderr, "%.*s <%d> %.*s\n",
//  ID2PF(pkg->version), cmp, ID2PF(ref->version));
	switch (ref->relop) {
	case rel_eq:
		return !cmp;
	case rel_ge:
		return cmp >= 0;
	case rel_lt:
		return cmp < 0;
	default:
		abort();
	}
}


static int old_conflicts(const struct pkg *pkg, const struct list *conf)
{
	const struct ref *ref;

	while (conf) {
		for (ref = conf->refs; ref; ref = ref->next)
			if (satisfies(pkg, ref))
				return 1;
		conf = conf->next;
	}
	return 0;
}


static int new_conflicts(const struct ref *refs)
{
	const struct ref *ref;
	const struct pkg *pkg;

	for (ref = refs; ref; ref = ref->next)
		for (pkg = ref->pkg->jrb->val; pkg; pkg = pkg->more)
			if ((pkg->flags & (QPKG_INSTALLED | QPKG_ADDING)) ||
			    pkg->mark)
				if (satisfies(pkg, ref))
					return 1;
	return 0;
}


/* ----- Recurse through lists and layers of dependencies ------------------ */


static void print_debug(const struct pkg *pkg, const struct stack *top,
    int level)
{
	const struct stack *p;

	fprintf(stderr, "%*s", level, "");
	fprintf(stderr, "%.*s %p", ID2PF(pkg->id), pkg);
	if (pkg->version)
		fprintf(stderr, " %.*s", ID2PF(pkg->version));
	fprintf(stderr, " (");
	for (p = top; p; p = p->next)
		fprintf(stderr, "%s%.*s",
		    p == top ? "" : " ", ID2PF(p->pkg->id));
	fprintf(stderr, ")");
	if (pkg->mark)
		fprintf(stderr, " +");
	if (pkg->flags & QPKG_INSTALLED)
		fprintf(stderr, " ***");
	fprintf(stderr, "\n");
}


static void resolve(struct list *next_deps, const struct ref *dep,
    struct stack *top, struct list *conf)
{
	static int level = 0;
	struct list more_deps;
	struct list more_conf = {
		.next = conf
	};
	struct stack stack;
	struct pkg *pkg;

	while (!dep) {
		if (!next_deps) {
			done();
			return;
		}
		assert(top->pkg->flags & QPKG_ADDING);
		top->pkg->flags &= ~QPKG_ADDING;
		top = top->next;
		dep = next_deps->refs;
		next_deps = next_deps->next;
	}
	for (pkg = dep->pkg->jrb->val; pkg; pkg = pkg->more) {
		if (best && n_install == n_best)
			break;
		if (pkg->flags & QPKG_PROVIDED)
			continue;
		if (debug)
			print_debug(pkg, top, level);
		if (!satisfies(pkg, dep))
			continue;
		if (pkg->flags & QPKG_ADDING) {
			fprintf(stderr, "package %.*s version %.*s "
			    "has cyclic dependency\n",
			    ID2PF(pkg->id), ID2PF(pkg->version));
				exit(1);
		}
		if ((pkg->flags & QPKG_INSTALLED) || pkg->mark) {
			assert(!old_conflicts(pkg, conf));
			resolve(next_deps, dep->next, top, conf);
			continue;
		}
		if (old_conflicts(pkg, conf))
			continue;
		if (new_conflicts(pkg->conflicts))
			continue;
		level++;
		append_install(pkg);
		more_deps.refs = dep->next;
		more_deps.next = next_deps;
		more_conf.refs = pkg->conflicts;
		more_conf.next = conf;
		stack.pkg = pkg;
		stack.next = top;
		pkg->flags |= QPKG_ADDING;
		resolve(&more_deps, pkg->depends, &stack, &more_conf);
		backtrack();
		level--;
	}

	/*
	 * @@@ this shouldn't be all of the story yet. If we fail a dependency
	 * due to a conflict, the current algorithm may leave packages marked
	 * as QPKG_ADDING, possibly triggering a false cyclic dependency error.
	 * See test/bug-adding for an example.
	 *
	 * In the case of cycle detection, we could avoid all the hassle of
	 * maintaining this flag and just walk down the stack to see if we're
	 * already trying to add the package (the stack should be correct with
	 * the current algorithm), but we'll need this kind of accurate
	 * tracking of package state for ordering the dependencies and for
	 * "Provides" anyway.
	 */
}


static struct list *installed_conflicts(void)
{
	struct list *list = NULL, *new;
	const struct jrb *n;
	const struct pkg *pkg;
	
	for (n = jrb_first(packages->root); n != jrb_nil(packages->root);
	    n = jrb_next(n)) {
		pkg = n->val;
		if (!pkg)
			continue;
		if (!(pkg->flags & QPKG_INSTALLED))
			continue;
		if (!pkg->conflicts)	/* optimization */
			continue;
		new = alloc_type(struct list);
		new->refs = pkg->conflicts;
		new->next = list;
		list = new;
	}
	return list;
}


static void free_conflicts(struct list *conf)
{
	struct list *next;

	while (conf) {
		next = conf->next;
		free(conf);
		conf = next;
	}
}


struct pkg **prereq(struct pkg *pkg)
{
	struct stack stack = {
		.pkg = pkg,
		.next = NULL
	};
	struct list conf = {
		.refs = pkg->conflicts,
		.next = installed_conflicts()
	};

	if (old_conflicts(pkg, &conf) || new_conflicts(pkg->conflicts)) {
		fprintf(stderr, "%.*s conflicts with installed packages\n",
		  ID2PF(pkg->id));
		exit(1);
	}
	pkg->flags |= QPKG_ADDING;
	resolve(NULL, pkg->depends, &stack, &conf);
	pkg->flags &= ~QPKG_ADDING;
	free(installs);
	free_conflicts(conf.next);
	return best;
}