/*
* Distributed under the terms of the GNU General Public License v2
* $Header: /pub/embedded/busybox-1.00/archival/emerge.c,v 1.5 2005/03/03 21:58:16 bcollar Exp $
*
********************************************************************
* 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., 59 Temple Place - Suite 330, Boston,
* MA 02111-1307, USA.
*/

/* 1) The ability to fetch a native portage built package .tbz2
 * 2) No changes in package format handling.
 * (ie no .ipk/.apk/.homebrew)
 * 3) The ability to record the contents of a package exactly the same way
 * portage does. (/var/db/pkg/$PN/CONTENTS)
 * 4) The ability to uninstall a binary package based on whats in listed in
 * the CONTENTS file.
 * 5) The ability to omit files from being installed based on a setting
 * called INSTALL_MASK. This should behave exactly like the current portage
 * implementation if the INSTALL_MASK feature which accepts globing.
 * ex: INSTALL_MASK="*.a /usr/lib/ *.o /usr/include *.pl"
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <assert.h>
#include <libbb.h>
#include <unarchive.h>
#include <getopt.h>
#include <errno.h>

#define file_exists(f) (access(f, R_OK) == 0)
#define dir_exists(f) (access(f, R_OK) == 0)

#define PM (PATH_MAX)
#define PKG_NAME_LEN (32)
#define CAT_NAME_LEN (16)
#define VERSION_LEN (16)
#define BINHOST_LEN (32)

typedef struct category_s {
	llist_t *packages;
	char name[CAT_NAME_LEN];
} category_t;

typedef struct package_s {
	llist_t *versions;
	category_t *category;
	char name[PKG_NAME_LEN];
} package_t;

extern int wget_main(int argc, char **argv);

/* versions are just strings; each version-string is separated by ';' */
/* see debug_print_index for how to run through this structure */

typedef struct configurables_s {
	char *root;
	char *index;
	char *tmpdir;
	char *binhost;
	char *pkgdir;
	int verbose;
	int debug;
	int pretend;
	int usepkgonly;
	int (*action) (package_t*);
	llist_t *packages;	/* the package-names given on the command line */
	int num_packages;	/* the number of package-names given */
} configurables_t;

static configurables_t conf;

#define PLENTY_OF_PACKAGES (20)
#define PLENTY_OF_REQUESTS (5)

typedef struct pkg_info_s {
  package_t** searchable_packages; /* sorted pointers to package names; use bsearch to find them */
  int  num_packages;		/* number of distinct packages (not including versions) in the index */
  llist_t *pkg_index;		/* a list of category structs: data is a (category_t) */
  package_t* (*find) (char* pkg);	/* the function to use to search for a pkg */
} pkg_info_t;

static pkg_info_t pkg_info;
static char msg[PM+64];

static void 
einfo(char *str)
{
	bb_fprintf(stdout, ">>> %s\n", str);
}

static void 
eerror(char *str)
{
	bb_fprintf(stderr, "!!! %s\n", str);
}

static void 
die(char *str)
{
	eerror(str);
	exit(EXIT_FAILURE);
}

static int mkalldirs(char *dir)
{
	return bb_make_directory(dir, -1, FILEUTILS_RECUR);
}

#if 0
static int
cmp_pkgs(const void* p1, const void* p2) 
{
	const package_t* pk1 = (const package_t*) p1;
	const package_t* pk2 = (const package_t*) p2;
	printf("cmp_p %s %s\n", pk1->name, pk2->name);
	return (strncmp(pk1->name, pk2->name, PKG_NAME_LEN));
}
#endif


/* What to do about package-name conflicts? so far, we're just returning the first
 * answer we find */
static package_t* 
dumbfind (char* pkgname)
{
	/* linearly search the index tree to find a package */
	/* pkgname is what was specified on the command line, like "emerge foo"
	 * look in the Index structure, find the foo, return the package structure
	 */
	package_t *package = 0;
	llist_t *c = pkg_info.pkg_index, *p = 0;
	category_t *cat;

	while (c != 0) {
		cat = (category_t *) c->data;
		p = cat->packages;
		while (p != 0) {
			package = (package_t *) p->data;
			if (strncmp(package->name, pkgname, PKG_NAME_LEN) == 0)
				return package;
			p = p->link;
		}
		c = c->link;
	}
	return 0;
}

/* bfind is currently broken, doh! */
static package_t* 
bfind (char* pkg) 
{
	return dumbfind(pkg);
#if 0
	package_t* result=0;
	static package_t tmp;
	if (pkg_info.num_packages <= 0) return 0;
	if (!pkg) return 0;
	if (pkg_info.searchable_packages == 0) {
		int idx = 0;
		llist_t *c = pkg_info.pkg_index, *p;
		category_t *cat;
		printf("building sorted array\n");
		/* need create the searchable list and sort with qsort */
		pkg_info.searchable_packages = malloc(pkg_info.num_packages*sizeof(package_t*));
		if (pkg_info.searchable_packages == 0) { 
			die("Failed to malloc area for searching package names");
		}
		/* traverse the whole package list and add to the pkg array */	
		while (c != 0) {
			cat = (category_t *) c->data;
			p = cat->packages;
			while (p != 0) {
				package_t* pk = (package_t*) p->data;
				printf("adding package %p, %s\n", pk, pk->name);
				pkg_info.searchable_packages[idx]=pk;
				printf("in array: %p %s\n", pkg_info.searchable_packages[idx], 
					pkg_info.searchable_packages[idx]->name);
				++idx;
				p = p->link;
			}
			c = c->link;
		}
		qsort(pkg_info.searchable_packages, pkg_info.num_packages, 
			sizeof(package_t*), cmp_pkgs);
		printf("in array after qsort: %p %s\n", pkg_info.searchable_packages[1], 
			pkg_info.searchable_packages[1]->name);
	}
	/* now find it with bsearch */
	strncpy(tmp.name, pkg, PKG_NAME_LEN);
	return bsearch((const void*)&tmp, (const void*)pkg_info.searchable_packages,
		pkg_info.num_packages, sizeof(package_t*), cmp_pkgs);
#endif
}

static int 
do_fetch(package_t * package)
{
	/* using the package in package-name (which is category/package), figure 
	 * out the actual package name and URL. Then fetch it. return 0 when it works.
	 */
	/* append the category/package to the binhost to create the url */

	static char fullurl[PM];
	static char targetdir[PM];
	static char pkgver[PKG_NAME_LEN+VERSION_LEN+2];
	int end;
	
	sprintf(pkgver, "%s-%s.tbz2", package->name, package->versions->data);
	sprintf(msg, "Fetching package %s/%s", package->category->name, pkgver);
	einfo(msg);	
	end = sprintf(targetdir, "%s/%s/%s", conf.pkgdir, package->category->name, package->name); 
	/* make sure the target dir exists */
	if ( mkalldirs(targetdir) != 0 )
		die("Failed to create target dir under pkgdir");
	/* check if pkgver already exists; if so, return ok */
	sprintf(targetdir, "%s/%s/%s/%s", conf.pkgdir, package->category->name, package->name, pkgver);
	if ( file_exists(targetdir) ) {
		sprintf(msg, "Package already fetched: %s", targetdir);
		einfo(msg);
		return 0;
	}
	targetdir[end]=0;
	sprintf(fullurl, "%s/%s/%s/%s", conf.binhost, package->category->name, 
			package->name, pkgver);
	{
		char *cmdline[] = { "wget", "-c", "-P", targetdir, fullurl, 0 };
		pid_t child=fork();
		if ( child == 0 ) {
			execvp("/proc/self/exe", cmdline);
		} else {
			int status, wgrval;
			wait(&status);
			if (WIFEXITED(status)) {
				wgrval = WEXITSTATUS(status);
				if (wgrval != 0) {
					die("Failed to fetch package");
				}
				/* link package into all */
				/* re-use fullurl for the All pkgdir (link target) and targetdir for the
				* package (link source) */
				sprintf(fullurl, "%s/All/%s", conf.pkgdir, pkgver);
				sprintf(msg, "Package saved to %s", fullurl);
				einfo(msg);
				sprintf(targetdir, "%s/%s/%s/%s", conf.pkgdir, 
					package->category->name, package->name, pkgver);
				if ( symlink(targetdir, fullurl) != 0 && errno != EEXIST ) {
					die("failed to link pkg into All directory");
				}
				return 0;
			}
		}
	}
	return 1;
}

static void do_info(void)
{
	bb_printf("PKGDIR=%s\n", (conf.pkgdir == NULL) ? "" : conf.pkgdir);
	bb_printf("PORTAGE_BINHOST=%s\n",
			  (conf.binhost == NULL) ? "" : conf.binhost);
	bb_printf("PORTAGE_TMPDIR=%s\n",
			  (conf.tmpdir == NULL) ? "" : conf.tmpdir);
	bb_printf("ROOT=%s\n", (conf.root == NULL) ? "" : conf.root);
}

static int check_if_installed(char *pkg)
{
	return 0;
	/* we know what packages are installed by looking in /var/db/portage ... */
}

static int checksum(package_t* p) { return 1; }
static int unpack(package_t* p) { return 1; }
static int remove_installed(package_t* p) { return 1; }
static int install(package_t* p) { return 1; }

static int do_merge(package_t* p)
{
	if (do_fetch(p) != 0) return 1;
	if (checksum(p) != 0) return 1;
	if (unpack(p) != 0) return 1;
	if (remove_installed(p) != 0) return 1;
	if (install(p) != 0) return 1;
	return 0;
}

static int do_unmerge(package_t* p)
{
	return 1;
}
static int do_query(package_t* p)
{
	return 1;
}
static int do_sync(package_t* p)
{
	return 1;
}
static int do_list(package_t* p)
{
	return 1;
}
static int do_update(package_t* p)
{
	return 1;
}
static int do_clean(package_t* p)
{
	return 1;
}

static void complete_configuration(void)
{
	/* if the items in the configuration struct are not set up, try to get from the environment.
	   if that fails--error! */
#undef FOO
#define FOO "Try adding it to your /etc/make.conf, exporting it as an environment variable, or specifying it on the command line"
	/* verify that environment variables exist before going on, fix them on disk if necessary */
	if (conf.pkgdir == 0)
		die("PKGDIR is not set. " FOO);
	if (conf.binhost == 0)
		die("PORTAGE_BINHOST is not set. " FOO);
	if (conf.tmpdir == 0)
		die("PORTAGE_TMPDIR is not set. " FOO);
	if (conf.root == 0)
		die("ROOT is not set. " FOO);
#undef FOO
}

/* busybox has their own funny getopt structure, which depends on bits... */
#define EMERGE_HELP (1<<0)
#define EMERGE_DEBUG (1<<1)
#define EMERGE_PRETEND (1<<2)
#define EMERGE_VERBOSE (1<<3)
#define EMERGE_FETCH (1<<4)
#define EMERGE_USEPKGONLY (1<<5)
#define EMERGE_UNMERGE (1<<6)
#define EMERGE_QUERY (1<<7)
#define EMERGE_INFO (1<<8)
#define EMERGE_SYNC (1<<9)
#define EMERGE_LIST (1<<10)
#define EMERGE_UPDATE (1<<11)
#define EMERGE_CLEAN (1<<12)

static struct option const emerge_long_options[] = {
	{"help", 0, 0, 'h'},
	{"debug", 0, 0, 'd'},
	{"pretend", 0, 0, 'p'},
	{"verbose", 0, 0, 'v'},
	{"fetch", 0, 0, 'f'},
	{"usepkgonly", 0, 0, 'K'},
	{"unmerge", 0, 0, 'C'},
	{"query", 0, 0, 'q'},
	{"info", 0, 0, 'i'},
	{"sync", 0, 0, 's'},
	{"list", 0, 0, 'l'},
	{"update", 0, 0, 'u'},
	{"clean", 0, 0, 'c'},
	{"root", 1, 0, 'R'},
	{"index", 1, 0, 'I'},
	{"pkgdir", 1, 0, 'P'},
	{"tmpdir", 1, 0, 'T'},
	{"binhost", 1, 0, 'B'},
	{NULL, 0, NULL, 0}
};

#define OPTIONS_FLAGS "hdpvfKCqislucR:I:P:T:B:"
void parseargs(int argc, char *argv[])
{
	int optend;
	unsigned long opt;
	char *root=0, *pkgdir=0, *tmpdir=0, *binhost=0, *cindex=0;
	opterr = 0;
	/* this complementaly only works if a conflict is specified in both directions,
	   thus f~q:q~f will block f & q from being specified together. that seems 
	   pretty wrong to me */
	bb_opt_complementaly = "f~Cqisluc:i~pfKCqsluc:q~f";
	bb_applet_long_options = emerge_long_options;

	/* need to allocate these flags, not let bb do it! */
	/* need to allocate!! also, conf.index processing seems broken */
	root = pkgdir = tmpdir = binhost = 0;
	opt = bb_getopt_ulflags(argc, argv, OPTIONS_FLAGS,
				&root, &cindex, &pkgdir, &tmpdir, &binhost);
	if (root != NULL) {
		conf.root = xmalloc(strnlen(root, PM));
		strncpy(conf.root, root, PM);
	}
	if (pkgdir != NULL) {
		conf.pkgdir = xmalloc(strnlen(pkgdir, PM));
		strncpy(conf.pkgdir, pkgdir, PM);
	}
	if (tmpdir != NULL) {
		conf.tmpdir = xmalloc(strnlen(tmpdir, PM));
		strncpy(conf.tmpdir, tmpdir, PM);
	}
	if (binhost != NULL) {
		conf.binhost = xmalloc(strnlen(binhost, BINHOST_LEN));
		strncpy(conf.binhost, binhost, BINHOST_LEN);
	}
	if (cindex != NULL) {
		conf.index = xmalloc(strnlen(cindex, PM));
		strncpy(conf.index, cindex, PM);
	}

	/* Check one and only one context option was given */
	/* this doesn't work... */
	if (opt & 0x80000000UL) {
		bb_printf("conflict!\n");
		bb_show_usage();
	}

	if (opt & EMERGE_HELP)
		bb_show_usage();

	/* runtime-configuration */
	if (opt & EMERGE_DEBUG)
		conf.debug = 1;
	if (opt & EMERGE_PRETEND)
		conf.pretend = 1;
	if (opt & EMERGE_VERBOSE)
		conf.verbose = 1;
	if (opt & EMERGE_USEPKGONLY)
		conf.usepkgonly = 1;

	/* actions */
	if (opt & EMERGE_INFO
		|| (argv[optind] && strncmp(argv[optind], "info", 4) == 0)) {
		/* info: perform info, then exit */
		do_info();
		exit(0);
	} else {
		if (opt & EMERGE_FETCH)
			conf.action = do_fetch;
		else if (opt & EMERGE_UNMERGE)
			conf.action = do_unmerge;
		else if (opt & EMERGE_QUERY)
			conf.action = do_query;
		else if (opt & EMERGE_SYNC)
			conf.action = do_sync;
		else if (opt & EMERGE_LIST)
			conf.action = do_list;
		else if (opt & EMERGE_UPDATE)
			conf.action = do_update;
		else if (opt & EMERGE_CLEAN)
			conf.action = do_clean;
		else
			conf.action = do_merge;
	}

	/* this adds the remainder of the arguments (i.e. the packages) reversed, so they
	   are processed in the order specified (which may be important */
	optend = argc - 1;

	while (optend >= optind) {
		conf.packages = llist_add_to(conf.packages, argv[optend]);
		optend--;
		++conf.num_packages;
	}
}


static void debug_print_index(void)
{
	llist_t *c = pkg_info.pkg_index, *p, *v;
	category_t *cat;
	package_t *pkg;

	while (c != 0) {
		cat = (category_t *) c->data;
		bb_printf("category %s\n", cat->name);
		p = cat->packages;
		while (p != 0) {
			pkg = (package_t *) p->data;
			bb_printf("  package %s\n", pkg->name);
			v = pkg->versions;
			bb_printf("    versions ");
			while (v != 0) {
				bb_printf("%s ", v->data);
				v = v->link;
			}
			bb_printf("\n");
			p = p->link;
		}
		c = c->link;
	}
}

#undef Z
#define Z (CAT_NAME_LEN+PKG_NAME_LEN+VERSION_LEN)
static char buf[Z], tmp[Z];
static void add_package_version(category_t * cat, int idx)
{
	/* buf[idx] is the '/' which separates the category and the package_version */
	int tidx = 0;
	package_t *pack;
	char *ver;

	++idx;
	while (buf[idx] != '{' && idx < PKG_NAME_LEN + CAT_NAME_LEN) {	/* is this condition right? */
		tmp[tidx++] = buf[idx++];
	}
	tmp[tidx] = 0;
	pack = xmalloc(sizeof(package_t));
	pack->versions = 0;
	strncpy(pack->name, tmp, tidx);
	pack->name[tidx] = 0;
	/* loop through versions in buf, extracting and putting into version_ll */
	++idx;
	tidx = 0;
	while (buf[idx] != '}' && idx < Z) {
		if (buf[idx] == ';') {
			tmp[tidx] = 0;
			ver = xmalloc(tidx + 1 * sizeof(char *));
			if (ver == 0)
				die("Failed to malloc space for version string");
			strncpy(ver, tmp, tidx);
			ver[tidx] = 0;
			pack->versions = llist_add_to(pack->versions, ver);
			pack->category = cat;
			tidx = 0;
			++idx;
			ver = 0;
		} else {
			/* still collecting version */
			tmp[tidx] = buf[idx];
			++tidx;
			++idx;
		}
	}
	cat->packages = llist_add_to(cat->packages, (char *) pack);
	++pkg_info.num_packages;
}

static int process_index(void)
{
	/* fill in the char** index data structure from the index file */
	int rval = 1, idx = 0, tidx = 0;
	category_t *cat = 0;
	FILE *index_file;

	// int fd = 0;

	index_file = bb_xfopen(conf.index, "r");
	if (index_file == 0)
		die("Failed to open index file");
	while (rval == 1) {
		idx = tidx = 0;
		rval = fscanf(index_file, "%s", buf);
		if (rval == EOF || rval != 1)
			/* file ended */
			break;
		while (buf[idx] != '/' && idx < CAT_NAME_LEN) {
			tmp[tidx++] = buf[idx++];
		}
		if (idx == CAT_NAME_LEN)
			die("Index has bad data, couldn't find '/'");
		tmp[idx] = 0;
		/* tmp has the category name */
		/* is this a new category? */
		if (cat == 0 || strncmp(cat->name, tmp, Z) != 0) {
			if (cat != 0) {
				/* add old category to llist */
				pkg_info.pkg_index = llist_add_to(pkg_info.pkg_index, (char *) cat);
				cat = 0;
			}
			/* there is no category at all (i.e. the first entry ever) */
			cat = xmalloc(sizeof(category_t));
			if (cat == 0)
				die("Failed to malloc category struct");
			cat->packages = 0;
			strncpy(cat->name, tmp, CAT_NAME_LEN);
		}
		/* process the package and the version of this entry */
		/* bb_printf("adding p_v to cat %s\n", cat->name);*/
		add_package_version(cat, idx);
	}
	if (cat != 0)		/* add the last one to the ll */
		pkg_info.pkg_index = llist_add_to(pkg_info.pkg_index, (char *) cat);
#undef L
	return 0;
}

int emerge_main(int argc, char **argv)
{
	char tmpp[PM];
	unsigned int len;
	llist_t *tlist;
	llist_t *tp;
	package_t *package = 0;
	
	if (argc < 2)
		bb_show_usage();

	conf.verbose = 0;
	conf.debug = 0;
	conf.pretend = 0;
	conf.usepkgonly = 0;
	conf.packages = 0;
	conf.num_packages = 0;
	conf.index = 0;
	
	/* command line overrides env vars. */
	conf.pkgdir = getenv("PKGDIR");
	conf.binhost = getenv("PORTAGE_BINHOST");
	conf.tmpdir = getenv("PORTAGE_TMPDIR");
	conf.root = getenv("ROOT");
	conf.index = getenv("INDEX_FILE");
	
	parseargs(argc, argv);

	printf("conf.index is %s\n",
		   (conf.index == NULL) ? "not set" : conf.index);
	tlist = conf.packages;
#if 0
	bb_printf("will handle packages ");
	while (tlist) {
		bb_printf("%s ", tlist->data);
		tlist = tlist->link;
	}
#endif

	/* for some of the actions, we won't need the index, but 
	 * I'll care about that later */
	 
	pkg_info.searchable_packages=0;
	pkg_info.num_packages=0;
	pkg_info.pkg_index=0;
	
	process_index();
	//debug_print_index();
	
	/* now configurations may have some items, and some not. function may exit */
	complete_configuration();

	/* make sure PKGDIR exists on disk */
	len = strnlen(conf.pkgdir, PM);
	strncpy(tmpp, conf.pkgdir, len);
	strncat(tmpp, "/All", PM - 1 - len);
	if (!dir_exists(tmpp))	/* dir_exists checks R_OK, not W_OK...fix that later! */
		if (mkalldirs(tmpp) != 0)
			die("FAILED to make PKGDIR/All");

	/* if there are "lots" of packages from the index and "lots" of packages 
	 * specified on the command line, it'll be worth it to sort the pkg_index
	 */
	if ( pkg_info.num_packages > PLENTY_OF_PACKAGES && 
		conf.num_packages > PLENTY_OF_REQUESTS ) {
		pkg_info.find = bfind;
	} else {
		pkg_info.find = dumbfind;
	}
	
	/* now we are all set up. perform the action */
	tp = conf.packages;
	while (tp != 0) {
		package = pkg_info.find(tp->data);
		if (!package) {
			sprintf(msg, "Could not find package %s", tp->data);
			die(msg);
		}
		
		if (conf.action(package) != 0) {
			sprintf(msg, "Failed on package %s\n", tp->data);
			die(msg);
		}
		tp = tp->link;
	}
	
	exit(EXIT_SUCCESS);
}
