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

#include <stddef.h>
#include <stdint.h>
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_GETOPT_H
#include <getopt.h>
#endif

#if defined(_WIN32) && !defined(__MINGW32__)
#include "XGetopt.h"
#endif

#include <stddef.h>

#include "ncconfigure.h"
#include "ncutil.h"
#include "zincludes.h"
#include "ncpathmgr.h"
#include "nclog.h"
#include "ncuri.h"

#undef DEBUG

#define DATANAME "data"

typedef enum Mapop {
MOP_NONE=0,
MOP_OBJDUMP=1,
MOP_CLEAR=2
} Mapop;

typedef enum OBJKIND {
OK_NONE=0,
OK_META=1,
OK_CHUNK=2,
OK_GROUP=3,
OK_IGNORE=4
} OBJKIND;

static struct Mops {
    Mapop mapop;
    const char* opname;
} mapops[] = {
{MOP_NONE,"none"},
{MOP_OBJDUMP,"objdump"},
{MOP_CLEAR,"clear"},
{MOP_NONE,NULL}
};

static struct Type {
    const char* typename;
    nc_type nctype;
    size_t typesize;
    const char format[16];
} types[] = {
{"ubyte",NC_UBYTE,1,"%u"},
{"byte",NC_BYTE,1,"%d"},
{"ushort",NC_USHORT,2,"%u"},
{"short",NC_SHORT,2,"%d"},
{"uint",NC_UINT,4,"%u"},
{"int",NC_INT,4,"%d"},
{"uint64",NC_UINT64,8,"%llu"},
{"int64",NC_INT64,8,"%lld"},
{"float",NC_FLOAT,4,"%f"},
{"double",NC_DOUBLE,8,"%lf"},
{"char",NC_CHAR,1,"'%c'"},
{"string",NC_STRING,sizeof(char*),"%*s"},
{NULL,NC_NAT,0,""}
};

/* Command line options */
struct Dumpptions {
    int debug;
    int meta_only;
    Mapop mop;
    char infile[4096];
    NCZM_IMPL impl;    
    char* rootpath;
    const struct Type* nctype;
    char format[16];
    int xflags;
#	define XNOZMETADATA 1	
    int strlen;
} dumpoptions;

/* Forward */
static int objdump(void);
static NCZM_IMPL implfor(const char* path);
static int printcontent(NCZMAP* map, const char* key, size64_t len, const char* content, OBJKIND kind);
static int breadthfirst(NCZMAP* map, const char*, NClist* stack);
static char* rootpathfor(const char* path);
static OBJKIND keykind(const char* key);
static void sortlist(NClist* l);
static const char* filenamefor(const char* f0);
static int chunkendianness(NCZMAP* map, const char* key, int* endianp);
static int hostendianness(void);

#define NCCHECK(expr) nccheck((expr),__LINE__)
static void nccheck(int stat, int line)
{
    if(stat) {
	fprintf(stderr,"%d: %s\n",line,nc_strerror(stat));
	fflush(stderr);
	exit(1);
    }
}

static void
zmapusage(void)
{
    fprintf(stderr,"usage: zmapio [-t <type>][-d][-v][-x] <file>\n");
    exit(1);
}

static int
hostendianness(void)
{
    return (NC_isLittleEndian() ? NC_ENDIAN_LITTLE : NC_ENDIAN_BIG);
}

static int
chunkendianness(NCZMAP* map, const char* key, int* endianp)
{
    int stat = NC_NOERR;
    char* prefix = NULL;
    char* zarraykey = NULL;
    char* metadata = NULL;
    NCjson* jzarray = NULL;
    const NCjson* jdtype = NULL;
    size64_t len = 0;
    int endianness = hostendianness();

    if(endianp) *endianp = endianness;

    if((stat = nczm_divide_at(key,-1,&prefix,NULL))) goto done;
    if(prefix == NULL || prefix[0] == '\0') goto done;

    zarraykey = calloc(1,strlen(prefix) + strlen("/" Z2ARRAY) + 1);
    if(zarraykey == NULL) {stat = NC_ENOMEM; goto done;}
    strcpy(zarraykey,prefix);
    strcat(zarraykey,"/" Z2ARRAY);

    switch ((stat = nczmap_len(map,zarraykey,&len))) {
    case NC_NOERR:
        break;
    case NC_EEMPTY:
    case NC_ENOOBJECT:
        stat = NC_NOERR;
        goto done;
    default:
        goto done;
    }

    metadata = calloc(1,(size_t)len + 1);
    if(metadata == NULL) {stat = NC_ENOMEM; goto done;}
    if((stat = nczmap_read(map,zarraykey,0,len,metadata))) goto done;
    if((stat = NCJparse(metadata,0,&jzarray)) < 0) {stat = NC_EINVAL; goto done;}
    if((stat = NCJdictget(jzarray,"dtype",&jdtype)) < 0) {stat = NC_EINVAL; goto done;}
    if((stat = ncz_dtype2nctype(NCJstring(jdtype),NC_NAT,0,NULL,&endianness,NULL))) goto done;
    if(endianness == NC_ENDIAN_NATIVE)
        endianness = hostendianness();
    if(endianp) *endianp = endianness;

done:
    nullfree(prefix);
    nullfree(zarraykey);
    nullfree(metadata);
    NCJreclaim(jzarray);
    return stat;
}

static Mapop
decodeop(const char* name)
{
    struct Mops* p = mapops;
    for(;p->opname != NULL;p++) {
	if(strcasecmp(p->opname,name)==0) return p->mapop;
    }
    return MOP_NONE;
}

static const struct Type*
decodetype(const char* name,  int* strlenp)
{
    struct Type* p = types;
    
    if(strncmp(name,"string/",strlen("string/"))==0) {
        *strlenp = atoi(name+strlen("string/"));
        name = "string";
    }
    
    for(;p->typename != NULL;p++) {
	if(strcasecmp(p->typename,name)==0) return p;
    }
    return NULL;
}

int
main(int argc, char** argv)
{
    int stat = NC_NOERR;
    int c;
    char* p;

    nc_initialize();

    /* Init options */
    memset((void*)&dumpoptions,0,sizeof(dumpoptions));

    while ((c = getopt(argc, argv, "dhvx:t:F:T:X:")) != EOF) {
	switch(c) {
	case 'd': 
	    dumpoptions.debug = 1;	    
	    break;
	case 'h': 
	    dumpoptions.meta_only = 1;	    
	    break;
	case 't': 
	    dumpoptions.nctype = decodetype(optarg,&dumpoptions.strlen);
	    if(dumpoptions.nctype == NULL) zmapusage();
	    break;
	case 'x': 
	    dumpoptions.mop = decodeop(optarg);
	    if(dumpoptions.mop == MOP_NONE) zmapusage();
	    break;
	case 'v': 
	    zmapusage();
	    goto done;
	case 'F': 
	    strcpy(dumpoptions.format,optarg);
	    break;
	case 'T':
	    nctracelevel(atoi(optarg));
	    break;
	case 'X':
	    for(p=optarg;*p;p++) {
		switch (*p) {
		case 'm': dumpoptions.xflags |= XNOZMETADATA; break;
	        default: fprintf(stderr,"Unknown -X argument: %c",*p); break;
		}
	    };
	    break;
	case '?':
	   fprintf(stderr,"unknown option\n");
	   goto fail;
	}
    }

    /* Default the kind */
    if(dumpoptions.nctype == NULL) {
	dumpoptions.nctype = &types[0];    
	fprintf(stderr,"Default type: %s\n",dumpoptions.nctype->typename); 
    }

    /* get file argument */
    argc -= optind;
    argv += optind;

    if (argc > 1) {
	fprintf(stderr, "zmapio: only one input file argument permitted\n");
	goto fail;
    }
    if (argc == 0) {
	fprintf(stderr, "zmapio: no input file specified\n");
	goto fail;
    }

    {
        char* p = NC_shellUnescape(argv[0]);
        strcpy(dumpoptions.infile,filenamefor(p));
	if(p) free(p);
    }

    if((dumpoptions.impl = implfor(dumpoptions.infile))== NCZM_UNDEF)
        zmapusage();

    if((dumpoptions.rootpath = rootpathfor(dumpoptions.infile))== NULL)
        zmapusage();

    switch (dumpoptions.mop) {
    default:
	fprintf(stderr,"Default action: objdump\n");
	/* fall thru */
    case MOP_OBJDUMP:
	if((stat = objdump())) goto done;
	break;
    }    

done:
    /* Reclaim dumpoptions */
    nullfree(dumpoptions.rootpath);
    nc_finalize();
    if(stat)
	fprintf(stderr,"fail: %s\n",nc_strerror(stat));
    return (stat ? 1 : 0);    
fail:
    stat = NC_EINVAL;
    goto done;
}

static NCZM_IMPL
implfor(const char* path)
{
    NCURI* uri = NULL;
    const char* mode = NULL;
    NClist* segments = nclistnew();

    NCZM_IMPL impl = NCZM_UNDEF;

    ncuriparse(path,&uri);
    if(uri == NULL) goto done;
    mode = ncurifragmentlookup(uri,"mode");
    if(mode == NULL) goto done;
    /* split on commas */
    NCCHECK(NC_split_delim(mode,',',segments));
    for(size_t i=0;i<nclistlength(segments);i++) {
        const char* value = nclistget(segments,i);
	if(strcmp(value,"file")==0) {impl = NCZM_FILE; goto done;}
	if(strcmp(value,"zip")==0) {impl = NCZM_ZIP; goto done;}
	if(strcmp(value,"s3")==0) {impl = NCZM_S3; goto done;}
    }
done:
    ncurifree(uri);
    nclistfreeall(segments);    
    return impl;
}

static char*
rootpathfor(const char* path)
{
    int stat = NC_NOERR;
    NCURI* uri = NULL;
    char* rootpath = NULL;
    NClist* segments = nclistnew();

    ncuriparse(path,&uri);
    if(uri == NULL) goto done;
    switch (dumpoptions.impl) {
    case NCZM_FILE:
    case NCZM_ZIP:
	rootpath = strdup("/"); /*constant*/
	break;
#ifdef NETCDF_ENABLE_S3  
    case NCZM_S3: {
	char* p = NULL;
        /* Split the path part */
        if((stat = nczm_split(uri->path,segments))) goto done;
	/* remove the bucket name */
	p = (char*)nclistremove(segments,0);
	nullfree(p); p = NULL;
        /* Put it back together */
        if((stat = NC_join(segments,&rootpath))) goto done;
	} break;
#endif
    default:
        stat = NC_EINVAL;
	goto done;
    }

done:
    nclistfreeall(segments); segments = NULL;
    ncurifree(uri); uri = NULL;
    if(stat)
        {nullfree(rootpath); rootpath = NULL;}
    return rootpath;
}

static int
objdump(void)
{
    int stat = NC_NOERR;
    NCZMAP* map = NULL;
    NClist* stack = nclistnew();
    char* obj = NULL;
    char* content = NULL;


    if((stat=nczmap_open(dumpoptions.impl, dumpoptions.infile, NC_NOCLOBBER, 0, NULL, &map)))
        goto done;

    /* Depth first walk all the groups to get all keys */
    if((stat = breadthfirst(map,"/",stack))) goto done;

    if(dumpoptions.debug) {

        fprintf(stderr,"stack:\n");
        for(size_t i=0;i<nclistlength(stack);i++)

            fprintf(stderr,"[%zu] %s\n",i,(char*)nclistget(stack,i));
    }    
    for(size_t depth=0;depth < nclistlength(stack);depth++) {
        size64_t len = 0;
	OBJKIND kind = 0;
	int hascontent = 0;
	nullfree(content); content = NULL;
	obj = nclistget(stack,depth);
	kind = keykind(obj);
	/* Now print info for this obj key */
        switch (stat=nczmap_len(map,obj,&len)) {
	    case NC_NOERR: hascontent = 1; break;
	    case NC_EEMPTY: case NC_ENOOBJECT: /* fall thru */ /* this is not a content bearing key */
	    case NC_EACCESS: hascontent = 0; len = 0; stat = NC_NOERR; break;
	    default: goto done;
	}
	if(!hascontent) continue; /* ignore it */
	if(len > 0) {
	    size_t padlen = (len+dumpoptions.nctype->typesize);
	    content = calloc(1,padlen+1);
  	    if((stat=nczmap_read(map,obj,0,len,content))) goto done;
	    content[len] = '\0';
	}
	if(hascontent) {
	    if(len > 0) {
                assert(content != NULL);
		if(kind == OK_CHUNK) {
		    len = ceildiv(len,dumpoptions.nctype->typesize);
		}
                printf("[%zu] %s : (%llu)",depth,obj,len);
		if(kind == OK_CHUNK &&  dumpoptions.nctype->nctype != NC_STRING)
                    printf(" (%s)",dumpoptions.nctype->typename);
                printf(" |");
                switch(kind) {
		case OK_GROUP:
		case OK_META:
	            if((stat = printcontent(map,obj,len,content,kind))) goto done;
		    break;
		case OK_CHUNK:
		    if(dumpoptions.meta_only) {
			printf("...");
		    } else {
	                if((stat = printcontent(map,obj,len,content,kind))) goto done;
		    }
		    break;
		default: break;
		}
	        printf("|\n");
	    } else {
	        printf("[%zu] %s : (%llu) ||\n",depth,obj,len);
	    }
	} else {
	    printf("[%zu] %s\n",depth,obj);
	}
    }
done:
    nullfree(content);
    nczmap_close(map,0);
    nclistfreeall(stack);
    return stat;
}

/* Depth first walk all the groups to get all keys */
static int
breadthfirstR(NCZMAP* map, NCbytes* prefix, NClist* stack)
{
    int stat = NC_NOERR;
    NClist* nextlevel = nclistnew();
    size_t mark;
    const char* content;
    int isroot = 0;

    content = ncbytescontents(prefix);
    if(content[0] == '/' && content[1] == '\0') isroot = 1;
    if((stat=nczmap_search(map,content,nextlevel))) goto done;
    /* Sort nextlevel */
    sortlist(nextlevel);
    /* Push new names onto the stack and recurse */
    mark = ncbyteslength(prefix); /* save this position */
    while(nclistlength(nextlevel) > 0) {
        char* subkey = nclistremove(nextlevel,0);
	if(!isroot) ncbytescat(prefix,"/");
	ncbytescat(prefix,subkey);
	nullfree(subkey);
        nclistpush(stack,ncbytesdup(prefix));
	if((stat = breadthfirstR(map,prefix,stack))) goto done;
	ncbytessetlength(prefix,mark); ncbytesnull(prefix);
    }
done:
   nclistfreeall(nextlevel);
   return stat;
}

/* Depth first walk all the groups to get all keys */
static int
breadthfirst(NCZMAP* map, const char* key, NClist* stack)
{
    int stat = NC_NOERR;
    NCbytes* prefix = ncbytesnew();

    if(key == NULL || key[0] == '\0')
        key = "/";
    ncbytescat(prefix,key);
    if(strlen(key) > 1 && key[strlen(key)-1]=='/') {
        ncbytessetlength(prefix,ncbyteslength(prefix)-1); /* remove trailing '/' */
	ncbytesnull(prefix);
    }
    stat = breadthfirstR(map,prefix,stack);
    ncbytesfree(prefix);
    return stat;
}


static int
printcontent(NCZMAP* map, const char* key, size64_t len, const char* content, OBJKIND kind)
{
    int stat = NC_NOERR;
    size64_t i, count;

    const char* format = NULL;
    size64_t strlen = (size64_t)dumpoptions.strlen;
    int data_endianness = hostendianness();
    int doswap = 0;

    format = dumpoptions.nctype->format;
    if(dumpoptions.format[0] != '\0')
        format = dumpoptions.format;
    count = len;

    if(kind == OK_CHUNK
       && dumpoptions.nctype->typesize > 1
       && dumpoptions.nctype->nctype != NC_STRING) {
        if((stat = chunkendianness(map,key,&data_endianness))) goto done;
        doswap = (data_endianness != hostendianness());
    }

#ifdef DEBUG
    printf("debug: len=%d strlen=%d count=%d\n",(int)len,(int)strlen,(int)count); fflush(stdout);
#endif

    for(i=0;i<count;i++) {
        /* If kind is chunk, then len is # of values, not # of bytes */
	switch(kind) {
	case OK_CHUNK:
	    if(i > 0) printf(", ");
	    switch(dumpoptions.nctype->nctype) {
	    case NC_BYTE: printf(format,((char*)content)[i]); break;
	    case NC_SHORT: {
		int16_t v;
		memcpy(&v,content + (i * sizeof(v)),sizeof(v));
		if(doswap) swapinline16(&v);
		printf(format,(int)v);
		} break;		
	    case NC_INT: {
		int32_t v;
		memcpy(&v,content + (i * sizeof(v)),sizeof(v));
		if(doswap) swapinline32(&v);
		printf(format,(int)v);
		} break;		
	    case NC_INT64: {
		int64_t v;
		memcpy(&v,content + (i * sizeof(v)),sizeof(v));
		if(doswap) swapinline64(&v);
		printf(format,(long long)v);
		} break;		
	    case NC_UBYTE: printf(format,((unsigned char*)content)[i]); break;
	    case NC_USHORT: {
		uint16_t v;
		memcpy(&v,content + (i * sizeof(v)),sizeof(v));
		if(doswap) swapinline16(&v);
		printf(format,(unsigned int)v);
		} break;		
	    case NC_UINT: {
		uint32_t v;
		memcpy(&v,content + (i * sizeof(v)),sizeof(v));
		if(doswap) swapinline32(&v);
		printf(format,(unsigned int)v);
		} break;		
	    case NC_UINT64: {
		uint64_t v;
		memcpy(&v,content + (i * sizeof(v)),sizeof(v));
		if(doswap) swapinline64(&v);
		printf(format,(unsigned long long)v);
		} break;		
	    case NC_FLOAT: {
		uint32_t raw;
		float v;
		memcpy(&raw,content + (i * sizeof(raw)),sizeof(raw));
		if(doswap) swapinline32(&raw);
		memcpy(&v,&raw,sizeof(v));
		printf(format,v);
		} break;		
	    case NC_DOUBLE: {
		uint64_t raw;
		double v;
		memcpy(&raw,content + (i * sizeof(raw)),sizeof(raw));
		if(doswap) swapinline64(&raw);
		memcpy(&v,&raw,sizeof(v));
		printf(format,v);
		} break;		
	    case NC_CHAR: printf(format,((char*)content)[i]); break;
	    case NC_STRING: printf(format,(int)strlen,((char*)(&content[i*strlen]))); break;
	    default: abort();
	    }
	    break;
	case OK_META:
	    printf("%c",content[i]);
	    break;
	default:
	    printf("%.2hhx", content[i]);
        }
    }
done:
    return stat;
}

static char chunkchars[] = ".0123456789";

static OBJKIND
keykind(const char* key)
{
    OBJKIND kind = OK_NONE;
    char* suffix = NULL;
    if(nczm_divide_at(key,-1,NULL,&suffix) == NC_NOERR) {
	if(suffix) {
            if(suffix[0] != '/')
		kind = OK_NONE;
	    else if(suffix[1] == '.') {
		if(strcmp(&suffix[1],".zmetadata")==0 && (dumpoptions.xflags & XNOZMETADATA))
		    kind = OK_IGNORE;
		else
	            kind = OK_META;
            } else if(suffix[strlen(suffix)-1] == '/')
		kind = OK_GROUP;
	    else {
		char* p = suffix+1;
	        for(;*p;p++) {
	            if(strchr(chunkchars,*p) == NULL) break;
		}
		kind = OK_CHUNK;
	    }
	}
    }
    nullfree(suffix);
    return kind;
}

/* bubble sort a list of strings */
static void
sortlist(NClist* l)
{
    size_t i, switched;

    if(nclistlength(l) <= 1) return;
    do {
	switched = 0;
        for(i=0;i<nclistlength(l)-1;i++) {
	    char* ith = nclistget(l,i);
	    char* ith1 = nclistget(l,i+1);
	    if(strcmp(ith,ith1) > 0) {
	        nclistset(l,i,ith1);
    	        nclistset(l,i+1,ith);
	        switched = 1;
	    }
	}
    } while(switched);
#if 0
for(i=0;i<nclistlength(l);i++)
fprintf(stderr,"sorted: [%d] %s\n",i,(const char*)nclistget(l,i));
#endif
}

static const char* urlexts[] = {"nzf", "zip", "nz4", NULL};

static const char*
filenamefor(const char* f0)
{
    static char result[4096];
    const char** extp;
    char* p;
    NCURI* uri = NULL;

    strcpy(result,f0); /* default */
    ncuriparse(f0,&uri);
    if(uri == NULL) {
        /* Not a URL */
        p = strrchr(f0,'.'); /* look at the extension, if any */
        if(p == NULL) goto done; /* No extension */
        p++;
        for(extp=urlexts;*extp;extp++) {
            if(strcmp(p,*extp)==0) break;
        }
        if(*extp == NULL) goto done; /* not found */
        /* Assemble the url */
        strcpy(result,"file://");
        strcat(result,f0); /* core path */
        strcat(result,"#mode=nczarr,");
        strcat(result,*extp);
    }
done:
    ncurifree(uri);
    return result;
}
