/*
 * Copyright (c) 2014 Red Hat.
 * 
 * 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 (at your
 * option) 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.
 */
#include <ctype.h>
#include "pmapi.h"

static int lineno;
static int count;
static char buffer[4096];

static inline char *
skip_whitespace(char *buf)
{
    while (buf[0] && isspace((int)buf[0]))
	buf++;
    return buf;
}

static inline char *
skip_nonwhitespace(char *buf)
{
    while (buf[0] && !isspace((int)buf[0]))
	buf++;
    return buf;
}

static inline char *
seek_character(char *buf, int seek)
{
    for (; *buf; buf++)
	if (seek == (int)(*buf))
	    return buf;
    return buf;
}

/*
 * Parse non-option commands: getopts, (short)usage, or end.
 * The return code indicates whether the end directive was observed.
 */
static int
command(pmOptions *opts, char *buf)
{
    char *start, *finish;

    start = skip_whitespace(buf);

    if (strncasecmp(start, "getopt", sizeof("getopt")-1) == 0) {
	start = skip_whitespace(skip_nonwhitespace(start));
	finish = skip_nonwhitespace(start);
	*finish = '\0';
	if (pmDebugOptions.desperate)
	    fprintf(stderr, "%s: getopt command: '%s'\n", pmGetProgname(), start);
	if ((opts->short_options = strdup(start)) == NULL)
	    pmNoMem("short_options", strlen(start), PM_FATAL_ERR);
	return 0;
    }

    if (strncasecmp(start, "usage", sizeof("usage")-1) == 0) {
	start = skip_whitespace(skip_nonwhitespace(start));
	if (pmDebugOptions.desperate)
	    fprintf(stderr, "%s: usage command: '%s'\n", pmGetProgname(), start);
	if ((opts->short_usage = strdup(start)) == NULL)
	    pmNoMem("short_usage", strlen(start), PM_FATAL_ERR);
	return 0;
    }

    if (strncasecmp(start, "end", sizeof("end")-1) == 0) {
	if (pmDebugOptions.desperate)
	    fprintf(stderr, "%s: end command\n", pmGetProgname());
	return 1;
    }

    fprintf(stderr, "%s: unrecognized command: '%s'\n", pmGetProgname(), buf);
    return 0;
}

static int
append_option(pmOptions *opts, pmLongOptions *longopt)
{
    pmLongOptions *entry;
    size_t size = sizeof(pmLongOptions);

    /* space for existing entries, new entry and the sentinal */
    size = (count + 1) * sizeof(pmLongOptions) + sizeof(pmLongOptions);
    if ((entry = realloc(opts->long_options, size)) == NULL)
	pmNoMem("append", size, PM_FATAL_ERR);
    opts->long_options = entry;
    entry += count++;
    /* if not first entry: find current sentinal, overwrite with new option */
    memcpy(entry, longopt, sizeof(pmLongOptions));
    memset(entry + 1, 0, sizeof(pmLongOptions));    /* insert new sentinal */
    return 0;
}

static int
append_text(pmOptions *opts, char *buf, size_t length)
{
    pmLongOptions text = PMAPI_OPTIONS_TEXT("");

    if (pmDebugOptions.desperate)
	fprintf(stderr, "%s: append: '%s'\n", pmGetProgname(), buf);
    if ((text.message = strdup(buf)) == NULL)
	pmNoMem("append_text", length, PM_FATAL_ERR);
    return append_option(opts, &text);
}

static pmLongOptions *
search_long_options(pmLongOptions *stock, const char *name)
{
    pmLongOptions *entry;

    for (entry = stock; entry->long_opt != NULL; entry++)
	if (strcmp(entry->long_opt, name) == 0)
	    return entry;
    return NULL;
}

static pmLongOptions *
search_short_options(pmLongOptions *stock, int opt)
{
    pmLongOptions *entry;

    for (entry = stock; entry->long_opt != NULL; entry++)
	if (entry->short_opt == opt)
	    return entry;
    return NULL;
}

static int
standard_options(pmOptions *opts, char *start)
{
    pmLongOptions stock[] = {
	PMAPI_GENERAL_OPTIONS,
	PMOPT_SPECLOCAL,
	PMOPT_LOCALPMDA,
	PMOPT_HOSTSFILE,
	PMOPT_HOST_LIST,
	PMOPT_ARCHIVE_LIST,
	PMOPT_ARCHIVE_FOLIO,
	PMOPT_CONTAINER,
	PMAPI_OPTIONS_END
    };
    pmLongOptions *entry;

    entry = (start[1] == '-') ?
		search_long_options(stock, start + 2) :
		search_short_options(stock, (int)start[1]);
    if (entry)
	return append_option(opts, entry);
    fprintf(stderr, "%s: cannot find PCP option \"%s\", line %d ignored\n",
		    pmGetProgname(), start, lineno);
    return -EINVAL;
}

static int
options(pmOptions *opts, char *buf, size_t length)
{
    char *start, *finish, *token;
    pmLongOptions option = { 0 };

    /*
     * Two cases to deal with here - a "standard" option e.g. --host
     * which is presented as a single word (no description) OR a full
     * description of a command line option, in one of these forms:
     *
     *     --label              dump the archive label
     *     --background=COLOR   render background with given color
     *     -d, --desc           get and print metric description
     *     -b=N, --batch=N	fetch N metrics at a time
     *     -L                   use a local context connection
     *     -X=N                 offset resulting values by N units
     */
    if (pmDebugOptions.desperate)
	fprintf(stderr, "%s: parsing option: '%s'", pmGetProgname(), buf);

    start = skip_whitespace(skip_nonwhitespace(buf));
    finish = skip_nonwhitespace(start);
    if (start[0] != '-') {
	*finish = '\0';
	return append_text(opts, buf, length);
    }

    token = skip_whitespace(finish);
    *finish = '\0';

    /* if a single word, this is the standard PCP option case - find it */
    if (!token || *token == '\0')
	return standard_options(opts, start);

    /* handle the first two example cases above -- long option only */
    if (start[1] == '-') {
	token = seek_character(start, '=');
	if (*token == '=') {
	    *token = '\0';	/* e.g. --background=COLOR  render ... */
	    token++;
	    finish = skip_nonwhitespace(token);
	    *finish = '\0';
	    if ((option.argname = strdup(token)) == NULL)
		pmNoMem("argname", strlen(token), PM_FATAL_ERR);
	    option.has_arg = 1;
	} /* else e.g. --label  dump the archive label */
	if ((option.long_opt = strdup(start + 2)) == NULL)
	    pmNoMem("longopt", strlen(start), PM_FATAL_ERR);
	token = skip_whitespace(finish + 1);
	if ((option.message = strdup(token)) == NULL)
	    pmNoMem("message", strlen(token), PM_FATAL_ERR);
	return append_option(opts, &option);
    }

    /* handle next two example cases above -- both long and short options */
    token = seek_character(start, ',');
    if (*token == ',') {
	/* e.g. -b=N, --batch=N   fetch N metrics at a time */
	option.short_opt = (int)start[1];

	/* move onto extracting --batch, [=N], and "fetch..." */
	token++;	/* move past the comma */
	if (*token == '\0' && token - buf < length)	/* move past a null */
	    token++;
	token = skip_whitespace(token);
	if ((token = seek_character(token, '-')) == NULL ||
	    (token - buf >= length) || (token[1] != '-')) {
	    fprintf(stderr, "%s: expected long option at \"%s\", line %d ignored\n",
		    pmGetProgname(), token, lineno);
	    return -EINVAL;
	}
	start = token + 2;	/* skip double-dash */
	if ((token = seek_character(start, '=')) != NULL && *token == '=') {
	    *token++ = '\0';
	    option.has_arg = 1;	/* now extract the argument name */
	    finish = skip_nonwhitespace(token);
	    *finish = '\0';
	    if ((option.argname = strdup(token)) == NULL)
		pmNoMem("argname", strlen(token), PM_FATAL_ERR);
	} else {
	    finish = skip_nonwhitespace(start);
	    *finish = '\0';
	}
	if ((option.long_opt = strdup(start)) == NULL)
	    pmNoMem("longopt", strlen(start), PM_FATAL_ERR);
	start = skip_whitespace(finish + 1);
	if ((option.message = strdup(start)) == NULL)
	    pmNoMem("message", strlen(start), PM_FATAL_ERR);
	return append_option(opts, &option);
    }

    /* handle final two example cases above -- short options only */
    if (isspace((int)start[1])) {
	fprintf(stderr, "%s: expected short option at \"%s\", line %d ignored\n",
		pmGetProgname(), start, lineno);
	return -EINVAL;
    }
    option.long_opt = "";
    option.short_opt = start[1];
    if ((token = seek_character(start, '=')) != NULL && *token == '=') {
	*token++ = '\0';
	option.has_arg = 1;	/* now extract the argument name */
	finish = skip_nonwhitespace(token);
	*finish = '\0';
	if ((option.argname = strdup(token)) == NULL)
	    pmNoMem("argname", strlen(token), PM_FATAL_ERR);
	/* e.g. -X=N  offset resulting values by N units */
	start = skip_whitespace(finish + 2);
    } else {
	/* e.g. -L    use a local context connection */
	start = skip_whitespace(start + 3);
    }
    if ((option.message = strdup(start)) == NULL)
	pmNoMem("message", strlen(start), PM_FATAL_ERR);
    return append_option(opts, &option);
}

static char *
build_short_options(pmOptions *opts)
{
    pmLongOptions *entry;
    char *shortopts;
    size_t size;
    int opt, index = 0;

    /* allocate for maximal case - every entry has a short opt and an arg */
    size = 1 + sizeof(char) * 2 * count;
    if ((shortopts = malloc(size)) == NULL)
	pmNoMem("shortopts", size, PM_FATAL_ERR);

    for (entry = opts->long_options; entry && entry->long_opt; entry++) {
	if ((opt = entry->short_opt) == 0)
	    continue;
	if (opt == '-' || opt == '|' || opt == '"')
	    continue;
	shortopts[index++] = (char)opt;
	if (entry->has_arg)
	    shortopts[index++] = ':';
    }
    shortopts[index] = '\0';
    return shortopts;
}

static int
setup(char *filename, pmOptions *opts)
{
    FILE *fp;
    size_t length;
    int sts = 0, ended = 0;

    if (filename)
	fp = fopen(filename, "r");
    else
	fp = fdopen(STDIN_FILENO, "r");
    if (!fp) {
	fprintf(stderr, "%s: cannot open %s for reading configuration\n",
		pmGetProgname(), filename? filename : "<stdin>");
	return -oserror();
    }

    while (fgets(buffer, sizeof(buffer)-1, fp) != NULL) {
	lineno++;

	length = strlen(buffer);
	if (length > 0 && buffer[length-1] == '\n')
	    buffer[--length] = '\0';

	/*
	 * Check for special command character (hash) - if found process
	 * the few commands it can represent -> getopts/usage/end.
	 * If we're finished already, we just tack the text on unchanged.
	 * Otherwise, we must deal with regular long options/headers/text.
	 */
	if (ended)
	    sts = append_text(opts, buffer, length);
	else if (buffer[0] == '#')
	    sts = ended = command(opts, buffer + 1);
	else
	    sts = options(opts, buffer, length);
	if (sts < 0)
	    break;
    }
    fclose(fp);

    /* if not given a getopt string with short options, just make one */
    if (sts >= 0 && !opts->short_options)
	opts->short_options = build_short_options(opts);

    return sts;
}

static char *
shell_string(const char *string)
{
    const char *p;
    int i = 0;

    buffer[i] = '\0';
    for (p = string; *p != '\0' && i < sizeof(buffer)-6; p++) {
	if (*p == '\'') {
	    buffer[i++] = '\'';
	    buffer[i++] = '\\';
	    buffer[i++] = '\'';
	    buffer[i++] = '\'';
	} else {
	    buffer[i++] = *p;
	}
	buffer[i] = '\0';
    }
    return buffer;
}

static pmLongOptions longopts[] = {
    PMAPI_OPTIONS_HEADER("Options"),
    PMOPT_DEBUG,
    { "usage", 0, 'u', 0, "generate usage message for calling script" },
    { "config", 1, 'c', "FILE", "read usage configuration from given FILE" },
    { "progname", 1, 'p', 0, "program name of calling script, for error reporting" },
    PMOPT_HELP,
    PMAPI_OPTIONS_END
};

static pmOptions localopts = {
    .flags = PM_OPTFLAG_POSIX,
    .short_options = "c:D:p:u?",
    .long_options = longopts,
    .short_usage = "[options] -- [progname arguments]",
};

int
main(int argc, char **argv)
{
    pmOptions	opts = { .flags = PM_OPTFLAG_POSIX };
    char	*progname = NULL;
    char	*config = NULL;
    int		c, i, usage = 0;

    /* first parse our own arguments, up to a double-hyphen */
    while ((c = pmgetopt_r(argc, argv, &localopts)) != EOF) {
	switch (c) {
	case 'D':
	    if ((c = pmSetDebug(localopts.optarg)) < 0) {
		pmprintf("%s: unrecognized debug options specification (%s)\n",
			pmGetProgname(), localopts.optarg);
		localopts.errors++;
	    }
	    break;
	case 'c':
	    config = localopts.optarg;
	    break;
	case 'p':
	    progname = localopts.optarg;
	    break;
	case 'u':
	    usage = 1;
	    break;
	case '?':
	    localopts.errors++;
	    break;
	}
    }

    if (localopts.errors) {
	pmUsageMessage(&localopts);
	exit(1);
    }

    if (setup(config, &opts) < 0)
	exit(1);
    argc -= (localopts.optind - 1);
    argv += (localopts.optind - 1);
    argv[0] = progname ? progname : pmGetProgname();

    if (usage) {
	if (progname)
	    pmSetProgname(progname);
	pmUsageMessage(&opts);
	exit(1);
    }

    i = 0;
    while ((c = pmgetopt_r(argc, argv, &opts)) != EOF) {
	if (i++)
	    putc(' ', stdout);
	if (c == 0) {
	    if (opts.optarg)
		printf("--%s '%s'", opts.long_options[opts.index].long_opt,
			shell_string(opts.optarg));
	    else
		printf("--%s", opts.long_options[opts.index].long_opt);
	}
	else if (opts.optarg)
	    printf("-%c '%s'", c, shell_string(opts.optarg));
	else
	    printf("-%c", c);
    }

    /* finally we report any remaining arguments (after a double-dash) */
    if (opts.optind < argc) {
	if (i)
	    putc(' ', stdout);
	printf("--");
	for (i = opts.optind; i < argc; i++)
	    printf(" '%s'", shell_string(argv[i]));
    }
    printf("\n");

    /* maybe error/warning messages buffered from pmgetopt_r() */
    pmflush();

    return 0;
}
