/*-
 * Copyright (c) 2008, 2009, 2012, 2013, 2016 - 2019, 2021  Peter Pentchev
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <ctype.h>
#include <err.h>
#include <fnmatch.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef HAVE_PCRE2
#ifdef HAVE_PCRE
#error HAVE_PCRE and HAVE_PCRE2 may not be defined simultaneously
#endif

#include "confget_pcre2.h"
#endif

#ifdef HAVE_PCRE
#include "confget_pcre.h"
#endif

#include "confget.h"
#include "confget_ini.h"

struct var {
	char	*name;
	char	*value;
	char	*section;
	bool	 display;
};

/* The configuration filename specified by the user. */
const char		*filename;
/* The configuration section specified by the user. */
const char		*section;
/* The number of the variable or pattern parameters after the options. */
static int		 margc;
/* The variables to extract or patterns to match against. */
static char		* const *margv;
/* Display the variable names, or only the values? */
static bool		 showvarname;
/* Look for more variables after a match is found? */
static bool		 manyvars;
/* Should section variables override default ones? */
bool			 override;
/* The pattern to match the variable value */
static const char	*matchvalue;
/* The prefix to use when displaying the variable name */
static const char	*output_prefix;
/* The postfix to use when displaying the variable name */
static const char	*output_postfix;

size_t			 varsalloc, varscount;
struct var		*vars;

static const confget_backend	* const backends[] = {
	&confget_ini_backend,
};
#define BACKEND_CNT	(sizeof(backends) / sizeof(backends[0]))

static size_t		 find_backend(const char * const _name);
static void		 init(int _argc, char * const *_argv);
static void		 usage(bool _error);
static void		 version(void);

/* The currently used configuration backend (ini, etc.) */
const confget_backend	*backend;

/* The currently processed input file */
FILE			*conffile;

/* Has the "-c" (check only) option been specified? */
bool			 cflag;
/* In check-only mode, has the variable been found? */
bool			 cfound;
/* Has the "-l" (list the section) option been specified? */
static bool		 lflag;
/* Has the "-L" (match varname against patterns) option been specified? */
static bool		 Lflag;
/* Query for the names of the sections in the configuration file? */
bool			 qsections;
/* Query for a single feature? */
bool			 qfeature;
/* Query for all the features? */
bool			 qfeatures;
/* Shell-quote the variable values? */
static bool		 Sflag;

#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
/* Treat patterns as regular expressions? */
static bool		 xflag;

static const var_pcre	*x_matchvalue, **x_margv;

#if defined(HAVE_PCRE)
static const pcre_extra	*x_matchvalue_extra, **x_margv_extra;
#endif
#endif

/***
 * Function:
 *	usage		- display program usage information and exit
 * Inputs:
 *	error		- is this invoked in error?
 * Returns:
 *	Nothing.
 * Modifies:
 *	Writes the usage information to the standard output or standard
 *	error depending on the "error" flag.
 */
void
usage(const bool error)
{
	const char * const s1 = "Usage:\n"
	    "confget [-cOSx] [-N | -n] [-f filename] [-m pattern] [-P postfix]"
	    " [-p prefix]\n"
	    "        [-s section] [-t type] var...\n\n"
	    "confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix]"
	    " [-p prefix]\n"
	    "        [-s section] [-t type] -L pattern...\n\n"
	    "confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix]"
	    " [-p prefix]\n"
	    "        [-s section] [-t type] -l\n"
	    "confget [-f filename] -q sections [-t type]\n\n"
	    "confget -q features\n"
	    "confget -q feature NAME\n\n"
	    "confget [-hTV]\n\n";
	const char * const s2 =
	    "\t-c\tcheck if the variable is defined in the file;\n"
	    "\t-f\tspecify the configuration file to read from,\n"
	    "\t\tor \"-\" for standard input;\n"
	    "\t-h\tdisplay usage information and exit;\n"
	    "\t-L\tspecify which variables to display;\n"
	    "\t-l\tlist all variables in the specified section;\n"
	    "\t-m\tonly display values that match the specified pattern;\n"
	    "\t-N\talways display the variable name;\n"
	    "\t-n\tnever display the variable name;\n"
	    "\t-O\tallow variables in the specified section to override\n"
	    "\t\tthose placed before any section definitions;\n";
	const char * const s3 =
	    "\t-P\tdisplay this string after the variable name;\n"
	    "\t-p\tdisplay this string before the variable name;\n"
	    "\t-q\tquery for a specific type of information, e.g. the list of\n"
	    "\t\tsections defined in the configuration file;\n"
	    "\t-S\tquote the values suitably for the Bourne shell;\n"
	    "\t-s\tspecify the configuration section to read;\n"
	    "\t-T\tlist the available configuration file types;\n"
	    "\t-t\tspecify the configuration file type;\n"
	    "\t-x\ttreat the match patterns as regular expressions;\n"
	    "\t-V\tdisplay program version information and exit.";

	if (error) {
		fprintf(stderr, "%s%s%s\n", s1, s2, s3);
		exit(1);
	}
	printf("%s%s%s\n", s1, s2, s3);
}

/***
 * Function:
 *	version		- display program version information
 * Inputs:
 *	None.
 * Returns:
 *	Nothing.
 * Modifies:
 *	Nothing; displays program version information on the standard output.
 */
void
version(void)
{
	puts("confget " VERSION_STRING);
}

/***
 * Function:
 *	init		- parse the command-line options
 * Inputs:
 *	argc, argv	- the command-line parameter
 * Returns:
 *	0 on success, -1 on error.
 * Modifies:
 *	Sets section and varname on success.
 *	Writes to the standard error on error.
 */
static void
init(const int argc, char * const * const argv)
{
	conffile = NULL;

	cflag = Lflag = lflag = Sflag = showvarname = manyvars = override = false;
	bool do_help = false, do_list = false, do_version = false,
	     show_name = false, hide_name = false;
	matchvalue = NULL;
	output_prefix = output_postfix = "";
	size_t bidx = find_backend("ini");
	int ch;
	while ((ch = getopt(argc, argv, "cf:hLlm:NnOP:p:q:Ss:Tt:xV-:")) != EOF) {
		switch (ch) {
			case 'c':
				cflag = true;
				manyvars = false;
				break;

			case 'f':
				filename = optarg;
				break;

			case 'h':
				do_help = true;
				break;

			case 'L':
				Lflag = true;
				showvarname = true;
				manyvars = true;
				break;

			case 'l':
				lflag = true;
				showvarname = true;
				manyvars = true;
				break;

			case 'm':
				matchvalue = optarg;
				break;

			case 'N':
				show_name = true;
				break;

			case 'n':
				hide_name = true;
				break;

			case 'O':
				override = true;
				break;

			case 'P':
				output_postfix = optarg;
				break;

			case 'p':
				output_prefix = optarg;
				break;

			case 'q':
				if (strcmp(optarg, "sections") == 0) {
					qsections = true;
				} else if (strcmp(optarg, "feature") == 0) {
					qfeature = true;
				} else if (strcmp(optarg, "features") == 0) {
					qfeatures = true;
				} else {
					errx(1, "Unsupported query type");
				}
				break;

			case 'S':
				Sflag = true;
				break;

			case 's':
				section = optarg;
				break;

			case 'T':
				do_list = true;
				break;

			case 't':
				bidx = find_backend(optarg);
				break;

			case 'x':
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
				xflag = true;
				break;
#else
				errx(1, "No regular expression support in this confget build");
#endif

			case 'V':
				do_version = true;
				break;

			case '-':
				if (strcmp(optarg, "help") == 0)
					do_help = true;
				else if (strcmp(optarg, "version") == 0)
					do_version = true;
				else
					errx(1, "Unknown long option");
				break;

			case '?':
			default:
				usage(true);
				break;
		}
	}

	if (do_version)
		version();
	if (do_help)
		usage(false);
	if (do_list) {
		printf("Supported backends:");
		for (size_t i = 0; i < BACKEND_CNT; i++)
			printf(" %s", backends[i]->name);
		puts("");
	}
	if (do_help || do_list || do_version)
		exit(0);

	margc = argc - optind;
	margv = argv + optind;
	const char * const features[][2] = {
		{ "BASE", VERSION_STRING },
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
		{"REGEX", "1.0"},
#if defined(HAVE_PCRE)
		{"REGEX_IMPL_C_PCRE", CONFGET_REGEX_IMPL_PCRE(PCRE_MAJOR, PCRE_MINOR)},
#endif
#if defined(HAVE_PCRE2)
		{"REGEX_IMPL_C_PCRE2", CONFGET_REGEX_IMPL_PCRE2(PCRE2_MAJOR, PCRE2_MINOR)},
#endif
#endif
		{ NULL, NULL },
	};
	if (qsections + qfeature + qfeatures + lflag + Lflag +
	    (margc > 0 && !(Lflag || qfeature)) > 1) {
		errx(1, "Only a single query at a time, please!");
	} else if (qfeatures) {
		if (margc > 0)
			errx(1, "No arguments with -q features");
		bool started = false;
		for (size_t i = 0; features[i][0] != NULL; i++) {
			if (started)
				putchar(' ');
			else
				started = true;
			printf("%s=%s", features[i][0], features[i][1]);
		}
		putchar('\n');
		exit(0);
	} else if (qfeature) {
		if (margc != 1)
			errx(1, "A single feature name expected");
		for (size_t i = 0; features[i][0] != NULL; i++)
			if (strcmp(margv[0], features[i][0]) == 0) {
				puts(features[i][1]);
				exit(0);
			}
		/* Feature not found */
		exit(1);
	}

	if (cflag && (lflag || Lflag))
		errx(1, "The -c flag may not be used with -l or -L");
	if (margc == 0 && !lflag && !qsections)
		errx(1, "No matching criteria specified");
	if ((margc > 0) && lflag)
		errx(1, "Too many matching criteria specified");
	if (override && (section == NULL || section[0] == '\0'))
		errx(1, "The -O flag only makes sense with a non-empty section name");
	if (margc > 1 && !lflag) {
		showvarname = true;
		manyvars = true;
	}
	if (show_name) {
		if (hide_name)
			errx(1, "The -N and -n flags may not be used together");
		showvarname = true;
	} else if (hide_name) {
		showvarname = false;
	}

#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
	if (xflag) {
		if (matchvalue)
			do_pcre_compile(matchvalue, &x_matchvalue, &x_matchvalue_extra, "value");
		else
			x_matchvalue = NULL;

		x_margv = malloc(margc * sizeof(*x_margv));
		if (x_margv == NULL)
			err(1, "Could not allocate memory");
#if defined(HAVE_PCRE)
		x_margv_extra = malloc(margc * sizeof(*x_margv_extra));
		if (x_margv_extra == NULL)
			err(1, "Could not allocate memory");
#endif
		for (size_t t = 0; t < (size_t)margc; t++)
			do_pcre_compile(margv[t], &x_margv[t], &x_margv_extra[t], "pattern");
	}
#endif

	if (qsections && strcmp(backends[bidx]->name, "ini") != 0)
		errx(1, "The query for sections is only supported for the "
		    "'ini' backend for the present");
	backend = backends[bidx];
}

/***
 * Function:
 *	find_backend		- find a confget backend by name
 * Inputs:
 *	name			- the name of the backend
 * Returns:
 *	The index in backends[] if found; exits on error.
 * Modifies:
 *	Nothing.
 */
static size_t
find_backend(const char * const name)
{
	const size_t len = strlen(name);
	size_t tentative = BACKEND_CNT;
	for (size_t i = 0; i < BACKEND_CNT; i++) {
		if (strncmp(name, backends[i]->name, len) != 0)
			continue;
		if (backends[i]->name[len] == '\0')
			return (i);
		if (tentative != BACKEND_CNT)
			errx(1, "Ambiguous backend prefix '%s'", name);
		tentative = i;
	}
	if (tentative == BACKEND_CNT)
		errx(1, "Unknown backend '%s'", name);
	return (tentative);
}

/***
 * Function:
 *	openfile		- open the requested file for reading
 * Inputs:
 *	None.
 * Returns:
 *	Nothing; exits on error.
 * Modifies:
 *	Stores the opened file into fp.
 */
static void
openfile(void)
{
	if (backend->openfile == NULL)
		errx(2,
		    "INTERNAL ERROR: backend '%s' does not define a openfile routine\n",
		    backend->name);
	backend->openfile();
}

/***
 * Function:
 *	readfile		- scan an INI file for the requested variable
 * Inputs:
 *	None.
 * Returns:
 *	Nothing; exits on error.
 * Modifies:
 *	May write to standard output if the variable has been found.
 */
static void
readfile(void)
{

	if (backend->readfile == NULL)
		errx(2,
		    "INTERNAL ERROR: backend '%s' does not define a readfile routine\n",
		    backend->name);
	backend->readfile();
}

/***
 * Function:
 *	closefile		- close a scanned INI file
 * Inputs:
 *	None.
 * Returns:
 *	Nothing; exits on error.
 * Modifies:
 *	Closes the file pointed to by fp.
 */
static void
closefile(void)
{

	if (backend->closefile == NULL)
		errx(2,
		    "INTERNAL ERROR: backend '%s' does not define a closefile routine\n",
		    backend->name);
	backend->closefile();
}

/***
 * Function:
 *	foundvar	- process the user-requested variable
 * Inputs:
 *	sec		- the section the variable was found in
 *	name		- the variable name
 *	value		- the variable value
 * Returns:
 *	Nothing; exits on error.
 * Modifies:
 *	In check-only mode, sets cfound.
 *	In normal mode, writes to standard output.
 */
void
foundvar(const char * const sec, const char * const name,
	const char * const value)
{
	if (!lflag) {
		bool found = false;
		for (int i = 0; i < margc; i++)
			if (Lflag) {
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
				if (xflag) {
					if (do_pcre_match(x_margv[i], x_margv_extra[i], name, margv[i])) {
						found = true;
						break;
					}
				} else
#endif
				if (fnmatch(margv[i], name, 0) == 0) {
					found = true;
					break;
				}
			} else {
				if (strcmp(name, margv[i]) == 0) {
					found = true;
					break;
				}
			}
		if (!found)
			return;
	}

	bool display = true;
	if (matchvalue != NULL) {
#if defined(HAVE_PCRE) || defined(HAVE_PCRE2)
		if (xflag) {
			if (!do_pcre_match(x_matchvalue, x_matchvalue_extra, value, matchvalue))
				display = false;
		} else
#endif
		if (fnmatch(matchvalue, value, 0) == FNM_NOMATCH)
			display = false;
		if (!display && !override)
			return;
	}

	for (size_t i = 0; i < varscount; i++)
	{
		struct var * const var = &vars[i];
		if (strcmp(var->name, name) != 0)
			continue;
		free(var->value);
		var->value = strdup(value);
		free(var->section);
		var->section = sec == NULL? NULL: strdup(sec);
		var->display = display;
		return;
	}
	if (varscount == varsalloc) {
		varsalloc += 16;
		vars = realloc(vars, varsalloc * sizeof(*vars));
		if (vars == NULL)
			err(1, "Could not allocate memory for the variables' values");
	}
	vars[varscount].name = strdup(name);
	vars[varscount].value = strdup(value);
	vars[varscount].section = sec == NULL? NULL: strdup(sec);
	vars[varscount].display = display;
	varscount++;
}

static int
compare_by_name(const void * const left, const void * const right)
{
	const struct var * const vleft = left;
	const struct var * const vright = right;

	return strcmp(vleft->name, vright->name);
}

static void
displayvars(void)
{
	cfound = false;
	qsort(vars, varscount, sizeof(vars[0]), compare_by_name);
	for (size_t i = 0; i < varscount; i++)
	{
		if (!vars[i].display)
			continue;
		if (cflag) {
			cfound = true;
			return;
		}
		const char * const name = vars[i].name;
		const char * const value = vars[i].value;

		if (showvarname)
			printf("%s%s%s=", output_prefix, name, output_postfix);
		if (!Sflag) {
			printf("%s\n", value);
		} else {
			printf("'");
			const char *p = value;
			while (*p) {
				while (*p && *p != '\'')
					putchar(*p++);
				if (*p == '\0')
					break;
				printf("'\"");
				while (*p == '\'')
					putchar(*p++);
				printf("\"'");
			}
			printf("'\n");
		}
	}
}

/***
 * Function:
 *	foundsection	- process a new section header
 * Inputs:
 *	sec		- the name of the new section
 * Returns:
 *	Nothing; exits on error.
 * Modifies:
 *	In "-q sections" mode, writes to standard output.
 */
void
foundsection(const char * const _sec)
{
	if (!qsections)
		return;

	static size_t alloc = 0, count = 0;
	static char **names = NULL;
	for (size_t i = 0; i < count; i++)
		if (strcmp(_sec, names[i]) == 0)
			return;
	/* A new section! */
	if (alloc == count) {
		const size_t nalloc = 2 * alloc + 1;
		char ** const newnames = realloc(names, nalloc * sizeof(names[0]));
		if (newnames == NULL)
			err(1, "Out of memory for the section names list");
		names = newnames;
		alloc = nalloc;
	}
	names[count] = strdup(_sec);
	if (names[count] == NULL)
		err(1, "Out of memory for the new section name");
	count++;

	/* And finally output it :) */
	puts(_sec);
}

/***
 * Main routine
 */
int
main(const int argc, char * const * const argv)
{

	init(argc, argv);

	openfile();
	readfile();
	closefile();

	displayvars();
	return cflag? !cfound: 0;
}
