/*
 * Copyright (C) 2013 Teluu Inc. (http://www.teluu.com)
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#include <pjsua2/json.hpp>
#include <pjlib-util/errno.h>
#include <pj/file_io.h>
#include "util.hpp"

#define THIS_FILE       "json.cpp"

using namespace pj;
using namespace std;

/* Json node operations */
static bool             jsonNode_hasUnread(const ContainerNode*);
static string           jsonNode_unreadName(const ContainerNode*n)
                                            PJSUA2_THROW(Error);
static float            jsonNode_readNumber(const ContainerNode*,
                                            const string&)
                                              PJSUA2_THROW(Error);
static bool             jsonNode_readBool(const ContainerNode*,
                                          const string&)
                                          PJSUA2_THROW(Error);
static string           jsonNode_readString(const ContainerNode*,
                                            const string&)
                                            PJSUA2_THROW(Error);
static StringVector     jsonNode_readStringVector(const ContainerNode*,
                                                  const string&)
                                                  PJSUA2_THROW(Error);
static ContainerNode    jsonNode_readContainer(const ContainerNode*,
                                               const string &)
                                               PJSUA2_THROW(Error);
static ContainerNode    jsonNode_readArray(const ContainerNode*,
                                           const string &)
                                           PJSUA2_THROW(Error);
static void             jsonNode_writeNumber(ContainerNode*,
                                             const string &name,
                                             float num)
                                             PJSUA2_THROW(Error);
static void             jsonNode_writeBool(ContainerNode*,
                                           const string &name,
                                           bool value)
                                           PJSUA2_THROW(Error);
static void             jsonNode_writeString(ContainerNode*,
                                             const string &name,
                                             const string &value)
                                             PJSUA2_THROW(Error);
static void             jsonNode_writeStringVector(ContainerNode*,
                                                   const string &name,
                                                   const StringVector &value)
                                                   PJSUA2_THROW(Error);
static ContainerNode    jsonNode_writeNewContainer(ContainerNode*,
                                                   const string &name)
                                                   PJSUA2_THROW(Error);
static ContainerNode    jsonNode_writeNewArray(ContainerNode*,
                                               const string &name)
                                               PJSUA2_THROW(Error);

static container_node_op json_op =
{
    &jsonNode_hasUnread,
    &jsonNode_unreadName,
    &jsonNode_readNumber,
    &jsonNode_readBool,
    &jsonNode_readString,
    &jsonNode_readStringVector,
    &jsonNode_readContainer,
    &jsonNode_readArray,
    &jsonNode_writeNumber,
    &jsonNode_writeBool,
    &jsonNode_writeString,
    &jsonNode_writeStringVector,
    &jsonNode_writeNewContainer,
    &jsonNode_writeNewArray
};

///////////////////////////////////////////////////////////////////////////////
JsonDocument::JsonDocument()
: root(NULL)
{
    pj_caching_pool_init(&cp, NULL, 0);
    pool = pj_pool_create(&cp.factory, "jsondoc", 512, 512, NULL);
    if (!pool)
        PJSUA2_RAISE_ERROR(PJ_ENOMEM);
}

JsonDocument::~JsonDocument()
{
    if (pool)
        pj_pool_release(pool);
    pj_caching_pool_destroy(&cp);
}

void JsonDocument::initRoot() const
{
    rootNode.op = &json_op;
    rootNode.data.doc = (void*)this;
    rootNode.data.data1 = (void*)root;
    rootNode.data.data2 = root->value.children.next;
}

void JsonDocument::loadFile(const string &filename) PJSUA2_THROW(Error)
{
    if (root)
        PJSUA2_RAISE_ERROR3(PJ_EINVALIDOP, "JsonDocument.loadString()",
                            "Document already initialized");

    if (!pj_file_exists(filename.c_str()))
        PJSUA2_RAISE_ERROR(PJ_ENOTFOUND);

    pj_ssize_t size = (pj_ssize_t)pj_file_size(filename.c_str());
    if (size <= 0)
        PJSUA2_RAISE_ERROR(PJ_ETOOSMALL);
    pj_status_t status;

    char *buffer = (char*)pj_pool_alloc(pool, size+1);
    pj_oshandle_t fd = 0;
    unsigned parse_size;
    char err_msg[120];
    pj_json_err_info err_info;

    err_msg[0] = '\0';

    status = pj_file_open(pool, filename.c_str(), PJ_O_RDONLY, &fd);
    if (status != PJ_SUCCESS)
        goto on_error;

    status = pj_file_read(fd, buffer, &size);
    if (status != PJ_SUCCESS)
        goto on_error;

    pj_file_close(fd);
    fd = NULL;

    if (size <= 0) {
        status = PJ_EEOF;
        goto on_error;
    }

    parse_size = (unsigned)size;
    root = pj_json_parse(pool, buffer, &parse_size, &err_info);
    if (root == NULL) {
        pj_ansi_snprintf(err_msg, sizeof(err_msg),
                         "JSON parsing failed: syntax error in file '%s' at "
                         "line %d column %d",
                         filename.c_str(), err_info.line, err_info.col);
        PJ_LOG(1,(THIS_FILE, "%s", err_msg));
        status = PJLIB_UTIL_EINJSON;
        goto on_error;
    }

    initRoot();
    return;

on_error:
    if (fd)
        pj_file_close(fd);
    if (err_msg[0])
        PJSUA2_RAISE_ERROR3(status, "loadFile()", err_msg);
    else
        PJSUA2_RAISE_ERROR(status);
}

void JsonDocument::loadString(const string &input) PJSUA2_THROW(Error)
{
    if (root)
        PJSUA2_RAISE_ERROR3(PJ_EINVALIDOP, "JsonDocument.loadString()",
                            "Document already initialized");

    unsigned size = (unsigned)input.size();
    char *buffer = (char*)pj_pool_alloc(pool, size+1);
    unsigned parse_size = (unsigned)size;
    pj_json_err_info err_info;

    pj_memcpy(buffer, input.c_str(), size);

    root = pj_json_parse(pool, buffer, &parse_size, &err_info);
    if (root == NULL) {
        char err_msg[80];

        pj_ansi_snprintf(err_msg, sizeof(err_msg),
                         "JSON parsing failed at line %d column %d",
                         err_info.line, err_info.col);
        PJ_LOG(1,(THIS_FILE, "%s", err_msg));
        PJSUA2_RAISE_ERROR3(PJLIB_UTIL_EINJSON, "loadString()", err_msg);
    }
    initRoot();
}

struct save_file_data
{
    pj_oshandle_t fd;
};

static pj_status_t json_file_writer(const char *s,
                                    unsigned size,
                                    void *user_data)
{
    save_file_data *sd = (save_file_data*)user_data;
    pj_ssize_t ssize = (pj_ssize_t)size;
    return pj_file_write(sd->fd, s, &ssize);
}

void JsonDocument::saveFile(const string &filename) PJSUA2_THROW(Error)
{
    struct save_file_data sd;
    pj_status_t status;

    /* Make sure root container has been created */
    getRootContainer();

    status = pj_file_open(pool, filename.c_str(), PJ_O_WRONLY, &sd.fd);
    if (status != PJ_SUCCESS)
        PJSUA2_RAISE_ERROR(status);

    status = pj_json_writef(root, &json_file_writer, &sd.fd);
    pj_file_close(sd.fd);

    if (status != PJ_SUCCESS)
        PJSUA2_RAISE_ERROR(status);
}

struct save_string_data
{
    string      output;
};

static pj_status_t json_string_writer(const char *s,
                                      unsigned size,
                                      void *user_data)
{
    save_string_data *sd = (save_string_data*)user_data;
    sd->output.append(s, size);
    return PJ_SUCCESS;
}

string JsonDocument::saveString() PJSUA2_THROW(Error)
{
    struct save_string_data sd;
    pj_status_t status;

    /* Make sure root container has been created */
    getRootContainer();

    status = pj_json_writef(root, &json_string_writer, &sd);
    if (status != PJ_SUCCESS)
        PJSUA2_RAISE_ERROR(status);

    return sd.output;
}

ContainerNode & JsonDocument::getRootContainer() const
{
    if (!root) {
        root = allocElement();
        pj_json_elem_obj(root, NULL);
        initRoot();
    }

    return rootNode;
}

pj_json_elem* JsonDocument::allocElement() const
{
    return (pj_json_elem*)pj_pool_alloc(pool, sizeof(pj_json_elem));
}

pj_pool_t *JsonDocument::getPool()
{
    return pool;
}

///////////////////////////////////////////////////////////////////////////////
struct json_node_data
{
    JsonDocument        *doc;
    pj_json_elem        *jnode;
    pj_json_elem        *childPtr;
};

static bool jsonNode_hasUnread(const ContainerNode *node)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    return jdat->childPtr != (pj_json_elem*)&jdat->jnode->value.children;
}

static void json_verify(struct json_node_data *jdat,
                        const char *op,
                        const string &name,
                        pj_json_val_type type)
{
    if (jdat->childPtr == (pj_json_elem*)&jdat->jnode->value.children)
        PJSUA2_RAISE_ERROR3(PJ_EEOF, op, "No unread element");

    /* If name is specified, then check if the names match, except
     * when the node name itself is empty and the parent node is
     * an array, then ignore the checking (JSON doesn't allow array
     * elements to have name).
     */
    if (jdat->jnode->type != PJ_JSON_VAL_ARRAY &&
        name.size() && jdat->childPtr->name.slen &&
        name.compare(0, name.size(), jdat->childPtr->name.ptr,
                                     jdat->childPtr->name.slen))
    {
        char err_msg[80];
        pj_ansi_snprintf(err_msg, sizeof(err_msg),
                         "Name mismatch: expecting '%s' got '%.*s'",
                         name.c_str(), (int)jdat->childPtr->name.slen,
                         jdat->childPtr->name.ptr);
        PJSUA2_RAISE_ERROR3(PJLIB_UTIL_EINJSON, op, err_msg);
    }

    if (type != PJ_JSON_VAL_NULL && jdat->childPtr->type != type) {
        char err_msg[80];
        pj_ansi_snprintf(err_msg, sizeof(err_msg),
                         "Type mismatch: expecting %d got %d",
                         type, jdat->childPtr->type);
        PJSUA2_RAISE_ERROR3(PJLIB_UTIL_EINJSON, op, err_msg);
    }
}

static string jsonNode_unreadName(const ContainerNode *node)
                                  PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "unreadName()", "", PJ_JSON_VAL_NULL);
    return pj2Str(jdat->childPtr->name);
}

static float jsonNode_readNumber(const ContainerNode *node,
                                 const string &name)
                                 PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "readNumber()", name, PJ_JSON_VAL_NUMBER);
    jdat->childPtr = jdat->childPtr->next;
    return jdat->childPtr->prev->value.num;
}

static bool jsonNode_readBool(const ContainerNode *node,
                              const string &name)
                              PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "readBool()", name, PJ_JSON_VAL_BOOL);
    jdat->childPtr = jdat->childPtr->next;
    return PJ2BOOL(jdat->childPtr->prev->value.is_true);
}

static string jsonNode_readString(const ContainerNode *node,
                                  const string &name)
                                  PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "readString()", name, PJ_JSON_VAL_STRING);
    jdat->childPtr = jdat->childPtr->next;
    return pj2Str(jdat->childPtr->prev->value.str);
}

static StringVector jsonNode_readStringVector(const ContainerNode *node,
                                              const string &name)
                                              PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "readStringVector()", name, PJ_JSON_VAL_ARRAY);

    StringVector result;
    pj_json_elem *child = jdat->childPtr->value.children.next;
    while (child != (pj_json_elem*)&jdat->childPtr->value.children) {
        if (child->type != PJ_JSON_VAL_STRING) {
            char err_msg[80];
            pj_ansi_snprintf(err_msg, sizeof(err_msg),
                             "Elements not string but type %d",
                             child->type);
            PJSUA2_RAISE_ERROR3(PJLIB_UTIL_EINJSON, "readStringVector()",
                                err_msg);
        }
        result.push_back(pj2Str(child->value.str));
        child = child->next;
    }

    jdat->childPtr = jdat->childPtr->next;
    return result;
}

static ContainerNode jsonNode_readContainer(const ContainerNode *node,
                                            const string &name)
                                            PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "readContainer()", name, PJ_JSON_VAL_OBJ);

    ContainerNode json_node;

    json_node.op = &json_op;
    json_node.data.doc = (void*)jdat->doc;
    json_node.data.data1 = (void*)jdat->childPtr;
    json_node.data.data2 = (void*)jdat->childPtr->value.children.next;

    jdat->childPtr = jdat->childPtr->next;
    return json_node;
}

static ContainerNode jsonNode_readArray(const ContainerNode *node,
                                        const string &name)
                                        PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    json_verify(jdat, "readArray()", name, PJ_JSON_VAL_ARRAY);

    ContainerNode json_node;

    json_node.op = &json_op;
    json_node.data.doc = (void*)jdat->doc;
    json_node.data.data1 = (void*)jdat->childPtr;
    json_node.data.data2 = (void*)jdat->childPtr->value.children.next;

    jdat->childPtr = jdat->childPtr->next;
    return json_node;
}

static pj_str_t alloc_name(JsonDocument *doc, const string &name)
{
    pj_str_t new_name;
    pj_strdup2(doc->getPool(), &new_name, name.c_str());
    return new_name;
}

static void jsonNode_writeNumber(ContainerNode *node,
                                 const string &name,
                                 float num)
                                 PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    pj_json_elem *el = jdat->doc->allocElement();
    pj_str_t nm = alloc_name(jdat->doc, name);
    pj_json_elem_number(el, &nm, num);
    pj_json_elem_add(jdat->jnode, el);
}

static void jsonNode_writeBool(ContainerNode *node,
                               const string &name,
                               bool value)
                               PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    pj_json_elem *el = jdat->doc->allocElement();
    pj_str_t nm = alloc_name(jdat->doc, name);
    pj_json_elem_bool(el, &nm, value);
    pj_json_elem_add(jdat->jnode, el);
}

static void jsonNode_writeString(ContainerNode *node,
                                 const string &name,
                                 const string &value)
                                 PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    pj_json_elem *el = jdat->doc->allocElement();
    pj_str_t nm = alloc_name(jdat->doc, name);
    pj_str_t new_val;
    pj_strdup2(jdat->doc->getPool(), &new_val, value.c_str());
    pj_json_elem_string(el, &nm, &new_val);

    pj_json_elem_add(jdat->jnode, el);
}

static void jsonNode_writeStringVector(ContainerNode *node,
                                       const string &name,
                                       const StringVector &value)
                                       PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    pj_json_elem *el = jdat->doc->allocElement();
    pj_str_t nm = alloc_name(jdat->doc, name);

    pj_json_elem_array(el, &nm);
    for (unsigned i=0; i<value.size(); ++i) {
        pj_str_t new_val;

        pj_strdup2(jdat->doc->getPool(), &new_val, value[i].c_str());
        pj_json_elem *child = jdat->doc->allocElement();
        pj_json_elem_string(child, NULL, &new_val);
        pj_json_elem_add(el, child);
    }

    pj_json_elem_add(jdat->jnode, el);
}

static ContainerNode jsonNode_writeNewContainer(ContainerNode *node,
                                                const string &name)
                                                PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    pj_json_elem *el = jdat->doc->allocElement();
    pj_str_t nm = alloc_name(jdat->doc, name);

    pj_json_elem_obj(el, &nm);
    pj_json_elem_add(jdat->jnode, el);

    ContainerNode json_node;

    json_node.op = &json_op;
    json_node.data.doc = (void*)jdat->doc;
    json_node.data.data1 = (void*)el;
    json_node.data.data2 = (void*)el->value.children.next;

    return json_node;
}

static ContainerNode jsonNode_writeNewArray(ContainerNode *node,
                                            const string &name)
                                            PJSUA2_THROW(Error)
{
    json_node_data *jdat = (json_node_data*)&node->data;
    pj_json_elem *el = jdat->doc->allocElement();
    pj_str_t nm = alloc_name(jdat->doc, name);

    pj_json_elem_array(el, &nm);
    pj_json_elem_add(jdat->jnode, el);

    ContainerNode json_node;

    json_node.op = &json_op;
    json_node.data.doc = (void*)jdat->doc;
    json_node.data.data1 = (void*)el;
    json_node.data.data2 = (void*)el->value.children.next;

    return json_node;
}
