/*******************************************************************************
* Copyright (C) 2006 Intel Corp. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*  - Redistributions of source code must retain the above copyright notice,
*    this list of conditions and the following disclaimer.
*
*  - 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.
*
*  - Neither the name of Intel Corp. nor the names of its
*    contributors may be used to endorse or promote products derived from this
*    software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 Intel Corp. OR THE 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.
*******************************************************************************/

/**
 * @author Denis Sadykov
 */
#ifdef HAVE_CONFIG_H
#include <wsman_config.h>
#endif

#include "u/libu.h"

#define FOUND_HELP ((void *)1)
/* ----------------------------------------------------------------------------
			Misc
----------------------------------------------------------------------------- */

struct tmp_buf {
	long int		lint;
	char			*ptr;
	char			**pptr;
	u_option_entry_t	*entry;
};

typedef struct tmp_help_buf {
	char		*part1;
	char		*part2;
} tmp_help_buf_t;

typedef struct help_buf {
	tmp_help_buf_t	*buf;
	unsigned int	num;
	size_t		maxlen;
} help_buf_t;

static void put_to_help_buf(help_buf_t *help_buf,
			    char count,
			    const char *format, ...)
{
	va_list			ap;
	tmp_help_buf_t		*buf = &(help_buf->buf[help_buf->num]);

	buf->part2 = NULL;
	va_start(ap, format);
	buf->part1 = u_strdup_vprintf(format, ap);
	if (!buf->part1) {
		va_end(ap);
		return;
	}
	va_end(ap);
	if (count)
		help_buf->maxlen = (strlen(buf->part1) > help_buf->maxlen) ?
				strlen(buf->part1) : help_buf->maxlen;
	help_buf->num += 1;
}

static void print_help_buf(help_buf_t *help_buf)
{
	unsigned int		i,	j;
	size_t			align;
	tmp_help_buf_t		*buf = help_buf->buf;

	for (i = 0; i < help_buf->num; i++) {
		printf("%s", buf[i].part1);
		if (buf[i].part2) {
			align = (help_buf->maxlen > 50) ?
				help_buf->maxlen + 5 - strlen(buf[i].part1) :
				50 - strlen(buf[i].part1);
			for (j = 0; j < align; j++) {
				printf(" ");
			}
			printf("%s", buf[i].part2);
		}
		printf("\n");
	}
}

static void free_help_buf(help_buf_t *help_buf)
{
	unsigned int		i;
	tmp_help_buf_t		*buf = help_buf->buf;

	for (i = 0; i < help_buf->num; i++) {
		u_free(buf[i].part1);
		if (buf[i].part2)
			u_free(buf[i].part2);
	}
}

static void print_help_header(u_option_context_t *ctx, help_buf_t *help_buf)
{
	lnode_t			*node;
	u_option_group_t	*grp;
	tmp_help_buf_t		*buf = help_buf->buf;

	put_to_help_buf(help_buf, 0, "Usage:");
	put_to_help_buf(help_buf, 0, "  %s [Option...] %s",
			ctx->prog_name, ctx->usage);
	put_to_help_buf(help_buf, 0, "");
	put_to_help_buf(help_buf, 0, "Help Options");
	put_to_help_buf(help_buf, 1, "  -?, --help");

	node = list_first(ctx->groups);
	if (node && list_next(ctx->groups, node)) {
		put_to_help_buf(help_buf, 1, "  --help-all");
		buf[help_buf->num - 1].part2 = u_strdup("Show help options");
	}
	if (node) {
		node = list_next(ctx->groups, node);
		while (node) {
			grp = (u_option_group_t *)node->list_data;
			put_to_help_buf(help_buf, 1, "  --help-%s", grp->name);
			if (grp->help_descr) {
				buf[help_buf->num - 1].part2 =
						u_strdup(grp->help_descr);
			}
			node = list_next(ctx->groups, node);
		}
	}
	put_to_help_buf(help_buf, 0, "");

}

static void print_help_group(u_option_group_t *group, help_buf_t *help_buf)
{
	unsigned int		i;
	tmp_help_buf_t		*buf = help_buf->buf;

	if (group->ismain) {
		put_to_help_buf(help_buf, 0, "Application Options");
	} else {
		if (group->descr)
			put_to_help_buf(help_buf, 0, "%s", group->descr);
		else
			put_to_help_buf(help_buf, 0, "%s", group->name);
	}

	for (i = 0; i < group->num_entries; i++) {
		if (group->entries[i].short_name &&
				group->entries[i].arg_descr) {
			put_to_help_buf(help_buf, 1, "  -%c, --%s=%s",
					group->entries[i].short_name,
					group->entries[i].name,
					group->entries[i].arg_descr);
		} else if (group->entries[i].short_name) {
			put_to_help_buf(help_buf, 1, "  -%c, --%s",
					group->entries[i].short_name,
					group->entries[i].name);
		} else if (group->entries[i].arg_descr){
			put_to_help_buf(help_buf, 1, "  --%s=%s",
					group->entries[i].name,
					group->entries[i].arg_descr);
		} else {
			put_to_help_buf(help_buf, 1, "  --%s",
					group->entries[i].name);
		}
		if (group->entries[i].descr) {
			buf[help_buf->num - 1].part2 =
				u_strdup_printf("%s",
					group->entries[i].descr);
		}
	}
	put_to_help_buf(help_buf, 0, "");
}

static void print_short_help(u_option_context_t *ctx)
{
	help_buf_t		help_buf;
	tmp_help_buf_t		buf[1024];
	lnode_t			*node;

	help_buf.buf = buf;
	help_buf.maxlen = 0;
	help_buf.num = 0;

	node = list_first(ctx->groups);
	print_help_header(ctx, &help_buf);
	if (node)
		print_help_group((u_option_group_t *)node->list_data, &help_buf);
	print_help_buf(&help_buf);
	free_help_buf(&help_buf);

}

static void print_long_help(u_option_context_t *ctx, char *hoption)
{
	lnode_t			*node = NULL,	*first;
	u_option_group_t	*grp;
	char			*help_str;
	help_buf_t		help_buf;
	tmp_help_buf_t		buf[1024];

	help_buf.buf = buf;
	help_buf.maxlen = 0;
	help_buf.num = 0;

	first = list_first(ctx->groups);
	if (!first) {
		print_help_header(ctx, &help_buf);
		print_help_buf(&help_buf);
		free_help_buf(&help_buf);
		return;
	}
	node = list_next(ctx->groups, first);
	while (node) {
		grp = (u_option_group_t *)node->list_data;
		help_str = u_strdup_printf("help-%s", grp->name);
		if (!help_str)
			continue;
		if (!strncmp(hoption, help_str, strlen(help_str))) {
			u_free(help_str);
			break;
		}
		u_free(help_str);
		node = list_next(ctx->groups, node);
	}

	if (!node) {
		if (!strncmp(hoption, "help-all", strlen("help-all")) &&
				strlen(hoption) == strlen("help-all")) {
			grp = (u_option_group_t *)first->list_data;
		} else if (!strncmp(hoption, "help", strlen("help")) &&
				strlen(hoption) == strlen("help")) {
			grp = NULL;
		} else {
			return;
		}
	}

	print_help_header(ctx, &help_buf);

	if (grp == NULL) {
		grp = (u_option_group_t *)first->list_data;
		print_help_group(grp, &help_buf);
	} else if (grp == (u_option_group_t *)first->list_data) {
		node = list_next(ctx->groups, first);
		while (node) {
			grp = (u_option_group_t *)node->list_data;
			print_help_group(grp, &help_buf);
			node = list_next(ctx->groups, node);
		}
		grp = (u_option_group_t *)first->list_data;
		print_help_group(grp, &help_buf);
	} else {
		print_help_group(grp, &help_buf);
	}

	print_help_buf(&help_buf);
	free_help_buf(&help_buf);
}

static unsigned int context_get_number_entries(u_option_context_t *ctx)
{
	lnode_t			*node;
	u_option_group_t	*grp;
	unsigned int		num_entr = 0;

	if (!ctx) {
		return 0;
	}

	node = list_first(ctx->groups);
	while (node) {
		grp = (u_option_group_t *)node->list_data;
		num_entr += grp->num_entries;
		node = list_next(ctx->groups, node);
	}

	return num_entr;
}

static char fill_tmp_data(struct tmp_buf *data,
			  int *filled,
			  char *optptr,
			  char *argptr,
			  u_option_entry_t *entry,
			  u_error_t **error)
{
	int	nd;
	unsigned int	count;
	char		**pptr;
	long int	val;

	for (nd = 0; nd < *filled; nd++) {
		if (data[nd].entry == entry) {
			break;
		}
	}

	switch(entry->arg) {
	case U_OPTION_ARG_INT:
		if (!isstrdigit(argptr)) {
			u_error_new(error, 0,
				"Cannot parse integer value \'%s\' for -%s\n",
				argptr, optptr);
			return 0;
		}
		val = strtol(argptr, (char **)NULL, 10);
		if (val == LONG_MIN || val == LONG_MAX) {
			u_error_new(error, 0,
				"Integer value \'%s\'for -%s out of range\n",
				argptr, optptr);
			return 0;
		}
		data[nd].lint = val;
		break;
	case U_OPTION_ARG_STRING:
		data[nd].ptr = u_strdup(argptr);
		break;
	case  U_OPTION_ARG_STRING_ARRAY:
		count = 0;
		pptr = data[nd].pptr;
		while (pptr != NULL && pptr[count] != NULL) {
			++count;
		}
		data[nd].pptr = u_malloc(sizeof(char *) * (count + 2));
		data[nd].pptr[count] = u_strdup(argptr);
		data[nd].pptr[count + 1] = NULL;
		while (count != 0) {
			count--;
			data[nd].pptr[count] = pptr[count];
		}
		if (pptr)
			u_free(pptr);
		break;
	case U_OPTION_ARG_NONE:
		data[nd].lint = 1;
		break;
	default:
		u_error_new(error, 0, "Unknown argument data type for -%s\n",
									optptr);
		return 0;
	}

	if (nd == *filled) {
		data[nd].entry = entry;
		*filled += 1;
	}

	return 1;
}

static void free_tmp_data(struct tmp_buf *data, int nd)
{
	int	i,	count;

	for (i = 0; i < nd; i++) {
		switch (data[i].entry->arg) {
		case U_OPTION_ARG_STRING:
			if (data[i].ptr) {
				u_free(data[i].ptr);
			}
			break;
		case U_OPTION_ARG_STRING_ARRAY:
			count = 0;
			while (data[i].pptr != NULL &&
						data[i].pptr[count] != NULL) {
				u_free(data[i].pptr[count]);
					count++;
				}
			if (data[i].pptr) {
				u_free(data[i].pptr);
			}
			break;
		}
	}
}

static void get_tmp_data(struct tmp_buf *data, int nd)
{
	int			i;
	u_option_entry_t	*entry;

	for (i = 0; i < nd; i++) {
		entry = data[i].entry;
		switch (entry->arg) {
		case U_OPTION_ARG_INT:
			*(long int *)entry->arg_data = data[i].lint;
			break;
		case U_OPTION_ARG_STRING:
			*(char **)entry->arg_data = data[i].ptr;
			break;
		case U_OPTION_ARG_STRING_ARRAY:
			*(char ***)entry->arg_data = data[i].pptr;
			break;
		case U_OPTION_ARG_NONE:
			*(char *)entry->arg_data = (char)data[i].lint;
		}
	}
}

static u_option_entry_t* find_long_opt(u_option_context_t *ctx, char *option)
{
	lnode_t			*node;
	u_option_group_t	*grp;
	unsigned int			e;
	size_t			nlen;

	if (!ctx) {
		return NULL;
	}

	if (!strncmp(option, "help", strlen("help")) ) {
		if (ctx->mode & U_OPTION_CONTEXT_HELP_ENABLED) {
			print_long_help(ctx, option);
                  return FOUND_HELP;
		}
	}

	node = list_first(ctx->groups);
	while (node) {
		grp = (u_option_group_t *)node->list_data;

		for (e = 0; e < grp->num_entries; e++) {
			nlen = strlen(grp->entries[e].name);

			if (!strncmp(option, grp->entries[e].name, nlen)) {
				if (strlen(option) != nlen &&
						*(option + nlen) != '=') {
					continue;
				}
				return &(grp->entries[e]);
			}
		}
		node = list_next(ctx->groups, node);
	}

	return NULL;
}

static u_option_entry_t* find_short_opt(u_option_context_t *ctx, char option)
{
	lnode_t			*node;
	u_option_group_t	*grp;
	unsigned int			e;

	if (!ctx) {
		return NULL;
	}

	if (option == '?') {
		if (ctx->mode & U_OPTION_CONTEXT_HELP_ENABLED) {
			print_short_help(ctx);
                  return FOUND_HELP;
		}
	}

	node = list_first(ctx->groups);
	while (node) {
		grp = (u_option_group_t *)node->list_data;

		for (e = 0; e < grp->num_entries; e++) {
			if (option == grp->entries[e].short_name) {
				return &(grp->entries[e]);
			}
		}
		node = list_next(ctx->groups, node);
	}

	return NULL;
}

/* ----------------------------------------------------------------------------
			Group
----------------------------------------------------------------------------- */

u_option_group_t* u_option_group_new(const char *name,
				     const char *descr,
				     const char *help_descr)
{
	u_option_group_t	*grp;

	grp = u_zalloc(sizeof(u_option_group_t));
	if (!grp)
		return NULL;

	if (name) {
		grp->name = u_strdup(name);
	}

	if (descr) {
		grp->descr = u_strdup(descr);
	}

	if (help_descr) {
		grp->help_descr = u_strdup(help_descr);
	}

	return grp;
}

void u_option_group_free(u_option_group_t *group)
{
	if (!group)
		return;

	if (group->name)
		u_free(group->name);
	if (group->descr)
		u_free(group->descr);
	if(group->help_descr)
		u_free(group->help_descr);
    u_free(group);
}

void u_option_group_add_entries(u_option_group_t *group,
				u_option_entry_t *entries)
{
	unsigned int	i;

	if (!group || !entries) {
		return;
	}

	for (i = 0; entries[i].name; i++);
	group->entries = entries;
	group->num_entries = i;

}

/* ----------------------------------------------------------------------------
			Context
----------------------------------------------------------------------------- */

u_option_context_t* u_option_context_new(const char *usage)
{
	u_option_context_t	*ctx;

	ctx = (u_option_context_t *) u_zalloc(sizeof(u_option_context_t));
	if (!ctx)
		return NULL;

	if (usage) {
		ctx->usage = u_strdup(usage);
	}

	ctx->groups = list_create(LISTCOUNT_T_MAX);


	ctx->mode |= U_OPTION_CONTEXT_HELP_ENABLED;

	return ctx;
}

void u_option_context_free(u_option_context_t *ctx)
{
	lnode_t		*node,	*tmp;

	if (!ctx)
		return;

	node = list_first(ctx->groups);
	while (node) {
		tmp = list_next(ctx->groups, node);
		u_option_group_free((u_option_group_t *)node->list_data);
		list_delete (ctx->groups, node);
		lnode_destroy(node);
		node = tmp;
	}
	list_destroy(ctx->groups);

	if (ctx->usage)
		u_free(ctx->usage);

	if (ctx->prog_name)
		u_free(ctx->prog_name);

	u_free(ctx);
}

void u_option_context_add_group(u_option_context_t *ctx,
				u_option_group_t *group)
{
	lnode_t		*node;

	if (!ctx || !group)
		return;

	node = lnode_create((void *)group);
	if (node)
		list_append(ctx->groups, node);
}

void u_option_context_add_main_entries(u_option_context_t *ctx,
				       u_option_entry_t *options,
				       const char *name)
{
	u_option_group_t	*grp;
	lnode_t			*node;

	if (!ctx || !options) {
		return;
	}

	grp = u_option_group_new(name, NULL, NULL);
	if (!grp)
		return;
	u_option_group_add_entries(grp, options);
	grp->ismain = 1;
	node = lnode_create((void *)grp);
	if (node)
		list_prepend(ctx->groups, node);
}

void u_option_context_set_ignore_unknown_options(u_option_context_t *ctx,
						 char ignore)
{
	if (!ctx)
		return;

	if (ignore == 1)
		ctx->mode |= U_OPTION_CONTEXT_IGNORE_UNKNOWN;
	else
		ctx->mode &= ~U_OPTION_CONTEXT_IGNORE_UNKNOWN;

}

void u_option_context_set_help_enabled(u_option_context_t *ctx,
					char help_enabled)
{
	if (!ctx)
		return;

	if (help_enabled == 1)
		ctx->mode |= U_OPTION_CONTEXT_HELP_ENABLED;
	else
		ctx->mode &= ~U_OPTION_CONTEXT_HELP_ENABLED;

}

/* ----------------------------------------------------------------------------
			Parse
 * return 0 on error
 * return 1 on ok
 * return 2 on help
----------------------------------------------------------------------------- */

char u_option_context_parse(u_option_context_t *ctx,
			    int *argc,
			    char ***argv,
			    u_error_t **error)
{
	char		**tmp_argv,		**largv;
	int		arg_ind = 0,		noopt = 0;
	char		*optptr = NULL,		*argptr = NULL;
	char		next_shopt = '\0';
	int		nd = 0;
	size_t		nlen;
	int		i,	j;
	char		retval = 0;
	int     l_argc = *argc;

	u_option_entry_t	*found=NULL;
	struct tmp_buf		*tmp_data = NULL;;

	if (!ctx)
		return 0;

	tmp_argv = u_zalloc(sizeof(char *) * (*argc));
	largv = u_zalloc(sizeof(char *) * (*argc));
	if (!tmp_argv || !largv) {
		goto err_alloc;
	}

	optptr = (*argv)[0];
	while (*optptr != '\0') {
		if (*optptr == '/')
			argptr = optptr;
		optptr++;
	}
	if (argptr) {
		ctx->prog_name = u_strdup(argptr + 1);
	} else {
		ctx->prog_name = u_strdup((*argv)[0]);
	}
	if (!ctx->prog_name) {
		goto err_alloc;
	}

	for (i = 1; i < *argc; i++) {
		largv[i] = strdup((*argv)[i]);
		if (!largv[i]) {
			u_free(ctx->prog_name);
			goto err_strdup;
		}
	}

	tmp_data = u_zalloc(sizeof(struct tmp_buf) *
					context_get_number_entries(ctx));
	if (!tmp_data) {
		u_free(ctx->prog_name);
		goto err_strdup;
	}

	for(i = 1; i < *argc;) {
		if (*largv[i] != '-') {
			tmp_argv[noopt] = largv[i];
			noopt++;
			goto cont;
		}

		argptr = NULL;
		if (next_shopt != '\0') {
			optptr += 1;
			*optptr = next_shopt;
			next_shopt = '\0';
		} else {
			optptr = largv[i] + 1;
		}
		if (*optptr == '-' && optptr == largv[i] + 1) {
			found = find_long_opt(ctx, optptr + 1);
                        if (found == FOUND_HELP) {
                          goto ret;
                        }
                        else if (found) {
				nlen = strlen(found->name);
				if (strlen(optptr + 1) != nlen) {
					argptr = optptr + nlen + 2;
					*(optptr + 1 + nlen) = '\0';
				} else {
					if (i + 1 < *argc) {
						argptr = largv[i + 1];
						arg_ind++;
					}
				}
			}
		} else {
			found = find_short_opt(ctx, *optptr);
                        if (found == FOUND_HELP) {
                          goto ret;
                        }
			else if (found) {
				if (arg_ind + i + 1 < *argc) {
					arg_ind++;
					argptr = largv[arg_ind + i];
				}
				next_shopt = *(optptr + 1);
				*(optptr + 1) = '\0';
			}
		}

		if (!found) {
			u_error_new(error, 0,
				"Unknown option %s\n", (*argv)[i]);
			goto ret;
		}

		if (found->arg == U_OPTION_ARG_NONE) {
			if (argptr) {
				arg_ind--;
				argptr = NULL;
			}
			if (found->arg_data) {
				if (!fill_tmp_data(tmp_data, &nd, NULL,
							NULL, found, error)) {
					goto ret;
				}
			}
			goto cont;
		}

		if (argptr == NULL) {
			u_error_new(error, 0,
				"Missing argument for -%s\n", optptr);
			goto ret;
		}

		if (found->arg_data == NULL) {
			goto cont;
		}

		if (!fill_tmp_data(tmp_data, &nd, optptr,
					argptr, found, error)) {
			goto ret;
		}
  cont:
		if (next_shopt == '\0') {
			i = (argptr == largv[i + arg_ind] || !argptr) ?
						i + arg_ind + 1 : i + 1;
			arg_ind = 0;
		}
	}
  ret:
		if (*error) {
			free_tmp_data(tmp_data, nd);
		} else {
			for (i = 1; i < *argc; i++) {
				for (j = 0; j < noopt; j++) {
					if (!strcmp((*argv)[i], tmp_argv[j])) {
						(*argv)[j + 1] = (*argv)[i];
						break;
					}
				}
			}
			get_tmp_data(tmp_data, nd);
                        retval = (found == FOUND_HELP) ? 2 : 1;
		}
		*argc = noopt + 1;

  err_strdup:
		for (i = 1; i < l_argc; i++)
			u_free(largv[i]);

  err_alloc:
		u_free(largv);
		u_free(tmp_argv);
		u_free(tmp_data);

	return retval;
}
