/*
 * Copyright (C) 2006 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * 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 of the
 * License, or 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

FILE_LICENCE ( GPL2_OR_LATER );

#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <getopt.h>

/** @file
 *
 * Parse command-line options
 *
 */

/**
 * Option argument
 *
 * This will point to the argument for the most recently returned
 * option, if applicable.
 */
char *optarg;

/**
 * Current option index
 *
 * This is an index into the argv[] array.  When getopt() returns -1,
 * @c optind is the index to the first element that is not an option.
 */
int optind;

/**
 * Current option character index
 *
 * This is an index into the current element of argv[].
 */
int nextchar;

/**
 * Unrecognised option
 *
 * When an unrecognised option is encountered, the actual option
 * character is stored in @c optopt.
 */
int optopt;

/**
 * Get option argument from argv[] array
 *
 * @v argc		Argument count
 * @v argv		Argument list
 * @ret argument	Option argument, or NULL
 *
 * Grab the next element of argv[], if it exists and is not an option.
 */
static const char * get_argv_argument ( int argc, char * const argv[] ) {
	char *arg;

	/* Don't overrun argv[] */
	if ( optind >= argc )
		return NULL;
	arg = argv[optind];

	/* If next argv element is an option, then it's not usable as
	 * an argument.
	 */
	if ( *arg == '-' )
		return NULL;

	/** Consume this argv element, and return it */
	optind++;
	return arg;
}

/**
 * Match long option
 *
 * @v argc		Argument count
 * @v argv		Argument list
 * @v opttext		Option text within current argv[] element
 * @v longopt		Long option specification
 * @ret option		Option to return from getopt()
 * @ret matched		Found a match for this long option
 */
static int match_long_option ( int argc, char * const argv[],
			       const char *opttext,
			       const struct option *longopt, int *option ) {
	size_t optlen;
	const char *argument = NULL;

	/* Compare option name */
	optlen = strlen ( longopt->name );
	if ( strncmp ( opttext, longopt->name, optlen ) != 0 )
		return 0;

	/* Check for inline argument */
	if ( opttext[optlen] == '=' ) {
		argument = &opttext[ optlen + 1 ];
	} else if ( opttext[optlen] ) {
		/* Long option with trailing garbage - no match */
		return 0;
	}

	/* Consume this argv element */
	optind++;

	/* If we want an argument but don't have one yet, try to grab
	 * the next argv element
	 */
	if ( ( longopt->has_arg != no_argument ) && ( ! argument ) )
		argument = get_argv_argument ( argc, argv );

	/* If we need an argument but don't have one, sulk */
	if ( ( longopt->has_arg == required_argument ) && ( ! argument ) ) {
		printf ( "Option \"%s\" requires an argument\n",
			 longopt->name );
		*option = ':';
		return 1;
	}

	/* If we have an argument where we shouldn't have one, sulk */
	if ( ( longopt->has_arg == no_argument ) && argument ) {
		printf ( "Option \"%s\" takes no argument\n", longopt->name );
		*option = ':';
		return 1;
	}

	/* Store values and return success */
	optarg = ( char * ) argument;
	if ( longopt->flag ) {
		*(longopt->flag) = longopt->val;
		*option = 0;
	} else {
		*option = longopt->val;
	}
	return 1;
}

/**
 * Match short option
 *
 * @v argc		Argument count
 * @v argv		Argument list
 * @v opttext		Option text within current argv[] element
 * @v shortopt		Option character from option specification
 * @ret option		Option to return from getopt()
 * @ret matched		Found a match for this short option
 */
static int match_short_option ( int argc, char * const argv[],
				const char *opttext, int shortopt,
				enum getopt_argument_requirement has_arg,
				int *option ) {
	const char *argument = NULL;

	/* Compare option character */
	if ( *opttext != shortopt )
		return 0;

	/* Consume option character */
	opttext++;
	nextchar++;
	if ( *opttext ) {
		if ( has_arg != no_argument ) {
			/* Consume remainder of element as inline argument */
			argument = opttext;
			optind++;
			nextchar = 0;
		}
	} else {
		/* Reached end of argv element */
		optind++;
		nextchar = 0;
	}

	/* If we want an argument but don't have one yet, try to grab
	 * the next argv element
	 */
	if ( ( has_arg != no_argument ) && ( ! argument ) )
		argument = get_argv_argument ( argc, argv );

	/* If we need an argument but don't have one, sulk */
	if ( ( has_arg == required_argument ) && ( ! argument ) ) {
		printf ( "Option \"%c\" requires an argument\n", shortopt );
		*option = ':';
		return 1;
	}

	/* Store values and return success */
	optarg = ( char * ) argument;
	*option = shortopt;
	return 1;
}

/**
 * Parse command-line options
 *
 * @v argc		Argument count
 * @v argv		Argument list
 * @v optstring		Option specification string
 * @v longopts		Long option specification table
 * @ret longindex	Index of long option (or NULL)
 * @ret option		Option found, or -1 for no more options
 *
 * Note that the caller must arrange for reset_getopt() to be called
 * before each set of calls to getopt_long().  In Etherboot, this is
 * done automatically by execv().
 */
int getopt_long ( int argc, char * const argv[], const char *optstring,
		  const struct option *longopts, int *longindex ) {
	const char *opttext = argv[optind];
	const struct option *longopt;
	int shortopt;
	enum getopt_argument_requirement has_arg;
	int option;

	/* Check for end of argv array */
	if ( optind >= argc )
		return -1;

	/* Check for end of options */
	if ( *(opttext++) != '-' )
		return -1;

	/* Check for long options */
	if ( *(opttext++) == '-' ) {
		/* "--" indicates end of options */
		if ( *opttext == '\0' ) {
			optind++;
			return -1;
		}
		for ( longopt = longopts ; longopt->name ; longopt++ ) {
			if ( ! match_long_option ( argc, argv, opttext,
						   longopt, &option ) )
				continue;
			if ( longindex )
				*longindex = ( longopt - longopts );
			return option;
		}
		optopt = '?';
		printf ( "Unrecognised option \"--%s\"\n", opttext );
		return '?';
	}

	/* Check for short options */
	if ( nextchar < 1 )
		nextchar = 1;
	opttext = ( argv[optind] + nextchar );
	while ( ( shortopt = *(optstring++) ) ) {
		has_arg = no_argument;
		while ( *optstring == ':' ) {
			has_arg++;
			optstring++;
		}
		if ( match_short_option ( argc, argv, opttext, shortopt,
					  has_arg, &option ) ) {
			return option;
		}
	}
	optopt = *opttext;
	printf ( "Unrecognised option \"-%c\"\n", optopt );
	return '?';
}
