#include <sys/utsname.h>
#include "anna.h"
#include "util.h"
#include "retriever.h"

struct debconfclient *debconf = NULL;
static char *running_kernel = NULL;
static const char *subarchitecture;
static int quiet = 0, verbose = 0, dedup = 0;

/* If any package name appears more than once in the Packages file,
 * keep only the last one. */
void remove_duplicates (di_packages *packages) {
	di_slist_node *n1, *n2, *n2_prev;
	di_package *p1, *p2;

	di_log(DI_LOG_LEVEL_DEBUG, "remove_duplicates() called...");

	for (n1 = packages->list.head; n1; n1 = n1->next) {
		n2_prev = n1;
		/* compare n1 to each node after it, as n2 */
		for (n2 = n1->next; n2; n2 = n2->next) {
			/* compare package name */
			p1 = n1->data;
			p2 = n2->data;
			if (strcmp(p1->package, p2->package) != 0) {
				/* keep this node;  remember the previous one */
				n2_prev = n2;
				continue;
			}

			di_log (DI_LOG_LEVEL_DEBUG,
			        "ignoring duplicate Packages entry for %s",
			        p1->package);

			/* delete p1, move p2 into n1 */
/* XXX:			di_package_destroy(p1); */
			n1->data = p1 = p2;
			n2->data = p2 = NULL;

			/* update list bottom, if n2 was the last entry */
			if (packages->list.bottom == n2) {
				packages->list.bottom = n2_prev;
			}

			/* unlink n2 from the list and delete it */
			n2_prev->next = n2->next;
/* XXX:			di_free(n2); */

			/* rewind to the previous node */
			n2 = n2_prev;
		}
	}
	di_log(DI_LOG_LEVEL_DEBUG, "remove_duplicates() exits.");

}

di_packages *get_packages (void) {
	di_packages_allocator *packages_allocator = di_system_packages_allocator_alloc();
	di_packages *packages = retriever_packages(packages_allocator);

	while (packages == NULL) {
		int r=retriever_error("packages");
		di_log(DI_LOG_LEVEL_WARNING, "bad d-i Packages file");
		if (r != 1) {
			/* Failed to handle error. */
			return NULL;
		}
		else {
			/* Error handled, retry. */
			packages_allocator = di_system_packages_allocator_alloc();
			packages = retriever_packages(packages_allocator);
		}
	}

	if (dedup)
		remove_duplicates(packages);
	return packages;
}

/* Go through the available packages to see if it contains at least
 * one package that is valid for the subarchitecture and corresponds
 * to the kernel version we are running */
int packages_ok (di_packages *packages) {
	di_slist_node *node;
	di_package *package;
	bool kernel_packages_present = false;

	for (node = packages->list.head; node; node = node->next) {
		package = node->data;

		if (!di_system_package_check_subarchitecture(package, subarchitecture))
			continue;
	 	if (((di_system_package *)package)->kernel_version) {
			if (running_kernel &&
			    strcmp(running_kernel, ((di_system_package *)package)->kernel_version) == 0) {
				kernel_packages_present = true;
				break;
			}
		}
	}

	if (!kernel_packages_present) {
		di_log(DI_LOG_LEVEL_WARNING, "no packages matching running kernel %s in archive", running_kernel);
#ifdef __GNU__
		/* GNU Mach does not have modules */
#else
		debconf_input(debconf, "critical", "anna/no_kernel_modules");
		if (debconf_go(debconf) == 30)
			return 0;
		debconf_get(debconf, "anna/no_kernel_modules");
		if (strcmp(debconf->value, "false") == 0)
			return 0;
#endif
	}

	return 1;
}

static int choose_modules(di_packages *status, di_packages **packages) {
	char *choose_modules_question = "anna/choose_modules" ;
	char *question_priority = "medium";
	char *choices_c, *choices;
	int package_count = 0;
	di_package *package, *status_package, **package_array;
	di_slist_node *node, *node1;
	bool standard_modules = true;
	bool lowmem_mode = false;

	/* Test lowmem level to know if packages with want_install status
	   will be shown */
	if ( get_lowmem_level() >= 2) {
		lowmem_mode = true;
		choose_modules_question="anna/choose_modules_lowmem";
		/* force priority to show question even in a non expert mode */
		question_priority = "high";
		di_log (DI_LOG_LEVEL_DEBUG,
			"lowmem_mode, want_install status packages will be shown");
	}

	for (node = status->list.head; node; node = node->next) {
		status_package = node->data;
		package = di_packages_get_package(*packages, status_package->package, 0);
		if (!package)
			continue;
		package->status = status_package->status;
		if (status_package->status == di_package_status_unpacked || status_package->status == di_package_status_installed) {
			for (node1 = package->depends.head; node1; node1 = node1->next) {
				di_package_dependency *d = node1->data;
				if (d->type == di_package_dependency_type_reverse_enhances) {
					package->status_want = di_package_status_want_install;
					if (verbose)
						di_log (DI_LOG_LEVEL_DEBUG, "install %s, enhances installed packages %s", package->package, status_package->package);
				}
			}
		}
	}

	debconf_get(debconf, "anna/standard_modules");
	if (strcmp(debconf->value, "false") == 0)
		standard_modules = false;

	for (node = (*packages)->list.head; node; node = node->next) {
		package = node->data;

		package->status_want = di_package_status_want_deinstall;

		if (package->type != di_package_type_real_package)
			continue;
		if (is_installed(package, status))
			continue;
		if (!di_system_package_check_subarchitecture(package, subarchitecture))
			continue;

		if (((di_system_package *)package)->kernel_version) {
			if (running_kernel && strcmp(running_kernel, ((di_system_package *)package)->kernel_version) == 0) {
				package->status_want = di_package_status_want_unknown;
				if (verbose)
					di_log (DI_LOG_LEVEL_DEBUG, "ask for %s, matches kernel", package->package);
			}
			else {
				continue;
			}

			for (node1 = package->depends.head; node1; node1 = node1->next) {
				di_package_dependency *d = node1->data;
				if (d->type == di_package_dependency_type_provides
				    && d->ptr && is_queued(d->ptr)) {
					package->status_want = di_package_status_want_install;
					if (verbose)
						di_log (DI_LOG_LEVEL_DEBUG, "install %s, queued by anna-install", package->package);
					continue;
				}
			}
		}

		if (package->priority >= di_package_priority_standard) {
			if (standard_modules || ((di_system_package *)package)->kernel_version) {
				package->status_want = di_package_status_want_install;
				if (verbose)
					di_log (DI_LOG_LEVEL_DEBUG, "install %s, priority >= standard", package->package);
			}
			else {
				package->status_want = di_package_status_want_unknown;
				if (verbose)
					di_log (DI_LOG_LEVEL_DEBUG, "ask for %s, priority >= standard", package->package);
			}
		}
		else if (is_queued(package)) {
			package->status_want = di_package_status_want_install;
			if (verbose)
				di_log (DI_LOG_LEVEL_DEBUG, "install %s, queued by anna-install", package->package);
		}
		else if (((di_system_package *)package)->installer_menu_item
			/* we don't want to see installed packages in choices list*/
		         && package->status != di_package_status_installed) {
			package->status_want = di_package_status_want_unknown;
			if (verbose)
				di_log (DI_LOG_LEVEL_DEBUG, "ask for %s, is menu item", package->package);
		}
	}

	/* Include packages in udeb_include */
	take_includes(*packages);

	/* Drop packages in udeb_exclude */
	drop_excludes(*packages);

	/* in lowmem mode, we add all packages in the instlist by setting
	   status to want_unknown except those which are menu item and
	   their dependencies (calculated by the next function) */
	if (lowmem_mode) {
		for (node = (*packages)->list.head; node; node = node->next) {
			package = node->data;
			if (package->status_want == di_package_status_want_install &&
			    ((di_system_package *)package)->installer_menu_item == 0 &&
			    !is_queued(package)) {
				package->status_want = di_package_status_want_unknown;
			}
		}
	}

	di_system_packages_resolve_dependencies_mark_anna(*packages, subarchitecture, running_kernel);

	/* Slight over-allocation, but who cares */
	package_array = di_new0(di_package *, di_hash_table_size((*packages)->table));
	/* Now build the asklist, figuring out which packages have been
	 * pulled into instlist */
	for (node = (*packages)->list.head; node; node = node->next) {
		package = node->data;
		if (package->status_want == di_package_status_want_unknown)
			package_array[package_count++] = package;
	}

	qsort(package_array, package_count, sizeof(di_package *), package_name_compare);
	choices_c = list_to_choices(package_array, true);
	choices = list_to_choices(package_array, false);
	debconf_subst(debconf, choose_modules_question, "CHOICES-C", choices_c);
	debconf_subst(debconf, choose_modules_question, "CHOICES", choices);

	debconf_input(debconf, question_priority, choose_modules_question);

	di_free(choices_c);
	di_free(choices);
	di_free(package_array);

	if (debconf_go(debconf) == 30)
		return 1;

	debconf_get(debconf, choose_modules_question);
	if (debconf->value != NULL) {
		char *choices = debconf->value;

		for (node = (*packages)->list.head; node; node = node->next) {
			package = node->data;
			/* Not very safe, but at least easy ;) */
			if (strstr(choices, package->package) != NULL)
				package->status_want = di_package_status_want_install;
		}
	}

	return 0;
}

void resume_progress_bar (int progress_step, int step_count, di_package *package) {
	debconf_progress_start(debconf, 0, step_count, "anna/progress_title");
	debconf_progress_set(debconf, progress_step);
	debconf_subst(debconf, "anna/progress_step_retr", "PACKAGE", package->package);
	debconf_progress_info(debconf, "anna/progress_step_retr");
}

static int
install_modules(di_packages *status, di_packages *packages) {
	di_slist_node *node;
	di_package *package;
	char *f, *fp, *dest_file;
	int step_count = 0;
	int progress_step=0;

	di_system_packages_resolve_dependencies_mark_anna(packages, subarchitecture, running_kernel);

	for (node = packages->list.head; node; node = node->next) {
		package = node->data;
		if (package->status_want == di_package_status_want_install && !is_installed(package, status)) {
			if (package->type == di_package_type_real_package)
				step_count++;
		} else
			package->status_want = di_package_status_want_unknown;
	}

	/* Short-circuit if there's no packages to install. */
	if (step_count <= 0)
		return 0;

#ifdef LOADTEMPLATES
	/* One step for loading templates. */
	step_count++;
#endif /* LOADTEMPLATES */

	for (node = packages->list.head; node; node = node->next) {
		package = node->data;
		/* One extra step for each package to be configured
		 * immediately.
		 */
		if (package->type == di_package_type_real_package && package->status_want == di_package_status_want_install && !((di_system_package *)package)->installer_menu_item)
			step_count++;
	}

	if (!quiet)
		debconf_progress_start(debconf, 0, step_count, "anna/progress_title");

	/* Retrieval and unpack pass. */
	for (node = packages->list.head; node; node = node->next) {
		package = node->data;
		if (package->type == di_package_type_real_package && package->status_want == di_package_status_want_install) {
			if (!package->filename) {
				di_log(DI_LOG_LEVEL_ERROR, "no Filename field for %s, ignoring", package->package);
				continue;
			}
			for (f = fp = package->filename; *fp != 0; fp++) {
				if (*fp == '/')
					f = ++fp;
			}
			dest_file = xasprintf("%s/%s", DOWNLOAD_DIR, f);

			di_log (DI_LOG_LEVEL_DEBUG, "retrieving %s %s", package->package, package->version);
			if (!quiet) {
				debconf_subst(debconf, "anna/progress_step_retr", "PACKAGE", package->package);
				debconf_progress_info(debconf, "anna/progress_step_retr");
			}
			for (;;) {
				if (retriever_retrieve(package, dest_file)) {
					di_log(DI_LOG_LEVEL_WARNING, "package retrieval failed");
					if (!quiet)
						/* error handling may use a progress bar, so stop the current one */
						debconf_progress_stop(debconf);
					if (retriever_error("retrieve") != 1) {
						/* Failed to handle error. */
						free(dest_file);
						return 6;
					}
					else {
						/* Handled error, retry. */
						if (!quiet)
							resume_progress_bar(progress_step, step_count, package);
						continue;
					}
				}

				if (! sha256sum(package->sha256, dest_file)) {
					di_log(DI_LOG_LEVEL_WARNING, "bad sha256sum");
					if (!quiet)
						/* error handling may use a progress bar, so stop the current one */
						debconf_progress_stop(debconf);
					if (retriever_error("retrieve") != 1) {
						/* Failed to handle error. */
						unlink(dest_file);
						free(dest_file);
						return 7;
					}
					else {
						/* Handled error, retry. */
						if (!quiet)
							resume_progress_bar(progress_step, step_count, package);
						continue;
					}
				}

				break;
			}

			if (!unpack_package(dest_file)) {
				if (!quiet)
					debconf_progress_stop(debconf);
				debconf_subst(debconf, "anna/install_failed", "PACKAGE", package->package);
				debconf_input(debconf, "critical", "anna/install_failed");
				debconf_go(debconf);
				unlink(dest_file);
				free(dest_file);
				return 8;
			}

			unlink(dest_file);
			free(dest_file);
			if (!quiet) {
				debconf_progress_step(debconf, 1);
				progress_step++;
			}
		}
	}

#ifdef LOADTEMPLATES
	/* Load debconf templates. We do this just once to avoid having to
	 * rewrite the templates database over and over again.
	 */
	debconf_progress_info(debconf, "anna/progress_step_loadtemplates");
	load_templates(packages);
	if (!quiet) {
		debconf_progress_step(debconf, 1);
		progress_step++;
	}
#endif /* LOADTEMPLATES */

	/* Configuration pass. */
	for (node = packages->list.head; node; node = node->next) {
		package = node->data;
		if (package->type == di_package_type_real_package && package->status_want == di_package_status_want_install) {
			if (!package->filename) {
				di_log(DI_LOG_LEVEL_ERROR, "no Filename field for %s, ignoring", package->package);
				continue;
			}
			if (((di_system_package *)package)->installer_menu_item)
				continue;

			if (!quiet) {
				debconf_subst(debconf, "anna/progress_step_conf", "PACKAGE", package->package);
				debconf_progress_info(debconf, "anna/progress_step_conf");
			}

			if (!configure_package(package->package)) {
				if (!quiet)
					debconf_progress_stop(debconf);
				debconf_subst(debconf, "anna/install_failed", "PACKAGE",
				              package->package);
				debconf_input(debconf, "critical", "anna/install_failed");
				debconf_go(debconf);
				return 8;
			}

			if (!quiet) {
				debconf_progress_step(debconf, 1);
				progress_step++;
			}
		}
	}

	if (!quiet)
		debconf_progress_stop(debconf);

	return 0;
}

int main(int argc, char **argv) {
	int ret;
	di_packages *packages, *status;
	di_packages_allocator *status_allocator;
	struct utsname uts;
	const char *quiet_env, *verbose_env;

	debconf = debconfclient_new();
	debconf_capb(debconf, "backup");

	di_system_init("anna");

	subarchitecture = di_system_subarch_analyze();

	if (uname(&uts) == 0) {
#ifdef __GNU__
		if (!strncmp(uts.version, "GNU-Mach ", 9)) {
			char *slash = index(uts.version, '/');
			if (slash)
				running_kernel = strndup (uts.version + 9, slash - (uts.version + 9));
			else
				running_kernel = strdup (uts.version + 9);
		} else
			running_kernel = strdup(uts.version);
#else
		running_kernel = strdup(uts.release);
#endif
	}

	quiet_env = getenv("ANNA_QUIET");
	if (quiet_env && strcmp(quiet_env, "1") == 0)
		quiet = 1;
	verbose_env = getenv("ANNA_VERBOSE");
	if (verbose_env && strcmp(verbose_env, "1") == 0)
		verbose = 1;
	if (CMD_SUCCESS == debconf_get(debconf, "apt-setup/_DEVEL_/repository")
		&& debconf->value != NULL
		&& strlen(debconf->value)) {
		/* if this repo is defined, net-retriever should grab additional
		   Packages files and cat them together, so we need to de-duplicate the
		   result */
		di_log(DI_LOG_LEVEL_INFO, "apt-setup/_DEVEL_/repository is configured (%s), so will apply remove_duplicates()", debconf->value);
		dedup = 1;
	}

	status_allocator = di_system_packages_allocator_alloc();
	status = di_system_packages_status_read_file(DI_SYSTEM_DPKG_STATUSFILE, status_allocator);

	if (argc <= 1) {
		fprintf(stderr, "need parameters\n");
		exit(1);
	}
	else if (strcmp(argv[1], "install") == 0) {
		di_slist_node *node;
		di_package *package = NULL;
		int i;

		if (get_retriever() == NULL) {
			fprintf(stderr, "no default retriever set\n");
			exit(1);
		}
		retriever_config();

		packages = get_packages();
		if (! packages) {
			retriever_cleanup();
			return 10;
		}

		for (node = packages->list.head; node; node = node->next) {
                	package = node->data;
			package->status_want = di_package_status_want_deinstall;
		}

		for (i = 2; i < argc; i++) {
			int installed = 0;
			int found = 0;

			for (node = status->list.head; node; node = node->next) {
				di_slist_node *node1;
				package = node->data;
				if (strcmp(package->package, argv[i]) == 0) {
					installed = 1;
					continue;
				}
				for (node1 = package->depends.head; node1; node1 = node1->next) {
					di_package_dependency *d = node1->data;
					if (d->type == di_package_dependency_type_provides
					    && d->ptr && strcmp(d->ptr->package, argv[i]) == 0) {
						installed = 1;
						continue;
					}
				}
			}

			if (installed) {
				//di_log (DI_LOG_LEVEL_DEBUG, "skipping already installed %s", argv[i]);
				continue;
			}

			for (node = packages->list.head; node; node = node->next) {
				package = node->data;

				if (!di_system_package_check_subarchitecture(package, subarchitecture))
					continue;

				if (strcmp(package->package, argv[i]) == 0) {
					package->status_want = di_package_status_want_install;
					found = 1;
					continue;
				}

				if (((di_system_package *)package)->kernel_version) {
		                        if (running_kernel && strcmp(running_kernel, ((di_system_package *)package)->kernel_version) != 0) {
						continue;
					}
					di_slist_node *node1;
					for (node1 = package->depends.head; node1; node1 = node1->next) {
						di_package_dependency *d = node1->data;
						if (d->type == di_package_dependency_type_provides
						    && d->ptr && strcmp(d->ptr->package, argv[i]) == 0) {
							package->status_want = di_package_status_want_install;
							found = 1;
							continue;
						}
					}
				}
			}
			if (! found) {
				fprintf(stderr, "unknown udeb %s\n", argv[i]);
				exit(1);
			}
		}
		ret = install_modules(status, packages);
	}
	else {
		if (argc == 2) {
			set_retriever(argv[1], 0);
		}
		else if (strcmp(argv[2], "default") == 0) {
			set_retriever(argv[1], 1);
		}
		retriever_config();

		packages = get_packages();
		if (! packages || ! packages_ok(packages)) {
			retriever_cleanup();
			return 10;
		}

		if (choose_modules(status, &packages) != 0) {
			ret = 10;
		}
		else  {
			ret = install_modules(status, packages);
		}
	}

	retriever_cleanup();
	return ret;
}
