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

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include "error.h"
#include "conffile.h"
#include "AVLTree.h"
#include "symbol.h"
#include "config.h"
#include "intl.h"
#include "vercmp.h"
#include "status.h"

#define READBUF 4096

struct pinfo {
	char *name;
	char *section;
	char *version;
	char *predepends, *depends, *recommends, *suggests, *provides, *tasks;
	unsigned short installed:1;
	unsigned short keep:1;
	unsigned short nokeep:1;
};

/* Life is all about priorities */
static const char *debian_priorities[] = {
	"required",
	"important",
	"standard",
	"optional",
	"extra",
	"ANY",
	NULL
};

static AVLTree *priorities = NULL, *keepsections = NULL, *nokeepsections = NULL, *guessdepends = NULL;
AVLTree *packages;

int chomp(char *s) {
	int r;
	r = strlen(s)-1;
	if(r>=0 && s[r] == '\n') {
		s[r--] = '\0';
		if(r>=0 && s[r] == '\r')
			s[r--] = '\0';
	}
	return r+2;
}

char *chop(char *s) {
	int r;
	r = chomp(s)-2;
	while(r>=0 && strchr(" \t\n\r", s[r]))
		s[r--] = '\0';
	while(*s && strchr(" \t\n\r", *s))
		s++;
	return s;
}

int pkgcmp(struct package *a, struct package *b) {
	return symcmp(a->name, b->name);
}

struct package *pkg_find(symbol_t s) {
	struct package p;
	AVLNode *n;
	p.name = s;
	n = AVLSearch(packages, &p);
	return n ? (struct package *)n->item : NULL;
}

struct package *new_package(symbol_t s) {
	struct package *pkg;

	pkg = xmalloc(sizeof(struct package));
	pkg->name = s;
	pkg->provider_count = 0;
	pkg->orphan_depends = 0;
	pkg->installed = 0;
	pkg->keep = 0;
	pkg->nokeep = 0;
	AVLInitTree(&pkg->depends, (AVLCompare)strcasecmp);
	AVLInitTree(&pkg->dependents, (AVLCompare)strcasecmp);
	AVLInitTree(&pkg->provides, (AVLCompare)strcasecmp);
	AVLInitTree(&pkg->providers, (AVLCompare)strcasecmp);
	return pkg;
}

struct package *get_package(symbol_t s)
{
	struct package *pkg = pkg_find(s);
	if(!pkg) {
		pkg = new_package(s);
		if (!AVLInsert(packages, pkg))
			perror_exit(ERROR_SYSTEM, "AVLInsert()");
	}
	return pkg;
}

static const struct pkg_version *get_version(symbol_t s);

static void warn_ignored_dependency(const char *pkg,
				    const struct pkg_version *pv) {
	if (Verbose && pkg) {
		if (pv->version)
			fprintf(stderr, "warning: package %s: unsatisfied dependency on %s %s\n",
				pkg, pv->name, formatversion(pv->version));
		else
			fprintf(stderr, "warning: package %s: unsatisfied dependency on %s\n",
				pkg, pv->name);
	}
}

#define PROCESS_DEP_IGNORE 0
#define PROCESS_DEP_CHECK 1
#define PROCESS_DEP_ERROR 2

static void process_dep(AVLTree *t, const char *s,
			symbol_t pkg, int check_version) {
	int alternative_present = 0;
	int ignored_dependency = 0;
	struct pkg_version pv;
	symbol_t first_pkg = NULL;
	enum depverrel verrel;

	while (s) {
		switch (*s) {
		case ' ':
		case '\t':
			++s;
			break;
		case '\0':
		case ',':
			if (ignored_dependency && !alternative_present) {
				get_package(pv.name);
				AVLInsert(t, pv.name);
				if (Verbose && pkg)
					fprintf(stderr, "warning: package %s: forcing depdency on %s\n",
						pkg, first_pkg);
			}
			first_pkg = NULL;
			alternative_present = 0;
			ignored_dependency = 0;
			if (*s == 0)
				return;
			++s;
			break;
		case '|':
			++s;
			break;
		default:
			s = parsedependency(s, &pv, &verrel);
			if (!pv.name || alternative_present)
				break;
			if (check_version) {
				if (!first_pkg)
					first_pkg = pv.name;
				const struct pkg_version *iv = get_version(pv.name);
				if (iv == NULL
				    || (pv.version != NULL
					&& !versionsatisfied3(iv->version, pv.version, verrel))) {
					if (check_version == PROCESS_DEP_ERROR) {
						warn_ignored_dependency(pkg, &pv);
						ignored_dependency = 1;
					}
					break;
				}
				alternative_present = 1;
			}
			get_package(pv.name); /* make sure it's present */
			AVLInsert(t, pv.name);
			break;
		}
	}
}

static void process_package(struct pinfo *p) {
	static struct pinfo null = {0};
	struct package *pkg;

	if(p->name && p->installed) {
		symbol_t name = symbol(p->name);
		pkg = get_package(name);
		pkg->installed	= 1;
		pkg->keep 	= p->keep;
		pkg->nokeep	= p->nokeep;
		process_dep(&pkg->provides, p->provides, name, 0);
		process_dep(&pkg->depends, p->predepends, name, PROCESS_DEP_ERROR);
		process_dep(&pkg->depends, p->depends, name, PROCESS_DEP_ERROR);
		process_dep(&pkg->depends, p->recommends, name, PROCESS_DEP_CHECK);
		process_dep(&pkg->depends, p->suggests, name, PROCESS_DEP_CHECK);
	}
	*p = null;
}

static void process_available(struct pinfo *p) {
	static struct pinfo null = {0};
	struct package *pkg;
	char *task_name;
	AVLTree tasks;
	AVLNode *c;

	if(p->name && p->tasks) {
		symbol_t name = symbol(p->name);
		pkg = pkg_find(name);
		if(pkg && pkg->installed) {
			AVLInitTree(&tasks, (AVLCompare)strcasecmp);
			process_dep(&tasks, p->tasks, name, PROCESS_DEP_CHECK);
			while((c = tasks.head) != NULL) {
				task_name = xstrcat("task-", (char *)c->item);
				pkg = get_package(symbol(task_name));
				AVLInsert(&pkg->depends, name);
				pkg->task = 1;
				AVLDeleteNode(&tasks, c);
			}
		}
	}
	*p = null;
}

static void parse_status(char *s, struct pinfo *p) {
	char *word;

	do word = strsep(&s, " \t");
	while(word && !*word);
	if(UseHold)
		p->keep |= !strcasecmp(word, "hold");

	do word = strsep(&s, " \t");
	while(word && !*word);
	/* usually word is "ok" now */

	do word = strsep(&s, " \t");
	while(word && !*word);
	p->installed = !strcasecmp(word, "installed");
}

static int parse_line(char *s, struct pinfo *p) {
	switch(tolower(*s)) {
		case 'd':
			if(!strncasecmp(s+1,"epends:",7))
				p->depends = xstrdup(chop(s+8));
		break;
		case 'e':
			if(UseEssential && !strncasecmp(s+1,"ssential:",9))
				p->keep |= !strcasecmp(chop(s+10), "yes");
		break;
		case 'p':
			switch(tolower(s[1])) {
				case 'r':
					switch(tolower(s[2])) {
						case 'e':
							if(UsePreDepends && !strncasecmp(s+3,"-depends:",9))
								p->predepends = xstrdup(chop(s+12));
						break;
						case 'i':
							if(!strncasecmp(s+3,"ority:",6))
								p->keep |= !!AVLSearch(priorities,chop(s+9));
						break;
						case 'o':
							if(!strncasecmp(s+3,"vides:",6))
								p->provides = xstrdup(chop(s+9));
						break;
					}
				break;
				case 'a':
					if(!strncasecmp(s+2,"ckage:",6))
						p->name = xstrdup(chop(s+8));
				break;
			}
		break;
		case 'r':
			if(UseRecommends && !strncasecmp(s+1,"ecommends:",10))
				p->recommends = xstrdup(chop(s+11));
		break;
		case 's':
			switch(tolower(s[1])) {
				case 'e':
					if(!strncasecmp(s+2,"ction:",6)) {
						char *r = strrchr(s, '/');
						r = chop(r ? r+1 : s+8);
						p->keep |= !!AVLSearch(keepsections, r);
						p->nokeep |= !!AVLSearch(nokeepsections, r);
					}
				break;
				case 'u':
					if(UseSuggests && !strncasecmp(s+2,"ggests:",7))
						p->suggests = xstrdup(chop(s+9));
				break;
				case 't':
					if(!strncasecmp(s+2,"atus:",5))
						parse_status(chop(s+7), p);
				break;
			}
		break;
		case 't':
			if(!strncasecmp(s+1,"ask:",4))
				p->tasks = xstrdup(chop(s+5));
		break;
		case 'v':
			if(!strncasecmp(s+1, "ersion:",7))
				p->version = xstrdup(chop(s+8));
		case ' ':
		case '\t':
		case '\r':
			if(!*chop(s))
				return 1;
			break;
		case '\0':
		case '\n':
			return 1;
	}
	return 0;
}

typedef void (*process_func)(struct pinfo *);
void readlines(const char *filename, process_func process_record) {
	struct pinfo p = {0};
	FILE *f;
	int fd;
	char *buf;
	char *s, *t;
	struct stat st;

	fd = open(filename, O_RDONLY);
	if(fd<0)
		perror_exit(ERROR_SYSTEM, DpkgStatus);

	if(!fstat(fd, &st)
	&& S_ISREG(st.st_mode)
	&& (buf = mmap(NULL,st.st_size,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0))) {
		if(buf[st.st_size-1] != '\n')
			error_exit(ERROR_SYSTEM,
				"%s is truncated (no trailing newline)\n", DpkgStatus);
		buf[st.st_size-1] = '\0';
		for(s = buf; s; s = t) {
			t = strchr(s, '\n');
			if(t)
				*t++ = '\0';
			if(parse_line(s, &p))
				process_record(&p);
		}
		if(munmap(buf, st.st_size))
			perror("munmap()");
		close(fd);
	} else {
		f = fdopen(fd, "r");
		if(!f)
			perror_exit(ERROR_SYSTEM, DpkgStatus);
		buf = alloca(READBUF);
		if(!buf)
			perror_exit(ERROR_SYSTEM, "alloca()");
		while(fgets(buf, READBUF, f))
			if(parse_line(buf, &p))
				process_record(&p);
		fclose(f);
	}
	process_record(&p);
}

AVLTree *initlist(const char *str) {
	AVLTree *tree;
	const char *s, *d = ", \t";
	int n;

	tree = AVLAllocTree((AVLCompare)strcasecmp);

	if (str) {
		for(s = str; *s; s += n + strspn(s, d)) {
			n = strcspn(s, d);
			AVLInsert(tree, xstrndup(s, n));
		}
	}

	return tree;
}

symbol_t guessbase(symbol_t name) {
	char *s;
	int n;

	if((s = strrchr(name, '-')) && AVLSearch(guessdepends, symbol(s+1))) {
		n = s - (char *)name;
		return nsymbol(s, n);
	}

	return NULL;
}

static int pkg_version_cmp(const void *a, const void *b) {
	const struct pkg_version *l = a;
	const struct pkg_version *r = b;
	return symcmp(l->name, r->name);
}

static AVLTree *versions;

static const struct pkg_version *get_version(symbol_t s)
{
	struct pkg_version pv;
	AVLNode *n;
	pv.name = s;
	n = AVLSearch(versions, &pv);
	return n ? (struct pkg_version *)n->item : NULL;
}

static void add_version(symbol_t name, const struct versionrevision *ver) {
	struct pkg_version *pv = xmalloc(sizeof(*pv));
	pv->name = name;
	pv->version = ver;
	AVLInsert(versions, pv);
}

static void process_version(struct pinfo *p) {
	if (p->name && p->version && p->installed) {
		const struct versionrevision *ver = parseversion(p->version);
		add_version(symbol(p->name), ver);
		if (p->provides) {
			AVLTree prov;
			AVLInitTree(&prov, (AVLCompare)symcmp);
			AVLNode *n;
			process_dep(&prov, p->provides, 0, PROCESS_DEP_IGNORE);
			for (n = prov.head; n; n = n->next)
				add_version(n->item, ver);
		}
	}
}

void readstatus(void) {
	const char **pri;

	AVLFreeNodes(packages);
	priorities = AVLAllocTree((AVLCompare)strcasecmp);
	if(!keepsections)
		keepsections = initlist(KeepSections);
	if(!nokeepsections)
		nokeepsections = initlist(NokeepSections);
	if(!guessdepends)
		guessdepends = initlist(GuessDepends);

	for(pri = debian_priorities; *pri && strcasecmp(MaxPriority, *pri); pri++)
		AVLInsert(priorities, (char *)*pri);
	if(!*pri)
		error_exit(ERROR_CONFIG, _("Unknown priority \"%s\"\n"), MaxPriority);

	versions = AVLAllocTree(pkg_version_cmp);
	readlines(DpkgStatus, process_version);

	readlines(DpkgStatus, process_package);         /* Read package status */
	if(UseTasks)
		readlines(DpkgAvailable, process_available);    /* Read additional info */
}
