/*
 * Crappy JSON dumper
 *
 * Copyright (c) 2018-2022 Ramiro Polla
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include <stdarg.h>

#include "json.h"

//---------------------------------------------------------------------
#define CHUNK_SIZE 4 * 1024 * 1024
typedef struct sbuf
{
    FILE *fp;
    char *data;
    size_t offset;
} sbuf;

static inline void sbuf_init(sbuf *ctx, FILE *fp)
{
    ctx->fp = fp;
    ctx->data = malloc(CHUNK_SIZE);
    ctx->offset = 0;
}

static inline void sbuf_flush(sbuf *ctx)
{
    if ( ctx->offset != 0 )
    {
        fwrite(ctx->data, ctx->offset, 1, ctx->fp);
        ctx->offset = 0;
    }
}

static inline void sbuf_fputc(sbuf *ctx, char c)
{
    if ( ctx->offset == CHUNK_SIZE )
        sbuf_flush(ctx);
    ctx->data[ctx->offset++] = c;
}

static inline void sbuf_fputs(sbuf *ctx, const char *str)
{
    while ( *str != '\0' )
        sbuf_fputc(ctx, *str++);
}

static inline void sbuf_fprintf(sbuf *ctx, const char *format, ...)
{
    va_list args;
    va_start(args, format);
    sbuf_flush(ctx);
    vfprintf(ctx->fp, format, args);
    va_end(args);
}

static inline void sbuf_spaces(sbuf *ctx, size_t num)
{
    if ( ctx->offset + num >= CHUNK_SIZE )
        sbuf_flush(ctx);
    while ( num-- )
        ctx->data[ctx->offset++] = ' ';
}

static inline void sbuf_free(sbuf *ctx)
{
    sbuf_flush(ctx);
    free(ctx->data);
}

//---------------------------------------------------------------------
static inline void output_lf(sbuf *ctx, json_t *jso, int level)
{
    if ( (jso->flags & JSON_PFLAGS_NO_LF) == 0 )
    {
        sbuf_fputc(ctx, '\n');
        sbuf_spaces(ctx, level * 2);
    }
    else if ( (jso->flags & JSON_PFLAGS_NO_SPACE) == 0 )
    {
        sbuf_fputc(ctx, ' ');
    }
}

static inline void output_lf_array(sbuf *ctx, json_t *jso, int level, int i)
{
    if ( (jso->flags & JSON_PFLAGS_SPLIT8) != 0 )
    {
        if ( (i & 7) == 0 )
            output_lf(ctx, jso, level);
        else if ( i != 0 )
            sbuf_fputc(ctx, ' ');
    }
    else
    {
        output_lf(ctx, jso, level);
    }
}

static char output_nibble(uint8_t val)
{
    if ( val > 9 )
        return 'A' + val - 10;
    return '0' + val;
}

static void output_u8(sbuf *ctx, uint8_t val)
{
    sbuf_fputc(ctx, '0');
    sbuf_fputc(ctx, 'x');
    sbuf_fputc(ctx, output_nibble(val >> 4));
    sbuf_fputc(ctx, output_nibble(val & 0x0F));
}

static void output_u16(sbuf *ctx, uint16_t val)
{
    sbuf_fputc(ctx, '0');
    sbuf_fputc(ctx, 'x');
    sbuf_fputc(ctx, output_nibble((val >> 12) & 0x0F));
    sbuf_fputc(ctx, output_nibble((val >>  8) & 0x0F));
    sbuf_fputc(ctx, output_nibble((val >>  4) & 0x0F));
    sbuf_fputc(ctx, output_nibble((val >>  0) & 0x0F));
}

static void output_num_32(sbuf *ctx, int32_t val)
{
    int is_neg = (val < 0);
    uint32_t abs_val = (uint32_t) (is_neg ? -val : val);
    if ( abs_val < 10 )
    {
        uint8_t u8 = (uint8_t) abs_val;
        if ( is_neg )
            sbuf_fputc(ctx, '-');
        sbuf_fputc(ctx, '0' + u8);
    }
    else if ( abs_val < 100 )
    {
        uint8_t u8 = (uint8_t) abs_val;
        if ( is_neg )
            sbuf_fputc(ctx, '-');
        sbuf_fputc(ctx, '0' + u8 / 10);
        sbuf_fputc(ctx, '0' + u8 % 10);
    }
    else
    {
        sbuf_fprintf(ctx, "%" PRId32, val);
    }
}

static void output_num_64(sbuf *ctx, int64_t val)
{
    if ( val == JSON_NULL )
    {
        sbuf_fputs(ctx, "null");
    }
    else
    {
        int is_neg = (val < 0);
        uint64_t abs_val = (uint64_t) (is_neg ? -val : val);
        if ( abs_val < 10 )
        {
            uint8_t u8 = (uint8_t) abs_val;
            if ( is_neg )
                sbuf_fputc(ctx, '-');
            sbuf_fputc(ctx, '0' + u8);
        }
        else if ( abs_val < 100 )
        {
            uint8_t u8 = (uint8_t) abs_val;
            if ( is_neg )
                sbuf_fputc(ctx, '-');
            sbuf_fputc(ctx, '0' + u8 / 10);
            sbuf_fputc(ctx, '0' + u8 % 10);
        }
        else
        {
            sbuf_fprintf(ctx, "%" PRId64, val);
        }
    }
}

static void output_string(sbuf *ctx, const char *str)
{
    sbuf_fputc(ctx, '"');
    sbuf_fputs(ctx, str);
    sbuf_fputc(ctx, '"');
}

static void json_print_element(sbuf *ctx, json_t *jso, int level)
{
    size_t len;
    if ( jso == NULL )
    {
        sbuf_fputs(ctx, "null");
        return;
    }
    switch ( JSON_TYPE(jso->flags) )
    {
    case JSON_TYPE_OBJECT:
        sbuf_fputc(ctx, '{');
        len = json_object_length(jso);
        for ( size_t i = 0; i < len; i++ )
        {
            if ( i != 0 )
                sbuf_fputc(ctx, ',');
            output_lf(ctx, jso, level+1);
            output_string(ctx, jso->obj->names[i]);
            sbuf_fputc(ctx, ':');
            json_print_element(ctx, jso->obj->values[i], level+1);
        }
        output_lf(ctx, jso, level);
        sbuf_fputc(ctx, '}');
        break;
    case JSON_TYPE_ARRAY:
        sbuf_fputc(ctx, '[');
        len = json_array_length(jso);
        for ( size_t i = 0; i < len; i++ )
        {
            if ( i != 0 )
                sbuf_fputc(ctx, ',');
            output_lf_array(ctx, jso, level+1, i);
            json_print_element(ctx, jso->array[i], level+1);
        }
        output_lf(ctx, jso, level);
        sbuf_fputc(ctx, ']');
        break;
    case JSON_TYPE_ARRAY_OF_INTS:
        sbuf_fputc(ctx, '[');
        len = json_array_length(jso);
        for ( size_t i = 0; i < len; i++ )
        {
            if ( i != 0 )
                sbuf_fputc(ctx, ',');
            output_lf_array(ctx, jso, level+1, i);
            output_num_32(ctx, jso->array_of_ints[i]);
        }
        output_lf(ctx, jso, level);
        sbuf_fputc(ctx, ']');
        break;
    case JSON_TYPE_U8_ARRAY:
        sbuf_fputc(ctx, '[');
        len = json_array_length(jso);
        for ( size_t i = 0; i < len; i++ )
        {
            if ( i != 0 )
                sbuf_fputc(ctx, ',');
            output_lf_array(ctx, jso, level+1, i);
            output_u8(ctx, jso->u8_array[i]);
        }
        output_lf(ctx, jso, level);
        sbuf_fputc(ctx, ']');
        break;
    case JSON_TYPE_U8_2DARRAY:
        {
            json_u8_2darray_t *ju8_2darray = jso->u8_2darray;
            const size_t width = ju8_2darray->width;
            const size_t height = ju8_2darray->height;
            sbuf_fputc(ctx, '[');
            for ( size_t i = 0; i < height; i++ )
            {
                if ( i != 0 )
                    sbuf_fputc(ctx, ',');
                output_lf(ctx, jso, level+1);
                sbuf_fputc(ctx, '[');
                for ( size_t j = 0; j < width; j++ )
                {
                    size_t idx = i * width + j;
                    uint8_t u8 = ju8_2darray->buf[idx];
                    if ( j != 0 )
                        sbuf_fputc(ctx, ',');
                    sbuf_fputc(ctx, ' ');
                    output_u8(ctx, u8);
                }
                sbuf_fputc(ctx, ' ');
                sbuf_fputc(ctx, ']');
            }
            output_lf(ctx, jso, level);
            sbuf_fputc(ctx, ']');
        }
        break;
    case JSON_TYPE_U16_2DARRAY:
        {
            json_u16_2darray_t *ju16_2darray = jso->u16_2darray;
            const size_t width = ju16_2darray->width;
            const size_t height = ju16_2darray->height;
            sbuf_fputc(ctx, '[');
            for ( size_t i = 0; i < height; i++ )
            {
                if ( i != 0 )
                    sbuf_fputc(ctx, ',');
                output_lf(ctx, jso, level+1);
                sbuf_fputc(ctx, '[');
                for ( size_t j = 0; j < width; j++ )
                {
                    size_t idx = i * width + j;
                    uint16_t u16 = ju16_2darray->buf[idx];
                    if ( j != 0 )
                        sbuf_fputc(ctx, ',');
                    sbuf_fputc(ctx, ' ');
                    output_u16(ctx, u16);
                }
                sbuf_fputc(ctx, ' ');
                sbuf_fputc(ctx, ']');
            }
            output_lf(ctx, jso, level);
            sbuf_fputc(ctx, ']');
        }
        break;
    case JSON_TYPE_STRING:
        output_string(ctx, jso->str);
        break;
    case JSON_TYPE_NUMBER:
        output_num_64(ctx, jso->val);
        break;
    case JSON_TYPE_FLOAT:
        sbuf_fprintf(ctx, "%g", jso->flt);
        break;
    case JSON_TYPE_BOOL:
        sbuf_fputs(ctx, (jso->val == 0) ? "false" : "true");
        break;
    }
}

void json_fputs(FILE *fp, json_t *jso)
{
    sbuf ctx;
    sbuf_init(&ctx, fp);
    json_print_element(&ctx, jso, 0);
    sbuf_fputc(&ctx, '\n');
    sbuf_free(&ctx);
}
