/*
 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
 *
 * This software is dual-licensed to you under the MIT License (MIT) and
 * the Universal Permissive License (UPL). See the LICENSE file in the root
 * directory for license terms. You may choose either license, or both.
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <ctype.h>
#include <assert.h>

#include "json_helper.h"
#include "device_model/device_model_private.h"
#include "external/json_parser/jsmn.h"
#include "iotcs.h"
#include "util/util.h"

#include "log/log.h"
#define IOTCSP_MODULE_LOG_CHANNEL LOG_CHANNEL_JSON
#include "log/log_template.h"

static jsmn_parser json_parser;
/* We expect no more than MAX_JSON_PARSER_TOKENS tokens */
static jsmntok_t json_token[IOTCS_JSON_TOKEN_NUM];

static jsmntype_t get_token_type(json_tokens_t* tok) {
    return json_token[tok->index].type;
}

static jsmntype_t get_token_type_by_pos(int pos) {
    return json_token[pos].type;
}

static int is_object(json_tokens_t* tok) {
    return get_token_type(tok) == JSMN_OBJECT;
}

static int is_array(json_tokens_t* tok) {
    return get_token_type(tok) == JSMN_ARRAY;
}

static int is_string(json_tokens_t* tok) {
    return get_token_type(tok) == JSMN_STRING;
}

static int is_string_by_pos(int pos) {
    return get_token_type_by_pos(pos) == JSMN_STRING;
}

/* methods for parsing primitives */
static iotcs_result json_prim_get_int(const char* str, int str_len, int64_t* out_value) {
    int64_t value = 0;
    int is_negative = 0;
    int i = 0;

    if (str[i] == '-') {
        is_negative = 1;
        i++;
    } else if (str[i] == '+') {
        i++;
    }

    // TODO: check long int overflow
    for (; i < str_len; i++) {
        if (isdigit((int) str[i])) {
            value = value * 10 + (str[i] - '0');
            continue;
        }
        if (isspace((int) str[i]) || str[i] == '\0') {
            break;
        }
        // find something unexpected
        return IOTCS_RESULT_FAIL;
    }

    if (util_is_valid_number((float) value)) {
        *out_value = is_negative ? -value : value;
    } else {
        return IOTCS_RESULT_FAIL;
    }
    return IOTCS_RESULT_OK;
}

static iotcs_result json_prim_get_flt(const char* str, int str_len, float* out_value) {
    float value = 0;
    float integer_part = 0;
    float fraction_part = 0;
    int fraction_divisor = 1;
    int is_negative = 0;
    int is_fraction = 0;
    int i = 0;

    if (str[i] == '-') {
        is_negative = 1;
        i++;
    } else if (str[i] == '+') {
        i++;
    }

    // TODO: check overflow
    for (; i < str_len; i++) {
        if (isdigit((int) str[i])) {
            if (is_fraction) {
                fraction_part = fraction_part * 10 + (str[i] - '0');
                fraction_divisor *= 10;
            } else {
                integer_part = integer_part * 10 + (str[i] - '0');
            }
            continue;
        }
        if (str[i] == '.') {
            if (!is_fraction) {
                is_fraction = 1;
                continue;
            } else {
                return IOTCS_RESULT_FAIL;
            }
        }
        if (isspace((int) str[i]) || str[i] == '\0') {
            break;
        }
        // find something unexpected
        return IOTCS_RESULT_FAIL;
    }

    value = (integer_part + fraction_part / fraction_divisor);
    if (util_is_valid_number((float) value)) {
        *out_value = is_negative ? -value : value;
    } else {
        return IOTCS_RESULT_FAIL;
    }
    return IOTCS_RESULT_OK;
}

static iotcs_bool is_equal_field_key(json_tokens_t* tok, const char *name) {
    jsmntok_t* token;
    token = &json_token[tok->index];
    if (token->type == JSMN_STRING && (int) strlen(name) == token->end - token->start &&
            strncmp(tok->json + token->start, name, token->end - token->start) == 0) {
        return IOTCS_TRUE;
    }
    return IOTCS_FALSE;
}

iotcs_result json_parse(const char *json, size_t json_len, json_tokens_t* out_tok) {
    int parser_rv = 0;

    // init or reset parser
    jsmn_init(&json_parser);
    // reset zero token of json_token
    json_token[0].type = JSMN_UNDEFINED;
    json_token[0].start = 0;
    json_token[0].end = 0;
    json_token[0].size = 0;

    parser_rv = jsmn_parse(&json_parser, json, json_len, json_token, IOTCS_JSON_TOKEN_NUM);
    LOG_DBG("JSON parse rv=%d", parser_rv);
    if (parser_rv < 0) {
        LOG_ERR("Failed to parse JSON %s", json);
        return IOTCS_RESULT_FAIL;
    }

    out_tok->count = parser_rv;
    out_tok->index = 0;
    out_tok->json = &json[0];

    return IOTCS_RESULT_OK;
}

static int get_value_pos_by_key(json_tokens_t* tok, const char* key) {
    json_object_iterator_t iter;
    json_tokens_t field_tok;
    // iter point on object
    if (json_get_object_iterator(tok, &iter) != IOTCS_RESULT_OK) {
        return -1;
    }

    while (json_object_iterator_has_next(&iter)) {
        // get point on key
        if (json_object_iterator_next(&iter, &field_tok) != IOTCS_RESULT_OK) {
            return IOTCS_RESULT_FAIL;
        }
        // check key by name
        if (is_equal_field_key(&field_tok, key)) {
            assert(field_tok.index + 1 < tok->count);
            return field_tok.index + 1; /* move from 'key' to 'value' - must exist */
        }
    }
    return -1;
}

iotcs_result json_get_object_by_key(json_tokens_t* tok, const char* key, json_tokens_t* out_tok) {
    int pos;
    if ((pos = get_value_pos_by_key(tok, key)) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }
    *out_tok = *tok;
    out_tok->index = pos;
    if (is_object(out_tok)) {
        return IOTCS_RESULT_OK;
    } else {
        return IOTCS_RESULT_FAIL;
    }
}

iotcs_result json_get_array_by_key(json_tokens_t* tok, const char* key, json_tokens_t* out_tok) {
    int pos;
    if ((pos = get_value_pos_by_key(tok, key)) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }
    *out_tok = *tok;
    out_tok->index = pos;
    if (is_array(out_tok)) {
        return IOTCS_RESULT_OK;
    } else {
        return IOTCS_RESULT_FAIL;
    }
}

iotcs_result json_get_string_value_by_key(json_tokens_t* tok,
        const char* key, char** value, int* value_length) {
    int pos;
    if ((pos = get_value_pos_by_key(tok, key)) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }

    if (!is_string_by_pos(pos)) {
        return IOTCS_RESULT_FAIL;
    }

    *value = (char*) (tok->json + json_token[pos].start);
    *value_length = json_token[pos].end - json_token[pos].start;
    return IOTCS_RESULT_OK;
}

iotcs_result json_get_int_value_by_key(json_tokens_t* tok,
        const char* key, int64_t* value) {
    int pos;
    if ((pos = get_value_pos_by_key(tok, key)) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }

    if (json_token[pos].type == JSMN_PRIMITIVE) {
        // JSMN_PRIMITIVE may be a number, a boolean (true, false) or null

        return json_prim_get_int(tok->json + json_token[pos].start,
                json_token[pos].end - json_token[pos].start, value);
    }
    return IOTCS_RESULT_FAIL;
}

iotcs_result json_get_float_value_by_key(json_tokens_t* tok, const char* key, float* value) {
    int pos;
    if ((pos = get_value_pos_by_key(tok, key)) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }

    if (json_token[pos].type == JSMN_PRIMITIVE) {
        // JSMN_PRIMITIVE may be a number, a boolean (true, false) or null

        return json_prim_get_flt(tok->json + json_token[pos].start,
                json_token[pos].end - json_token[pos].start, value);
    }
    return IOTCS_RESULT_FAIL;
}

#if defined IOTCS_STORAGE_SUPPORT
iotcs_result json_get_storage_object_url_by_key(json_tokens_t* tok, const char** url) {
    int pos;

    //TODO: possible we can get some additional information about default storage object here

    if ((pos = get_value_pos_by_key(tok, "name")) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }

    if (json_token[pos].type != JSMN_STRING) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }

    *url = util_safe_strncpy((char*) (tok->json + json_token[pos].start),
            json_token[pos].end - json_token[pos].start);

    return IOTCS_RESULT_OK;
}
#endif

iotcs_result json_get_bool_value_by_key(json_tokens_t* tok,
        const char* key, iotcs_bool* value) {
    int pos;
    if ((pos = get_value_pos_by_key(tok, key)) < 0) {
        return IOTCS_RESULT_INVALID_ARGUMENT;
    }

    if (json_token[pos].type == JSMN_PRIMITIVE) {
        if (strncmp(tok->json + json_token[pos].start, "false",
                json_token[pos].end - json_token[pos].start) == 0) {
            *value = IOTCS_FALSE;
            return IOTCS_RESULT_OK;
        } else if (strncmp(tok->json + json_token[pos].start,
                "true", json_token[pos].end - json_token[pos].start) == 0) {
            *value = IOTCS_TRUE;
            return IOTCS_RESULT_OK;
        }
    }
    return IOTCS_RESULT_FAIL;
}

/* number of child objects for this token: prim - 0, field - 1, array - item num, object - fields num */
int json_child_items_count(json_tokens_t* tok) {
    return json_token[tok->index].size;
}

int json_get_length(json_tokens_t* tok) {
    return json_token[tok->index].end - json_token[tok->index].start;
}

iotcs_result json_get_string_and_move(json_tokens_t* tok, char** out_string, int* out_len) {
    if (!is_string(tok)) {
        LOG_ERRS("Unexpected elem type");
        return IOTCS_RESULT_FAIL;
    }

    *out_len = json_token[tok->index].end - json_token[tok->index].start;
    *out_string = (char*) (tok->json + json_token[tok->index].start);
    tok->index++;

    return IOTCS_RESULT_OK;
}

static int get_next_object_field(json_object_iterator_t* iter) {
    int pos = iter->next;
    int value_end;

    if (!is_string_by_pos(pos)) {
        return -1;
    }

    ++pos; /* move from 'field' key to field 'value' - must exist */
    assert(pos < iter->tok->count);

    /* get value's end index in json string */
    value_end = json_token[pos].end;

    /* loop until we find token outside of 'value':
     * if value is primitive - we skip one token
     * if value is composite - we skip all tokens in object/array */
    do {
        /* next token */
        ++pos;
        /* check if we are outside of token array: no more tokens */
        if (pos >= iter->tok->count) {
            return -1;
        }
        /* check that 'pos' token start index is inside 'value' */
    } while (json_token[pos].start < value_end);

    /* check if found token is outside of the json object we are iterating on */
    if (json_token[iter->tok->index].end < json_token[pos].start) {
        return -1;
    }

    return pos;
}

iotcs_result json_get_object_iterator(json_tokens_t* tok, json_object_iterator_t* iter) {

    if (!is_object(tok)) {
        return IOTCS_RESULT_FAIL;
    }

    if (json_child_items_count(tok) == 0) {
        iter->next = -1;
    } else {
        iter->tok = tok;
        iter->next = tok->index + 1; /* skip object token */
    }
    return IOTCS_RESULT_OK;
}

iotcs_bool json_object_iterator_has_next(json_object_iterator_t* iter) {
    return iter->next != -1 ? IOTCS_TRUE : IOTCS_FALSE;
}

iotcs_result json_object_iterator_next(json_object_iterator_t* iter, json_tokens_t* next_tok) {

    if (!json_object_iterator_has_next(iter)) {
        return IOTCS_RESULT_FAIL;
    }

    *next_tok = *iter->tok;
    next_tok->index = iter->next;

    iter->next = get_next_object_field(iter);

    return IOTCS_RESULT_OK;
}

static int get_next_array_value(json_array_iterator_t* iter) {
    int pos = iter->next;
    int value_end;

    /* get value's end index in json string */
    value_end = json_token[pos].end;

    /* loop until we find token outside of 'value':
     * if value is primitive - we skip one token
     * if value is composite - we skip all tokens in object/array */
    do {
        /* next token */
        ++pos;
        /* check if we are outside of token array: no more tokens */
        if (pos >= iter->tok->count) {
            return -1;
        }
        /* check that 'pos' token start index is inside 'value' */
    } while (json_token[pos].start < value_end);

    /* check if found token is outside of the json object we are iterating on */
    if (json_token[iter->tok->index].end < json_token[pos].start) {
        return -1;
    }

    return pos;
}

iotcs_result json_get_array_iterator(json_tokens_t* tok, json_array_iterator_t* iter) {

    if (!is_array(tok)) {
        return IOTCS_RESULT_FAIL;
    }

    if (json_child_items_count(tok) == 0) {
        iter->next = -1;
    } else {
        iter->tok = tok;
        iter->next = tok->index + 1; /* skip object token */
    }
    return IOTCS_RESULT_OK;
}

iotcs_bool json_array_iterator_has_next(json_array_iterator_t* iter) {
    return iter->next != -1 ? IOTCS_TRUE : IOTCS_FALSE;
}

iotcs_result json_array_iterator_next(json_array_iterator_t* iter, json_tokens_t* next_tok) {

    if (!json_array_iterator_has_next(iter)) {
        return IOTCS_RESULT_FAIL;
    }

    *next_tok = *iter->tok;
    next_tok->index = iter->next;

    iter->next = get_next_array_value(iter);

    return IOTCS_RESULT_OK;
}

iotcs_result json_get_iotcs_value(json_tokens_t* tok, iotcs_typed_value *ptyped_value) {
    float value = 0;
    float fraction_part = 0;
    int integer_part = 0;
    int fraction_divisor = 1;
    int is_negative = 0;
    int is_fraction = 0;
    int i = 0;
    int str_len;
    const char* str;

    if (json_token[tok->index].type == JSMN_PRIMITIVE) {
        // JSMN_PRIMITIVE may be a number, a boolean (true, false) or null
        if (strcmp(tok->json + json_token[tok->index].start, "true") == 0) {
            ptyped_value->type = IOTCS_VALUE_TYPE_BOOLEAN;
            ptyped_value->value.bool_value = IOTCS_TRUE;
        } else if (strcmp(tok->json + json_token[tok->index].start, "false") == 0) {
            ptyped_value->type = IOTCS_VALUE_TYPE_BOOLEAN;
            ptyped_value->value.bool_value = IOTCS_FALSE;
        } else if (strcmp(tok->json + json_token[tok->index].start, "null") == 0) {
            ptyped_value->type = IOTCS_VALUE_TYPE_NONE;
        } else {
            str = (char*) (tok->json + json_token[tok->index].start);
            str_len = json_token[tok->index].end - json_token[tok->index].start;
            if (str[i] == '-') {
                is_negative = 1;
                i++;
            } else if (str[i] == '+') {
                i++;
            }

            for (; i < str_len; i++) {
                if (isdigit((int) str[i])) {
                    if (is_fraction) {
                        fraction_part = fraction_part * 10 + (str[i] - '0');
                        fraction_divisor *= 10;
                    } else {
                        integer_part = integer_part * 10 + (str[i] - '0');
                    }
                    continue;
                }
                if (str[i] == '.') {
                    if (!is_fraction) {
                        is_fraction = 1;
                        continue;
                    } else {
                        return IOTCS_RESULT_FAIL;
                    }
                }
                if (isspace((int) str[i]) || str[i] == '\0') {
                    break;
                }
                // find something unexpected
                return IOTCS_RESULT_FAIL;
            }

            if (fraction_part == 0) {
                ptyped_value->type = IOTCS_VALUE_TYPE_INT;
                ptyped_value->value.int_value = integer_part;
            } else {
                ptyped_value->type = IOTCS_VALUE_TYPE_NUMBER;
                ptyped_value->value.number_value = ((float)integer_part + fraction_part / fraction_divisor);
            }
        }
        return IOTCS_RESULT_OK;
    } else if (json_token[tok->index].type == JSMN_STRING) {
        ptyped_value->type = IOTCS_VALUE_TYPE_STRING;
        ptyped_value->value.string_value = util_safe_strncpy((char*) (tok->json + json_token[tok->index].start), json_token[tok->index].end - json_token[tok->index].start);
        return IOTCS_RESULT_OK;
    }
    return IOTCS_RESULT_FAIL;
}
