/**
 * Implementation of HPy_BuildValue.
 *
 * Note: HPy_BuildValue is a runtime helper functions, i.e., it is not a part
 * of the HPy context, but is available to HPy extensions to incorporate at
 * compile time.
 *
 * HPy_BuildValue creates a new value based on a format string from the values
 * passed in variadic arguments. Returns HPy_NULL in case of an error and raises
 * an exception.
 *
 * HPy_BuildValue does not always build a tuple. It builds a tuple only if its format
 * string contains two or more format units. If the format string is empty, it returns
 * None; if it contains exactly one format unit, it returns whatever object is described
 * by that format unit. To force it to return a tuple of size 0 or one, parenthesize the
 * format string.
 *
 * Building complex values with HPy_BuildValue is more convenient than the equivalent
 * code that uses more granular APIs with proper error handling and cleanup. Moreover,
 * HPy_BuildValue provides straightforward way to port existing code that uses
 * Py_BuildValue.
 *
 * HPy_BuildValue always returns a new handle that will be owned by the caller. Even
 * an artificial example 'HPy_BuildValue(ctx, "O", h)' does not simply forward
 * the value stored in 'h' but duplicates the handle.
 *
 * Supported Formatting Strings
 * ----------------------------
 *
 * Numbers
 * ~~~~~~~
 *
 * ``i (int) [int]``
 *     Convert a plain C int to a Python integer object.
 *
 * ``l (int) [long int]``
 *     Convert a C long int to a Python integer object.
 *
 * ``I (int) [unsigned int]``
 *     Convert a C unsigned int to a Python integer object.
 *
 * ``k (int) [unsigned long]``
 *     Convert a C unsigned long to a Python integer object.
 *
 * ``L (int) [long long]``
 *     Convert a C long long to a Python integer object.
 *
 * ``K (int) [unsigned long long]``
 *     Convert a C unsigned long long to a Python integer object.
 *
 * ``f (float) [float]``
 *     Convert a C float to a Python floating point number.
 *
 * ``d (float) [double]``
 *     Convert a C double to a Python floating point number.
 *
 * Collections
 * ~~~~~~~~~~~
 *
 * ``(items) (tuple) [matching-items]``
 *     Convert a sequence of C values to a Python tuple with the same number of items.
 *
 * ``[items] (list) [matching-items]``
 *     Convert a sequence of C values to a Python list with the same number of items.
 *
 * ``{key:value} (dict) [matching-items]``
 *     Convert a sequence of C values to a Python dict with the same number of items.
 *
 * Misc
 * ~~~~~~~
 *
 * ``O (Python object) [HPy]``
 *      Pass an untouched Python object represented by the handle.
 *
 *      If the object passed in is a HPy_NULL, it is assumed that this was caused because
 *      the call producing the argument found an error and set an exception. Therefore,
 *      HPy_BuildValue will also immediately stop and return HPy_NULL but will not raise
 *      any new exception. If no exception has been raised yet, SystemError is set.
 *
 *      Any HPy handle passed to HPy_BuildValue is always owned by the caller. HPy_BuildValue
 *      never closes the handle nor transfers its ownership. If the handle is used, then
 *      HPy_BuildValue creates a duplicate of the handle.
 *
 * ``S (Python object) [HPy]``
 *      Alias for 'O'.
 *
 * API
 * ---
 *
 */

#include "hpy.h"
#include <stdarg.h>
#include <stdio.h>

#define MESSAGE_BUF_SIZE 128

static HPy_ssize_t count_items(HPyContext *ctx, const char *fmt, char end);
static HPy build_tuple(HPyContext *ctx, const char **fmt, va_list *values, HPy_ssize_t size, char expected_end);
static HPy build_list(HPyContext *ctx, const char **fmt, va_list *values, HPy_ssize_t size);
static HPy build_dict(HPyContext *ctx, const char **fmt, va_list *values);
static HPy build_single(HPyContext *ctx, const char **fmt, va_list *values, int *needs_close);

HPyAPI_HELPER
HPy HPy_BuildValue(HPyContext *ctx, const char *fmt, ...)
{
    va_list values;
    HPy result;
    va_start(values, fmt);
    HPy_ssize_t size = count_items(ctx, fmt, '\0');
    if (size < 0) {
        result = HPy_NULL;
    } else if (size == 0) {
        result = HPy_Dup(ctx, ctx->h_None);
    } else if (size == 1) {
        int needs_close;
        result = build_single(ctx, &fmt, &values, &needs_close);
        if (!needs_close) {
            result = HPy_Dup(ctx, result);
        }
    } else {
        result = build_tuple(ctx, &fmt, &values, size, '\0');
    }
    va_end(values);
    return result;
}

static HPy_ssize_t count_items(HPyContext *ctx, const char *fmt, char end)
{
    HPy_ssize_t level = 0, result = 0;
    char top_level_par = 'X';
    while (level != 0 || *fmt != end) {
        char c = *fmt++;
        switch (c) {
            case '\0': {
                // Premature end
                // We try to provide slightly better diagnostics than CPython
                char msg[MESSAGE_BUF_SIZE];
                char par_type;
                if (end == ')') {
                    par_type = '(';
                } else if (end == ']') {
                    par_type = '[';
                } else if (end == '}') {
                    par_type = '{';
                } else {
                    if (level == 0 || top_level_par == 'X') {
                        HPyErr_SetString(ctx, ctx->h_SystemError, "internal error in HPy_BuildValue");
                        return -1;
                    }
                    par_type = top_level_par;
                }
                snprintf(msg, MESSAGE_BUF_SIZE, "unmatched '%c' in the format string passed to HPy_BuildValue", par_type);
                HPyErr_SetString(ctx, ctx->h_SystemError, msg);
                return -1;
            }

            case '[':
            case '(':
            case '{':
                if (level == 0) {
                    top_level_par = c;
                    result++;
                }
                level++;
                break;

            case ']':
            case ')':
            case '}':
                level--;
                break;

            case ',':
            case ' ':
                break;

            default:
                if (level == 0) {
                    result++;
                }
        }
    }
    return result;
}

static HPy build_single(HPyContext *ctx, const char **fmt, va_list *values, int *needs_close)
{
    char format_char = *(*fmt)++;
    *needs_close = 1;
    switch (format_char) {
        case '(': {
            HPy_ssize_t size = count_items(ctx, *fmt, ')');
            if (size < 0) {
                return HPy_NULL;
            }
            return build_tuple(ctx, fmt, values, size, ')');
        }

        case '[': {
            HPy_ssize_t size = count_items(ctx, *fmt, ']');
            if (size < 0) {
                return HPy_NULL;
            }
            return build_list(ctx, fmt, values, size);
        }

        case '{': {
            return build_dict(ctx, fmt, values);
        }

        case 'i':
            return HPyLong_FromLong(ctx, (long)va_arg(*values, int));

        case 'I':
            return HPyLong_FromUnsignedLong(ctx, (unsigned long)va_arg(*values, unsigned int));

        case 'k':
            return HPyLong_FromUnsignedLong(ctx, va_arg(*values, unsigned long));

        case 'l':
            return HPyLong_FromLong(ctx, va_arg(*values, long));

        case 'L':
            return HPyLong_FromLongLong(ctx, va_arg(*values, long long));

        case 'K':
            return HPyLong_FromUnsignedLongLong(ctx, va_arg(*values, unsigned long long));

        case 's':
            return HPyUnicode_FromString(ctx, va_arg(*values, const char*));

        case 'O':
        case 'S': {
            HPy handle = va_arg(*values, HPy);
            if (HPy_IsNull(handle)) {
                if (!HPyErr_Occurred(ctx)) {
                    HPyErr_SetString(ctx, ctx->h_SystemError, "HPy_NULL object passed to HPy_BuildValue");
                }
                return handle;
            }
            *needs_close = 0;
            return handle;
        }

        case 'N': {
            HPyErr_SetString(ctx, ctx->h_SystemError,
                             "HPy_BuildValue does not support the 'N' formatting unit. "
                             "Instead, use the 'O' formatting unit and manually close "
                             "the handle in the caller if necessary. HPy API functions "
                             "never 'steal' handles and always make a duplicate handle if "
                             "needed, the 'ownership' of the original handle is never "
                             "'transferred'. ");
            return HPy_NULL;
        }

        case 'f': // Note: floats are promoted to doubles when passed in "..."
        case 'd':
            return HPyFloat_FromDouble(ctx, va_arg(*values, double));

        default: {
            char message[MESSAGE_BUF_SIZE];
            snprintf(message, MESSAGE_BUF_SIZE, "bad format char '%c' in the format string passed to HPy_BuildValue", format_char);
            HPyErr_SetString(ctx, ctx->h_SystemError, message);
            return HPy_NULL;
        }
    } // switch
}

static HPy build_dict(HPyContext *ctx, const char **fmt, va_list *values)
{
    HPy dict = HPyDict_New(ctx);
    int expect_comma = 0;
    while (**fmt != '}' && **fmt != '\0') {
        if (**fmt == ' ') {
            (*fmt)++;
            continue;
        }
        if (**fmt == ',') {
            if (!expect_comma) {
                HPyErr_SetString(ctx, ctx->h_SystemError,
                    "unexpected ',' in the format string passed to HPy_BuildValue");
                HPy_Close(ctx, dict);
                return HPy_NULL;
            }
            (*fmt)++;
            expect_comma = 0;
            continue;
        } else {
            if (expect_comma) {
                HPyErr_SetString(ctx, ctx->h_SystemError,
                    "missing ',' in the format string passed to HPy_BuildValue");
                HPy_Close(ctx, dict);
                return HPy_NULL;
            }
        }
        int needs_key_close, needs_value_close;
        HPy key = build_single(ctx, fmt, values, &needs_key_close);
        if (HPy_IsNull(key)) {
            HPy_Close(ctx, dict);
            return HPy_NULL;
        }
        if (**fmt != ':') {
            HPyErr_SetString(ctx, ctx->h_SystemError,
                            "missing ':' in the format string passed to HPy_BuildValue");
            if (needs_key_close) {
                HPy_Close(ctx, key);
            }
            HPy_Close(ctx, dict);
            return HPy_NULL;
        } else {
            (*fmt)++;
        }
        HPy value = build_single(ctx, fmt, values, &needs_value_close);
        if (HPy_IsNull(value)) {
            if (needs_key_close) {
                HPy_Close(ctx, key);
            }
            HPy_Close(ctx, dict);
            return HPy_NULL;
        }
        int res = HPy_SetItem(ctx, dict, key, value);
        if (needs_key_close) {
            HPy_Close(ctx, key);
        }
        if (needs_value_close) {
            HPy_Close(ctx, value);
        }
        if (res < 0) {
            HPy_Close(ctx, dict);
            return HPy_NULL;
        }

        expect_comma = 1;
    }
    if (**fmt != '}') {
        // count_items does not check the type of the matching paren, that's what we do here
        HPy_Close(ctx, dict);
        HPyErr_SetString(ctx, ctx->h_SystemError,
                         "unmatched '{' in the format string passed to HPy_BuildValue");
        return HPy_NULL;
    }
    ++*fmt;
    return dict;
}

static HPy build_list(HPyContext *ctx, const char **fmt, va_list *values, HPy_ssize_t size)
{
    HPyListBuilder builder = HPyListBuilder_New(ctx, size);
    for (HPy_ssize_t i = 0; i < size; ++i) {
        int needs_close;
        HPy item = build_single(ctx, fmt, values, &needs_close);
        if (HPy_IsNull(item)) {
            HPyListBuilder_Cancel(ctx, builder);
            return HPy_NULL;
        }
        HPyListBuilder_Set(ctx, builder, i, item);
        if (needs_close) {
            HPy_Close(ctx, item);
        }
        if (**fmt == ',') {
            (*fmt)++;
        }
    }
    if (**fmt != ']') {
        // count_items does not check the type of the matching paren, that's what we do here
        HPyListBuilder_Cancel(ctx, builder);
        HPyErr_SetString(ctx, ctx->h_SystemError,
                         "unmatched '[' in the format string passed to HPy_BuildValue");
        return HPy_NULL;
    }
    ++*fmt;
    return HPyListBuilder_Build(ctx, builder);
}

static HPy build_tuple(HPyContext *ctx, const char **fmt, va_list *values, HPy_ssize_t size, char expected_end)
{
    HPyTupleBuilder builder = HPyTupleBuilder_New(ctx, size);
    for (HPy_ssize_t i = 0; i < size; ++i) {
        int needs_close;
        HPy item = build_single(ctx, fmt, values, &needs_close);
        if (HPy_IsNull(item)) {
            HPyTupleBuilder_Cancel(ctx, builder);
            return HPy_NULL;
        }
        HPyTupleBuilder_Set(ctx, builder, i, item);
        if (needs_close) {
            HPy_Close(ctx, item);
        }
        if (**fmt == ',') {
            (*fmt)++;
        }
    }
    if (**fmt != expected_end) {
        // count_items does not check the type of the matching paren, that's what we do here
        // if expected_end == '\0', then there would have to be a bug in count_items
        HPyTupleBuilder_Cancel(ctx, builder);
        if (expected_end == '\0') {
            HPyErr_SetString(ctx, ctx->h_SystemError, "internal error in HPy_BuildValue");
        } else {
            HPyErr_SetString(ctx, ctx->h_SystemError,
                             "unmatched '[' in the format string passed to HPy_BuildValue");
        }
        return HPy_NULL;
    }
    if (expected_end != '\0') {
        ++*fmt;
    }
    return HPyTupleBuilder_Build(ctx, builder);
}
