/*********************************************************************
 *   Copyright 2018, University Corporation for Atmospheric Research
 *   See netcdf/README file for copying and redistribution conditions.
 *   $Id$
 *********************************************************************/

#include "config.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netcdf.h>
#include <assert.h>
#include <ctype.h>
#include "utils.h"
#include "nccomps.h"
#ifndef isascii
EXTERNL int isascii(int c);
#endif

/*
 * Print error message to stderr and exit
 */
void
error(const char *fmt, ...)
{
    va_list args ;

    (void) fprintf(stderr,"%s: ", progname);
    va_start(args, fmt) ;
    (void) vfprintf(stderr,fmt,args) ;
    va_end(args) ;

    (void) fprintf(stderr, "\n") ;
    (void) fflush(stderr);	/* to ensure log files are current */
    exit(EXIT_FAILURE);
}

void *
emalloc (			/* check return from malloc */
	size_t size)
{
    void   *p;

    p = (void *) malloc (size==0 ? 1 : size); /* malloc(0) not portable */
    if (p == 0) {
	error ("out of memory\n");
    }
    return p;
}

void *
ecalloc (			/* check return from calloc */
	size_t size)
{
    void   *p;

    p = (void *) calloc (1,(size==0 ? 1 : size)); /* calloc(0) not portable */
    if (p == 0) {
	error ("out of memory\n");
    }
    return p;
}

void *
erealloc (void* p0,			/* check return from realloc */
	size_t size)
{
    void   *p;

    if(p0 == NULL)
        return emalloc(size);
    if(size == 0)
    	error("realloc with zero size");
    p = (void *) realloc (p0,size); /* realloc(0) not portable */
    if (p == 0) {
	error ("out of memory\n");
    }
    return p;
}

void
check(int err, const char* file, const char* fcn, const int line)
{
    fprintf(stderr,"%s\n",nc_strerror(err));
    fprintf(stderr,"Location: file %s; fcn %s line %d\n",(file?file:"?"),(fcn?fcn:"?"),line);
    fflush(stderr); fflush(stdout);
    exit(1);
}


/* 
 * Returns malloced name with chars special to CDL escaped.
 * Caller should free result when done with it.
 */
char*
escaped_name(const char* cp) {
    char *ret;			/* string returned */
    char *sp;
    assert(cp != NULL);

    /* For some reason, and on some machines (e.g. tweety)
       utf8 characters such as \343 are considered control character. */
/*    if(*cp && (isspace(*cp) | iscntrl(*cp)))*/
    if((*cp >= 0x01 && *cp <= 0x20) || (*cp == 0x7f))
    {
	error("name begins with space or control-character: %c",*cp);
    }

    ret = emalloc(4*strlen(cp) + 1); /* max if every char escaped */
    sp = ret;
    *sp = 0;			    /* empty name OK */
    /* Special case: leading number allowed, but we must escape it for CDL */
    if((*cp >= '0' && *cp <= '9'))
    {
	*sp++ = '\\';
    }
    for (; *cp; cp++) {
	if (isascii((int)*cp)) {
	    if(iscntrl((int)*cp)) {	/* render control chars as two hex digits, \%xx */
		snprintf(sp, 4+1,"\\%%%.2x", *cp);
		sp += 4;
	    } else {
		switch (*cp) {
		case ' ':
		case '!':
		case '"':
		case '#':
		case '$':
		case '&':
		case '\'':
		case '(':
		case ')':
		case '*':
		case ',':
		case ':':
		case ';':
		case '<':
		case '=':
		case '>':
		case '?':
		case '[':
		case ']':
		case '\\':
		case '^':
		case '`':
		case '{':
		case '|':
		case '}':
		case '~':
		    *sp++ = '\\';
		    *sp++ = *cp;
		    break;
		default:		/* includes '/' */
		    *sp++ = *cp;
		    break;
		}
	    }
	} else { 		/* not ascii, assume just UTF-8 byte */
	    *sp++ = *cp;
	}
    }
    *sp = 0;
    return ret;
}


/* 
 * Print name with escapes for special characters
 */
void
print_name(const char* name) {
    char *ename = escaped_name(name);
    fputs(ename, stdout);
    free(ename);
}

/* 
 * Returns malloced string with selected chars escaped.
 * Caller should free result when done with it.
 */
char*
escaped_string(const char* cp) {
    char *ret;			/* string returned */
    char *sp;
    assert(cp != NULL);

    /* For some reason, and on some machines (e.g. tweety)
       utf8 characters such as \343 are considered control character. */

    ret = emalloc(4*strlen(cp) + 1); /* max if every char escaped */
    sp = ret;
    *sp = 0;			    /* empty name OK */
    for (; *cp; cp++) {
	if (isascii((int)*cp)) {
	    if(iscntrl((int)*cp)) {	/* render control chars as two hex digits, \%xx */
		snprintf(sp, 4+1,"\\%%%.2x", *cp);
		sp += 4;
	    } else if(*cp == '"') {
		*sp++ = '\\';
		*sp++ = '"';
	    } else 
  	        *sp++ = *cp;
	} else { 		/* not ascii, assume just UTF-8 byte */
	    *sp++ = *cp;
	}
    }
    *sp = 0;
    return ret;
}


/* Convert a full path name to a group to the specific groupid. */
int 
nc_inq_grpid2(int ncid, const char *grpname0, int *grpidp)
{
    int ret = NC_NOERR;
    char* grpname = NULL;
#ifdef USE_NETCDF4
    char *sp = NULL;
#endif

    grpname = strdup(grpname0);
    if(grpname == NULL) {ret = NC_ENOMEM; goto done;}

#ifdef USE_NETCDF4
    /* If '/' doesn't occur in name, just return id found by nc_inq_grpid() */
    sp = strrchr(grpname, '/');
    if(!sp) { /* No '/' in grpname, so return nc_inq_grpid() result */
	ret = nc_inq_grp_ncid(ncid, grpname, grpidp);
	goto done;
    } 
    {  /* Parse group name out and get grpid using that */
	char* p, *q;
        int next;

        p = grpname;
        if(grpname[0] == '/') {
	    /* get ncid of the root group */
	    ncid = getrootid(ncid);
	    p++; /* skip leading '/' */
        }
	/* Walk down looking for each group in path in turn */
	while(*p) {
	    q = strchr(p,'/');
	    if(q == NULL) q = p+strlen(p); /* point to trailing nul */    
	    else *q++ = '\0';
	    /* Lookup this path segment wrt to current group */
	    if((ret=nc_inq_ncid(ncid,p,&next))) goto done;
	    /* move to next segment */
	    p = q;
	    ncid = next;
	}
	if(grpidp) *grpidp = ncid;
    }	

#else	/* !USE_NETCDF4 */
    /* Just return root */
    if(grpidp) *grpidp = ncid;
#endif	/* USE_NETCDF4 */
done:
    if(grpname) free(grpname);
    return ret;
}

/* Convert a full path name to a varid to the specific varid + grpid */
int 
nc_inq_varid2(int ncid, const char *path0, int* varidp, int* grpidp)
{
    int ret = NC_NOERR;
    int grpid, varid;
    char *v, *g, *prefix;
    /* If '/' doesn't occur in name, just return id found by
     * nc_inq_grpid()
     */
    char* path = NULL;

    path = strdup(path0);
    if(path == NULL) {ret = NC_ENOMEM; goto done;}

    /* Find the rightmost '/' and tag the start of the path */
    g = strrchr(path,'/');
    if(g == NULL) {
	v = path;
	prefix = "/"; /* make sure not free'd */
    } else {
	*g++ = '\0'; /* separate out the prefix */
	prefix = path;
	v = g;
    }
    /* convert the group prefix to a group id */
    if((ret=nc_inq_grpid2(ncid,prefix,&grpid)))
	goto done;
    /* Lookup the var in the terminal group */
    if((ret=nc_inq_varid(grpid,v,&varid)))
	goto done;
    if(grpidp)
	*grpidp = grpid;
    if(varidp)
	*varidp = varid;
done:
    if(path) free(path);
    return ret;
}

/* Missing functionality that should be in nc_inq_dimid(), to get
 * dimid from a full dimension path name that may include group
 * names */
int 
nc_inq_dimid2(int ncid, const char *dimname, int *dimidp) {
    int ret = NC_NOERR;
    /* If '/' doesn't occur in dimname, just return id found by
     * nc_inq_dimid() */
    char *sp = strrchr(dimname, '/');
    if(!sp) { /* No '/' in dimname, so return nc_inq_dimid() result */
	ret = nc_inq_dimid(ncid, dimname, dimidp);
    } 
#ifdef USE_NETCDF4
    else {  /* Parse group name out and get dimid using that */
      size_t grp_namelen = (size_t)(sp - dimname);
      char *grpname = emalloc(grp_namelen+1);
      
      int grpid;
      strncpy(grpname, dimname, grp_namelen+1);
      grpname[grp_namelen] = '\0';
      ret = nc_inq_grp_full_ncid(ncid, grpname, &grpid);
      if(ret == NC_NOERR) {
	ret = nc_inq_dimid(grpid, dimname, dimidp);
      }
      free(grpname);
    }	
#endif	/* USE_NETCDF4 */
    return ret;
}

/*
 * return 1 if varid identifies a record variable
 * else return 0
 */
int
isrecvar(int ncid, int varid)
{
    int ndims;
    int is_recvar = 0;
    int *dimids;

    NC_CHECK( nc_inq_varndims(ncid, varid, &ndims) );
#ifdef USE_NETCDF4
    if (ndims > 0) {
	int nunlimdims;
	int *recdimids;
	int dim, recdim;
	dimids = (int *) emalloc((size_t)(ndims + 1) * sizeof(int));
	NC_CHECK( nc_inq_vardimid(ncid, varid, dimids) );
	NC_CHECK( nc_inq_unlimdims(ncid, &nunlimdims, NULL) );
	recdimids = (int *) emalloc((size_t)(nunlimdims + 1) * sizeof(int));
	NC_CHECK( nc_inq_unlimdims(ncid, NULL, recdimids) );
	for (dim = 0; dim < ndims && is_recvar == 0; dim++) {
	    for(recdim = 0; recdim < nunlimdims; recdim++) {
		if(dimids[dim] == recdimids[recdim]) {
		    is_recvar = 1;
		    break;
		}		
	    }
	}
	free(dimids);
	free(recdimids);
    }
#else
    if (ndims > 0) {
	int recdimid;
	dimids = (int *) emalloc((ndims + 1) * sizeof(int));
	NC_CHECK( nc_inq_vardimid(ncid, varid, dimids) );
	NC_CHECK( nc_inq_unlimdim(ncid, &recdimid) );
	if(dimids[0] == recdimid)
	    is_recvar = 1;
	free(dimids);
    }
#endif /* USE_NETCDF4 */
    return is_recvar;
}

static idnode_t*
newidnode(void) {
    idnode_t *newvp = (idnode_t*) emalloc(sizeof(idnode_t));
    return newvp;
}

/*
 * Get a new, empty variable list.
 */
idnode_t*
newidlist(void) {
    idnode_t *vp = newidnode();

    vp -> next = 0;
    vp -> id = -1;		/* bad id */

    return vp;
}

void
idadd(idnode_t* vlist, int varid) {
    idnode_t *newvp = newidnode();
    
    newvp -> next = vlist -> next;
    newvp -> id = varid;
    vlist -> next = newvp;
}

/* 
 * return true if id is member of list that idlist points to.
 */
bool_t
idmember(const idnode_t* idlist, int id)
{
    idnode_t *vp = idlist -> next;

    for (; vp ; vp = vp->next)
      if (vp->id == id)
	return true;
    return false;    
}

/*
 * Release a variable list.
 */
void
freeidlist(idnode_t *idlist)
{
   while(idlist) {
      idnode_t *vp = idlist->next;
      free(idlist);
      idlist = vp;
   }
}

/* 
 * Return true if group identified by grpid is member of grpids, a list of groups.
 * nlgrps is number of groups in the list.
 */
bool_t
group_wanted(int grpid, int nlgrps, const idnode_t* grpids)
{
    /* If -g not specified, all groups are wanted */
    if(nlgrps == 0) return true;
    /* if -g specified, look for match in group id list */
    return idmember(grpids, grpid);
}

/* Determine whether a group named formatting_specs.lgrps[igrp] exists
 * in a netCDF file or group with id ncid.  If so, return the count of
 * how many matching groups were found, else return a count of 0.  If
 * the name begins with "/", it is interpreted as an absolute group
 * name, in which case only 0 or 1 is returned.  Otherwise, interpret
 * it as a relative name, and the total number of occurrences within
 * the file/group identified by ncid is returned.  
 *
 * Also has side effect of updating the ngrpids and the associate
 * grpids array that represent the group list specified by the -g
 * option.  TODO: put this in its own function instead.
 */
static size_t
nc_inq_grpname_count(int ncid, int igrp, char **lgrps, idnode_t *grpids) {
    size_t count = 0;
#ifdef USE_NETCDF4
    int numgrps;
    int *ncids;
    int g;
    int grpid;
    int status;
#endif
    char *grpname = lgrps[igrp];

    /* permit empty string to also designate root group */
    if(grpname[0] == '\0' || NCSTREQ(grpname,"/")) { 
	count = 1;
	idadd(grpids, ncid);
	return count;
    }
#ifdef USE_NETCDF4
    /* Handle absolute group names */
    if(grpname[0] == '/') {
	int grpid;
	status = nc_inq_grp_full_ncid(ncid, grpname, &grpid);
	if(status == NC_NOERR) {
	    count = 1;
	    idadd(grpids, grpid);
	} else if(status == NC_ENOGRP) {
	    count = 0;
	} else {
	    error("when looking up group %s: %s ", grpname, nc_strerror(status));
	}
	return count;
    }
    
    /* look in this group */
    status = nc_inq_grp_ncid(ncid, grpname, &grpid);
    if (status == NC_NOERR) {
	count++;
	idadd(grpids, grpid);
    }
    /* if this group has subgroups, call recursively on each of them */
    NC_CHECK( nc_inq_grps(ncid, &numgrps, NULL) );
    if(numgrps > 0) {
	/* Allocate memory to hold the list of group ids. */
	ncids = emalloc((size_t)numgrps * sizeof(int));
	/* Get the list of group ids. */
	NC_CHECK( nc_inq_grps(ncid, NULL, ncids) );
	/* Call this function recursively for each group. */
	for (g = 0; g < numgrps; g++) {
	    count += nc_inq_grpname_count(ncids[g], igrp, lgrps, grpids);
	}
	free(ncids);
    }
#endif /* USE_NETCDF4 */
    return count;    
}

/* Check if any group names specified with "-g grp1,...,grpn" are
 * missing.  Returns total number of matching groups if no missing
 * groups detected, otherwise exits. */
size_t
grp_matches(int ncid, int nlgrps, char** lgrps, idnode_t *grpids) {
    int ig;
    size_t total = 0;

    for (ig=0; ig < nlgrps; ig++) {
	size_t count = nc_inq_grpname_count(ncid, ig, lgrps, grpids);
	if(count == 0) {
	    error("%s: No such group", lgrps[ig]);
	    return 0;
	}
	total += count;
    }
    return total;
}

/* Returns 1 if string s1 ends with string s2, 0 otherwise. */
int
strendswith(const char *s1, const char *s2) {
    size_t m1 = strlen(s1);
    size_t m2 = strlen(s2);
    if (m1 < m2)
	return 0;
    return (strcmp(s1 + (m1 - m2), s2) == 0);
}

/* Get varid of variable with name using nested group syntax
 * "gp1/gp2/var" or "/gp1/gp2/var".  In the former case, grpname of
 * grp corresponding to grpid must end in "gp1/gp2".  In the latter
 * case, grpname for grpid must be exactly "/gp1/gp2".  If variable
 * named "var" is not in group grpid, returns NC_ENOTVAR, else sets
 * varid and returns NC_NOERR.  */
int 
nc_inq_gvarid(int grpid, const char *varname, int *varidp) {
    /* if varname has no "/" chars, then
          return varidp from nc_inq_varid(grpid, varname, varidp)
       if varname begins with "/"
          
       else
          get groupname corresponding to grpid
          get vargroup = substring of varname up to last "/"
          get relname = substring of varname after last "/"
          if (varname starts with "/" and groupname == vargroup) ||
             (groupname ends with vargroup)
             return nc_inq_varid(grpid, relname, varidp)
          else
             return NC_ENOTVAR
    */
    
#ifdef USE_NETCDF4
    char *vargroup;
    char *relname;
    char *groupname;
    int status;
    if (varname[0] == '\0')
	return NC_ENOTVAR;
    vargroup = strdup(varname);
    if (vargroup == NULL) 
	return NC_ENOMEM;
    relname = strrchr(vargroup, NC_GRP_DELIM);
    if (relname != NULL) {	/* name has a "/" in it */
	size_t len;		/* length of full group name for grpid */
	*relname++ = '\0';	/* split vargroup string in two,
				 * vargroup and relname */
	if ( (status = nc_inq_grpname_full(grpid, &len, NULL)) != NC_NOERR ) {
	    free(vargroup);
	    return status;
	}
	groupname = (char *)emalloc(len + 1);
	if ( (status = nc_inq_grpname_full(grpid, &len, groupname)) == NC_NOERR ) {
	    if(varname[0] == NC_GRP_DELIM) {
		if( strcmp(groupname, vargroup) == 0)
		    status = nc_inq_varid(grpid, relname, varidp);
		else
		    status = NC_ENOTVAR;
	    } else {
		if(strendswith(groupname, vargroup))
		    status = nc_inq_varid(grpid, relname, varidp);
		else
		    status = NC_ENOTVAR;
	    }
	}
	free(vargroup);
	free(groupname);
	return status;
    }
    free(vargroup);
#endif	/* USE_NETCDF4 */
    return nc_inq_varid(grpid, varname, varidp);
}

/* Determine whether a variable named varname exists in any group in
   an open netCDF file with id ncid.  If so, return the count of how
   many matching variables were found, else return a count of 0.  The
   variable name can be absolute such as "/foo" or "/GRP1/GRP1A/foo",
   in which case there is only one group to look in, given by the path
   from the root group.  Alternatively, the variable name can be
   relative, such as "foo" or "GRPA/GRPB/foo", in which case every
   group is examined for a variable with that relative name.  */
size_t
nc_inq_varname_count(int ncid, char *varname) {
    /* 
       count = 0;
       status = nc_inq_gvarid(ncid, varname, varid);
       if (status == NC_NOERR)
          count++;
       for each subgroup gid {
          count += nc_inq_varname_count(gid, varname);
       }
       return count;
    */
    size_t count = 0;
    int varid;
    /* look in this group */
    int status = nc_inq_gvarid(ncid, varname, &varid);
#ifdef USE_NETCDF4
    int numgrps;
    int *ncids;
    int g;
#endif

    if (status == NC_NOERR)
	count++;

#ifdef USE_NETCDF4
    /* if this group has subgroups, call recursively on each of them */
    NC_CHECK( nc_inq_grps(ncid, &numgrps, NULL) );
	 
    /* Allocate memory to hold the list of group ids. */
    ncids = emalloc((size_t)(numgrps + 1) * sizeof(int));
	
    /* Get the list of group ids. */
    NC_CHECK( nc_inq_grps(ncid, NULL, ncids) );
	
    /* Call this function for each group. */
    for (g = 0; g < numgrps; g++) {
	count += nc_inq_varname_count(ncids[g], varname);
    }
    free(ncids);
#endif /* USE_NETCDF4 */
    return count;    
   
}

/* Check if any variable names specified with "-v var1,...,varn" are
 * missing.  Returns 0 if no missing variables detected, otherwise
 * exits. */
int
missing_vars(int ncid, int nlvars, char **lvars) {
    int iv;
    for (iv=0; iv < nlvars; iv++) {
	if(nc_inq_varname_count(ncid, lvars[iv]) == 0) {
	    error("%s: No such variable", lvars[iv]);
	}
    }
    return 0;
}

void
make_lvars(char *optarg, int *nlvarsp, char ***lvarsp)
{
    char *cp = optarg;
    int nvars = 1;
    char ** cpp;

    /* compute number of variable names in comma-delimited list */
    *nlvarsp = 1;
    while (*cp++)
      if (*cp == ',')
 	nvars++;
    *nlvarsp = nvars;
    *lvarsp = (char **) emalloc((size_t)nvars * sizeof(char*));
    cpp = *lvarsp;
    /* copy variable names into list */
    for (cp = strtok(optarg, ","); cp != NULL; cp = strtok((char *) NULL, ",")) {
	*cpp = strdup(cp);
	cpp++;
    }
}

void
make_lgrps(char *optarg, int *nlgrps, char ***lgrpsp, idnode_t **grpidsp)
{
    char *cp = optarg;
    int ngrps = 1;
    char ** cpp;

    /* compute number of group names in comma-delimited list */
    while (*cp++)
      if (*cp == ',')
 	ngrps++;
    *nlgrps = ngrps;
    *lgrpsp = (char **) emalloc((size_t)ngrps * sizeof(char*));
    cpp = *lgrpsp;
    /* copy group names into list */
    for (cp = strtok(optarg, ","); cp != NULL; cp = strtok((char *) NULL, ",")) {
	*cpp = strdup(cp);
	cpp++;
    }
    /* make empty list of grpids, to be filled in after input file opened */
    *grpidsp = newidlist();
}

/* initialize and return a new empty stack of grpids */
static ncgiter_t *
gs_init() {
    ncgiter_t *s = emalloc(sizeof(ncgiter_t));
    s->ngrps = 0;
    s->top = NULL;
    return s;
}

/* free a stack and all its nodes */
static void
gs_free(ncgiter_t *s) {
    grpnode_t *n0, *n1;
    n0 = s->top;
    while (n0) {
	n1 = n0->next;
	free(n0);
	n0 = n1;
    }
    free(s);
}

/* test if a stack is empty */
static int
gs_empty(ncgiter_t *s)
{
    return s->ngrps == 0;
}

/* push a grpid on stack */
static void
gs_push(ncgiter_t *s, int grpid)
{
    grpnode_t *node = emalloc(sizeof(grpnode_t));
 
    node->grpid = grpid;
    node->next = gs_empty(s) ? NULL : s->top;
    s->top = node;
    s->ngrps++;
}

/* pop value off stack and return */
static int 
gs_pop(ncgiter_t *s)
{
    if (gs_empty(s)) {
	return -1;		/* underflow, stack is empty */
    } else {			/* pop a node */
	grpnode_t *top = s->top;
	int value = top->grpid;
	s->top = top->next;
	/* TODO: first call to free gets seg fault with libumem */
	free(top);
	s->ngrps--;
	return value;
    }
}

#ifdef UNUSED
/* Return top value on stack without popping stack.  Defined for
 * completeness but not used (here). */
static int 
gs_top(ncgiter_t *s)
{
    if (gs_empty(s)) {
	return -1;		/* underflow, stack is empty */
    } else {			/* get top value */
	grpnode_t *top = s->top;
	int value = top->grpid;
	return value;
    }
}
#endif

/* Like netCDF-4 function nc_inq_grps(), but can be called from
 * netCDF-3 only code as well.  Maybe this is what nc_inq_grps()
 * should do if built without netCDF-4 data model support. */
static int
nc_inq_grps2(int ncid, int *numgrps, int *grpids)
{
    int stat = NC_NOERR;

    /* just check if ncid is valid id of open netCDF file */
    NC_CHECK(nc_inq(ncid, NULL, NULL, NULL, NULL));

#ifdef USE_NETCDF4
    NC_CHECK(nc_inq_grps(ncid, numgrps, grpids));
#else
    *numgrps = 0;
#endif
    return stat;
}

/* Initialize group iterator for start group and all its descendant
 * groups. */
int
nc_get_giter(int grpid,	       /* start group id */
	    ncgiter_t **iterp  /* returned opaque iteration state */
    ) 
{
    int stat = NC_NOERR;

    stat = nc_inq(grpid, NULL, NULL, NULL, NULL); /* check if grpid is valid */
    if(stat != NC_EBADGRPID && stat != NC_EBADID) {
	*iterp = gs_init();
	gs_push(*iterp, grpid);
    }

    return stat;
}

/* 
 * Get group id of next group.  On first call gets start group id,
 * subsequently returns other subgroup ids in preorder.  Returns zero
 * when no more groups left.
 */
int
nc_next_giter(ncgiter_t *iterp, int *grpidp) {
    int stat = NC_NOERR;
    int numgrps;
    int *grpids;
    int i;

    if(gs_empty(iterp)) {
	*grpidp = 0;		/* not a group, signals iterator is done */
    } else {
	*grpidp = gs_pop(iterp);
	NC_CHECK(nc_inq_grps2(*grpidp, &numgrps, NULL));
	if(numgrps > 0) {
	    grpids = (int *)emalloc(sizeof(int) * (size_t)numgrps);
	    NC_CHECK(nc_inq_grps2(*grpidp, &numgrps, grpids));
	    for(i = numgrps - 1; i >= 0; i--) { /* push ids on stack in reverse order */
		gs_push(iterp, grpids[i]);
	    }
	    free(grpids);
	}
    }
    return stat;
}

/*
 * Release group iter.
 */
void
nc_free_giter(ncgiter_t *iterp)
{
    gs_free(iterp);
}

/* 
 * Get total number of groups (including the top-level group and all
 * descendant groups, recursively) and all descendant subgroup ids
 * (including the input rootid of the start group) for a group and
 * all its descendants, in preorder.
 *
 * If grpids or numgrps is NULL, it will be ignored.  So typical use
 * is to call with grpids NULL to get numgrps, allocate enough space
 * for the group ids, then call again to get them.
 */
int
nc_inq_grps_full(int rootid, size_t *numgrps, int *grpids)
{
    int stat = NC_NOERR;
    ncgiter_t *giter;		/* pointer to group iterator */
    int grpid;
    size_t count;

    NC_CHECK(nc_get_giter(rootid, &giter));
    
    count = 0;
    NC_CHECK(nc_next_giter(giter, &grpid));
    while(grpid != 0) {
	if(grpids)
	    grpids[count] = grpid;
	count++;
	NC_CHECK(nc_next_giter(giter, &grpid));
    }
    if(numgrps)
	*numgrps = count;
    nc_free_giter(giter);
    return stat;
}

int
getrootid(int grpid)
{
    int current = grpid;
#ifdef USE_NETCDF4
    int parent = current;
    /* see if root id */
    for(;;) {
        int stat = nc_inq_grp_parent(current,&parent);
        if(stat) break;
	current = parent;
    }
#endif
    return current;
}

#if 0
static int
parseFQN(int ncid, const char* fqn0, VarID* idp)
{
    int stat = NC_NOERR;
    char* fqn;
    VarID vid;
    char* p;
    char* q;
    char* segment;

    vid.grpid = ncid;
    if(fqn0 == NULL || fqn0[1] != '/')
	{stat = NC_EBADNAME; goto done;}
    fqn = strdup(fqn0+1); /* skip leading '/'*/
    p = fqn;
    for(;;) {
	int newgrp;
	segment = p;
	q = p;
        while(*p != '\0' && *p != '/') {
	    if(*p == '\\') p++;
	    *q++ = *p++;
	}
        if(*p == '\0') break;
	*p++ = '\0';
	if((stat=nc_inq_grp_ncid(vid.grpid,segment,&newgrp))) goto done;
	vid.grpid = newgrp;
    }
    /* Segment should point to the varname */
    if((stat=nc_inq_varid(vid.grpid,segment,&vid.varid))) goto done;
done:
    if(fqn) free(fqn);
    if(stat == NC_NOERR && idp != NULL) *idp = vid;
    return stat;
}
#endif

/*********************************************************************************/
void nc_get_att_single_string(const int ncid, const int varid,
                              const struct ncatt_t *att, char **str_out) {
    if (att->type == NC_CHAR) {
        // NC_CHAR type attribute
        // Use a call to nc_get_att_text which expects to output the attribute value
        // into a char * pointing to allocated memory. The number of bytes to allocate
        // is the attribute length (which is the number of elements in a vector, 1 for
        // scalar) times the size of each element in bytes. The attribute length is
        // held in att->len, and the attribute element size is in att->tinfo->size.
        *str_out = emalloc((att->len + 1) * att->tinfo->size);
	(*str_out)[att->len] = '\0';
        NC_CHECK(nc_get_att_text(ncid, varid, att->name, *str_out));
    } else if (att->type == NC_STRING) {
        // NC_STRING type attribute
        // Use a call to nc_get_att_string which expects to output the attribute value
        // into a vector of char pointers, where each entry points to allocated memory.
        // The vector of char pointers needs to be allocated to the length (number of strings)
        // times the size of each entry (size of a char *).
        char **att_strings = emalloc((att->len + 1) * att->tinfo->size);
        NC_CHECK(nc_get_att_string(ncid, varid, att->name, att_strings));
        // str_out needs to be allocated to a size large enough to hold the string that
        // the first pointer in att_strings is pointing to.
        size_t att_str_len = strlen(att_strings[0]);
        *str_out = emalloc((att_str_len + 1) * att->tinfo->size);
	(*str_out)[att_str_len] = '\0';
        strncpy(*str_out, att_strings[0], att_str_len);
        nc_free_string(att->len, att_strings); /* Warning: does not free att_strings */
	free(att_strings);
    } else {
        fprintf(stderr,"nc_get_att_single_string: unknown attribute type: %d\n", att->type);
        fprintf(stderr,"                          must use one of:  NC_CHAR, NC_STRING\n");
        fflush(stderr); fflush(stdout);
        exit(2);
    }
}
