/**
 * @file json_helpers.h
 * @brief Typed jansson accessors that report "missing" and "wrong type" as errors.
 *
 * Every asset in this library is described by a JSON document, and jansson's own
 * accessors answer "missing key", "wrong type", and "index past the end" all
 * with the same `NULL`. These wrappers split those apart -- AKERR_KEY,
 * AKERR_TYPE, AKERR_OUTOFBOUNDS -- so a malformed asset file produces a message
 * naming the key and what was wrong with it, instead of a `NULL` dereference
 * three frames later.
 *
 * Conventions that run through the whole set, so they need not be repeated per
 * function:
 *
 * - **Absence is an error here.** Unlike the search functions in libakstdlib,
 *   these treat a missing key as AKERR_KEY. An *optional* key is expressed by
 *   passing the resulting error to akgl_get_json_with_default() rather than by
 *   the accessor staying quiet.
 * - **The result comes back through @p dest**, because the return value is the
 *   error context.
 * - **`json_t *` results are borrowed, not owned.** Objects and arrays are
 *   returned as pointers into the document; they are valid until the document is
 *   freed and must not be `json_decref`'d.
 * - **@p dest is not `NULL`-checked** except where noted, so a `NULL` there is a
 *   crash rather than an error context.
 * - **The document itself is never modified.**
 */

#ifndef _AKGL_JSON_HELPERS_H_
#define _AKGL_JSON_HELPERS_H_

#include <akerror.h>
#include <akgl/types.h>
#include <jansson.h>
#include <akgl/staticstring.h>

/**
 * @brief Read a nested object out of a JSON object.
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up. A `NULL` key is reported as a missing
 *             key rather than as a `NULL` pointer.
 * @param dest Receives a borrowed pointer to the nested object; not written on
 *             any failure path. Not checked for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj is `NULL`.
 * @throws AKERR_KEY If @p key is absent. The message names it.
 * @throws AKERR_TYPE If @p key is present but is not an object.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
/**
 * @brief Read a boolean out of a JSON object.
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up.
 * @param dest Receives the value; not written on any failure path. Not checked
 *             for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj is `NULL`.
 * @throws AKERR_KEY If @p key is absent.
 * @throws AKERR_TYPE If @p key is present but is not `true` or `false`. A `0`
 *         or `1` is a number in JSON, and is refused here.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
/**
 * @brief Read an integer out of a JSON object.
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up.
 * @param dest Receives the value, narrowed to `int`. Not written on any failure
 *             path, and not checked for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj is `NULL`.
 * @throws AKERR_KEY If @p key is absent.
 * @throws AKERR_TYPE If @p key is present but is not an integer. This is strict:
 *         `3.0` is a real in JSON and is refused, not truncated. Use
 *         akgl_get_json_number_value() where either spelling should be accepted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
/**
 * @brief Read a number out of a JSON object as a `float`.
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up.
 * @param dest Receives the value, narrowed to `float`. Not written on any
 *             failure path, and not checked for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj is `NULL`.
 * @throws AKERR_KEY If @p key is absent.
 * @throws AKERR_TYPE If @p key is present but is not a number. Integers and
 *         reals are both accepted, so `1` and `1.0` behave alike.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float32_t *dest);
/**
 * @brief Read a number out of a JSON object as a `double`.
 *
 * The full-precision form of akgl_get_json_number_value(), for the physics
 * constants, which are `double`.
 *
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up.
 * @param dest Receives the value. Not written on any failure path, and not
 *             checked for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj is `NULL`.
 * @throws AKERR_KEY If @p key is absent.
 * @throws AKERR_TYPE If @p key is present but is not a number.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, float64_t *dest);
/**
 * @brief Read a string out of a JSON object into a pooled akgl_String.
 *
 * Unlike the other accessors this copies, because the caller wants a buffer it
 * can keep rather than a pointer into the document. `*dest` doubles as an input:
 * `NULL` means "claim one for me", non-`NULL` means "write into this one".
 *
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up. Required -- checked here, unlike the
 *             other accessors in this file.
 * @param dest Address of the destination string. Required, and it must be
 *             *initialized*: set `*dest` to `NULL` to have a pool string claimed
 *             for you, or to a claimed string to write in place. An
 *             indeterminate `*dest` is dereferenced. Either way the caller
 *             releases it with akgl_heap_release_string().
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj, @p key, or @p dest is `NULL`.
 * @throws AKERR_KEY If @p key is absent.
 * @throws AKERR_TYPE If @p key is present but is not a string.
 * @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
 *
 * @note A value longer than #AKGL_MAX_STRING_LENGTH is truncated silently, and
 *       is left without a terminator -- this is `strncpy`, not `strlcpy`.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
/**
 * @brief Read an array out of a JSON object.
 * @param obj  The object to read from. Required.
 * @param key  The member name to look up.
 * @param dest Receives a borrowed pointer to the array; not written on any
 *             failure path. Not checked for `NULL`. Use `json_array_size()` on
 *             it and the `akgl_get_json_array_index_*` family to walk it.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p obj is `NULL`.
 * @throws AKERR_KEY If @p key is absent.
 * @throws AKERR_TYPE If @p key is present but is not an array.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
/**
 * @brief Read one element of a JSON array as an object.
 * @param array The array to read from. Required.
 * @param index Zero-based element index. A negative index is reported the same
 *              way as one past the end.
 * @param dest  Receives a borrowed pointer to the element; not written on any
 *              failure path. Not checked for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p array is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If @p index is outside the array. The message
 *         reports the index.
 * @throws AKERR_TYPE If the element is not an object.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
/**
 * @brief Read one element of a JSON array as an integer.
 * @param array The array to read from. Required.
 * @param index Zero-based element index.
 * @param dest  Receives the value, narrowed to `int`; not written on any failure
 *              path. Not checked for `NULL`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p array is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
 * @throws AKERR_TYPE If the element is not an integer. Strict, as in
 *         akgl_get_json_integer_value().
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
/**
 * @brief Read one element of a JSON array into a pooled akgl_String.
 * @param array The array to read from. Required.
 * @param index Zero-based element index.
 * @param dest  Address of the destination string, with the same claim-or-reuse
 *              contract as akgl_get_json_string_value(): required, must be
 *              initialized, and released by the caller.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p array or @p dest is `NULL`.
 * @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
 * @throws AKERR_TYPE If the element is not a string.
 * @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
 *
 * @note Truncates silently at #AKGL_MAX_STRING_LENGTH, as
 *       akgl_get_json_string_value() does.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);

/**
 * @brief Turn a "key not found" error from one of the accessors above into a default value.
 *
 * This is how an optional key is spelled. Run the accessor, hand its error
 * context here along with the fallback, and a missing key becomes @p dest
 * holding @p defval and a `NULL` return; anything else propagates untouched:
 *
 * ```c
 * int width = 0;
 * int width_default = 32;
 * PASS(errctx,
 *      akgl_get_json_with_default(
 *          akgl_get_json_integer_value(json, "width", &width),
 *          &width_default,
 *          &width,
 *          sizeof(int)
 *          ));
 * ```
 *
 * A `NULL` @p e -- the accessor succeeded -- is the ordinary case and returns at
 * once without touching @p dest. When the error *is* consumed it is also
 * released, so the caller must not release it again.
 *
 * @param e       The error context to inspect, straight from an accessor.
 *                `NULL` means "no error", which is not itself an error.
 * @param defval  The fallback value to copy. Required when @p e is non-`NULL`.
 * @param dest    Where to copy it. Required when @p e is non-`NULL`. Must be the
 *                same destination the accessor was given, and at least
 *                @p defsize bytes.
 * @param defsize Bytes to copy out of @p defval. Trusted, not derived -- it must
 *                match the type both sides actually are, since this is a
 *                `memcpy` through `void *` with no type information.
 * @return `NULL` when the error was consumed or there was none, otherwise an
 *         error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p e is non-`NULL` and @p defval or @p dest is
 *         `NULL`.
 * @throws AKERR_* Whatever @p e carried, if it is not one of the statuses this
 *         defaults on -- an AKERR_TYPE from a key that exists but is the wrong
 *         type propagates, which is right: that is a malformed document, not an
 *         omitted setting.
 *
 * @note It defaults on AKERR_KEY and AKERR_INDEX, but *not* on
 *       AKERR_OUTOFBOUNDS -- which is the status the
 *       `akgl_get_json_array_index_*` family actually raises for a short array.
 *       So this pairs with the object accessors and does not currently give an
 *       array index a default.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);

#endif // _AKGL_JSON_HELPERS_H_
