/*
 * dselect - Debian package maintenance user interface
 * main.cc - main program
 *
 * Copyright © 1994-1996 Ian Jackson <ijackson@chiark.greenend.org.uk>
 * Copyright © 2000,2001 Wichert Akkerman <wakkerma@debian.org>
 * Copyright © 2006-2015 Guillem Jover <guillem@debian.org>
 *
 * This 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 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, see <https://www.gnu.org/licenses/>.
 */

#include <config.h>
#include <compat.h>

#include <sys/types.h>
#include <sys/wait.h>

#include <errno.h>
#include <limits.h>
#ifdef HAVE_LOCALE_H
#include <locale.h>
#endif
#include <ctype.h>
#include <string.h>
#include <fcntl.h>
#include <dirent.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

// Solaris requires curses.h to be included before <term.h>.
#include "dselect-curses.h"

#if defined(HAVE_NCURSESW_TERM_H)
#include <ncursesw/term.h>
#elif defined(HAVE_NCURSES_TERM_H)
#include <ncurses/term.h>
#else
#include <term.h>
#endif

#include <dpkg/i18n.h>
#include <dpkg/dpkg.h>
#include <dpkg/dpkg-db.h>
#include <dpkg/options.h>
#include <dpkg/fsys.h>

#include "dselect.h"
#include "bindings.h"
#include "pkglist.h"

static const char printforhelp[] = N_("Type dselect --help for help.");

bool expertmode = false;

static keybindings packagelistbindings(packagelist_kinterps,
                                       packagelist_korgbindings);

struct table_t {
	const char *name;
	const int num;
};

static const struct table_t colortable[] = {
	{ "black",	COLOR_BLACK	},
	{ "red",	COLOR_RED	},
	{ "green",	COLOR_GREEN	},
	{ "yellow",	COLOR_YELLOW	},
	{ "blue",	COLOR_BLUE	},
	{ "magenta",	COLOR_MAGENTA	},
	{ "cyan",	COLOR_CYAN	},
	{ "white",	COLOR_WHITE	},
	{ nullptr,	0		},
};

static const struct table_t attrtable[] = {
	{ "normal",	A_NORMAL	},
	{ "standout",	A_STANDOUT	},
	{ "underline",	A_UNDERLINE	},
	{ "reverse",	A_REVERSE	},
	{ "blink",	A_BLINK		},
	{ "bright",	A_BLINK		}, // On some terminals.
	{ "dim",	A_DIM		},
	{ "bold",	A_BOLD		},
	{ nullptr,	0		},
};

/*
 * A slightly confusing mapping from dselect's internal names to
 * the user-visible names.
 */
static const struct table_t screenparttable[] = {
	{ "list",		list		},
	{ "listsel",		listsel		},
	{ "title",		title		},
	{ "infohead",		thisstate	},
	{ "pkgstate",		selstate	},
	{ "pkgstatesel",	selstatesel	},
	{ "listhead",		colheads	},
	{ "query",		query		},
	{ "info",		info_body	},
	{ "infodesc",		info_head	},
	{ "infofoot",		whatinfo	},
	{ "helpscreen",		helpscreen	},
	{ nullptr,		0		},
};

// Original colors.
struct colordata color[] = {
	/*
	 * Fore		Back		Attr
	 * ====		====		====
	 */

	// default, not used
	{ COLOR_WHITE,	COLOR_BLACK,	0			},
	// list
	{ COLOR_WHITE,	COLOR_BLACK,	0			},
	// listsel
	{ COLOR_WHITE,	COLOR_BLACK,	A_REVERSE		},
	// title
	{ COLOR_WHITE,	COLOR_RED,	0			},
	// thisstate
	{ COLOR_WHITE,	COLOR_BLUE,	0			},
	// selstate
	{ COLOR_WHITE,	COLOR_BLACK,	A_BOLD			},
	// selstatesel
	{ COLOR_WHITE,	COLOR_BLACK,	A_REVERSE | A_BOLD	},
	// colheads
	{ COLOR_WHITE,	COLOR_BLUE,	0			},
	// query
	{ COLOR_WHITE,	COLOR_RED,	0			},
	// info_body
	{ COLOR_WHITE,	COLOR_BLACK,	0			},
	// info_head
	{ COLOR_WHITE,	COLOR_BLACK,	A_BOLD			},
	// whatinfo
	{ COLOR_WHITE,	COLOR_BLUE,	0			},
	// help
	{ COLOR_WHITE,	COLOR_BLACK,	0			},
};

struct menuentry {
	const char *command;
	const char *key;
	const char *option;
	const char *menuent;
	urqfunction *fn;
};

static const menuentry menuentries[] = {
	{
		"access",
		N_("a"),
		N_("[A]ccess"),
		N_("Choose the access method to use."),
		&urq_setup,
	}, {
		"update",
		N_("u"),
		N_("[U]pdate"),
		N_("Update list of available packages, if possible."),
		&urq_update,
	}, {
		"select",
		N_("s"),
		N_("[S]elect"),
		N_("Request which packages you want on your system."),
		&urq_list,
	}, {
		"install",
		N_("i"),
		N_("[I]nstall"),
		N_("Install and upgrade wanted packages."),
		&urq_install,
	}, {
		"config",
		N_("c"),
		N_("[C]onfig"),
		N_("Configure any packages that are unconfigured."),
		&urq_config,
	}, {
		"remove",
		N_("r"),
		N_("[R]emove"),
		N_("Remove unwanted software."),
		&urq_remove,
	}, {
		"quit",
		N_("q"),
		N_("[Q]uit"),
		N_("Quit dselect."),
		&urq_quit,
	}, {
		nullptr,
		nullptr,
		N_("menu"),
		nullptr,
		&urq_menu,
	}, {
		nullptr,
	}
};

static const char programdesc[] =
	N_("Debian '%s' package handling frontend version %s.\n");

static const char licensestring[] = N_(
"This is free software; see the GNU General Public License version 2 or\n"
"later for copying conditions. There is NO warranty.\n");

static void DPKG_ATTR_NORET
printversion(const struct cmdinfo *ci, const char *value)
{
	printf(gettext(programdesc), DSELECT, PACKAGE_RELEASE);
	printf("%s", gettext(licensestring));

	m_output(stdout, _("<standard output>"));

	exit(0);
}

static void DPKG_ATTR_NORET
usage(const struct cmdinfo *ci, const char *value)
{
	int i;

	printf(_(
"Usage: %s [<option>...] [<command>...]\n"
"\n"), DSELECT);

	printf(_("Commands:\n"));
	for (i = 0; menuentries[i].command; i++)
		printf("  %-10s  %s\n",
		       menuentries[i].command,
		       menuentries[i].menuent);
	fputs("\n", stdout);

	printf(_(
"Options:\n"
"      --admindir <directory>       Use <directory> instead of %s.\n"
"      --instdir <directory>        Use <directory> instead of %s.\n"
"      --root <directory>           Use <directory> instead of %s.\n"
"      --expert                     Turn on expert mode.\n"
"  -D, --debug <file>               Turn on debugging, send output to <file>.\n"
"      --color <color-spec>         Configure screen colors.\n"
"      --colour <color-spec>        Ditto.\n"
), ADMINDIR, "/", "/");

	printf(_(
"  -?, --help                       Show this help message.\n"
"      --version                    Show the version.\n"
"\n"));

	printf(_("<color-spec> is <screen-part>:[<foreground>],[<background>][:<attr>[+<attr>]...]\n"));

	printf(_("<screen-part> is:"));
	for (i = 0; screenparttable[i].name; i++)
		printf(" %s", screenparttable[i].name);
	fputs("\n", stdout);

	printf(_("<color> is:"));
	for (i = 0; colortable[i].name; i++)
		printf(" %s", colortable[i].name);
	fputs("\n", stdout);

	printf(_("<attr> is:"));
	for (i = 0; attrtable[i].name; i++)
		printf(" %s", attrtable[i].name);
	fputs("\n", stdout);

	m_output(stdout, _("<standard output>"));

	exit(0);
}

// These are called by C code, so need to have C calling convention.
extern "C" {

static void
set_debug(const struct cmdinfo*, const char *v)
{
	FILE *fp;

	fp = fopen(v, "a");
	if (!fp)
		ohshite(_("cannot open debug file '%s'\n"), v);

	debug_set_output(fp, v);
	debug_set_mask(dbg_general | dbg_depcon);
}

static void
set_expert(const struct cmdinfo*, const char *v)
{
	expertmode = true;
}

static int
findintable(const struct table_t *table, const char *item,
            const char *tablename)
{
	int i;

	for (i = 0; item && (table[i].name != nullptr); i++)
		if (strcasecmp(item, table[i].name) == 0)
			return table[i].num;

	ohshit(_("invalid %s '%s'"), tablename, item);
}

/*
 *  The string's format is:
 *    screenpart:[forecolor][,backcolor][:[<attr>,...]
 *  Examples:
 *    --color title:black,cyan:bright+underline
 *    --color list:red,yellow
 *    --color colheads:,green:bright
 *    --color selstate::reverse  // FIXME: Doesn't work.
 */
static void
set_color(const struct cmdinfo*, const char *string)
{
	char *s;
	char *colors, *attributes;
	int screenpart;

	// strtok() modifies strings, keep string const.
	s = m_strdup(string);
	screenpart = findintable(screenparttable, strtok(s, ":"),
	                         _("screen part"));
	colors = strtok(nullptr, ":");
	attributes = strtok(nullptr, ":");

	if ((colors == nullptr || ! strlen(colors)) &&
	    (attributes == nullptr || ! strlen(attributes))) {
		ohshit(_("missing color specification"));
	}

	if (colors != nullptr && strlen(colors)) {
		char *colorname;

		colorname = strtok(colors, ",");
		if (colorname != nullptr && strlen(colorname)) {
			// Normalize attributes to prevent confusion.
			color[screenpart].attr = A_NORMAL;
			color[screenpart].fore = findintable(colortable,
			                                     colorname,
			                                     _("color"));
		}
		colorname = strtok(nullptr, ",");
		if (colorname != nullptr && strlen(colorname)) {
			color[screenpart].attr = A_NORMAL;
			color[screenpart].back = findintable(colortable,
			                                     colorname,
			                                     _("color"));
		}
	}

	if (attributes != nullptr && strlen(attributes)) {
		for (char *attrib = strtok(attributes, "+");
		     attrib != nullptr && strlen(attrib);
		     attrib = strtok(nullptr, "+")) {
			int aval;

			aval = findintable(attrtable, attrib,
			                   _("color attribute"));
			if (aval == A_NORMAL)
				// Set to normal.
				color[screenpart].attr = aval;
			else
				// Add to existing attribs.
				color[screenpart].attr = color[screenpart].attr | aval;
		}
	}

	free(s);
}

} /* End of extern "C". */

static const struct cmdinfo cmdinfos[] = {
	{ "admindir",     0,  1, nullptr,  nullptr,   set_admindir, 1 },
	{ "instdir",      0,  1, nullptr,  nullptr,   set_instdir, 1 },
	{ "root",         0,  1, nullptr,  nullptr,   set_root, 1  },
	{ "debug",       'D', 1, nullptr,  nullptr,   set_debug    },
	{ "expert",      'E', 0, nullptr,  nullptr,   set_expert   },
	{ "help",        '?', 0, nullptr,  nullptr,   usage        },
	{ "version",      0,  0, nullptr,  nullptr,   printversion },
	/* US spelling. */
	{ "color",        0,  1, nullptr,  nullptr,   set_color    },
	/* UK spelling. */
	{ "colour",       0,  1, nullptr,  nullptr,   set_color    },
	{ nullptr,        0,  0, nullptr,  nullptr,   nullptr      }
};

static bool cursesareon = false;

void
curseson()
{
	if (cursesareon)
		return;

	const char *cup, *smso;

	initscr();
	cup = tigetstr("cup");
	smso = tigetstr("smso");
	if (!cup || !smso) {
		endwin();
		if (!cup)
			fputs(_("Terminal does not appear to support cursor addressing.\n"),
			      stderr);
		if (!smso)
			fputs(_("Terminal does not appear to support highlighting.\n"),
			      stderr);
		fprintf(stderr,
		        _("Set your TERM variable correctly, use a better terminal,\n"
		          "or make do with the per-package management tool %s.\n"),
		        DPKG);
		ohshit(_("terminal lacks necessary features, giving up"));
	}

	cursesareon = true;
}

void
cursesoff()
{
	if (!cursesareon)
		return;

	clear();
	refresh();
	endwin();

	cursesareon = false;
}

urqresult
urq_list(void)
{
	modstatdb_open(static_cast<modstatdb_rw>(msdbrw_writeifposs |
	                                         msdbrw_available_readonly));

	curseson();

	packagelist *l = new packagelist(&packagelistbindings);
	l->resolvesuggest();
	l->display();
	delete l;

	modstatdb_shutdown();

	return urqr_normal;
}

static void
display_menu_entry(int i, int so)
{
	const menuentry *me = &menuentries[i];

	varbuf buf;
	buf.add_fmt(" %c %d. %-11.11s %-80.80s ",
	            so ? '*' : ' ', i,
	            gettext(me->option),
	            gettext(me->menuent));

	int x, y DPKG_ATTR_UNUSED;
	getmaxyx(stdscr, y, x);

	attrset(so ? A_REVERSE : A_NORMAL);
	mvaddnstr(i + 2, 0, buf.str(), x - 1);
	attrset(A_NORMAL);
}

static int
refreshmenu(void)
{
	curseson();
	cbreak();
	noecho();
	nonl();
	keypad(stdscr, TRUE);

	int x, y DPKG_ATTR_UNUSED;
	getmaxyx(stdscr, y, x);

	varbuf buf;
	buf.add_fmt(gettext(programdesc), DSELECT, PACKAGE_RELEASE);

	clear();
	attrset(A_BOLD);
	mvaddnstr(0, 0, buf.str(), x - 1);

	attrset(A_NORMAL);
	const struct menuentry *mep;
	int i;
	for (mep = menuentries, i = 0; mep->option && mep->menuent; mep++, i++)
		display_menu_entry(i, 0);

	attrset(A_BOLD);
	addstr(_("\n\n"
	       "Move around with Ctrl+P and Ctrl+N, cursor keys, initial letters, or digits;\n"
	       "Press <enter> to confirm selection. Ctrl+L redraws screen.\n\n"));

	attrset(A_NORMAL);
	addstr(_("Copyright (C) 1994-1996 Ian Jackson.\n"
	         "Copyright (C) 2000,2001 Wichert Akkerman.\n"));
	addstr(gettext(licensestring));

	modstatdb_init();
	if (!modstatdb_can_lock())
		addstr(_("\n\n"
		         "Read-only access: only preview of selections is available!"));
	modstatdb_done();

	return i;
}

urqresult
urq_menu(void)
{
	int entries, c;
	entries = refreshmenu();

	int cursor = 0;
	display_menu_entry(0, 1);
	for (;;) {
		refresh();
		do {
			c = getch();
			if (c == KEY_RESIZE) {
				refreshmenu();
				display_menu_entry(cursor, 1);
				continue;
			}
		} while (c == ERR && errno == EINTR);
		if (c == ERR)  {
			if (errno != 0) {
				ohshite(_("failed to getch in main menu"));
			} else {
				clearok(stdscr, TRUE);
				clear();
				refreshmenu();
				display_menu_entry(cursor, 1);
			}
		}

		if (c == CTRL('n') ||
		    c == KEY_DOWN ||
		    c == ' ' ||
		    c == 'j') {
			display_menu_entry(cursor, 0);
			cursor++;
			cursor %= entries;
			display_menu_entry(cursor, 1);
		} else if (c == CTRL('p') ||
		           c == KEY_UP ||
		           c == CTRL('h') ||
		           c == KEY_BACKSPACE ||
		           c == KEY_DC ||
		           c == 'k') {
			display_menu_entry(cursor, 0);
			cursor += entries - 1;
			cursor %= entries;
			display_menu_entry(cursor, 1);
		} else if (c == '\n' ||
		           c == '\r' ||
		           c == KEY_ENTER) {
			clear();
			refresh();

			// FIXME: Trap errors in urq_...
			urqresult res = menuentries[cursor].fn();
			switch (res) {
			case urqr_quitmenu:
				return urqr_quitmenu;
			case urqr_normal:
				cursor++;
				cursor %= entries;
			case urqr_fail:
				break;
			default:
				internerr("unknown menufn %d", res);
			}
			refreshmenu();
			display_menu_entry(cursor, 1);
		} else if (c == CTRL('l')) {
			clearok(stdscr, TRUE);
			clear();
			refreshmenu();
			display_menu_entry(cursor, 1);
		} else if (isdigit(c)) {
			char buf[2];
			buf[0] = c;
			buf[1] = 0;
			c = atoi(buf);
			if (c < entries) {
				display_menu_entry(cursor, 0);
				cursor = c;
				display_menu_entry(cursor, 1);
			} else {
				beep();
			}
		} else if (isalpha(c)) {
			c = tolower(c);
			int i = 0;
			while (i < entries && gettext(menuentries[i].key)[0] != c)
				i++;
			if (i < entries) {
				display_menu_entry(cursor, 0);
				cursor = i;
				display_menu_entry(cursor, 1);
			} else {
				beep();
			}
		} else {
			beep();
		}
	}
}

urqresult
urq_quit(void)
{
	// FIXME: Check packages OK.
	return urqr_quitmenu;
}

static void
dselect_catch_fatal_error()
{
	cursesoff();
	catch_fatal_error();
}

int
main(int, const char *const *argv)
{
	dpkg_locales_init(DSELECT);
	dpkg_set_progname(DSELECT);

	push_error_context_func(dselect_catch_fatal_error, print_fatal_error, nullptr);

	dpkg_options_load(DSELECT, cmdinfos);
	dpkg_options_parse(&argv, cmdinfos, printforhelp);

	debug(dbg_general,
	      "root=%s admindir=%s", dpkg_fsys_get_dir(), dpkg_db_get_dir());

	if (*argv) {
		const char *a;
		while ((a = *argv++) != nullptr) {
			const menuentry *me = menuentries;
			while (me->command && strcmp(me->command, a))
				me++;
			if (!me->command)
				badusage(_("unknown action string '%s'"), a);
			me->fn();
		}
	} else {
		urq_menu();
	}

	cursesoff();
	dpkg_program_done();
	dpkg_locales_done();

	return 0;
}
