// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * Copyright (C) 2021-2024 Cyril Hrubis <metan@ucw.cz>
 */

/**
 * @file ujson_reader.h
 * @brief A recursive descend JSON parser.
 *
 * All the function that parse JSON return zero on success and non-zero on a
 * failure. Once an error has happened all subsequent attempts to parse more
 * return with non-zero exit status immediatelly. This is designed so that we
 * can parse several values without checking each return value and only check
 * if error has happened at the end of the sequence.
 */

#ifndef UJSON_READER_H
#define UJSON_READER_H

#include <stdio.h>
#include <ujson_common.h>

/**
 * @brief An ujson_reader initializer with default values.
 *
 * @param buf A pointer to a buffer with JSON data.
 * @param buf_len A JSON data buffer lenght.
 * @param rflags enum ujson_reader_flags.
 *
 * @return An ujson_reader initialized with default values.
 */
#define UJSON_READER_INIT(buf, buf_len, rflags) { \
	.max_depth = UJSON_RECURSION_MAX, \
	.err_print = UJSON_ERR_PRINT, \
	.err_print_priv = UJSON_ERR_PRINT_PRIV, \
	.json = buf, \
	.len = buf_len, \
	.flags = rflags \
}

/** @brief Reader flags. */
enum ujson_reader_flags {
	/** @brief If set warnings are treated as errors. */
	UJSON_READER_STRICT = 0x01,
};

/**
 * @brief A JSON parser internal state.
 */
struct ujson_reader {
	/** Pointer to a null terminated JSON string */
	const char *json;
	/** A length of the JSON string */
	size_t len;
	/** A current offset into the JSON string */
	size_t off;
	/** An offset to the start of the last array or object */
	size_t sub_off;
	/** Recursion depth increased when array/object is entered decreased on leave */
	unsigned int depth;
	/** Maximal recursion depth */
	unsigned int max_depth;

	/** Reader flags. */
	enum ujson_reader_flags flags;

	/** Handler to print errors and warnings */
	void (*err_print)(void *err_print_priv, const char *line);
	void *err_print_priv;

	char err[UJSON_ERR_MAX];
	char buf[];
};

/**
 * @brief An ujson_val initializer.
 *
 * @param sbuf A pointer to a buffer used for string values.
 * @param sbuf_size A length of the buffer used for string values.
 *
 * @return An ujson_val initialized with default values.
 */
#define UJSON_VAL_INIT(sbuf, sbuf_size) { \
	.buf = sbuf, \
	.buf_size = sbuf_size, \
}

/**
 * @brief A parsed JSON key value pair.
 */
struct ujson_val {
	/**
	 * @brief A value type
	 *
	 * UJSON_VALUE_VOID means that no value was parsed.
	 */
	enum ujson_type type;

	/** An user supplied buffer and size to store a string values to. */
	char *buf;
	size_t buf_size;

	/**
	 * @brief An index to attribute list.
	 *
	 * This is set by the ujson_obj_first_filter() and
	 * ujson_obj_next_filter() functions.
	 */
	size_t idx;

	/** An union to store the parsed value into. */
	union {
		/** @brief A boolean value. */
		int val_bool;
		/** @brief An integer value. */
		long long val_int;
		/** @brief A string value. */
		const char *val_str;
	};

	/**
	 * @brief A floating point value.
	 *
	 * Since integer values are subset of floating point values val_float
	 * is always set when val_int was set.
	 */
	double val_float;

	/** @brief An ID for object values */
	char id[UJSON_ID_MAX];

	char buf__[];
};

/**
 * @brief Allocates a JSON value.
 *
 * @param buf_size A maximal buffer size for a string value, pass 0 for default.
 * @return A newly allocated JSON value.
 */
ujson_val *ujson_val_alloc(size_t buf_size);

/**
 * @brief Frees a JSON value.
 *
 * @param self A JSON value previously allocated by ujson_val_alloc().
 */
void ujson_val_free(ujson_val *self);

/**
 * @brief Checks is result has valid type.
 *
 * @param res An ujson value.
 * @return Zero if result is not valid, non-zero otherwise.
 */
static inline int ujson_val_valid(struct ujson_val *res)
{
	return !!res->type;
}

/**
 * @brief Fills the reader error.
 *
 * Once buffer error is set all parsing functions return immediatelly with type
 * set to UJSON_VOID.
 *
 * @param self An ujson_reader
 * @param fmt A printf like format string
 * @param ... A printf like parameters
 */
void ujson_err(ujson_reader *self, const char *fmt, ...)
               __attribute__((format(printf, 2, 3)));

/**
 * @brief Prints error stored in the buffer.
 *
 * The error takes into consideration the current offset in the buffer and
 * prints a few preceding lines along with the exact position of the error.
 *
 * The error is passed to the err_print() handler.
 *
 * @param self A ujson_reader
 */
void ujson_err_print(ujson_reader *self);

/**
 * @brief Prints a warning.
 *
 * Uses the print handler in the buffer to print a warning along with a few
 * lines of context from the JSON at the current position.
 *
 * @param self A ujson_reader
 * @param fmt A printf-like error string.
 * @param ... A printf-like parameters.
 */
void ujson_warn(ujson_reader *self, const char *fmt, ...)
               __attribute__((format(printf, 2, 3)));

/**
 * @brief Returns true if error was encountered.
 *
 * @param self A ujson_reader
 * @return True if error was encountered false otherwise.
 */
static inline int ujson_reader_err(ujson_reader *self)
{
	return !!self->err[0];
}

/**
 * @brief Returns the type of next element in buffer.
 *
 * @param self An ujson_reader
 * @return A type of next element in the buffer.
 */
enum ujson_type ujson_next_type(ujson_reader *self);

/**
 * @brief Returns if first element in JSON is object or array.
 *
 * @param self A ujson_reader
 * @return On success returns UJSON_OBJ or UJSON_ARR. On failure UJSON_VOID.
 */
enum ujson_type ujson_reader_start(ujson_reader *self);

/**
 * @brief Starts parsing of a JSON object.
 *
 * @param self An ujson_reader
 * @param res An ujson_val to store the parsed value to.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_obj_first(ujson_reader *self, struct ujson_val *res);

/**
 * @brief Parses next value from a JSON object.
 *
 * If the res->type is UJSON_OBJ or UJSON_ARR it has to be parsed or skipped
 * before next call to this function.
 *
 * @param self An ujson_reader.
 * @param res A ujson_val to store the parsed value to.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_obj_next(ujson_reader *self, struct ujson_val *res);

/**
 * @brief A loop over a JSON object.
 *
 * @code
 * UJSON_OBJ_FOREACH(reader, val) {
 *	printf("Got value id '%s' type '%s'", val->id, ujson_type_name(val->type));
 *	...
 * }
 * @endcode
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the next parsed value to.
 */
#define UJSON_OBJ_FOREACH(self, res) \
	for (ujson_obj_first(self, res); ujson_val_valid(res); ujson_obj_next(self, res))

/**
 * @brief Utility function for log(n) lookup in a sorted array.
 *
 * @param list Analphabetically sorted array.
 * @param list_len Array length.
 *
 * @return An array index or (size_t)-1 if key wasn't found.
 */
size_t ujson_lookup(const void *arr, size_t memb_size, size_t list_len,
                    const char *key);

/**
 * @brief A JSON object attribute description i.e. key and type.
 */
typedef struct ujson_obj_attr {
	/** @brief A JSON object key name. */
	const char *key;
	/**
	 * @brief A JSON object value type.
	 *
	 * Note that because integer numbers are subset of floating point
         * numbers if requested type was UJSON_FLOAT it will match if parsed
         * type was UJSON_INT and the val_float will be set in addition to
         * val_int.
         */
	enum ujson_type type;
} ujson_obj_attr;

/** @brief A JSON object description */
typedef struct ujson_obj {
	/**
	 * @brief A list of attributes.
	 *
	 * Attributes we are looking for, the parser sets the val->idx for these.
	 */
	const ujson_obj_attr *attrs;
	/** @brief A size of attrs array. */
	size_t attr_cnt;
} ujson_obj;

static inline size_t ujson_obj_lookup(const ujson_obj *obj, const char *key)
{
	return ujson_lookup(obj->attrs, sizeof(*obj->attrs), obj->attr_cnt, key);
}

/** @brief An ujson_obj_attr initializer. */
#define UJSON_OBJ_ATTR(keyv, typev) \
	{.key = keyv, .type = typev}

/** @brief An ujson_obj_attr intializer with an array index. */
#define UJSON_OBJ_ATTR_IDX(key_idx, keyv, typev) \
	[key_idx] = {.key = keyv, .type = typev}

/**
 * @brief Starts parsing of a JSON object with attribute lists.
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the parsed value to.
 * @param obj An ujson_obj object description.
 * @param ign A list of keys to ignore.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_obj_first_filter(ujson_reader *self, struct ujson_val *res,
                           const struct ujson_obj *obj, const struct ujson_obj *ign);

/**
 * @brief An empty object attribute list.
 *
 * To be passed to UJSON_OBJ_FOREACH_FITLER() as ignore list.
 */
extern const struct ujson_obj *ujson_empty_obj;

/**
 * @brief Parses next value from a JSON object with attribute lists.
 *
 * If the res->type is UJSON_OBJ or UJSON_ARR it has to be parsed or skipped
 * before next call to this function.
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the parsed value to.
 * @param obj An ujson_obj object description.
 * @param ign A list of keys to ignore. If set to NULL all unknown keys are
 *            ignored, if set to ujson_empty_obj all unknown keys produce warnings.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_obj_next_filter(ujson_reader *self, struct ujson_val *res,
                          const struct ujson_obj *obj, const struct ujson_obj *ign);

/**
 * @brief A loop over a JSON object with a pre-defined list of expected attributes.
 *
 * @code
 * static struct ujson_obj_attr attrs[] = {
 *	UJSON_OBJ_ATTR("bool", UJSON_BOOL),
 *	UJSON_OBJ_ATTR("number", UJSON_INT),
 * };
 *
 * static struct ujson_obj obj = {
 *	.attrs = filter_attrs,
 *	.attr_cnt = UJSON_ARRAY_SIZE(filter_attrs)
 * };
 *
 * UJSON_OBJ_FOREACH_FILTER(reader, val, &obj, NULL) {
 *	printf("Got value id '%s' type '%s'",
 *	       attrs[val->idx].id, ujson_type_name(val->type));
 *	...
 * }
 * @endcode
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the next parsed value to.
 * @param obj An ujson_obj with a description of attributes to parse.
 * @param ign An ujson_obj with a description of attributes to ignore.
 */
#define UJSON_OBJ_FOREACH_FILTER(self, res, obj, ign) \
	for (ujson_obj_first_filter(self, res, obj, ign); \
	     ujson_val_valid(res); \
	     ujson_obj_next_filter(self, res, obj, ign))

/**
 * @brief Skips parsing of a JSON object.
 *
 * @param self An ujson_reader.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_obj_skip(ujson_reader *self);

/**
 * @brief Starts parsing of a JSON array.
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the parsed value to.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_arr_first(ujson_reader *self, struct ujson_val *res);

/**
 * @brief Parses next value from a JSON array.
 *
 * If the res->type is UJSON_OBJ or UJSON_ARR it has to be parsed or skipped
 * before next call to this function.
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the parsed value to.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_arr_next(ujson_reader *self, struct ujson_val *res);

/**
 * @brief A loop over a JSON array.
 *
 * @code
 * UJSON_ARR_FOREACH(reader, val) {
 *	printf("Got value type '%s'", ujson_type_name(val->type));
 *	...
 * }
 * @endcode
 *
 * @param self An ujson_reader.
 * @param res An ujson_val to store the next parsed value to.
 */
#define UJSON_ARR_FOREACH(self, res) \
	for (ujson_arr_first(self, res); ujson_val_valid(res); ujson_arr_next(self, res))

/**
 * @brief Skips parsing of a JSON array.
 *
 * @param self A ujson_reader.
 *
 * @return Zero on success, non-zero otherwise.
 */
int ujson_arr_skip(ujson_reader *self);

/**
 * @brief A JSON reader state.
 */
typedef struct ujson_reader_state {
	size_t off;
	unsigned int depth;
} ujson_reader_state;

/**
 * @brief Returns a parser state at the start of current object/array.
 *
 * This function could be used for the parser to return to the start of the
 * currently parsed object or array.
 *
 * @param self A ujson_reader
 * @return A state that points to a start of the last object or array.
 */
static inline ujson_reader_state ujson_reader_state_save(ujson_reader *self)
{
	struct ujson_reader_state ret = {
		.off = self->sub_off,
		.depth = self->depth,
	};

	return ret;
}

/**
 * @brief Returns the parser to a saved state.
 *
 * This function could be used for the parser to return to the start of
 * object or array saved by t the ujson_reader_state_get() function.
 *
 * @param self A ujson_reader
 * @param state An parser state as returned by the ujson_reader_state_get().
 */
static inline void ujson_reader_state_load(ujson_reader *self, ujson_reader_state state)
{
	if (ujson_reader_err(self))
		return;

	self->off = state.off;
	self->sub_off = state.off;
	self->depth = state.depth;
}

/**
 * @brief Resets the parser to a start.
 *
 * @param self A ujson_reader
 */
static inline void ujson_reader_reset(ujson_reader *self)
{
	self->off = 0;
	self->sub_off = 0;
	self->depth = 0;
	self->err[0] = 0;
}

/**
 * @brief Loads a file into an ujson_reader buffer.
 *
 * The reader has to be later freed by ujson_reader_free().
 *
 * @param path A path to a file.
 * @return A ujson_reader or NULL in a case of a failure.
 */
ujson_reader *ujson_reader_load(const char *path);

/**
 * @brief Frees an ujson_reader buffer.
 *
 * @param self A ujson_reader allocated by ujson_reader_load() function.
 */
void ujson_reader_free(ujson_reader *self);

/**
 * @brief Prints errors and warnings at the end of parsing.
 *
 * Checks if self->err is set and prints the error with ujson_reader_err()
 *
 * Checks if there is any text left after the parser has finished with
 * ujson_reader_consumed() and prints a warning if there were any non-whitespace
 * characters left.
 *
 * @param self A ujson_reader
 */
void ujson_reader_finish(ujson_reader *self);

/**
 * @brief Returns non-zero if whole buffer has been consumed.
 *
 * @param self A ujson_reader.
 * @return Non-zero if whole buffer was consumed.
 */
static inline int ujson_reader_consumed(ujson_reader *self)
{
	return self->off >= self->len;
}

#endif /* UJSON_H */