/*****************************************************************************
 * json.c: JSON tracer plugin
 *****************************************************************************
 * Copyright © 2021 Videolabs
 *
 * Authors : Nicolas Le Quec
 *
 * This program 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.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <vlc_common.h>
#include <vlc_configuration.h>
#include <vlc_plugin.h>
#include <vlc_fs.h>
#include <vlc_charset.h>
#include <vlc_tracer.h>
#include <vlc_memstream.h>

#include <stdbool.h>
#include <stdarg.h>
#include <errno.h>
#include <assert.h>
#include <ctype.h>

#define JSON_FILENAME "vlc-log.json"

#define TIME_FROM_TICK(ts) NS_FROM_VLC_TICK(ts)

typedef struct
{
    FILE *stream;
} vlc_tracer_sys_t;

static void PrintUTF8Char(FILE *stream, uint32_t character)
{
    /* If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF),
       then it may be represented as a six-character sequence: \uxxxx */
    if (character < 0x10000)
    {
        fprintf(stream, "\\u%04x", character);
    }
    /* To escape an extended character that is not in the Basic Multilingual
      Plane, the character is represented as a 12-character sequence, encoding
      the UTF-16 surrogate pair. */

    else if (0x10000 <= character && character <= 0x10FFFF) {
        unsigned int code;
        uint16_t units[2];

        code = (character - 0x10000);
        units[0] = 0xD800 | (code >> 10);
        units[1] = 0xDC00 | (code & 0x3FF);

        fprintf(stream, "\\u%04x\\u%04x", units[0], units[1]);
    }
}

static void JsonPrintString(FILE *stream, const char *str)
{
    if (!IsUTF8(str))
    {
        fputs("\"invalid string\"", stream);
        return;
    }

    fputc('\"', stream);

    unsigned char byte;
    while (*str != '\0')
    {
        switch (*str)
        {
        case '/':
            fputs("\\/", stream);
            break;
        case '\b':
            fputs("\\b", stream);
            break;
        case '\f':
            fputs("\\f", stream);
            break;
        case '\n':
            fputs("\\n", stream);
            break;
        case '\r':
            fputs("\\r", stream);
            break;
        case '\t':
            fputs("\\t", stream);
            break;
        case '\\':
        case '\"':
            fprintf(stream, "\\%c", *str);
            break;
        default:
            byte = *str;
            if (byte <= 0x1F || byte == 0x7F)
            {
                fprintf(stream, "\\u%04x", byte);
            }
            else if (byte < 0x80)
            {
                fputc(byte, stream);
            }
            else
            {
                uint32_t bytes;
                ssize_t len = vlc_towc(str, &bytes);
                assert(len > 0);
                PrintUTF8Char(stream, bytes);
                str += len - 1;
            }
        }
        str++;
    }
    fputc('\"', stream);
}

static void JsonPrintKeyValueNumber(FILE *stream, const char *key, int64_t value)
{
    JsonPrintString(stream, key);
    fprintf(stream, ": \"%"PRId64"\"", value);
}

static void JsonPrintKeyValueUnsigned(FILE *stream, const char *key, uint64_t value)
{
    JsonPrintString(stream, key);
    fprintf(stream, ": \"%"PRIu64"\"", value);
}

static void JsonPrintKeyValueNumberFromDouble(FILE *stream, const char *key, double value)
{
    JsonPrintString(stream, key);
    vlc_fprintf_c(stream, ": \"%le\"", value);
}

static void JsonPrintKeyValueLabel(FILE *stream, const char *key, const char *value)
{
    JsonPrintString(stream, key);
    fputs(": ", stream);
    JsonPrintString(stream, value);
}

static void JsonStartObjectSection(FILE *stream, const char* name)
{
    if (name != NULL)
        fprintf(stream, "\"%s\": {", name);
    else
        fputc('{', stream);
}

static void JsonEndObjectSection(FILE *stream)
{
    fputc('}', stream);
}

static void TraceJson(void *opaque, vlc_tick_t ts, const struct vlc_tracer_trace *trace)
{
    vlc_tracer_sys_t *sys = opaque;
    FILE* stream = sys->stream;

    flockfile(stream);
    JsonStartObjectSection(stream, NULL);
    JsonPrintKeyValueNumber(stream, "Timestamp", TIME_FROM_TICK(ts));
    fputc(',', stream);

    JsonStartObjectSection(stream, "Body");

    const struct vlc_tracer_entry *entry = trace->entries;
    while (entry->key != NULL)
    {
        switch (entry->type)
        {
            case VLC_TRACER_UINT:
                JsonPrintKeyValueUnsigned(stream, entry->key, entry->value.uinteger);
                break;
            case VLC_TRACER_INT:
                JsonPrintKeyValueNumber(stream, entry->key, entry->value.integer);
                break;
            case VLC_TRACER_DOUBLE:
                JsonPrintKeyValueNumberFromDouble(stream, entry->key, entry->value.double_);
                break;
            case VLC_TRACER_STRING:
                JsonPrintKeyValueLabel(stream, entry->key, entry->value.string);
                break;
            default:
                vlc_assert_unreachable();
                break;
        }
        entry++;
        if (entry->key != NULL)
        {
            fputc(',', stream);
        }
    }
    JsonEndObjectSection(stream);
    JsonEndObjectSection(stream);
    fputc('\n', stream);
    funlockfile(stream);
}

static void Close(void *opaque)
{
    vlc_tracer_sys_t *sys = opaque;
    fclose(sys->stream);
    free(sys);
}

static const struct vlc_tracer_operations json_ops =
{
    TraceJson,
    Close
};

static const struct vlc_tracer_operations *Open(vlc_object_t *obj,
                                               void **restrict sysp)
{
    vlc_tracer_sys_t *sys = malloc(sizeof (*sys));
    if (unlikely(sys == NULL))
        return NULL;

    const char *filename = JSON_FILENAME;

    char *path = var_InheritString(obj, "json-tracer-file");
#ifdef __APPLE__
    if (path == NULL)
    {
        char *home = config_GetUserDir(VLC_HOME_DIR);
        if (home != NULL)
        {
            if (asprintf(&path, "%s/Library/Logs/"JSON_FILENAME, home) == -1)
                path = NULL;
            free(home);
        }
    }
#endif
    if (path != NULL)
        filename = path;

    /* Open the log file and remove any buffering for the stream */
    msg_Dbg(obj, "opening logfile `%s'", filename);
    sys->stream = vlc_fopen(filename, "at");
    if (sys->stream == NULL)
    {
        msg_Err(obj, "error opening log file `%s': %s", filename,
            vlc_strerror_c(errno) );
        free(path);
        free(sys);
        return NULL;
    }
    free(path);

    setvbuf(sys->stream, NULL, _IOLBF, 0);

    *sysp = sys;
    return &json_ops;
}

#define LOGFILE_NAME_TEXT N_("Log filename")
#define LOGFILE_NAME_LONGTEXT N_("Specify the log filename.")

vlc_module_begin()
    set_shortname(N_("Tracer"))
    set_description(N_("JSON tracer"))
    set_subcategory(SUBCAT_ADVANCED_MISC)
    set_capability("tracer", 0)
    set_callback(Open)

    add_savefile("json-tracer-file", NULL, LOGFILE_NAME_TEXT, LOGFILE_NAME_LONGTEXT)
vlc_module_end()
