/*
 * Copyright (c), Edward Thomson <ethomson@edwardthomson.com>
 * All rights reserved.
 *
 * This file is part of adopt, distributed under the MIT license.
 * For full terms and conditions, see the included LICENSE file.
 *
 * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT.
 *
 * This file was produced by using the `rename.pl` script included with
 * adopt.  The command-line specified was:
 *
 * ./rename.pl cli_opt --filename=opt --include=common.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage
 */

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

#if defined(__sun) || defined(__illumos__)
# include <alloca.h>
#endif

#include "common.h"
#include "opt.h"

#ifdef _WIN32
# include <windows.h>
#else
# include <fcntl.h>
# include <sys/ioctl.h>
#endif

#ifdef _MSC_VER
# define alloca _alloca
#endif

#define spec_is_option_type(x) \
	((x)->type == CLI_OPT_TYPE_BOOL || \
	 (x)->type == CLI_OPT_TYPE_SWITCH || \
	 (x)->type == CLI_OPT_TYPE_VALUE)

GIT_INLINE(const cli_opt_spec *) spec_for_long(
	int *is_negated,
	int *has_value,
	const char **value,
	const cli_opt_parser *parser,
	const char *arg)
{
	const cli_opt_spec *spec;
	char *eql;
	size_t eql_pos;

	eql = strchr(arg, '=');
	eql_pos = (eql = strchr(arg, '=')) ? (size_t)(eql - arg) : strlen(arg);

	for (spec = parser->specs; spec->type; ++spec) {
		/* Handle -- (everything after this is literal) */
		if (spec->type == CLI_OPT_TYPE_LITERAL && arg[0] == '\0')
			return spec;

		/* Handle --no-option arguments for bool types */
		if (spec->type == CLI_OPT_TYPE_BOOL &&
		    strncmp(arg, "no-", 3) == 0 &&
		    strcmp(arg + 3, spec->name) == 0) {
			*is_negated = 1;
			return spec;
		}

		/* Handle the typical --option arguments */
		if (spec_is_option_type(spec) &&
		    spec->name &&
		    strcmp(arg, spec->name) == 0)
			return spec;

		/* Handle --option=value arguments */
		if (spec->type == CLI_OPT_TYPE_VALUE &&
		    spec->name && eql &&
		    strncmp(arg, spec->name, eql_pos) == 0 &&
		    spec->name[eql_pos] == '\0') {
			*has_value = 1;
			*value = arg[eql_pos + 1] ? &arg[eql_pos + 1] : NULL;
			return spec;
		}
	}

	return NULL;
}

GIT_INLINE(const cli_opt_spec *) spec_for_short(
	const char **value,
	const cli_opt_parser *parser,
	const char *arg)
{
	const cli_opt_spec *spec;

	for (spec = parser->specs; spec->type; ++spec) {
		/* Handle -svalue short options with a value */
		if (spec->type == CLI_OPT_TYPE_VALUE &&
		    arg[0] == spec->alias &&
		    arg[1] != '\0') {
			*value = &arg[1];
			return spec;
		}

		/* Handle typical -s short options */
		if (arg[0] == spec->alias) {
			*value = NULL;
			return spec;
		}
	}

	return NULL;
}

GIT_INLINE(const cli_opt_spec *) spec_for_arg(cli_opt_parser *parser)
{
	const cli_opt_spec *spec;
	size_t args = 0;

	for (spec = parser->specs; spec->type; ++spec) {
		if (spec->type == CLI_OPT_TYPE_ARG) {
			if (args == parser->arg_idx) {
				parser->arg_idx++;
				return spec;
			}

			args++;
		}

		if (spec->type == CLI_OPT_TYPE_ARGS && args == parser->arg_idx)
			return spec;
	}

	return NULL;
}

GIT_INLINE(int) spec_is_choice(const cli_opt_spec *spec)
{
	return ((spec + 1)->type &&
	       ((spec + 1)->usage & CLI_OPT_USAGE_CHOICE));
}

/*
 * If we have a choice with switches and bare arguments, and we see
 * the switch, then we no longer expect the bare argument.
 */
GIT_INLINE(void) consume_choices(const cli_opt_spec *spec, cli_opt_parser *parser)
{
	/* back up to the beginning of the choices */
	while (spec->type && (spec->usage & CLI_OPT_USAGE_CHOICE))
		--spec;

	if (!spec_is_choice(spec))
		return;

	do {
		if (spec->type == CLI_OPT_TYPE_ARG)
			parser->arg_idx++;
		++spec;
	} while(spec->type && (spec->usage & CLI_OPT_USAGE_CHOICE));
}

static cli_opt_status_t parse_long(cli_opt *opt, cli_opt_parser *parser)
{
	const cli_opt_spec *spec;
	char *arg = parser->args[parser->idx++];
	const char *value = NULL;
	int is_negated = 0, has_value = 0;

	opt->arg = arg;

	if ((spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2])) == NULL) {
		opt->spec = NULL;
		opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION;
		goto done;
	}

	opt->spec = spec;

	/* Future options parsed as literal */
	if (spec->type == CLI_OPT_TYPE_LITERAL)
		parser->in_literal = 1;

	/* --bool or --no-bool */
	else if (spec->type == CLI_OPT_TYPE_BOOL && spec->value)
		*((int *)spec->value) = !is_negated;

	/* --accumulate */
	else if (spec->type == CLI_OPT_TYPE_ACCUMULATOR && spec->value)
		*((int *)spec->value) += spec->switch_value ? spec->switch_value : 1;

	/* --switch */
	else if (spec->type == CLI_OPT_TYPE_SWITCH && spec->value)
		*((int *)spec->value) = spec->switch_value;

	/* Parse values as "--foo=bar" or "--foo bar" */
	else if (spec->type == CLI_OPT_TYPE_VALUE) {
		if (has_value)
			opt->value = (char *)value;
		else if ((parser->idx + 1) <= parser->args_len)
			opt->value = parser->args[parser->idx++];

		if (spec->value)
			*((char **)spec->value) = opt->value;
	}

	/* Required argument was not provided */
	if (spec->type == CLI_OPT_TYPE_VALUE &&
	    !opt->value &&
	    !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL))
		opt->status = CLI_OPT_STATUS_MISSING_VALUE;
	else
		opt->status = CLI_OPT_STATUS_OK;

	consume_choices(opt->spec, parser);

done:
	return opt->status;
}

static cli_opt_status_t parse_short(cli_opt *opt, cli_opt_parser *parser)
{
	const cli_opt_spec *spec;
	char *arg = parser->args[parser->idx++];
	const char *value;

	opt->arg = arg;

	if ((spec = spec_for_short(&value, parser, &arg[1 + parser->in_short])) == NULL) {
		opt->spec = NULL;
		opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION;
		goto done;
	}

	opt->spec = spec;

	if (spec->type == CLI_OPT_TYPE_BOOL && spec->value)
		*((int *)spec->value) = 1;

	else if (spec->type == CLI_OPT_TYPE_ACCUMULATOR && spec->value)
		*((int *)spec->value) += spec->switch_value ? spec->switch_value : 1;

	else if (spec->type == CLI_OPT_TYPE_SWITCH && spec->value)
		*((int *)spec->value) = spec->switch_value;

	/* Parse values as "-ifoo" or "-i foo" */
	else if (spec->type == CLI_OPT_TYPE_VALUE) {
		if (value)
			opt->value = (char *)value;
		else if ((parser->idx + 1) <= parser->args_len)
			opt->value = parser->args[parser->idx++];

		if (spec->value)
			*((char **)spec->value) = opt->value;
	}

	/*
	 * Handle compressed short arguments, like "-fbcd"; see if there's
	 * another character after the one we processed.  If not, advance
	 * the parser index.
	 */
	if (spec->type != CLI_OPT_TYPE_VALUE && arg[2 + parser->in_short] != '\0') {
		parser->in_short++;
		parser->idx--;
	} else {
		parser->in_short = 0;
	}

	/* Required argument was not provided */
	if (spec->type == CLI_OPT_TYPE_VALUE && !opt->value)
		opt->status = CLI_OPT_STATUS_MISSING_VALUE;
	else
		opt->status = CLI_OPT_STATUS_OK;

	consume_choices(opt->spec, parser);

done:
	return opt->status;
}

static cli_opt_status_t parse_arg(cli_opt *opt, cli_opt_parser *parser)
{
	const cli_opt_spec *spec = spec_for_arg(parser);

	opt->spec = spec;
	opt->arg = parser->args[parser->idx];

	if (!spec) {
		parser->idx++;
		opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION;
	} else if (spec->type == CLI_OPT_TYPE_ARGS) {
		if (spec->value)
			*((char ***)spec->value) = &parser->args[parser->idx];

		/*
		 * We have started a list of arguments; the remainder of
		 * given arguments need not be examined.
		 */
		parser->in_args = (parser->args_len - parser->idx);
		parser->idx = parser->args_len;
		opt->args_len = parser->in_args;
		opt->status = CLI_OPT_STATUS_OK;
	} else {
		if (spec->value)
			*((char **)spec->value) = parser->args[parser->idx];

		parser->idx++;
		opt->status = CLI_OPT_STATUS_OK;
	}

	return opt->status;
}

static int support_gnu_style(unsigned int flags)
{
	if ((flags & CLI_OPT_PARSE_FORCE_GNU) != 0)
		return 1;

	if ((flags & CLI_OPT_PARSE_GNU) == 0)
		return 0;

	/* TODO: Windows */
#if defined(_WIN32) && defined(UNICODE)
	if (_wgetenv(L"POSIXLY_CORRECT") != NULL)
		return 0;
#else
	if (getenv("POSIXLY_CORRECT") != NULL)
		return 0;
#endif

	return 1;
}

void cli_opt_parser_init(
	cli_opt_parser *parser,
	const cli_opt_spec specs[],
	char **args,
	size_t args_len,
	unsigned int flags)
{
	assert(parser);

	memset(parser, 0x0, sizeof(cli_opt_parser));

	parser->specs = specs;
	parser->args = args;
	parser->args_len = args_len;
	parser->flags = flags;

	parser->needs_sort = support_gnu_style(flags);
}

GIT_INLINE(const cli_opt_spec *) spec_for_sort(
	int *needs_value,
	const cli_opt_parser *parser,
	const char *arg)
{
	int is_negated, has_value = 0;
	const char *value;
	const cli_opt_spec *spec = NULL;
	size_t idx = 0;

	*needs_value = 0;

	if (strncmp(arg, "--", 2) == 0) {
		spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2]);
		*needs_value = !has_value;
	}

	else if (strncmp(arg, "-", 1) == 0) {
		spec = spec_for_short(&value, parser, &arg[1]);

		/*
		 * Advance through compressed short arguments to see if
		 * the last one has a value, eg "-xvffilename".
		 */
		while (spec && !value && arg[1 + ++idx] != '\0')
			spec = spec_for_short(&value, parser, &arg[1 + idx]);

		*needs_value = (value == NULL);
	}

	return spec;
}

/*
 * Some parsers allow for handling arguments like "file1 --help file2";
 * this is done by re-sorting the arguments in-place; emulate that.
 */
static int sort_gnu_style(cli_opt_parser *parser)
{
	size_t i, j, insert_idx = parser->idx, offset;
	const cli_opt_spec *spec;
	char *option, *value;
	int needs_value, changed = 0;

	parser->needs_sort = 0;

	for (i = parser->idx; i < parser->args_len; i++) {
		spec = spec_for_sort(&needs_value, parser, parser->args[i]);

		/* Not a "-" or "--" prefixed option.  No change. */
		if (!spec)
			continue;

		/* A "--" alone means remaining args are literal. */
		if (spec->type == CLI_OPT_TYPE_LITERAL)
			break;

		option = parser->args[i];

		/*
		 * If the argument is a value type and doesn't already
		 * have a value (eg "--foo=bar" or "-fbar") then we need
		 * to copy the next argument as its value.
		 */
		if (spec->type == CLI_OPT_TYPE_VALUE && needs_value) {
			/*
			 * A required value is not provided; set parser
			 * index to this value so that we fail on it.
			 */
			if (i + 1 >= parser->args_len) {
				parser->idx = i;
				return 1;
			}

			value = parser->args[i + 1];
			offset = 1;
		} else {
			value = NULL;
			offset = 0;
		}

		/* Caller error if args[0] is an option. */
		if (i == 0)
			return 0;

		/* Shift args up one (or two) and insert the option */
		for (j = i; j > insert_idx; j--)
			parser->args[j + offset] = parser->args[j - 1];

		parser->args[insert_idx] = option;

		if (value)
			parser->args[insert_idx + 1] = value;

		insert_idx += (1 + offset);
		i += offset;

		changed = 1;
	}

	return changed;
}

cli_opt_status_t cli_opt_parser_next(cli_opt *opt, cli_opt_parser *parser)
{
	assert(opt && parser);

	memset(opt, 0x0, sizeof(cli_opt));

	if (parser->idx >= parser->args_len) {
		opt->args_len = parser->in_args;
		return CLI_OPT_STATUS_DONE;
	}

	/* Handle options in long form, those beginning with "--" */
	if (strncmp(parser->args[parser->idx], "--", 2) == 0 &&
	    !parser->in_short &&
	    !parser->in_literal)
		return parse_long(opt, parser);

	/* Handle options in short form, those beginning with "-" */
	else if (parser->in_short ||
	         (strncmp(parser->args[parser->idx], "-", 1) == 0 &&
		  !parser->in_literal))
		return parse_short(opt, parser);

	/*
	 * We've reached the first "bare" argument.  In POSIX mode, all
	 * remaining items on the command line are arguments.  In GNU
	 * mode, there may be long or short options after this.  Sort any
	 * options up to this position then re-parse the current position.
	 */
	if (parser->needs_sort && sort_gnu_style(parser))
		return cli_opt_parser_next(opt, parser);

	return parse_arg(opt, parser);
}

GIT_INLINE(int) spec_included(const cli_opt_spec **specs, const cli_opt_spec *spec)
{
	const cli_opt_spec **i;

	for (i = specs; *i; ++i) {
		if (spec == *i)
			return 1;
	}

	return 0;
}

static cli_opt_status_t validate_required(
	cli_opt *opt,
	const cli_opt_spec specs[],
	const cli_opt_spec **given_specs)
{
	const cli_opt_spec *spec, *required;
	int given;

	/*
	 * Iterate over the possible specs to identify requirements and
	 * ensure that those have been given on the command-line.
	 * Note that we can have required *choices*, where one in a
	 * list of choices must be specified.
	 */
	for (spec = specs, required = NULL, given = 0; spec->type; ++spec) {
		if (!required && (spec->usage & CLI_OPT_USAGE_REQUIRED)) {
			required = spec;
			given = 0;
		} else if (!required) {
			continue;
		}

		if (!given)
			given = spec_included(given_specs, spec);

		/*
		 * Validate the requirement unless we're in a required
		 * choice.  In that case, keep the required state and
		 * validate at the end of the choice list.
		 */
		if (!spec_is_choice(spec)) {
			if (!given) {
				opt->spec = required;
				opt->status = CLI_OPT_STATUS_MISSING_ARGUMENT;
				break;
			}

			required = NULL;
			given = 0;
		}
	}

	return opt->status;
}

cli_opt_status_t cli_opt_parse(
	cli_opt *opt,
	const cli_opt_spec specs[],
	char **args,
	size_t args_len,
	unsigned int flags)
{
	cli_opt_parser parser;
	const cli_opt_spec **given_specs;
	size_t given_idx = 0;

	cli_opt_parser_init(&parser, specs, args, args_len, flags);

	given_specs = alloca(sizeof(const cli_opt_spec *) * (args_len + 1));

	while (cli_opt_parser_next(opt, &parser)) {
		if (opt->status != CLI_OPT_STATUS_OK &&
		    opt->status != CLI_OPT_STATUS_DONE)
			return opt->status;

		if ((opt->spec->usage & CLI_OPT_USAGE_STOP_PARSING))
			return (opt->status = CLI_OPT_STATUS_DONE);

		given_specs[given_idx++] = opt->spec;
	}

	given_specs[given_idx] = NULL;

	return validate_required(opt, specs, given_specs);
}

int cli_opt_foreach(
	const cli_opt_spec specs[],
	char **args,
	size_t args_len,
	unsigned int flags,
	int (*callback)(cli_opt *, void *),
	void *callback_data)
{
	cli_opt_parser parser;
	cli_opt opt;
	int ret;

	cli_opt_parser_init(&parser, specs, args, args_len, flags);

	while (cli_opt_parser_next(&opt, &parser)) {
		if ((ret = callback(&opt, callback_data)) != 0)
			return ret;
	}

	return 0;
}

static int spec_name_fprint(FILE *file, const cli_opt_spec *spec)
{
	int error;

	if (spec->type == CLI_OPT_TYPE_ARG)
		error = fprintf(file, "%s", spec->value_name);
	else if (spec->type == CLI_OPT_TYPE_ARGS)
		error = fprintf(file, "%s", spec->value_name);
	else if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
		error = fprintf(file, "-%c", spec->alias);
	else
		error = fprintf(file, "--%s", spec->name);

	return error;
}

int cli_opt_status_fprint(
	FILE *file,
	const char *command,
	const cli_opt *opt)
{
	const cli_opt_spec *choice;
	int error;

	if (command && (error = fprintf(file, "%s: ", command)) < 0)
		return error;

	switch (opt->status) {
	case CLI_OPT_STATUS_DONE:
		error = fprintf(file, "finished processing arguments (no error)\n");
		break;
	case CLI_OPT_STATUS_OK:
		error = fprintf(file, "no error\n");
		break;
	case CLI_OPT_STATUS_UNKNOWN_OPTION:
		error = fprintf(file, "unknown option: %s\n", opt->arg);
		break;
	case CLI_OPT_STATUS_MISSING_VALUE:
		if ((error = fprintf(file, "argument '")) < 0 ||
		    (error = spec_name_fprint(file, opt->spec)) < 0 ||
		    (error = fprintf(file, "' requires a value.\n")) < 0)
			break;
		break;
	case CLI_OPT_STATUS_MISSING_ARGUMENT:
		if (spec_is_choice(opt->spec)) {
			int is_choice = 1;

			if (spec_is_choice((opt->spec)+1))
				error = fprintf(file, "one of");
			else
				error = fprintf(file, "either");

			if (error < 0)
				break;

			for (choice = opt->spec; is_choice; ++choice) {
				is_choice = spec_is_choice(choice);

				if (!is_choice)
					error = fprintf(file, " or");
				else if (choice != opt->spec)
					error = fprintf(file, ",");

				if ((error < 0) ||
				    (error = fprintf(file, " '")) < 0 ||
				    (error = spec_name_fprint(file, choice)) < 0 ||
				    (error = fprintf(file, "'")) < 0)
					break;

				if (!spec_is_choice(choice))
					break;
			}

			if ((error < 0) ||
			    (error = fprintf(file, " is required.\n")) < 0)
				break;
		} else {
			if ((error = fprintf(file, "argument '")) < 0 ||
			    (error = spec_name_fprint(file, opt->spec)) < 0 ||
			    (error = fprintf(file, "' is required.\n")) < 0)
				break;
		}

		break;
	default:
		error = fprintf(file, "unknown status: %d\n", opt->status);
		break;
	}

	return error;
}

