/*
 * Emit and parse JSON
 */

autoload Sort;
autoload Ctype;

namespace JSON {

    public void to_json_file(file output, poly arg)
    {
	void do_indent(int indent) {
	    File::putc('\n', output);
	    for (int i = 0; i < indent; i++)
		File::putc('\t', output);
	}

	void json_string(string a) {
	    File::putc('"', output);
	    for (int i = 0; i < String::length(a); i++) {
		int c = a[i];
		switch (c) {
		case '\n':
		    c = 'n';
		case '"':
		case '\\':
		    File::putc('\\', output);
		    break;
		default:
		    break;
		}
		File::putc(c, output);
	    }
	    File::putc('"', output);
	}

	void putc(int c) {
	    File::putc(c, output);
	}

	void puts(string s) {
	    File::fprintf(output, "%s", s);
	}

	void json(poly arg, int indent);

	void json_hash(poly[string] a, int indent) {
	    string[] keys = hash_keys(a);
	    putc('{');
	    bool first = true;

	    Sort::qsort(&keys, bool func(poly a, poly b) { return a > b; });
	    for (int i = 0; i < dim(keys); i++) {
		string key = keys[i];

		if (!first)
		    putc(',');
		first = false;
		do_indent(indent+1);
		json_string(key);
		puts(": ");
		json(a[key], indent+1);
	    }
	    do_indent(indent);
	    putc('}');
	}

	void json_array(poly[] a, int indent) {
	    putc('[');
	    bool first = true;

	    for (int i = 0; i < dim(a); i++) {
		if (!first)
		    putc(',');
		first = false;
		do_indent(indent + 1);
		json(a[i], indent + 1);
	    }
	    do_indent(indent);
	    putc(']');
	}

	void json_int(int arg) {
	    File::fprintf (output, "%d", arg);
	}

	void json_float(real arg) {
	    File::fprintf(output, "%.13g", arg);
	}

	void json_bool(bool arg) {
	    File::fprintf(output, "%s", arg ? "true" : "false");
	}

	void json_null(void arg) {
	    File::fprintf(output, "null");
	}

	json = void func(poly arg, int indent) {
	    if (is_hash(arg))
		return json_hash(arg, indent);
	    if (is_array(arg))
		return json_array(arg, indent);
	    if (is_string(arg))
		return json_string(arg);
	    if (is_int(arg))
		return json_int(arg);
	    if (is_number(arg))
		return json_float(arg);
	    if (is_bool(arg))
		return json_bool(arg);
	    if (is_void(arg))
		return json_null(arg);
	};

	json(arg, 0);
    }

    public string to_json(poly arg)
    {
	file output = File::string_write();
	to_json_file(output, arg);
	return File::string_string(output);
    }

    typedef enum {
	_string,
	_int,
	_float,
	_bool,
	_null,
	_oc,
	_cc,
	_os,
	_cs,
	_comma,
	_colon,
	_end,
	_error
    } term_t;

    typedef struct {
	union {
	    int		ival;
	    string	sval;
	    real	fval;
	    bool	bval;
	    void	nval;
	} val;
	term_t	token;
    } token_t;

    typedef struct {
	file	f;
	int	line;
	string	token;
    } json_input_t;

    json_input_t start_json(string s) {
	return (json_input_t) { .f = File::string_read(s), .line = 1 };
    }

    public exception json_parse_error(int line, string token);

    token_t lex(*json_input_t f) {

	int ch() {
	    int c = File::getc(f->f);
	    if (c == '\n')
		f->line++;
	    f->token = f->token + String::new(c);
	    return c;
	}

	void unch(int c) {
	    if (c == '\n')
		f->line--;
	    f->token = String::substr(f->token, 0, String::length(f->token) - 1);
	    File::ungetc(c, f->f);
	}

	bool is_int_char(int c) {
	    if (Ctype::isdigit(c))
		return true;
	    if (c == '-')
		return true;
	    return false;
	}

	bool is_float_char(int c) {
	    if (Ctype::isdigit(c))
		return true;
	    if (c == '-')
		return true;
	    if (c == '.')
		return true;
	    if (c == 'e' || c == 'E')
		return true;
	    return false;
	}

	bool match_keyword(string keyword) {
	    int l = String::length(keyword);
	    for (int i = 1; i < l; i++) {
		int c = ch();
		if (keyword[i] != c)
		    return false;
	    }
	    return true;
	}

	try {
	    f->token = "";
	    for (;;) {
		int	c = ch();

		switch (c) {
		case '\n':
		case ' ':
		case '\t':
		    continue;
		case '{':
		    return (token_t) { .token = term_t._oc };
		case '}':
		    return (token_t) { .token = term_t._cc };
		case '[':
		    return (token_t) { .token = term_t._os };
		case ']':
		    return (token_t) { .token = term_t._cs };
		case ',':
		    return (token_t) { .token = term_t._comma };
		case ':':
		    return (token_t) { .token = term_t._colon };
		case '0': case '1': case '2': case '3': case '4':
		case '5': case '6': case '7': case '8': case '9':
		case '-': case '.':
		    string nval = "";
		    bool has_float = false;
		    while (is_float_char(c)) {
			nval = nval + String::new(c);
			if (!is_int_char(c))
			    has_float = true;
			if (File::end(f->f))
			    break;
			c = ch();
		    }
		    unch(c);
		    if (has_float)
			return (token_t) { .val = { .fval = atof(nval) }, .token = term_t._float };
		    return (token_t) { .val = { .ival = atoi(nval) }, .token = term_t._int };
		case '"':
		    string sval = "";
		    for (;;) {
			c = ch();
			if (c == '"')
			    break;
			if (c == '\\') {
			    c = ch();
			    switch (c) {
			    case 'n':
				c = '\n';
				break;
			    case 't':
				c = '\t';
				break;
			    default:
				break;
			    }
			}
			sval = sval + String::new(c);
		    }
		    return (token_t) { .val = { .sval = sval }, .token = term_t._string };
		case 't':
		    if (match_keyword("true"))
			return (token_t) { .val = { .bval = true }, .token = term_t._bool };
		    raise json_parse_error(f->line, f->token);
		    break;
		case 'f':
		    if (match_keyword("false"))
			return (token_t) { .val = { .bval = false }, .token = term_t._bool };
		    raise json_parse_error(f->line, f->token);
		    break;
		case 'n':
		    if (match_keyword("null"))
			return (token_t) { .val = { .nval = <> }, .token = term_t._null };
		    raise json_parse_error(f->line, f->token);
		    break;
		default:
		    break;
		}
		break;
	    }
	}
	catch File::io_eof(file f) {
	    return (token_t) { .token = term_t._end };
	}
	return (token_t) { .token = term_t._error };
    }

    public poly from_json_file(file input) {
	json_input_t f = { .f = input, .line = 1 };
	token_t	token;

	void next() {
	    token = lex(&f);
	}

	void expect(term_t t) {
	    if (token.token != t)
		raise json_parse_error(f.line, f.token);
	}

	poly value();

	poly[string] hash() {
	    poly[string] ret = {};
	    for (;;) {
		if (token.token == term_t._cc) {
		    next();
		    return ret;
		}
		expect(term_t._string);
		string name = token.val.sval;
		next();
		expect(term_t._colon);
		next();
		poly val = value();
		ret[name] = val;
		switch (token.token) {
		case term_t._comma:
		    next();
		    continue;
		case term_t._cc:
		    break;
		default:
		    raise json_parse_error(f.line, f.token);
		}
	    }
	}

	poly[] array() {
	    poly[...] ret = {};
	    for (;;) {
		if (token.token == term_t._cs) {
		    next();
		    return ret;
		}
		poly val = value();
		ret[dim(ret)] = val;
		switch (token.token) {
		case term_t._comma:
		    next();
		    continue;
		case term_t._cs:
		    break;
		default:
		    raise json_parse_error(f.line, f.token);
		}
	    }
	}

	value = poly func() {
	    switch (token.token) {
	    case term_t._oc:
		    next();
		    return hash();
	    case term_t._os:
		    next();
		    return array();
	    case term_t._int:
		    int ival = token.val.ival;
		    next();
		    return ival;
	    case term_t._float:
		    real fval = token.val.fval;
		    next();
		    return fval;
	    case term_t._string:
		    string sval = token.val.sval;
		    next();
		    return sval;
	    case term_t._bool:
		    bool bval = token.val.bval;
		    next();
		    return bval;
	    case term_t._null:
		    void nval = token.val.nval;
		    next();
		    return nval;
	    }
	};

	next();
	return value();
    }

    public poly from_json(string arg) {
	return from_json_file(File::string_read(arg));
    }
}
