/*****************************************************************************\
 *  serializer_json.c - Serializer for JSON.
 *****************************************************************************
 *  Copyright (C) SchedMD LLC.
 *
 *  This file is part of Slurm, a resource management program.
 *  For details, see <https://slurm.schedmd.com/>.
 *  Please also read the included file: DISCLAIMER.
 *
 *  Slurm 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.
 *
 *  In addition, as a special exception, the copyright holders give permission
 *  to link the code of portions of this program with the OpenSSL library under
 *  certain conditions as described in each individual source file, and
 *  distribute linked combinations including the two. You must obey the GNU
 *  General Public License in all respects for all of the code used other than
 *  OpenSSL. If you modify file(s) with this exception, you may extend this
 *  exception to your version of the file(s), but you are not obligated to do
 *  so. If you do not wish to do so, delete this exception statement from your
 *  version.  If you delete this exception statement from all source files in
 *  the program, then also delete it here.
 *
 *  Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
\*****************************************************************************/

#include "config.h"
#include "math.h"

#if HAVE_JSON_C_INC
#include <json-c/json.h>
#else
#include <json/json.h>
#endif

#include "slurm/slurm.h"
#include "src/common/slurm_xlator.h"

#include "src/common/data.h"
#include "src/common/log.h"
#include "src/common/read_config.h"
#include "src/common/xassert.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"
#include "src/interfaces/serializer.h"

#define SERIALIZER_JSON_DEFAULT_FORMAT JSON_C_TO_STRING_PLAIN
#define SERIALIZER_JSON_DEFAULT_FLAGS SER_FLAGS_PRETTY

/*
 * These variables are required by the generic plugin interface.  If they
 * are not found in the plugin, the plugin loader will ignore it.
 *
 * plugin_name - A string giving a human-readable description of the
 * plugin.  There is no maximum length, but the symbol must refer to
 * a valid string.
 *
 * plugin_type - A string suggesting the type of the plugin or its
 * applicability to a particular form of data or method of data handling.
 * If the low-level plugin API is used, the contents of this string are
 * unimportant and may be anything.  Slurm uses the higher-level plugin
 * interface which requires this string to be of the form
 *
 *	<application>/<method>
 *
 * where <application> is a description of the intended application of
 * the plugin (e.g., "auth" for Slurm authentication) and <method> is a
 * description of how this plugin satisfies that application.  Slurm will
 * only load authentication plugins if the plugin_type string has a prefix
 * of "auth/".
 *
 * plugin_version - an unsigned 32-bit integer containing the Slurm version
 * (major.minor.micro combined into a single number).
 */
const char plugin_name[] = "Serializer JSON plugin";
const char plugin_type[] = "serializer/json";
const uint32_t plugin_version = SLURM_VERSION_NUMBER;
const char *mime_types[] = {
	"application/json",
	"application/jsonrequest",
	NULL
};
static serializer_flags_t global_flags = SERIALIZER_JSON_DEFAULT_FLAGS;

#define MAGIC_CONVERT_FOREACH 0x0a0b0808

typedef struct {
	int magic; /* MAGIC_CONVERT_FOREACH */
	json_object *jobj;
	serializer_flags_t flags;
} convert_foreach_arg_t;

static json_object *_data_to_json(const data_t *d, serializer_flags_t flags);

/* Merge global_flags and flags into the coherent set of flags */
static serializer_flags_t _merge_flags(serializer_flags_t flags)
{
	serializer_flags_t ret = global_flags;

	/* Avoid conflicting flags from global */
	if (flags & (SER_FLAGS_COMPACT|SER_FLAGS_PRETTY))
		ret &= ~(SER_FLAGS_COMPACT|SER_FLAGS_PRETTY);

	return (ret | flags);
}

extern int serialize_p_init(serializer_flags_t flags)
{
	if (flags != SER_FLAGS_NONE)
		global_flags = flags;

	log_flag(DATA, "loaded");

	return SLURM_SUCCESS;
}

extern void serialize_p_fini(void)
{
	log_flag(DATA, "unloaded");
}

static json_object *_try_parse(const char *src, size_t stringlen,
			       struct json_tokener *tok)
{
	json_object *jobj = json_tokener_parse_ex(tok, src, stringlen);

	if (jobj == NULL) {
		enum json_tokener_error jerr = json_tokener_get_error(tok);
		error("%s: JSON parsing error %zu bytes: %s",
		      __func__, stringlen, json_tokener_error_desc(jerr));
		return NULL;
	}
	if (tok->char_offset < stringlen)
		log_flag(DATA, "%s: Extra %zu characters after JSON string detected",
		     __func__, (stringlen - tok->char_offset));

	return jobj;
}

static data_t *_json_to_data(json_object *jobj, data_t *d)
{
	size_t arraylen = 0;

	if (!d)
		d = data_new();

	switch (json_object_get_type(jobj)) {
	case json_type_null:
		data_set_null(d);
		break;
	case json_type_boolean:
		data_set_bool(d, json_object_get_boolean(jobj));
		break;
	case json_type_double:
		data_set_float(d, json_object_get_double(jobj));
		break;
	case json_type_int:
		data_set_int(d, json_object_get_int64(jobj));
		break;
	case json_type_object:
		data_set_dict(d);
		/* warning: json_object_object_foreach is an evil macro */
		json_object_object_foreach(jobj, key, val) {
			_json_to_data(val, data_key_set(d, key));
		}
		break;
	case json_type_array:
		arraylen = json_object_array_length(jobj);
		data_set_list(d);
		for (size_t i = 0; i < arraylen; i++)
			_json_to_data(json_object_array_get_idx(jobj, i),
				      data_list_append(d));
		break;
	case json_type_string:
		data_set_string(d, json_object_get_string(jobj));
		break;
	default:
		fatal_abort("%s: unknown JSON type", __func__);
	};

	return d;
}

static data_for_each_cmd_t _convert_dict_json(const char *key,
					      const data_t *data,
					      void *arg)
{
	convert_foreach_arg_t *args = arg;
	json_object *jobj = args->jobj;
	json_object *jobject = _data_to_json(data, args->flags);

	xassert(args->magic == MAGIC_CONVERT_FOREACH);

	json_object_object_add(jobj, key, jobject);
	return DATA_FOR_EACH_CONT;
}

static data_for_each_cmd_t _convert_list_json(const data_t *data, void *arg)
{
	convert_foreach_arg_t *args = arg;
	json_object *jobj = args->jobj;
	json_object *jarray = _data_to_json(data, args->flags);

	xassert(args->magic == MAGIC_CONVERT_FOREACH);

	json_object_array_add(jobj, jarray);
	return DATA_FOR_EACH_CONT;
}

static json_object *_data_to_json(const data_t *d, serializer_flags_t flags)
{
	if (!d)
		return NULL;

	switch (data_get_type(d)) {
	case DATA_TYPE_NULL:
		return NULL;
		break;
	case DATA_TYPE_BOOL:
		return json_object_new_boolean(data_get_bool(d));
		break;
	case DATA_TYPE_FLOAT:
		if (!(flags & SER_FLAGS_COMPLEX)) {
			if (isinf(data_get_float(d)))
				return json_object_new_double(
					(double) INFINITE64);
			else if (isnan(data_get_float(d)))
				return json_object_new_double(
					(double) NO_VAL64);
		}

		return json_object_new_double(data_get_float(d));
		break;
	case DATA_TYPE_INT_64:
		return json_object_new_int64(data_get_int(d));
		break;
	case DATA_TYPE_DICT:
	{
		json_object *jobj = json_object_new_object();
		convert_foreach_arg_t args = {
			.magic = MAGIC_CONVERT_FOREACH,
			.jobj = jobj,
			.flags = flags,
		};
		if (data_dict_for_each_const(d, _convert_dict_json, &args) < 0)
			error("%s: unexpected error calling _convert_dict_json()",
			      __func__);
		return jobj;
	}
	case DATA_TYPE_LIST:
	{
		json_object *jobj = json_object_new_array();
		convert_foreach_arg_t args = {
			.magic = MAGIC_CONVERT_FOREACH,
			.jobj = jobj,
			.flags = flags,
		};
		if (data_list_for_each_const(d, _convert_list_json, &args) < 0)
			error("%s: unexpected error calling _convert_list_json()",
			      __func__);
		return jobj;
	}
	case DATA_TYPE_STRING:
	{
		const char *str = data_get_string(d);
		if (str)
			return json_object_new_string(str);
		else
			return json_object_new_string("");
		break;
	}
	default:
		fatal_abort("%s: unknown type", __func__);
	};
}

extern int serialize_p_data_to_string(char **dest, size_t *length,
				      const data_t *src,
				      serializer_flags_t flags)
{
	struct json_object *jobj = NULL;
	int jflags = 0;

	flags = _merge_flags(flags);
	jobj = _data_to_json(src, flags);

	/* can't be pretty and compact at the same time! */
	xassert((flags & (SER_FLAGS_PRETTY | SER_FLAGS_COMPACT)) !=
		(SER_FLAGS_PRETTY | SER_FLAGS_COMPACT));

	if (flags & SER_FLAGS_PRETTY)
		jflags = JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY;
	else if (flags & SER_FLAGS_COMPACT)
		jflags = JSON_C_TO_STRING_PLAIN;
	else
		jflags = SERIALIZER_JSON_DEFAULT_FORMAT;

	/* string will die with jobj */
	*dest = xstrdup(json_object_to_json_string_ext(jobj, jflags));
	if (length) {
		/* add 1 for \0 */
		*length = strlen(*dest) + 1;
	}

	/* put is equiv to free() */
	json_object_put(jobj);

	return SLURM_SUCCESS;
}

extern int serialize_p_string_to_data(data_t **dest, const char *src,
				      size_t length)
{
	json_object *jobj = NULL;
	data_t *data = NULL;
	struct json_tokener *tok = json_tokener_new();
	int rc;

	if (!tok)
		return ENOMEM;

	if (!src)
		return ESLURM_DATA_PTR_NULL;

	/* json-c has hard limit of 32 bits */
	if (length >= INT32_MAX) {
		error("%s: unable to parse JSON: too large",
		      __func__);
		return ESLURM_DATA_TOO_LARGE;
	}

	jobj = _try_parse(src, length, tok);
	if (jobj) {
		data = _json_to_data(jobj, NULL);
		json_object_put(jobj);
		rc = SLURM_SUCCESS;
	} else
		rc = ESLURM_REST_FAIL_PARSING;

	json_tokener_free(tok);

	*dest = data;
	return rc;
}
