/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/

/***
  This file is part of systemd.

  Copyright 2014 Lennart Poettering

  systemd is free software; you can redistribute it and/or modify it
  under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or
  (at your option) any later version.

  systemd 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/

#include <math.h>
#include <sys/types.h>

#include "alloc-util.h"
#include "hexdecoct.h"
#include "json.h"
#include "macro.h"
#include "string-util.h"
#include "utf8.h"

int json_variant_new(JsonVariant **ret, JsonVariantType type) {
        JsonVariant *v;

        v = new0(JsonVariant, 1);
        if (!v)
                return -ENOMEM;
        v->type = type;
        *ret = v;
        return 0;
}

static int json_variant_deep_copy(JsonVariant *ret, JsonVariant *variant) {
        int r;

        assert(ret);
        assert(variant);

        ret->type = variant->type;
        ret->size = variant->size;

        if (variant->type == JSON_VARIANT_STRING) {
                ret->string = memdup(variant->string, variant->size+1);
                if (!ret->string)
                        return -ENOMEM;
        } else if (variant->type == JSON_VARIANT_ARRAY || variant->type == JSON_VARIANT_OBJECT) {
                size_t i;

                ret->objects = new0(JsonVariant, variant->size);
                if (!ret->objects)
                        return -ENOMEM;

                for (i = 0; i < variant->size; ++i) {
                        r = json_variant_deep_copy(&ret->objects[i], &variant->objects[i]);
                        if (r < 0)
                                return r;
                }
        } else
                ret->value = variant->value;

        return 0;
}

static JsonVariant *json_object_unref(JsonVariant *variant);

static JsonVariant *json_variant_unref_inner(JsonVariant *variant) {
        if (!variant)
                return NULL;

        if (variant->type == JSON_VARIANT_ARRAY || variant->type == JSON_VARIANT_OBJECT)
                return json_object_unref(variant);
        else if (variant->type == JSON_VARIANT_STRING)
                free(variant->string);

        return NULL;
}

static JsonVariant *json_raw_unref(JsonVariant *variant, size_t size) {
        if (!variant)
                return NULL;

        for (size_t i = 0; i < size; ++i)
                json_variant_unref_inner(&variant[i]);

        free(variant);
        return NULL;
}

static JsonVariant *json_object_unref(JsonVariant *variant) {
        size_t i;

        assert(variant);

        if (!variant->objects)
                return NULL;

        for (i = 0; i < variant->size; ++i)
                json_variant_unref_inner(&variant->objects[i]);

        free(variant->objects);
        return NULL;
}

static JsonVariant **json_variant_array_unref(JsonVariant **variant) {
        size_t i = 0;
        JsonVariant *p = NULL;

        if (!variant)
                return NULL;

        while((p = (variant[i++])) != NULL) {
                if (p->type == JSON_VARIANT_STRING)
                       free(p->string);
                free(p);
        }

        free(variant);

        return NULL;
}

DEFINE_TRIVIAL_CLEANUP_FUNC(JsonVariant **, json_variant_array_unref);

JsonVariant *json_variant_unref(JsonVariant *variant) {
        if (!variant)
                return NULL;

        if (variant->type == JSON_VARIANT_ARRAY || variant->type == JSON_VARIANT_OBJECT)
                json_object_unref(variant);
        else if (variant->type == JSON_VARIANT_STRING)
                free(variant->string);

        free(variant);

        return NULL;
}

char *json_variant_string(JsonVariant *variant){
        assert(variant);
        assert(variant->type == JSON_VARIANT_STRING);

        return variant->string;
}

bool json_variant_bool(JsonVariant *variant) {
        assert(variant);
        assert(variant->type == JSON_VARIANT_BOOLEAN);

        return variant->value.boolean;
}

intmax_t json_variant_integer(JsonVariant *variant) {
        assert(variant);
        assert(variant->type == JSON_VARIANT_INTEGER);

        return variant->value.integer;
}

double json_variant_real(JsonVariant *variant) {
        assert(variant);
        assert(variant->type == JSON_VARIANT_REAL);

        return variant->value.real;
}

JsonVariant *json_variant_element(JsonVariant *variant, unsigned index) {
        assert(variant);
        assert(variant->type == JSON_VARIANT_ARRAY || variant->type == JSON_VARIANT_OBJECT);
        assert(index < variant->size);
        assert(variant->objects);

        return &variant->objects[index];
}

JsonVariant *json_variant_value(JsonVariant *variant, const char *key) {
        size_t i;

        assert(variant);
        assert(variant->type == JSON_VARIANT_OBJECT);
        assert(variant->objects);

        for (i = 0; i < variant->size; i += 2) {
                JsonVariant *p = &variant->objects[i];
                if (p->type == JSON_VARIANT_STRING && streq(key, p->string))
                        return &variant->objects[i + 1];
        }

        return NULL;
}

static void inc_lines(unsigned *line, const char *s, size_t n) {
        const char *p = s;

        if (!line)
                return;

        for (;;) {
                const char *f;

                f = memchr(p, '\n', n);
                if (!f)
                        return;

                n -= (f - p) + 1;
                p = f + 1;
                (*line)++;
        }
}

static int unhex_ucs2(const char *c, uint16_t *ret) {
        int aa, bb, cc, dd;
        uint16_t x;

        assert(c);
        assert(ret);

        aa = unhexchar(c[0]);
        if (aa < 0)
                return -EINVAL;

        bb = unhexchar(c[1]);
        if (bb < 0)
                return -EINVAL;

        cc = unhexchar(c[2]);
        if (cc < 0)
                return -EINVAL;

        dd = unhexchar(c[3]);
        if (dd < 0)
                return -EINVAL;

        x =     ((uint16_t) aa << 12) |
                ((uint16_t) bb << 8) |
                ((uint16_t) cc << 4) |
                ((uint16_t) dd);

        if (x <= 0)
                return -EINVAL;

        *ret = x;

        return 0;
}

static int json_parse_string(const char **p, char **ret) {
        _cleanup_free_ char *s = NULL;
        size_t n = 0, allocated = 0;
        const char *c;

        assert(p);
        assert(*p);
        assert(ret);

        c = *p;

        if (*c != '"')
                return -EINVAL;

        c++;

        for (;;) {
                int len;

                /* Check for EOF */
                if (*c == 0)
                        return -EINVAL;

                /* Check for control characters 0x00..0x1f */
                if (*c > 0 && *c < ' ')
                        return -EINVAL;

                /* Check for control character 0x7f */
                if (*c == 0x7f)
                        return -EINVAL;

                if (*c == '"') {
                        if (!s) {
                                s = strdup("");
                                if (!s)
                                        return -ENOMEM;
                        } else
                                s[n] = 0;

                        *p = c + 1;

                        *ret = s;
                        s = NULL;
                        return JSON_STRING;
                }

                if (*c == '\\') {
                        char ch = 0;
                        c++;

                        if (*c == 0)
                                return -EINVAL;

                        if (IN_SET(*c, '"', '\\', '/'))
                                ch = *c;
                        else if (*c == 'b')
                                ch = '\b';
                        else if (*c == 'f')
                                ch = '\f';
                        else if (*c == 'n')
                                ch = '\n';
                        else if (*c == 'r')
                                ch = '\r';
                        else if (*c == 't')
                                ch = '\t';
                        else if (*c == 'u') {
                                uint16_t x;
                                int r;

                                r = unhex_ucs2(c + 1, &x);
                                if (r < 0)
                                        return r;

                                c += 5;

                                if (!GREEDY_REALLOC(s, allocated, n + 4))
                                        return -ENOMEM;

                                if (!utf16_is_surrogate(x))
                                        n += utf8_encode_unichar(s + n, x);
                                else if (utf16_is_trailing_surrogate(x))
                                        return -EINVAL;
                                else {
                                        uint16_t y;

                                        if (c[0] != '\\' || c[1] != 'u')
                                                return -EINVAL;

                                        r = unhex_ucs2(c + 2, &y);
                                        if (r < 0)
                                                return r;

                                        c += 6;

                                        if (!utf16_is_trailing_surrogate(y))
                                                return -EINVAL;

                                        n += utf8_encode_unichar(s + n, utf16_surrogate_pair_to_unichar(x, y));
                                }

                                continue;
                        } else
                                return -EINVAL;

                        if (!GREEDY_REALLOC(s, allocated, n + 2))
                                return -ENOMEM;

                        s[n++] = ch;
                        c ++;
                        continue;
                }

                len = utf8_encoded_valid_unichar(c);
                if (len < 0)
                        return len;

                if (!GREEDY_REALLOC(s, allocated, n + len + 1))
                        return -ENOMEM;

                memcpy(s + n, c, len);
                n += len;
                c += len;
        }
}

static int json_parse_number(const char **p, union json_value *ret) {
        bool negative = false, exponent_negative = false, is_double = false;
        double x = 0.0, y = 0.0, exponent = 0.0, shift = 1.0;
        intmax_t i = 0;
        const char *c;

        assert(p);
        assert(*p);
        assert(ret);

        c = *p;

        if (*c == '-') {
                negative = true;
                c++;
        }

        if (*c == '0')
                c++;
        else {
                if (!strchr("123456789", *c) || *c == 0)
                        return -EINVAL;

                do {
                        if (!is_double) {
                                int64_t t;

                                t = 10 * i + (*c - '0');
                                if (t < i) /* overflow */
                                        is_double = false;
                                else
                                        i = t;
                        }

                        x = 10.0 * x + (*c - '0');
                        c++;
                } while (strchr("0123456789", *c) && *c != 0);
        }

        if (*c == '.') {
                is_double = true;
                c++;

                if (!strchr("0123456789", *c) || *c == 0)
                        return -EINVAL;

                do {
                        y = 10.0 * y + (*c - '0');
                        shift = 10.0 * shift;
                        c++;
                } while (strchr("0123456789", *c) && *c != 0);
        }

        if (*c == 'e' || *c == 'E') {
                is_double = true;
                c++;

                if (*c == '-') {
                        exponent_negative = true;
                        c++;
                } else if (*c == '+')
                        c++;

                if (!strchr("0123456789", *c) || *c == 0)
                        return -EINVAL;

                do {
                        exponent = 10.0 * exponent + (*c - '0');
                        c++;
                } while (strchr("0123456789", *c) && *c != 0);
        }

        *p = c;

        if (is_double) {
                ret->real = ((negative ? -1.0 : 1.0) * (x + (y / shift))) * exp10((exponent_negative ? -1.0 : 1.0) * exponent);
                return JSON_REAL;
        } else {
                ret->integer = negative ? -i : i;
                return JSON_INTEGER;
        }
}

int json_tokenize(
                const char **p,
                char **ret_string,
                union json_value *ret_value,
                void **state,
                unsigned *line) {

        const char *c;
        int t;
        int r;

        enum {
                STATE_NULL,
                STATE_VALUE,
                STATE_VALUE_POST,
        };

        assert(p);
        assert(*p);
        assert(ret_string);
        assert(ret_value);
        assert(state);

        t = PTR_TO_INT(*state);
        c = *p;

        if (t == STATE_NULL) {
                if (line)
                        *line = 1;
                t = STATE_VALUE;
        }

        for (;;) {
                const char *b;

                b = c + strspn(c, WHITESPACE);
                if (*b == 0)
                        return JSON_END;

                inc_lines(line, c, b - c);
                c = b;

                switch (t) {

                case STATE_VALUE:

                        if (*c == '{') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE);
                                return JSON_OBJECT_OPEN;

                        } else if (*c == '}') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_OBJECT_CLOSE;

                        } else if (*c == '[') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE);
                                return JSON_ARRAY_OPEN;

                        } else if (*c == ']') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_ARRAY_CLOSE;

                        } else if (*c == '"') {
                                r = json_parse_string(&c, ret_string);
                                if (r < 0)
                                        return r;

                                *ret_value = JSON_VALUE_NULL;
                                *p = c;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return r;

                        } else if (strchr("-0123456789", *c)) {
                                r = json_parse_number(&c, ret_value);
                                if (r < 0)
                                        return r;

                                *ret_string = NULL;
                                *p = c;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return r;

                        } else if (startswith(c, "true")) {
                                *ret_string = NULL;
                                ret_value->boolean = true;
                                *p = c + 4;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_BOOLEAN;

                        } else if (startswith(c, "false")) {
                                *ret_string = NULL;
                                ret_value->boolean = false;
                                *p = c + 5;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_BOOLEAN;

                        } else if (startswith(c, "null")) {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 4;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_NULL;

                        } else
                                return -EINVAL;

                case STATE_VALUE_POST:

                        if (*c == ':') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE);
                                return JSON_COLON;
                        } else if (*c == ',') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE);
                                return JSON_COMMA;
                        } else if (*c == '}') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_OBJECT_CLOSE;
                        } else if (*c == ']') {
                                *ret_string = NULL;
                                *ret_value = JSON_VALUE_NULL;
                                *p = c + 1;
                                *state = INT_TO_PTR(STATE_VALUE_POST);
                                return JSON_ARRAY_CLOSE;
                        } else
                                return -EINVAL;
                }

        }
}

static bool json_is_value(JsonVariant *var) {
        assert(var);

        return var->type != JSON_VARIANT_CONTROL;
}

static int json_scoped_parse(JsonVariant **tokens, size_t *i, size_t n, JsonVariant *scope) {
        bool arr = scope->type == JSON_VARIANT_ARRAY;
        int terminator = arr ? JSON_ARRAY_CLOSE : JSON_OBJECT_CLOSE;
        size_t allocated = 0, size = 0;
        JsonVariant *key = NULL, *value = NULL, *var = NULL, *items = NULL;
        enum {
                STATE_KEY,
                STATE_COLON,
                STATE_COMMA,
                STATE_VALUE
        } state = arr ? STATE_VALUE : STATE_KEY;

        assert(tokens);
        assert(i);
        assert(scope);

        while((var = *i < n ? tokens[(*i)++] : NULL) != NULL) {
                bool stopper;
                int r;

                stopper = !json_is_value(var) && var->value.integer == terminator;

                if (stopper) {
                        if (state != STATE_COMMA && size > 0)
                                goto error;

                        goto out;
                }

                if (state == STATE_KEY) {
                        if (var->type != JSON_VARIANT_STRING)
                                goto error;
                        else {
                                key = var;
                                state = STATE_COLON;
                        }
                }
                else if (state == STATE_COLON) {
                        if (key == NULL)
                                goto error;

                        if (json_is_value(var))
                                goto error;

                        if (var->value.integer != JSON_COLON)
                                goto error;

                        state = STATE_VALUE;
                }
                else if (state == STATE_VALUE) {
                        _cleanup_json_variant_unref_ JsonVariant *v = NULL;
                        size_t toadd = arr ? 1 : 2;

                        if (!json_is_value(var)) {
                                int type = (var->value.integer == JSON_ARRAY_OPEN) ? JSON_VARIANT_ARRAY : JSON_VARIANT_OBJECT;

                                r = json_variant_new(&v, type);
                                if (r < 0)
                                        goto error;

                                r = json_scoped_parse(tokens, i, n, v);
                                if (r < 0)
                                        goto error;

                                value = v;
                        }
                        else
                                value = var;

                        if(!GREEDY_REALLOC(items, allocated, size + toadd))
                                goto error;

                        if (arr) {
                                r = json_variant_deep_copy(&items[size], value);
                                if (r < 0)
                                        goto error;
                        } else {
                                r = json_variant_deep_copy(&items[size], key);
                                if (r < 0)
                                        goto error;

                                r = json_variant_deep_copy(&items[size+1], value);
                                if (r < 0)
                                        goto error;
                        }

                        size += toadd;
                        state = STATE_COMMA;
                }
                else if (state == STATE_COMMA) {
                        if (json_is_value(var))
                                goto error;

                        if (var->value.integer != JSON_COMMA)
                                goto error;

                        key = NULL;
                        value = NULL;

                        state = arr ? STATE_VALUE : STATE_KEY;
                }
        }

error:
        json_raw_unref(items, size);
        return -EBADMSG;

out:
        scope->size = size;
        scope->objects = items;

        return scope->type;
}

static int json_parse_tokens(JsonVariant **tokens, size_t ntokens, JsonVariant **rv) {
        size_t it = 0;
        int r;
        JsonVariant *e;
        _cleanup_json_variant_unref_ JsonVariant *p = NULL;

        assert(tokens);
        assert(ntokens);

        e = tokens[it++];
        r = json_variant_new(&p, JSON_VARIANT_OBJECT);
        if (r < 0)
                return r;

        if (e->type != JSON_VARIANT_CONTROL && e->value.integer != JSON_OBJECT_OPEN)
                return -EBADMSG;

        r = json_scoped_parse(tokens, &it, ntokens, p);
        if (r < 0)
                return r;

        *rv = p;
        p = NULL;

        return 0;
}

static int json_tokens(const char *string, size_t size, JsonVariant ***tokens, size_t *n) {
        _cleanup_free_ char *buf = NULL;
        _cleanup_(json_variant_array_unrefp) JsonVariant **items = NULL;
        union json_value v = {};
        void *json_state = NULL;
        const char *p;
        int t, r;
        size_t allocated = 0, s = 0;

        assert(string);
        assert(n);

        if (size <= 0)
                return -EBADMSG;

        buf = strndup(string, size);
        if (!buf)
                return -ENOMEM;

        p = buf;
        for (;;) {
                _cleanup_json_variant_unref_ JsonVariant *var = NULL;
                _cleanup_free_ char *rstr = NULL;

                t = json_tokenize(&p, &rstr, &v, &json_state, NULL);

                if (t < 0)
                        return t;
                else if (t == JSON_END)
                        break;

                if (t <= JSON_ARRAY_CLOSE) {
                        r = json_variant_new(&var, JSON_VARIANT_CONTROL);
                        if (r < 0)
                                return r;
                        var->value.integer = t;
                } else {
                        switch (t) {
                        case JSON_STRING:
                                r = json_variant_new(&var, JSON_VARIANT_STRING);
                                if (r < 0)
                                        return r;
                                var->size = strlen(rstr);
                                var->string = strdup(rstr);
                                if (!var->string) {
                                        return -ENOMEM;
                                }
                                break;
                        case JSON_INTEGER:
                                r = json_variant_new(&var, JSON_VARIANT_INTEGER);
                                if (r < 0)
                                        return r;
                                var->value = v;
                                break;
                        case JSON_REAL:
                                r = json_variant_new(&var, JSON_VARIANT_REAL);
                                if (r < 0)
                                        return r;
                                var->value = v;
                                break;
                        case JSON_BOOLEAN:
                                r = json_variant_new(&var, JSON_VARIANT_BOOLEAN);
                                if (r < 0)
                                        return r;
                                var->value = v;
                                break;
                        case JSON_NULL:
                                r = json_variant_new(&var, JSON_VARIANT_NULL);
                                if (r < 0)
                                        return r;
                                break;
                        }
                }

                if (!GREEDY_REALLOC(items, allocated, s+2))
                        return -ENOMEM;

                items[s++] = var;
                items[s] = NULL;
                var = NULL;
        }

        *n = s;
        *tokens = items;
        items = NULL;

        return 0;
}

int json_parse(const char *string, JsonVariant **rv) {
        _cleanup_(json_variant_array_unrefp) JsonVariant **s = NULL;
        JsonVariant *v = NULL;
        size_t n = 0;
        int r;

        assert(string);
        assert(rv);

        r = json_tokens(string, strlen(string), &s, &n);
        if (r < 0)
                return r;

        r = json_parse_tokens(s, n, &v);
        if (r < 0)
                return r;

        *rv = v;
        return 0;
}