/* ==========================================================================
 * parseopt.c
 * ==========================================================================
 * Command line options parsing.
 * --------------------------------------------------------------------------
 *   AUTHOR:  Marco Pantaleoni         E-mail: panta@elasticworld.org
 *
 *   Created: Wed Nov 15 19:24:43 CET 2000
 *
 *   $Id: parseopt.c,v 1.1.1.1 2001/04/03 10:19:16 antirez Exp $
 * --------------------------------------------------------------------------
 *    Copyright (C) 2000 Marco Pantaleoni. All rights reserved.
 *
 *  The contents of this file are subject to the elastiC License version 1.0
 *  (the "elastiC License"); you may not use this file except in compliance
 *  with the elastiC License. You may obtain a copy of the elastiC License at
 *  http://www.elasticworld.org/LICENSE
 *
 *  IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTORS BE LIABLE TO ANY PARTY
 *  FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 *  ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
 *  DERIVATIVES THEREOF, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 *
 *  THE AUTHOR AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
 *  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
 *  IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHOR AND DISTRIBUTORS HAVE
 *  NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
 *  MODIFICATIONS.
 *
 *  See the elastiC License for the specific language governing rights and
 *  limitations under the elastiC License.
 * ==========================================================================
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#if HAVE_STRING_H
#include <string.h>
#endif
#if HAVE_CTYPE_H
#include <ctype.h>
#endif

#include "parseopt.h"

#define OPT_LONGNAME(spec)				((spec)->long_name)
#define OPT_SHORTNAME(spec)				((spec)->short_name)
#define OPT_TYPE(spec)					((spec)->argument_type)
#define OPT_DEFAULT(spec)				((spec)->defval)
#define OPT_DESC(spec)					((spec)->description)
#define OPT_ARGDESC(spec)				((spec)->argdescription)
#define OPT_VALIDATE(spec)				((spec)->validate_cb)
#define OPT_VALIDATE_CLIENTDATA(spec)	((spec)->validate_clientdata)
#define OPT_ARGUMENT(spec)				((spec)->argument)
#define OPT_VALUE(spec)					((spec)->value)

#define MAXOPTNAME	256

static popt_specifier *find_spec(popt_specifier *specifiers,
								 int nspecifiers,
								 int *prefixlen,
								 int allow_partial,
								 const char *long_name, char short_name, int *opti);

static int common_prefix(const char *str1, const char *str2);

popt_return popt_parse(int argc, char * const argv[],
					   popt_specifier *specifiers,
					   int allow_partial,
					   int allow_collapsed,
					   popt_parameter parameter_cb,
					   void *parameter_cb_clientdata)
{
	const char     *src;										/* running pointer in option string                       */
	int             long_option;								/* flag indicating a long option format                   */
	int             argument_follows;							/* flag indicating that argument follows in option string */
	char            option_name[MAXOPTNAME];					/* option name */
	const char     *argument;									/* argument to option */
	char           *dst;
	popt_specifier *spec;
	int             opti;

	int             nspecifiers;
	int            *prefixlen = NULL;							/* array of unique long name prefix lengths */
	int             cpl;

	int         i, j;
	int         rv;

	nspecifiers = 0;
	spec = specifiers;
	while ((OPT_LONGNAME(spec) != NULL) ||
		   (OPT_SHORTNAME(spec) != '\0'))
	{
		nspecifiers++;
		spec++;
	}

	if (allow_partial)
	{
		/* build unique prefix list */

		/* start by building an array of *common* prefix lengths */

		prefixlen = malloc(sizeof(int) * nspecifiers);
		if (! prefixlen) return popt_error_unknown;

		for (i = 0; i < nspecifiers; i++)
			prefixlen[i] = 0;

		for (i = 0; i < nspecifiers; i++)
		{
			if (! OPT_LONGNAME(&specifiers[i]))
				continue;

			for (j = 0; j < nspecifiers; j++)
			{
				if (! OPT_LONGNAME(&specifiers[j]))
					continue;

				if (j == i) continue;

				/* how long is the common prefix ? */
				cpl = common_prefix(OPT_LONGNAME(&specifiers[i]), OPT_LONGNAME(&specifiers[j]));
				if (cpl >= prefixlen[i])
					prefixlen[i] = cpl;
			}
		}

		/*
		 * ... then finish by turning it in an array of *unique* prefix lengths
		 * (NOTE: the unique prefix length is the minimum number of characters
		 *        needed to know to uniquely distinguish a string in the set)
		 */
		for (i = 0; i < nspecifiers; i++)
		{
			if (! OPT_LONGNAME(&specifiers[i])) continue;

			if (strlen(OPT_LONGNAME(&specifiers[i])) < prefixlen[i] + 1)
				return popt_error_unknown;						/* there are non-unique options */

			prefixlen[i] = prefixlen[i] + 1;
		}
	}

	/* parse arguments */

	for (i = 1; i < argc; i++)
	{
		src  = argv[i];

		if ((! src) ||
			(src[0] != '-'))
		{
			/* found a non-option */

			if (parameter_cb)
			{
				rv = parameter_cb( src, parameter_cb_clientdata );
				if (! rv) return popt_error_unknown;
			}

			continue;
		}

		long_option = 0;
		src++;
		if (src[0] == '-')
		{
			/* long option */
			src++;
			long_option = 1;
		}

		if (long_option || (! allow_collapsed))
		{
			/* long options */

			dst = option_name;
			while (*src && (*src != '='))
			{
				*dst++ = *src++;
				if (dst - option_name >= MAXOPTNAME - 1) break;
			}
			*dst = '\0';

			spec = find_spec(specifiers, nspecifiers, prefixlen, allow_partial, option_name, '\0', &opti);
			if (! spec) return popt_error_unknown_option;

			if (OPT_VALUE(spec))
				*OPT_VALUE(spec) = *OPT_VALUE(spec) + 1;
		} else if (allow_collapsed)
		{
			/* short options, allow collapsed */

			while (*src)
			{
				spec = find_spec(specifiers, nspecifiers, prefixlen, allow_partial, NULL, *src, &opti);
				if (! spec) return popt_error_unknown_option;

				if (OPT_VALIDATE(spec))
				{
					rv = OPT_VALIDATE(spec)(spec, NULL, OPT_VALIDATE_CLIENTDATA(spec));
					if (! rv) return popt_error_invalid;
				}

				if (OPT_ARGUMENT(spec))
					*OPT_ARGUMENT(spec) = NULL;
				if (OPT_VALUE(spec))
					*OPT_VALUE(spec) = *OPT_VALUE(spec) + 1;

				src++;
			}
		} else
		{
			/* short options, no collapsed */

			if ((src[0] == '\0') ||
				(src[1] != '\0'))
				return popt_error_invalid;
			spec = find_spec(specifiers, nspecifiers, prefixlen, allow_partial, NULL, *src, &opti);
			if (! spec) return popt_error_unknown_option;

			if (OPT_ARGUMENT(spec))
				*OPT_ARGUMENT(spec) = NULL;
			if (OPT_VALUE(spec))
				*OPT_VALUE(spec) = *OPT_VALUE(spec) + 1;
		}

		if ((*src == '=') &&
			((OPT_TYPE(spec) == popt_argument_none) || (! long_option)))
		{
			/* bad format: argument was not expected */
			return popt_error_unexpected_argument;
		}

		argument_follows = 0;
		if ((OPT_TYPE(spec) != popt_argument_none) && long_option && (*src == '='))
		{
			/* argument follows in this string */

			src++;
			argument_follows = 1;

			argument = src;
		} else if (OPT_TYPE(spec) != popt_argument_none)
		{
			/* argument is next option */

			i++;
			if (i >= argc)
			{
				if (OPT_TYPE(spec) == popt_argument_optional_default)
					argument = OPT_DEFAULT(spec);
				else
					return popt_error_missing_argument;
			} else
				argument = argv[i];
		} else
			argument = NULL;

		if (OPT_ARGUMENT(spec))
		{
			if (OPT_TYPE(spec) != popt_argument_none)
			{
				if (argument)
					*OPT_ARGUMENT(spec) = argument;
				else if (OPT_TYPE(spec) == popt_argument_optional_default)
					*OPT_ARGUMENT(spec) = OPT_DEFAULT(spec);
				else
					*OPT_ARGUMENT(spec) = NULL;
			}
		}
	}

	return popt_ok;
}

int popt_validate_int(popt_specifier *spec, const char *argument, void *resp)
{
	char *eptr;
	long res;

	if (! argument) return FALSE;

	res = strtol(argument, &eptr, 0);
	if ((*argument != '\0') && (*eptr == '\0'))
	{
		*OPT_VALUE(spec) = res;
		if (resp)
			*((long *)resp) = res;
		return TRUE;
	}
	return FALSE;
}

int popt_validate_float(popt_specifier *spec, const char *argument, void *resp)
{
	char   *eptr;
	double  res;

	if (! argument) return FALSE;

	res = strtod(argument, &eptr);
	if ((*argument == '\0') || (eptr == argument))
		return FALSE;
	if (resp)
		*((double *)resp) = res;
	return TRUE;
}

int popt_validate_yesno(popt_specifier *spec, const char *argument, void *resp)
{
	if ((! argument) || (*argument == '\0'))
		return FALSE;

	if ((strcasecmp(argument, "yes") == 0) ||
		(strcasecmp(argument, "y") == 0))
	{
		*OPT_VALUE(spec) = 1;
		if (resp)
			*((int *)resp) = 1;
		return TRUE;
	} else if ((strcasecmp(argument, "no") == 0) ||
			   (strcasecmp(argument, "n") == 0))
	{
		*OPT_VALUE(spec) = 0;
		if (resp)
			*((int *)resp) = 0;
		return TRUE;
	}

	return FALSE;
}

int popt_validate_bool(popt_specifier *spec, const char *argument, void *resp)
{
	if ((! argument) || (*argument == '\0'))
		return FALSE;

	if ((strcasecmp(argument, "true") == 0) ||
		(strcasecmp(argument, "t") == 0))
	{
		*OPT_VALUE(spec) = 1;
		if (resp)
			*((int *)resp) = 1;
		return TRUE;
	} else if ((strcasecmp(argument, "false") == 0) ||
			   (strcasecmp(argument, "f") == 0))
	{
		*OPT_VALUE(spec) = 0;
		if (resp)
			*((int *)resp) = 0;
		return TRUE;
	}

	return FALSE;
}

#define MAXLINE 256
#define MAXOPTDESC (MAXOPTNAME + 2 + 22)

char *popt_make_help(popt_specifier *specifiers)
{
	char *help = NULL, *newhelp;
	char line[MAXLINE];
	char short_opt_desc[3];
	char long_opt[MAXOPTDESC];
	char long_opt_desc[MAXOPTDESC];

	int  short_present;
	int  long_present;

	popt_specifier *spec;

	int linelen;
	int helplen = 0;

	spec = specifiers;
	while (spec)
	{
		if ((OPT_LONGNAME(spec)  == NULL) &&
			(OPT_SHORTNAME(spec) == '\0'))
		{
			spec = NULL;
			break;
		}

		short_present = 0;
		long_present  = 0;

		if ((OPT_SHORTNAME(spec) != '\0') &&
			isprint(OPT_SHORTNAME(spec)))
		{
			snprintf(short_opt_desc, 3, "-%c", OPT_SHORTNAME(spec));
			short_present = 1;
		} else
			snprintf(short_opt_desc, 3, "%-2s", "");

		if (OPT_LONGNAME(spec))
		{
			if (OPT_ARGDESC(spec))
				snprintf(long_opt, MAXOPTDESC,
						 "%s--%s=%s",
						 short_present ? ", " : "  ",
						 OPT_LONGNAME(spec), OPT_ARGDESC(spec));
			else
				snprintf(long_opt, MAXOPTDESC,
						 "%s--%s",
						 short_present ? ", " : "  ",
						 OPT_LONGNAME(spec));
			snprintf(long_opt_desc, MAXOPTDESC, "%-40s", long_opt);
			long_present = 1;
		} else if (OPT_ARGDESC(spec))
		{
			snprintf(long_opt_desc, MAXOPTDESC, " %-41s", OPT_ARGDESC(spec));
		} else
			snprintf(long_opt_desc, MAXOPTDESC, "%-42s", "");

		snprintf(line, MAXLINE,
				 "   %s%s %s\n",
				 short_opt_desc, long_opt_desc, OPT_DESC(spec));

		linelen = strlen(line);

		if (! help)
			newhelp = malloc(linelen + 1);
		else
			newhelp = realloc(help, helplen + linelen + 1);
		if (! newhelp)
		{
			if (help) free(help);
			return NULL;
		}
		help = newhelp;
		strcat(help + helplen, line);
		helplen += linelen;

		spec++;
	}

	return help;
}

static popt_specifier *find_spec(popt_specifier *specifiers,
								 int             nspecifiers,
								 int            *prefixlen,
								 int             allow_partial,
								 const char *long_name, char short_name, int *opti)
{
	popt_specifier *spec;
	int idx;

	if (opti)
		*opti = -1;

	idx = 0;
	spec = specifiers;
	while (spec)
	{
		if ((OPT_LONGNAME(spec)  == NULL) &&
			(OPT_SHORTNAME(spec) == '\0'))
		{
			spec = NULL;
			break;
		}

		if (allow_partial && prefixlen)
		{
			if (long_name && (strncmp(OPT_LONGNAME(spec), long_name, prefixlen[idx]) == 0))
				break;
		} else
		{
			if (long_name && (strcmp(OPT_LONGNAME(spec), long_name) == 0))
				break;
		}

		if ((! long_name) && (OPT_SHORTNAME(spec) == short_name))
			break;

		spec++;
		idx++;
	}

	if (opti)
		*opti = idx;

	return spec;
}

static int common_prefix(const char *str1, const char *str2)
{
	int pl;

	pl = 0;
	while ((str1 && str2) &&
		   (*str1 && *str2) &&
		   (*str1++ == *str2++))
		pl++;

	return pl;
}
