#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <getopt.h>
#include <ctype.h>
#ifndef WIN32
# include <err.h>
#endif
#include "json.h"
#include "base64.h"

/*
 * Copyright (C) 2016-2019 Jan-Piet Mens <jp@mens.de>
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#define SPACER		"   "
#define FLAG_ARRAY	0x01
#define FLAG_PRETTY	0x02
#define FLAG_NOBOOL	0x04
#define FLAG_BOOLEAN	0x08
#define FLAG_NOSTDIN	0x10
#define FLAG_MASK	(FLAG_ARRAY | FLAG_PRETTY | FLAG_NOBOOL | FLAG_BOOLEAN | FLAG_NOSTDIN)

/* Size of buffer blocks for pipe slurping */
#define SLURP_BLOCK_SIZE 4096

static JsonNode *pile;		/* pile of nested objects/arrays */

#ifdef _WIN32
# define err(n, s)	{ fprintf(stderr, s); exit(n); }
# define errx(n, f, a)	{ fprintf(stderr, f, a); exit(n); }
# define fseeko	fseek
# define ftello	ftell
#endif

JsonTag flags_to_tag(int flags) {
	return flags / (FLAG_MASK + 1);
}

int tag_to_flags(JsonTag tag) {
	return (FLAG_MASK + 1) * tag;
}

void json_copy_to_object(JsonNode * obj, JsonNode * object_or_array, int clobber)
{
	JsonNode *node, *node_child, *obj_child;

	if (obj->tag != JSON_OBJECT && obj->tag != JSON_ARRAY)
		return;

	json_foreach(node, object_or_array) {
		if (!clobber & (json_find_member(obj, node->key) != NULL))
			continue;	/* Don't clobber existing keys */
		if (obj->tag == JSON_OBJECT) {
			if (node->tag == JSON_STRING)
				json_append_member(obj, node->key, json_mkstring(node->string_));
			else if (node->tag == JSON_NUMBER)
				json_append_member(obj, node->key, json_mknumber(node->number_));
			else if (node->tag == JSON_BOOL)
				json_append_member(obj, node->key, json_mkbool(node->bool_));
			else if (node->tag == JSON_NULL)
				json_append_member(obj, node->key, json_mknull());
			else if (node->tag == JSON_OBJECT) {
				/* Deep-copy existing object to new object */
				json_append_member(obj, node->key, (obj_child = json_mkobject()));
				json_foreach(node_child, node) {
					json_copy_to_object(obj_child, node_child, clobber);
				}
			} else
				fprintf(stderr, "PANIC: unhandled JSON type %d\n", node->tag);
		} else if (obj->tag == JSON_ARRAY) {
			if (node->tag == JSON_STRING)
				json_append_element(obj, json_mkstring(node->string_));
			if (node->tag == JSON_NUMBER)
				json_append_element(obj, json_mknumber(node->number_));
			if (node->tag == JSON_BOOL)
				json_append_element(obj, json_mkbool(node->bool_));
			if (node->tag == JSON_NULL)
				json_append_element(obj, json_mknull());
		}
	}
}

const char *maybe_stdin(const char* filename)
{
	return strcmp(filename, "-") ? filename : "/dev/fd/0";
}
		

char *slurp_file(const char* filename, size_t *out_len, bool fold_newlines)
{
	char *buf;
	int i = 0;
	int ch;
	off_t buffer_len;
	FILE *fp;

	if ((fp = fopen(maybe_stdin(filename), "r")) == NULL) {
		perror(filename);
		errx(1, "Cannot open %s for reading", filename);
	}
	if (fseeko(fp, 0, SEEK_END) != 0) {
		/* If we cannot seek, we're operating off a pipe and
		   need to dynamically grow the buffer that we're
		   reading into */
		buffer_len = SLURP_BLOCK_SIZE;
	} else {
		buffer_len = ftello(fp) + 1;
		fseeko(fp, 0, SEEK_SET);
	}

	if ((buf = malloc(buffer_len)) == NULL) {
		errx(1, "File %s is too large to be read into memory", filename);
	}
	while ((ch = fgetc(fp)) != EOF) {
		if (i == (buffer_len - 1)) {
			buffer_len += SLURP_BLOCK_SIZE;
			if ((buf = realloc(buf, buffer_len)) == NULL) {
				errx(1, "File %s is too large to be read into memory", filename);
			}
		}
		if (ch != '\n' || !fold_newlines) {
			buf[i++] = ch;
		}
	}
        fclose(fp);
	buf[i] = 0;
	*out_len = i;
	return (buf);
}

JsonNode *jo_mknull(JsonTag type) {
	switch (type) {
		case JSON_STRING:
			return json_mkstring("");
			break;
		case JSON_NUMBER:
			return json_mknumber(0);
			break;
		case JSON_BOOL:
			return json_mkbool(false);
			break;
		default:
			return json_mknull();
			break;
	}
}

JsonNode *jo_mkbool(bool b, JsonTag type) {
	switch (type) {
		case JSON_STRING:
			return json_mkstring(b ? "true" : "false");
			break;
		case JSON_NUMBER:
			return json_mknumber(b ? 1 : 0);
			break;
		default:
			return json_mkbool(b);
			break;
	}
}

JsonNode *jo_mkstring(char *str, JsonTag type) {
	switch (type) {
		case JSON_NUMBER:
			/* Length of string */
			return json_mknumber(strlen(str));
			break;
		case JSON_BOOL:
			/* True if not empty */
			return json_mkbool(strlen(str) > 0);
			break;
		default:
			return json_mkstring(str);
			break;
	}
}

JsonNode *jo_mknumber(char *str, JsonTag type) {
	/* ASSUMPTION: str already tested as valid number */
	double n = strtod(str, NULL);

	switch (type) {
		case JSON_STRING:
			/* Just return the original representation */
			return json_mkstring(str);
			break;
		case JSON_BOOL:
			return json_mkbool(n != 0);
			break;
		default:
			/* ASSUMPTION: str already tested as valid number */
			return json_mknumber(n);
			break;
	}
}

/*
 * Attempt to "sniff" the type of data in `str' and return
 * a JsonNode of the correct JSON type.
 */

JsonNode *vnode(char *str, int flags)
{
	JsonTag type = flags_to_tag(flags);

	if (strlen(str) == 0) {
		return jo_mknull(type);
	}

	/* If str begins with a double quote, keep it a string */

	if (*str == '"') {
#if 0
		char *bp = str + strlen(str) - 1;

		if (bp > str && *bp == '"')
			*bp = 0;		/* Chop closing double quote */
		return json_mkstring(str + 1);
#endif
		return jo_mkstring(str, type);
	}

	char *endptr;
	double num = strtod(str, &endptr);

	if (!*endptr && isfinite(num)) {
		return jo_mknumber(str, type);
	}

	if (!(flags & FLAG_NOBOOL)) {
		if (strcmp(str, "true") == 0) {
			return jo_mkbool(true, type);
		} else if (strcmp(str, "false") == 0) {
			return jo_mkbool(false, type);
		} else if (strcmp(str, "null") == 0) {
			return jo_mknull(type);
		}
	}

	if (*str == '\\') {
		++str;
	} else {
		if (*str == '@' || *str == '%' || *str == ':') {
			char *filename = str + 1, *content;
			bool binmode = (*str == '%');
			bool jsonmode = (*str == ':');
			size_t len = 0;
			JsonNode *j = NULL;
	
			if ((content = slurp_file(filename, &len, false)) == NULL) {
				errx(1, "Error reading file %s", filename);
			}
	
			if (binmode) {
				char *encoded;
	
				if ((encoded = base64_encode(content, len)) == NULL) {
					errx(1, "Cannot base64-encode file %s", filename);
				}
	
				j = json_mkstring(encoded);
				free(encoded);
			} else if (jsonmode) {
				j = json_decode(content);
				if (j == NULL) {
					errx(1, "Cannot decode JSON in file %s", filename);
				}
			}
	
			// If it got this far without valid JSON, just consider it a string
			if (j == NULL) {
				char *bp = content + strlen(content) - 1;
	
				if (*bp == '\n') *bp-- = 0;
				if (*bp == '\r') *bp = 0;
				j = json_mkstring(content);
			}
			free(content);
			return (j);
		}
	}

	if (*str == '{' || *str == '[') {
		if (type == JSON_STRING) {
			return json_mkstring(str);
		}
		JsonNode *obj = json_decode(str);

		if (obj == NULL) {
			/* JSON cannot be decoded; return the string */
			// fprintf(stderr, "Cannot decode JSON from %s\n", str);

			obj = json_mkstring(str);
		}

		return (obj);
	}

	return jo_mkstring(str, type);
}

/*
 * Attempt to sniff `str' into a boolean; return a
 * corresponding JsonNode for it.
 */

JsonNode *boolnode(char *str)
{
	if (strlen(str) == 0) {
		return json_mknull();
	}

	if (tolower((unsigned char) *str) == 't') {
		return json_mkbool(1);
	}

	return json_mkbool(atoi(str));
}

int usage(char *prog)
{
	fprintf(stderr, "Usage: %s [-a] [-B] [-d keydelim] [-p] [-e] [-v] [-V] [--] [-s|-n|-b] [word...]\n", prog);
	fprintf(stderr, "\tword is key=value or key@value\n");
	fprintf(stderr, "\t-a creates an array of words\n");
	fprintf(stderr, "\t-B disable boolean true/false\n");
	fprintf(stderr, "\t-d key will be object path separated by keydelim\n");
	fprintf(stderr, "\t-p pretty-prints JSON on output\n");
	fprintf(stderr, "\t-e if stdin is empty do not wait for input quit\n");
	fprintf(stderr, "\t-s coerce type guessing to string\n");
	fprintf(stderr, "\t-b coerce type guessing to bool\n");
	fprintf(stderr, "\t-n coerce type guessing to number\n");
	fprintf(stderr, "\t-v show version\n");
	fprintf(stderr, "\t-V show version in JSON\n");

	return (-1);
}

/*
 * Check whether we're being given nested arrays or objects.
 * `kv' contains the "key" such as "number" or "point[]" or
 * "geo[lat]". `value' the actual value for that element.
 *
 * Returns true if nesting is completely handled, otherwise:
 *   *keyp   -> remaining key for caller to insert "value"
 *   *baseop -> object node in which caller should insert "value"
 */

bool resolve_nested(int flags, char **keyp, char key_delim, char *value, JsonNode **baseop)
{
	char *member = NULL, *bo, *bc, *so;		/* bracket open, close, sub-object */
	JsonNode *op;
	int found = false;

	if (key_delim) {
		/* First construct nested object */
		while ((so = strchr(*keyp, key_delim)) != NULL) {
			*so = 0;
			if ((op = json_find_member(*baseop, *keyp)) == NULL) {
				/* Add a nested object node */
				op = json_mkobject();
				json_append_member(*baseop, *keyp, op);
			}
			*baseop = op;
			*keyp = so + 1;
		}
	}

	/* Now check for trailing geo[] or geo[lat] */
	if ((bo = strchr(*keyp, '[')) != NULL) {
		if (*(bo+1) == ']') {
			*bo = 0;
		} else if ((bc = strchr(bo + 1, ']')) == NULL) {
			fprintf(stderr, "missing closing bracket on %s\n", *keyp);
			return (false);
		} else {
			*bo = *bc = 0;
			member = bo + 1;
		}

		/*
		 * *keyp is now `geo' for both `geo[]` and `geo[lat]`
		 * member is null for the former and "lat" for the latter.
		 * Find an existing object in *baseop for this member name
		 * or create a new one if we don't have it.
		 */

		if ((op = json_find_member(*baseop, *keyp)) != NULL) {
			found = true;
		} else {
			op = (member == NULL) ? json_mkarray() : json_mkobject();
		}

		if (member == NULL) {		/* we're doing an array */
			if (flags & FLAG_BOOLEAN) {
				json_append_element(op, boolnode(value));
			} else {
				json_append_element(op, vnode(value, flags));
			}
		} else {			/* we're doing an object */
			if (flags & FLAG_BOOLEAN) {
				json_append_member(op, member, boolnode(value));
			} else {
				json_append_member(op, member, vnode(value, flags));
			}
		}

		if (!found) {
			json_append_member(*baseop, *keyp, op);
		}

		return (true);
	}
	return (false);
}

int member_to_object(JsonNode *object, int flags, char key_delim, char *kv)
{
	/* we expect key=value or key:value (boolean on last) */
	char *p = strchr(kv, '=');
	char *q = strchr(kv, '@');
	char *r = strchr(kv, ':');

	if ((r && *(r+1) == '=') && !q) {
		char *filename = p + 1;
		char *content;
		size_t len;

		if ((content = slurp_file(filename, &len, false)) == NULL) {
			errx(1, "Error reading file %s", filename);
		}

		JsonNode *o = json_decode(content);
		free(content);

		if (o == NULL) {
			errx(1, "Cannot decode JSON in file %s", filename);
		}

		*r = 0;		/* Chop at ":=" */
		json_append_member(object, kv, o);
		return (0);
	}

	if (!p && !q && !r) {
		return (-1);
	}


	if (p) {
		if (p) {
			*p = 0;

			if (!resolve_nested(flags, &kv, key_delim, p+1, &object))
				json_append_member(object, kv, vnode(p+1, flags));
		}
	} else {
		if (q) {
			*q = 0;

			if (!resolve_nested(flags | FLAG_BOOLEAN, &kv, key_delim, q+1, &object))
				json_append_member(object, kv, boolnode(q+1));
		}
	}
	return (0);
}

/*
 * Append kv to the array or object.
 */

void append_kv(JsonNode *object_or_array, int flags, char key_delim, char *kv)
{
	if (flags & FLAG_ARRAY) {
		json_append_element(object_or_array, vnode(kv, flags));
	} else {
		if (member_to_object(object_or_array, flags, key_delim, kv) == -1) {
			fprintf(stderr, "Argument `%s' is neither k=v nor k@v\n", kv);
		}
	}
}

#ifdef _WIN32
#include <windows.h>
char* utf8_from_locale(const char *str, size_t len)
{
	wchar_t* wcsp;
	char* mbsp;
	size_t mbssize, wcssize;

	if (len == 0) {
		return strdup("");
	}
	if (len == -1) {
		len = strlen(str);
	}
	wcssize = MultiByteToWideChar(GetACP(), 0, str, len,  NULL, 0);
	wcsp = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
	if (!wcsp) {
		return NULL;
	}
	wcssize = MultiByteToWideChar(GetACP(), 0, str, len, wcsp, wcssize + 1);
	wcsp[wcssize] = 0;

	mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsp, -1, NULL, 0, NULL, NULL);
	mbsp = (char*) malloc((mbssize + 1));
	if (!mbsp) {
		free(wcsp);
		return NULL;
	}
	mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsp, -1, mbsp, mbssize, NULL, NULL);
	mbsp[mbssize] = 0;
	free(wcsp);
	return mbsp;
}
# define utf8_free(p) free(p)

char* locale_from_utf8(const char *utf8, size_t len)
{
	wchar_t* wcsp;
	char* mbsp;
	size_t mbssize, wcssize;

	if (len == 0) {
		return strdup("");
	}
	if (len == -1) {
		len = strlen(utf8);
	}
	wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, len,  NULL, 0);
	wcsp = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
	if (!wcsp) {
		return NULL;
	}
	wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, len, wcsp, wcssize + 1);
	wcsp[wcssize] = 0;
	mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsp, -1, NULL, 0, NULL, NULL);
	mbsp = (char*) malloc((mbssize + 1));
	if (!mbsp) {
		free(wcsp);
		return NULL;
	}
	mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsp, -1, mbsp, mbssize, NULL, NULL);
	mbsp[mbssize] = 0;
	free(wcsp);
	return mbsp;
}
# define locale_free(p) free(p)
#else
# define utf8_from_locale(p, l) (p)
# define utf8_free(p)
# define locale_from_utf8(p, l) (p)
# define locale_free(p)
#endif

char *stringify(JsonNode *json, int flags)
{
	int pretty = flags & FLAG_PRETTY;

	return json_stringify(json, (pretty) ? SPACER : NULL);
}

int version(int flags)
{
	JsonNode *json = json_mkobject();
	char *js;

	json_append_member(json, "program", json_mkstring("jo"));
	json_append_member(json, "author", json_mkstring("Jan-Piet Mens"));
	json_append_member(json, "repo", json_mkstring("https://github.com/jpmens/jo"));
	json_append_member(json, "version", json_mkstring(PACKAGE_VERSION));

	if ((js = stringify(json, flags)) != NULL) {
		printf("%s\n", js);
		free(js);
	}
	json_delete(json);
	return (0);
}

int main(int argc, char **argv)
{
	int c, key_delim = 0;
	bool showversion = false;
	char *kv, *js_string, *progname, buf[BUFSIZ], *p;
	int ttyin = isatty(fileno(stdin)), ttyout = isatty(fileno(stdout));
	int flags = 0;
	JsonNode *json, *op;

#if HAVE_PLEDGE
	if (pledge("stdio rpath", NULL) == -1) {
		err(1, "pledge");
	}
#endif

	progname = (progname = strrchr(*argv, '/')) ? progname + 1 : *argv;

	while ((c = getopt(argc, argv, "aBd:hpevV")) != EOF) {
		switch (c) {
			case 'a':
				flags |= FLAG_ARRAY;
				break;
			case 'B':
				flags |= FLAG_NOBOOL;
				break;
			case 'd':
				key_delim = optarg[0];
				break;
			case 'h':
				usage(progname);
				return (0);
			case 'p':
				flags |= FLAG_PRETTY;
				break;
			case 'e':
				flags |= FLAG_NOSTDIN;
				break;
			case 'v':
				printf("jo %s\n", PACKAGE_VERSION);
				exit(0);
			case 'V':
				showversion = true;
				break;
			default:
				exit(usage(progname));
		}
	}

	if (showversion) {
		return(version(flags));
	}

	argc -= optind;
	argv += optind;

	pile = json_mkobject();
	json = (flags & FLAG_ARRAY) ? json_mkarray() : json_mkobject();

	if (argc == 0) {
		if (flags & FLAG_NOSTDIN) {
			return(0);
		}
		while (fgets(buf, sizeof(buf), stdin) != NULL) {
			if (buf[strlen(buf) - 1] == '\n')
				buf[strlen(buf) - 1] = 0;
			p = ttyin ? utf8_from_locale(buf, -1) : buf;
			append_kv(json, flags, key_delim, p);
			if (ttyin) utf8_free(p);
		}
	} else {
		while ((kv = *argv++)) {
			if (kv[0] == '-') {
				/* Set one-shot coerce flag */
				switch (kv[1]) {
					case 'b':
						flags |= tag_to_flags(JSON_BOOL);
						break;
					case 's':
						flags |= tag_to_flags(JSON_STRING);
						break;
					case 'n':
						flags |= tag_to_flags(JSON_NUMBER);
						break;
					default:
						exit(usage(progname));
				}
			} else {
				p = utf8_from_locale(kv, -1);
				append_kv(json, flags, key_delim, p);
				utf8_free(p);
				/* Reset any one-shot coerce flags */
				flags &= FLAG_MASK;
			}
		}
	}

	/*
	 * See if we have any nested objects or arrays in the pile,
	 * and copy these into our main object if so.
	 */

	json_foreach(op, pile) {
		JsonNode *o;

		if (op->tag == JSON_ARRAY) {
			o = json_mkarray();
		} else if (op->tag == JSON_OBJECT) {
			o = json_mkobject();
		} else {
			continue;
		}
		json_copy_to_object(o, op, 0);
		json_append_member(json, op->key, o);
	}


	if ((js_string = stringify(json, flags)) == NULL) {
		fprintf(stderr, "Invalid JSON\n");
		exit(2);
	}

	p = ttyout ? locale_from_utf8(js_string, -1) : js_string;
	printf("%s\n", p);
	if (ttyout) locale_free(p);
	free(js_string);
	json_delete(json);
	json_delete(pile);
	return (0);
}
