#include <errno.h>
#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#if HAVE_ALLOCA_H
# include <alloca.h>
#endif

#include "popt.h"

struct optionStackEntry {
    int argc;
    char ** argv;
    int next;
    char * nextArg;
    char * nextCharArg;
    struct poptAlias * currAlias;
};

struct poptContext_s {
    struct optionStackEntry optionStack[POPT_OPTION_DEPTH], * os;
    char ** leftovers;
    int numLeftovers;
    int nextLeftover;
    const struct poptOption * options;
    int restLeftover;
    char * appName;
    struct poptAlias * aliases;
    int numAliases;
    int flags;
};

#ifndef HAVE_STRERROR
static char * strerror(int errno) {
    extern int sys_nerr;
    extern char * sys_errlist[];

    if ((0 <= errno) && (errno < sys_nerr))
	return sys_errlist[errno];
    else
	return "unknown errno";
}
#endif

poptContext poptGetContext(char * name ,int argc, char ** argv, 
			   const struct poptOption * options, int flags) {
    poptContext con = malloc(sizeof(*con));

    con->os = con->optionStack;
    con->os->argc = argc;
    con->os->argv = argv;
    con->os->currAlias = NULL;
    con->os->nextCharArg = NULL;
    con->os->nextArg = NULL;

    if (flags & POPT_KEEP_FIRST)
	con->os->next = 0;			/* include argv[0] */
    else
	con->os->next = 1;			/* skip argv[0] */

    con->leftovers = malloc(sizeof(char *) * (argc + 1));
    con->numLeftovers = 0;
    con->nextLeftover = 0;
    con->restLeftover = 0;
    con->options = options;
    con->aliases = NULL;
    con->numAliases = 0;
    con->flags = 0;
    
    if (!name)
	con->appName = NULL;
    else
	con->appName = strcpy(malloc(strlen(name) + 1), name);

    return con;
}

void poptResetContext(poptContext con) {
    con->os = con->optionStack;
    con->os->currAlias = NULL;
    con->os->nextCharArg = NULL;
    con->os->nextArg = NULL;
    con->os->next = 1;			/* skip argv[0] */

    con->numLeftovers = 0;
    con->nextLeftover = 0;
    con->restLeftover = 0;
}

/* returns 'val' element, -1 on last item, POPT_ERROR_* on error */
int poptGetNextOpt(poptContext con) {
    char * optString, * chptr, * localOptString;
    char * longArg = NULL;
    char * origOptString;
    long aLong;
    char * end;
    const struct poptOption * opt = NULL;
    int done = 0;
    int i;

    while (!done) {
	while (!con->os->nextCharArg && con->os->next == con->os->argc 
		&& con->os > con->optionStack)
	    con->os--;
	if (!con->os->nextCharArg && con->os->next == con->os->argc)
	    return -1;

	if (!con->os->nextCharArg) {
		
	    origOptString = con->os->argv[con->os->next++];

	    if (con->restLeftover || *origOptString != '-') {
		con->leftovers[con->numLeftovers++] = origOptString;
		continue;
	    }

	    /* Make a copy we can hack at */
	    localOptString = optString = 
			strcpy(alloca(strlen(origOptString) + 1), 
			origOptString);

	    if (!optString[0])
		return POPT_ERROR_BADOPT;

	    if (optString[1] == '-' && !optString[2]) {
		con->restLeftover = 1;
		continue;
	    } else if (optString[1] == '-') {
		optString += 2;

		if (!con->os->currAlias || !con->os->currAlias->longName || 
		    strcmp(con->os->currAlias->longName, optString)) {

		    i = con->numAliases - 1;
		    while (i >= 0 && (!con->aliases[i].longName ||
			strcmp(con->aliases[i].longName, optString))) i--;

		    if (i >= 0) {
			if ((con->os - con->optionStack + 1) 
				== POPT_OPTION_DEPTH)
			    return POPT_ERROR_OPTSTOODEEP;

			con->os++;
			con->os->next = 0;
			con->os->nextArg = con->os->nextCharArg = NULL;
			con->os->currAlias = con->aliases + i;
			con->os->argc = con->os->currAlias->argc;
			con->os->argv = con->os->currAlias->argv;
			continue;
		    }
		}

		chptr = optString;
		while (*chptr && *chptr != '=') chptr++;
		if (*chptr == '=') {
		    longArg = origOptString + (chptr - localOptString) + 1;
		    *chptr = '\0';
		}

		opt = con->options;
		while (opt->longName || opt->shortName) {
		    if (opt->longName && !strcmp(optString, opt->longName))
			break;
		    opt++;
		}

		if (!opt->longName && !opt->shortName) return POPT_ERROR_BADOPT;
	    } else 
		con->os->nextCharArg = origOptString + 1;
	}

	if (con->os->nextCharArg) {
	    origOptString = con->os->nextCharArg;

	    con->os->nextCharArg = NULL;

	    if (!con->os->currAlias || *origOptString != 
		con->os->currAlias->shortName) {

		i = con->numAliases - 1;
		while (i >= 0 &&
		    con->aliases[i].shortName != *origOptString) i--;

		if (i >= 0) {
		    if ((con->os - con->optionStack + 1) == POPT_OPTION_DEPTH)
			return POPT_ERROR_OPTSTOODEEP;

		    /* We'll need this on the way out */
		    origOptString++;
		    if (*origOptString)
			con->os->nextCharArg = origOptString;

		    con->os++;
		    con->os->next = 0;
		    con->os->nextArg = con->os->nextCharArg = NULL;
		    con->os->currAlias = con->aliases + i;
		    con->os->argc = con->os->currAlias->argc;
		    con->os->argv = con->os->currAlias->argv;
		    continue;
		}
	    }

	    opt = con->options;
	    while ((opt->longName || opt->shortName) && 
		    *origOptString != opt->shortName) opt++;
	    if (!opt->longName && !opt->shortName) return POPT_ERROR_BADOPT;

	    origOptString++;
	    if (*origOptString)
		con->os->nextCharArg = origOptString;
	}

	if (opt->arg && opt->argInfo == POPT_ARG_NONE) 
	    *((int *)opt->arg) = 1;
	else if (opt->argInfo != POPT_ARG_NONE) {
	    if (longArg) {
		con->os->nextArg = longArg;
	    } else if (con->os->nextCharArg) {
		con->os->nextArg = con->os->nextCharArg;
		con->os->nextCharArg = NULL;
	    } else { 
		while (con->os->next == con->os->argc && 
		       con->os > con->optionStack)
		    con->os--;
		if (con->os->next == con->os->argc)
		    return POPT_ERROR_NOARG;

		con->os->nextArg = con->os->argv[con->os->next++];
	    }

	    if (opt->arg) {
		switch (opt->argInfo) {
		  case POPT_ARG_STRING:
		    *((char **) opt->arg) = con->os->nextArg;
		    break;

		  case POPT_ARG_INT:
		  case POPT_ARG_LONG:
		    aLong = strtol(con->os->nextArg, &end, 0);
		    if (*end) 
			return POPT_ERROR_BADNUMBER;

		    if (aLong == LONG_MIN || aLong == LONG_MAX)
			return POPT_ERROR_OVERFLOW;
		    if (opt->argInfo == POPT_ARG_LONG) {
			*((long *) opt->arg) = aLong;
		    } else {
			if (aLong > INT_MAX || aLong < INT_MIN)
			    return POPT_ERROR_OVERFLOW;
			*((int *) opt->arg) =aLong;
		    }
		    break;

		  default:
		    printf("option type not implemented in popt\n");
		    exit(1);
		}
	    }
	}

	if (opt->val) done = 1;
    }

    return opt->val;
}

char * poptGetOptArg(poptContext con) {
    char * ret = con->os->nextArg;
    con->os->nextArg = NULL;
    return ret;
}

char * poptGetArg(poptContext con) {
    if (con->numLeftovers == con->nextLeftover) return NULL;
    return (con->leftovers[con->nextLeftover++]);
}

char * poptPeekArg(poptContext con) {
    if (con->numLeftovers == con->nextLeftover) return NULL;
    return (con->leftovers[con->nextLeftover]);
}

char ** poptGetArgs(poptContext con) {
    if (con->numLeftovers == con->nextLeftover) return NULL;

    /* some apps like [like RPM ;-) ] need this NULL terminated */
    con->leftovers[con->numLeftovers] = NULL;

    return (con->leftovers + con->nextLeftover);
}

void poptFreeContext(poptContext con) {
    int i;

    for (i = 0; i < con->numAliases; i++) {
	free(con->aliases[i].longName);
	free(con->aliases[i].argv);
    }

    free(con->leftovers);
    if (con->appName) free(con->appName);
    if (con->aliases) free(con->aliases);
    free(con);
}

int poptAddAlias(poptContext con, struct poptAlias newAlias, int flags) {
    int aliasNum = con->numAliases++;
    struct poptAlias * alias;

    /* SunOS won't realloc(NULL, ...) */
    if (!con->aliases)
	con->aliases = malloc(sizeof(newAlias) * con->numAliases);
    else
	con->aliases = realloc(con->aliases, 
			       sizeof(newAlias) * con->numAliases);
    alias = con->aliases + aliasNum;
    
    *alias = newAlias;
    if (alias->longName)
	alias->longName = strcpy(malloc(strlen(alias->longName) + 1), 
				    alias->longName);
    else
	alias->longName = NULL;

    return 0;
}

int poptParseArgvString(char * s, int * argcPtr, char *** argvPtr) {
    char * buf = strcpy(alloca(strlen(s) + 1), s);
    char * bufStart = buf;
    char * src, * dst;
    char quote = '\0';
    int argvAlloced = 5;
    char ** argv = malloc(sizeof(*argv) * argvAlloced);
    char ** argv2;
    int argc = 0;
    int i;

    src = s;
    dst = buf;
    argv[argc] = buf;

    memset(buf, '\0', strlen(s) + 1);

    while (*src) {
	if (quote == *src) {
	    quote = '\0';
	} else if (quote) {
	    if (*src == '\\') {
		src++;
		if (!*src) {
		    free(argv);
		    return POPT_ERROR_BADQUOTE;
		}
		if (*src != quote) *buf++ = '\\';
	    }
	    *buf++ = *src;
	} else if (isspace(*src)) {
	    if (*argv[argc]) {
		buf++, argc++;
		if (argc == argvAlloced) {
		    argvAlloced += 5;
		    argv = realloc(argv, sizeof(*argv) * argvAlloced);
		}
		argv[argc] = buf;
	    }
	} else switch (*src) {
	  case '"':
	  case '\'':
	    quote = *src;
	    break;
	  case '\\':
	    src++;
	    if (!*src) {
		free(argv);
		return POPT_ERROR_BADQUOTE;
	    }
	    /* fallthrough */
	  default:
	    *buf++ = *src;
	}

	src++;
    }

    if (strlen(argv[argc])) {
	argc++, buf++;
    }

    dst = malloc(argc * sizeof(*argv) + (buf - bufStart));
    argv2 = (void *) dst;
    dst += argc * sizeof(*argv);
    memcpy(argv2, argv, argc * sizeof(*argv));
    memcpy(dst, bufStart, buf - bufStart);

    for (i = 0; i < argc; i++) {
	argv2[i] = dst + (argv[i] - bufStart);
    }

    free(argv);

    *argvPtr = argv2;
    *argcPtr = argc;

    return 0;
}

static void configLine(poptContext con, char * line) {
    int nameLength = strlen(con->appName);
    char * opt;
    struct poptAlias alias;
    
    if (strncmp(line, con->appName, nameLength)) return;
    line += nameLength;
    if (!*line || !isspace(*line)) return;
    while (*line && isspace(*line)) line++;

    if (!strncmp(line, "alias", 5)) {
	line += 5;
	if (!*line || !isspace(*line)) return;
	while (*line && isspace(*line)) line++;
	if (!*line) return;

	opt = line;
	while (*line && !isspace(*line)) line++;
	if (!*line) return;
	*line++ = '\0';
	while (*line && isspace(*line)) line++;
	if (!*line) return;

	if (!strlen(opt)) return;

	if (poptParseArgvString(line, &alias.argc, &alias.argv)) return;

	if (opt[0] == '-' && opt[1] == '-') {
	    alias.longName = opt + 2;
	    alias.shortName = '\0';
	    poptAddAlias(con, alias, 0);
	} else if (opt[0] == '-' && !opt[2]) {
	    alias.longName = NULL;
	    alias.shortName = opt[1];
	    poptAddAlias(con, alias, 0);
	}
    }
}

int poptReadConfigFile(poptContext con, char * fn) {
    char * file, * chptr, * end;
    char * buf, * dst;
    int fd, rc;
    int fileLength;

    fd = open(fn, O_RDONLY);
    if (fd < 0) {
	if (errno == ENOENT)
	    return 0;
	else 
	    return POPT_ERROR_ERRNO;
    }

    fileLength = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, 0);

    file = mmap(NULL, fileLength, PROT_READ, MAP_PRIVATE, fd, 0);
    if (file == (void *) -1) {
	rc = errno;
	close(fd);
	errno = rc;
	return POPT_ERROR_ERRNO;
    }
    close(fd);

    dst = buf = alloca(fileLength + 1);

    chptr = file;
    end = (file + fileLength);
    while (chptr < end) {
	switch (*chptr) {
	  case '\n':
	    *dst = '\0';
	    dst = buf;
	    while (*dst && isspace(*dst)) dst++;
	    if (*dst && *dst != '#') {
		configLine(con, dst);
	    }
	    chptr++;
	    break;
	  case '\\':
	    *dst++ = *chptr++;
	    if (chptr < end) {
		if (*chptr == '\n') 
		    dst--, chptr++;	
		    /* \ at the end of a line does not insert a \n */
		else
		    *dst++ = *chptr++;
	    }
	    break;
	  default:
	    *dst++ = *chptr++;
	}
    }

    return 0;
}

int poptReadDefaultConfig(poptContext con, int useEnv) {
    char * envName, * envValue;
    char * fn, * home, * chptr;
    int rc, skip;
    struct poptAlias alias;

    if (!con->appName) return 0;

    rc = poptReadConfigFile(con, "/etc/popt");
    if (rc) return rc;
    if (getuid() != geteuid()) return 0;

    if ((home = getenv("HOME"))) {
	fn = alloca(strlen(home) + 20);
	sprintf(fn, "%s/.popt", home);
	rc = poptReadConfigFile(con, fn);
	if (rc) return rc;
    }

    envName = alloca(strlen(con->appName) + 20);
    strcpy(envName, con->appName);
    chptr = envName;
    while (*chptr) {
	*chptr = toupper(*chptr);
	chptr++;
    }
    strcat(envName, "_POPT_ALIASES");

    if (useEnv && (envValue = getenv(envName))) {
	envValue = strcpy(alloca(strlen(envValue) + 1), envValue);

	while (envValue && *envValue) {
	    chptr = strchr(envValue, '=');
	    if (!chptr) {
		envValue = strchr(envValue, '\n');
		if (envValue) envValue++;
		continue;
	    }

	    *chptr = '\0';

	    skip = 0;
	    if (!strncmp(envValue, "--", 2)) {
		alias.longName = envValue + 2;
		alias.shortName = '\0';
	    } else if (*envValue == '-' && strlen(envValue) == 2) {
		alias.longName = NULL;	
		alias.shortName = envValue[1];
	    } else {
		skip = 1;
	    }

	    envValue = chptr + 1;
	    chptr = strchr(envValue, '\n');
	    if (chptr) *chptr = '\0';

	    if (!skip) {
		poptParseArgvString(envValue, &alias.argc, &alias.argv);
		poptAddAlias(con, alias, 0);
	    }

	    if (chptr)
		envValue = chptr + 1;
	    else
		envValue = NULL;
	}
    }

    return 0;
}

char * poptBadOption(poptContext con, int flags) {
    struct optionStackEntry * os;

    if (flags & POPT_BADOPTION_NOALIAS)
	os = con->optionStack;
    else
	os = con->os;

    return os->argv[os->next - 1];
}

#define POPT_ERROR_NOARG	-10
#define POPT_ERROR_BADOPT	-11
#define POPT_ERROR_OPTSTOODEEP	-13
#define POPT_ERROR_BADQUOTE	-15	/* only from poptParseArgString() */
#define POPT_ERROR_ERRNO	-16	/* only from poptParseArgString() */

const char * poptStrerror(const int error) {
    switch (error) {
      case POPT_ERROR_NOARG:
	return "missing argument";
      case POPT_ERROR_BADOPT:
	return "unknown option";
      case POPT_ERROR_OPTSTOODEEP:
	return "aliases nested too deeply";
      case POPT_ERROR_BADQUOTE:
	return "error in paramter quoting";
      case POPT_ERROR_BADNUMBER:
	return "invalid numeric value";
      case POPT_ERROR_OVERFLOW:
	return "number too large or too small";
      case POPT_ERROR_ERRNO:
	return strerror(errno);
      default:
	return "unknown error";
    }
}

int poptStuffArgs(poptContext con, char ** argv) {
    int i;

    if ((con->os - con->optionStack) == POPT_OPTION_DEPTH)
	return POPT_ERROR_OPTSTOODEEP;

    for (i = 0; argv[i]; i++);

    con->os++;
    con->os->next = 0;
    con->os->nextArg = con->os->nextCharArg = NULL;
    con->os->currAlias = NULL;
    con->os->argc = i;
    con->os->argv = argv;

    return 0;
}
