#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "getopt.h"

/*
 * Standard getopt global variables.  optreset starts as non-zero in order to
 * trigger initialization behaviour.
 */
const char * optarg = NULL;
int optind = 1;
int opterr = 1;
int optreset = 1;

/*
 * Quasi-internal global variables -- these are used via GETOPT macros.
 */
const char * getopt_dummy = "(dummy)";
int getopt_initialized = 0;

/*
 * Internal variables.
 */
static const char * cmdname = NULL;
static struct opt {
	const char * os;
	size_t olen;
	int hasarg;
} * opts = NULL;
static size_t nopts;
static size_t opt_missing;
static size_t opt_default;
static size_t opt_found;
static const char * packedopts;
static char popt[3];
static int atexit_registered = 0;

/* Print a message. */
#define PRINTMSG(...)	do {						\
	if (cmdname != NULL)						\
		fprintf(stderr, "%s: ", cmdname);			\
	fprintf(stderr, __VA_ARGS__);					\
	fprintf(stderr, "\n");						\
} while (0)

/* Print an error message and die. */
#define DIE(...)	do {						\
	PRINTMSG(__VA_ARGS__);						\
	abort();							\
} while (0)

/* Print a warning, if warnings are enabled. */
#define WARN(...)	do {						\
	if (opterr == 0)						\
		break;							\
	if (opt_missing != opt_default)					\
		break;							\
	PRINTMSG(__VA_ARGS__);						\
} while (0)

/* Free allocated options array. */
static void
atexit_handler(void)
{

	free(opts);
	opts = NULL;
}

/* Reset internal state. */
static void
reset(int argc, char * const argv[])
{
	const char * p;

	/* If we have arguments, stash argv[0] for error messages. */
	if (argc > 0) {
		/* Find the basename, without leading directories. */
		for (p = cmdname = argv[0]; *p != '\0'; p++) {
			if (*p == '/')
				cmdname = p + 1;
		}
	}

	/* Discard any registered command-line options. */
	free(opts);
	opts = NULL;

	/* Register atexit handler if we haven't done so already. */
	if (!atexit_registered) {
		atexit(atexit_handler);
		atexit_registered = 1;
	}

	/* We will start scanning from the first option. */
	optind = 1;

	/* We're not in the middle of any packed options. */
	packedopts = NULL;

	/* We haven't found any option yet. */
	opt_found = (size_t)(-1);

	/* We're not initialized yet. */
	getopt_initialized = 0;

	/* Finished resetting state. */
	optreset = 0;
}

/* Search for an option string. */
static size_t
searchopt(const char * os)
{
	size_t i;

	/* Scan the array of options. */
	for (i = 0; i < nopts; i++) {
		/* Is there an option in this slot? */
		if (opts[i].os == NULL)
			continue;

		/* Does this match up to the length of the option string? */
		if (strncmp(opts[i].os, os, opts[i].olen))
			continue;

		/* Do we have <option>\0 or <option>= ? */
		if ((os[opts[i].olen] == '\0') || (os[opts[i].olen] == '='))
			return (i);
	}

	/* Not found. */
	return (opt_default);
}

const char *
getopt(int argc, char * const argv[])
{
	const char * os = NULL;
	const char * canonical_os = NULL;

	/* No argument yet. */
	optarg = NULL;

	/* Reset the getopt state if needed. */
	if (optreset)
		reset(argc, argv);

	/* If not initialized, return dummy option. */
	if (!getopt_initialized)
		return (GETOPT_DUMMY);

	/* If we've run out of arguments, we're done. */
	if (optind >= argc)
		return (NULL);

	/*
	 * If we're not already in the middle of a packed single-character
	 * options, see if we should start.
	 */
	if ((packedopts == NULL) && (argv[optind][0] == '-') &&
	    (argv[optind][1] != '-') && (argv[optind][1] != '\0')) {
		/* We have one or more single-character options. */
		packedopts = &argv[optind][1];
	}

	/* If we're processing single-character options, fish one out. */
	if (packedopts != NULL) {
		/* Construct the option string. */
		popt[0] = '-';
		popt[1] = *packedopts;
		popt[2] = '\0';
		os = popt;

		/* We've done this character. */
		packedopts++;

		/* Are we done with this string? */
		if (*packedopts == '\0') {
			packedopts = NULL;
			optind++;
		}
	}

	/* If we don't have an option yet, do we have dash-dash? */
	if ((os == NULL) && (argv[optind][0] == '-') &&
	    (argv[optind][1] == '-')) {
		/* If this is not "--\0", it's an option. */
		if (argv[optind][2] != '\0')
			os = argv[optind];

		/* Either way, we want to eat the string. */
		optind++;
	}

	/* If we have found nothing which looks like an option, we're done. */
	if (os == NULL)
		return (NULL);

	/* Search for the potential option. */
	opt_found = searchopt(os);

	/* If the option is not registered, give up now. */
	if (opt_found == opt_default) {
		WARN("unknown option: %s", os);
		return (os);
	}

	/* The canonical option string is the one registered. */
	canonical_os = opts[opt_found].os;

	/* Does the option take an argument? */
	if (opts[opt_found].hasarg) {
		/*
		 * If we're processing packed single-character options, the
		 * rest of the string is the argument to this option.
		 */
		if (packedopts != NULL) {
			optarg = packedopts;
			packedopts = NULL;
			optind++;
		}

		/*
		 * If the option string is <option>=<value>, extract that
		 * value as the option argument.
		 */
		if (os[opts[opt_found].olen] == '=')
			optarg = &os[opts[opt_found].olen + 1];

		/*
		 * If we don't have an argument yet, take one from the
		 * remaining command line.
		 */
		if ((optarg == NULL) && (optind < argc))
			optarg = argv[optind++];

		/* If we still have no option, declare it MIA. */
		if (optarg == NULL) {
			WARN("option requires an argument: %s",
			    opts[opt_found].os);
			opt_found = opt_missing;
		}
	} else {
		/* If we have --foo=bar, something went wrong. */
		if (os[opts[opt_found].olen] == '=') {
			WARN("option doesn't take an argument: %s",
			    opts[opt_found].os);
			opt_found = opt_default;
		}
	}

	/* Return the canonical option string. */
	return (canonical_os);
}

size_t
getopt_lookup(const char * os)
{

	/* Can't reset here. */
	if (optreset)
		DIE("Can't reset in the middle of getopt loop");

	/* We should only be called after initialization is complete. */
	assert(getopt_initialized);

	/* GETOPT_DUMMY should never get passed back to us. */
	assert(os != GETOPT_DUMMY);

	/*
	 * Make sure the option passed back to us corresponds to the one we
	 * found earlier.
	 */
	assert((opt_found == opt_missing) || (opt_found == opt_default) ||
	    ((opt_found < nopts) && (strcmp(os, opts[opt_found].os) == 0)));

	/* Return the option number we identified earlier. */
	return (opt_found);
}

void
getopt_register_opt(const char * os, size_t ln, int hasarg)
{

	/* Can't reset here. */
	if (optreset)
		DIE("Can't reset in the middle of getopt loop");

	/* We should only be called during initialization. */
	assert(!getopt_initialized);

	/* We should have space allocated for registering options. */
	assert(opts != NULL);

	/* We should not have registered an option here yet. */
	assert(opts[ln].os == NULL);

	/* Options should be "-X" or "--foo". */
	if ((os[0] != '-') || (os[1] == '\0') ||
	    ((os[1] == '-') && (os[2] == '\0')) ||
	    ((os[1] != '-') && (os[2] != '\0')))
		DIE("Not a valid command-line option: %s", os);

	/* Make sure we haven't already registered this option. */
	if (searchopt(os) != opt_default)
		DIE("Command-line option registered twice: %s", os);

	/* Record option. */
	opts[ln].os = os;
	opts[ln].olen = strlen(os);
	opts[ln].hasarg = hasarg;
}

void
getopt_register_missing(size_t ln)
{

	/* Can't reset here. */
	if (optreset)
		DIE("Can't reset in the middle of getopt loop");

	/* We should only be called during initialization. */
	assert(!getopt_initialized);

	/* Record missing-argument value. */
	opt_missing = ln;
}

void
getopt_setrange(size_t ln)
{
	size_t i;

	/* Can't reset here. */
	if (optreset)
		DIE("Can't reset in the middle of getopt loop");

	/* We should only be called during initialization. */
	assert(!getopt_initialized);

	/* Allocate space for options. */
	opts = malloc(ln * sizeof(struct opt));
	if ((ln > 0) && (opts == NULL))
		DIE("Failed to allocate memory in getopt");

	/* Initialize options. */
	for (i = 0; i < ln; i++)
		opts[i].os = NULL;

	/* Record the number of (potential) options. */
	nopts = ln;

	/* Record default missing-argument and no-such-option values. */
	opt_missing = opt_default = ln + 1;
}
