/* opkg_remove.c - the opkg package management system

   Carl D. Worth

   Copyright (C) 2001 University of Southern California

   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, 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.
*/

#include <stdio.h>
#include <glob.h>
#include <unistd.h>

#include "opkg_message.h"
#include "opkg_remove.h"
#include "opkg_cmd.h"
#include "pkg_alternatives.h"
#include "file_util.h"
#include "sprintf_alloc.h"
#include "libbb/libbb.h"

/*
 * Returns number of the number of packages depending on the packages provided by this package.
 * Every package implicitly provides itself.
 */
int pkg_has_installed_dependents(pkg_t * pkg, abstract_pkg_t *** pdependents)
{
	abstract_pkg_t **provider, **provides = pkg_get_ptr(pkg, PKG_PROVIDES);
	unsigned int i, n_installed_dependents = 0;

	provider = provides;

	while (provider && *provider) {
		abstract_pkg_t *providee = *provider++;
		abstract_pkg_t **dependers = providee->depended_upon_by;
		abstract_pkg_t *dep_ab_pkg;
		if (dependers == NULL)
			continue;
		while ((dep_ab_pkg = *dependers++) != NULL) {
			if (dep_ab_pkg->state_status == SS_INSTALLED) {
				n_installed_dependents++;
			}
		}
	}

	/* if caller requested the set of installed dependents */
	if (pdependents) {
		int p = 0;
		abstract_pkg_t **dependents =
		    xcalloc((n_installed_dependents + 1),
			    sizeof(abstract_pkg_t *));

		provider = provides;
		*pdependents = dependents;

		while (provider && *provider) {
			abstract_pkg_t *providee = *provider++;
			abstract_pkg_t **dependers = providee->depended_upon_by;
			abstract_pkg_t *dep_ab_pkg;
			if (dependers == NULL)
				continue;
			while ((dep_ab_pkg = *dependers++) != NULL) {
				if (dep_ab_pkg->state_status == SS_INSTALLED
				    && !(dep_ab_pkg->state_flag & SF_MARKED)) {
					dependents[p++] = dep_ab_pkg;
					dep_ab_pkg->state_flag |= SF_MARKED;
				}
			}
		}
		dependents[p] = NULL;
		/* now clear the marks */
		for (i = 0; i < p; i++) {
			abstract_pkg_t *dep_ab_pkg = dependents[i];
			dep_ab_pkg->state_flag &= ~SF_MARKED;
		}
	}
	return n_installed_dependents;
}

static int opkg_remove_dependent_pkgs(pkg_t * pkg, abstract_pkg_t ** dependents)
{
	int i;
	int a;
	int count;
	pkg_vec_t *dependent_pkgs;
	abstract_pkg_t *ab_pkg;

	if ((ab_pkg = pkg->parent) == NULL) {
		opkg_msg(ERROR, "Internal error: pkg %s isn't in hash table\n",
			 pkg->name);
		return 0;
	}

	if (dependents == NULL)
		return 0;

	// here i am using the dependencies_checked
	if (ab_pkg->dependencies_checked == 2)	// variable to make out whether this package
		return 0;	// has already been encountered in the process
	// of marking packages for removal - Karthik
	ab_pkg->dependencies_checked = 2;

	i = 0;
	count = 1;
	dependent_pkgs = pkg_vec_alloc();

	while (dependents[i] != NULL) {
		abstract_pkg_t *dep_ab_pkg = dependents[i];

		if (dep_ab_pkg->dependencies_checked == 2) {
			i++;
			continue;
		}
		if (dep_ab_pkg->state_status == SS_INSTALLED) {
			for (a = 0; a < dep_ab_pkg->pkgs->len; a++) {
				pkg_t *dep_pkg = dep_ab_pkg->pkgs->pkgs[a];
				if (dep_pkg->state_status == SS_INSTALLED) {
					pkg_vec_insert(dependent_pkgs, dep_pkg);
					count++;
				}
			}
		}
		i++;
		/* 1 - to keep track of visited ab_pkgs when checking for possiblility of a broken removal of pkgs.
		 * 2 - to keep track of pkgs whose deps have been checked alrdy  - Karthik */
	}

	if (count == 1) {
		pkg_vec_free(dependent_pkgs);
		return 0;
	}

	int err = 0;
	for (i = 0; i < dependent_pkgs->len; i++) {
		err = opkg_remove_pkg(dependent_pkgs->pkgs[i], 0);
		if (err) {
			break;
		}
	}
	pkg_vec_free(dependent_pkgs);
	return err;
}

static void print_dependents_warning(pkg_t * pkg, abstract_pkg_t ** dependents)
{
	abstract_pkg_t *dep_ab_pkg;
	opkg_msg(ERROR, "Package %s is depended upon by packages:\n",
		 pkg->name);
	while ((dep_ab_pkg = *dependents++) != NULL) {
		if (dep_ab_pkg->state_status == SS_INSTALLED)
			opkg_msg(ERROR, "\t%s\n", dep_ab_pkg->name);
	}
	opkg_msg(ERROR,
		 "These might cease to work if package %s is removed.\n\n",
		 pkg->name);
	opkg_msg(ERROR,
		 "Force removal of this package with --force-depends.\n");
	opkg_msg(ERROR, "Force removal of this package and its dependents\n");
	opkg_msg(ERROR, "with --force-removal-of-dependent-packages.\n");
}

/*
 * Find and remove packages that were autoinstalled and are orphaned
 * by the removal of pkg.
 */
static int remove_autoinstalled(pkg_t * pkg)
{
	int j;
	int err = 0;
	int n_deps;
	pkg_t *p;
	struct compound_depend *cdep;
	abstract_pkg_t **dependents;

	for (cdep = pkg_get_ptr(pkg, PKG_DEPENDS); cdep && cdep->type; cdep++) {
		if (cdep->type != PREDEPEND
		    && cdep->type != DEPEND && cdep->type != RECOMMEND)
			continue;
		for (j = 0; j < cdep->possibility_count; j++) {
			p = pkg_hash_fetch_installed_by_name(cdep->
							     possibilities[j]->
							     pkg->name);

			/* If the package is not installed, this could have
			 * been a circular dependency and the package has
			 * already been removed.
			 */
			if (!p)
				return -1;

			if (!p->auto_installed)
				continue;

			n_deps = pkg_has_installed_dependents(p, &dependents);
			if (n_deps == 0) {
				opkg_msg(NOTICE, "%s was autoinstalled and is "
					 "now orphaned, removing.\n", p->name);
				if (opkg_remove_pkg(p, 0) != 0) {
					err = -1;
				}
			} else
				opkg_msg(INFO, "%s was autoinstalled and is "
					 "still required by %d "
					 "installed packages.\n",
					 p->name, n_deps);

			if (dependents)
				free(dependents);
		}
	}

	return err;
}

int opkg_remove_pkg(pkg_t * pkg, int from_upgrade)
{
	int err;
	abstract_pkg_t *parent_pkg = NULL;

/*
 * If called from an upgrade and not from a normal remove,
 * ignore the essential flag.
 */
	if (pkg->essential && !from_upgrade) {
		if (conf->force_removal_of_essential_packages) {
			opkg_msg(NOTICE,
				 "Removing essential package %s under your coercion.\n"
				 "\tIf your system breaks, you get to keep both pieces\n",
				 pkg->name);
		} else {
			opkg_msg(NOTICE,
				 "Refusing to remove essential package %s.\n"
				 "\tRemoving an essential package may lead to an unusable system, but if\n"
				 "\tyou enjoy that kind of pain, you can force opkg to proceed against\n"
				 "\tits will with the option: --force-removal-of-essential-packages\n",
				 pkg->name);
			return -1;
		}
	}

	if ((parent_pkg = pkg->parent) == NULL)
		return 0;

	/* While remove pkg with '--force-removal-of-dependent-packages',
	   pkg may be added to remove list multiple times, add status
	   check to make sure pkg only be removed once. */
	if (conf->force_removal_of_dependent_packages &&
	        pkg->state_flag & SF_FILELIST_CHANGED &&
	        pkg->state_status == SS_NOT_INSTALLED)
		return 0;

	/* only attempt to remove dependent installed packages if
	 * force_depends is not specified or the package is being
	 * replaced.
	 */
	if (!conf->force_depends && !(pkg->state_flag & SF_REPLACE)) {
		abstract_pkg_t **dependents;
		int has_installed_dependents =
		    pkg_has_installed_dependents(pkg, &dependents);

		if (has_installed_dependents) {
			/*
			 * if this package is depended upon by others, then either we should
			 * not remove it or we should remove it and all of its dependents
			 */

			if (!conf->force_removal_of_dependent_packages) {
				print_dependents_warning(pkg, dependents);
				free(dependents);
				return -1;
			}

			/* remove packages depending on this package - Karthik */
			err = opkg_remove_dependent_pkgs(pkg, dependents);
			if (err) {
				free(dependents);
				return err;
			}
		}
		if (dependents)
			free(dependents);
	}

	if (from_upgrade == 0) {
		opkg_msg(NOTICE, "Removing package %s from %s...\n",
			 pkg->name, pkg->dest->name);
	}
	pkg->state_flag |= SF_FILELIST_CHANGED;

	pkg->state_want = SW_DEINSTALL;
	opkg_state_changed++;

	if (pkg_run_script(pkg, "prerm", "remove") != 0) {
		if (!conf->force_remove) {
			opkg_msg(ERROR, "not removing package \"%s\", "
				 "prerm script failed\n", pkg->name);
			opkg_msg(NOTICE,
				 "You can force removal of packages with failed "
				 "prerm scripts with the option: \n"
				 "\t--force-remove\n");
			return -1;
		}
	}

	/* DPKG_INCOMPATIBILITY: dpkg is slightly different here. It
	   maintains an empty filelist rather than deleting it. That seems
	   like a big pain, and I don't see that that should make a big
	   difference, but for anyone who wants tighter compatibility,
	   feel free to fix this. */
	remove_data_files_and_list(pkg);

	err = pkg_run_script(pkg, "postrm", "remove");

	remove_maintainer_scripts(pkg);
	pkg->state_status = SS_NOT_INSTALLED;
	pkg_alternatives_update(pkg);

	if (parent_pkg)
		parent_pkg->state_status = SS_NOT_INSTALLED;

	/* remove autoinstalled packages that are orphaned by the removal of this one */
	if (conf->autoremove) {
		if (remove_autoinstalled(pkg) != 0) {
			err = -1;
		}
	}
	return err;
}

void remove_data_files_and_list(pkg_t * pkg)
{
	str_list_t installed_dirs;
	str_list_t *installed_files;
	str_list_elt_t *iter;
	char *file_name;
	conffile_t *conffile;
	int removed_a_dir;
	pkg_t *owner;
	int rootdirlen = 0;

	installed_files = pkg_get_installed_files(pkg);
	if (installed_files == NULL) {
		opkg_msg(ERROR, "Failed to determine installed "
			 "files for %s. None removed.\n", pkg->name);
		return;
	}

	str_list_init(&installed_dirs);

	/* don't include trailing slash */
	if (conf->offline_root)
		rootdirlen = strlen(conf->offline_root);

	for (iter = str_list_first(installed_files); iter;
	     iter = str_list_next(installed_files, iter)) {
		file_name = (char *)iter->data;

		owner = file_hash_get_file_owner(file_name);
		if (owner != pkg)
			/* File may have been claimed by another package. */
			continue;

		if (file_is_dir(file_name)) {
			str_list_append(&installed_dirs, file_name);
			continue;
		}

		conffile = pkg_get_conffile(pkg, file_name + rootdirlen);
		if (conffile) {
			if (conffile_has_been_modified(conffile)) {
				opkg_msg(NOTICE,
					 "Not deleting modified conffile %s.\n",
					 file_name);
				continue;
			}
		}

		if (!conf->noaction) {
			opkg_msg(INFO, "Deleting %s.\n", file_name);
			unlink(file_name);
		} else
			opkg_msg(INFO, "Not deleting %s. (noaction)\n",
				 file_name);

		file_hash_remove(file_name);
	}

	/* Remove empty directories */
	if (!conf->noaction) {
		do {
			removed_a_dir = 0;
			for (iter = str_list_first(&installed_dirs); iter;
			     iter = str_list_next(&installed_dirs, iter)) {
				file_name = (char *)iter->data;

				if (rmdir(file_name) == 0) {
					opkg_msg(INFO, "Deleting %s.\n",
						 file_name);
					removed_a_dir = 1;
					str_list_remove(&installed_dirs, &iter);
				}
			}
		} while (removed_a_dir);
	}

	pkg_free_installed_files(pkg);
	pkg_remove_installed_files_list(pkg);

	/* Don't print warning for dirs that are provided by other packages */
	for (iter = str_list_first(&installed_dirs); iter;
	     iter = str_list_next(&installed_dirs, iter)) {
		file_name = (char *)iter->data;

		owner = file_hash_get_file_owner(file_name);
		if (owner) {
			free(iter->data);
			iter->data = NULL;
			str_list_remove(&installed_dirs, &iter);
		}
	}

	/* cleanup */
	while (!void_list_empty(&installed_dirs)) {
		iter = str_list_pop(&installed_dirs);
		free(iter->data);
		free(iter);
	}
	str_list_deinit(&installed_dirs);
}

void remove_maintainer_scripts(pkg_t * pkg)
{
	int i, err;
	char *globpattern;
	glob_t globbuf;

	if (conf->noaction)
		return;

	sprintf_alloc(&globpattern, "%s/%s.*", pkg->dest->info_dir, pkg->name);

	err = glob(globpattern, 0, NULL, &globbuf);
	free(globpattern);
	if (err)
		return;

	for (i = 0; i < globbuf.gl_pathc; i++) {
		opkg_msg(INFO, "Deleting %s.\n", globbuf.gl_pathv[i]);
		unlink(globbuf.gl_pathv[i]);
	}
	globfree(&globbuf);
}
