/*
 * Copyright (c) The FFmpeg developers
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg 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.
 *
 * FFmpeg 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 FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <limits.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "libavutil/mem.h"
#include "libavutil/avassert.h"
#include "libavutil/bprint.h"
#include "libavutil/error.h"
#include "libavutil/hash.h"
#include "libavutil/intreadwrite.h"
#include "libavutil/macros.h"
#include "libavutil/opt.h"
#include "avtextformat.h"

#define SECTION_ID_NONE (-1)

#define SHOW_OPTIONAL_FIELDS_AUTO      (-1)
#define SHOW_OPTIONAL_FIELDS_NEVER       0
#define SHOW_OPTIONAL_FIELDS_ALWAYS      1

static const struct {
    double bin_val;
    double dec_val;
    char bin_str[4];
    char dec_str[4];
} si_prefixes[] = {
    { 1.0, 1.0, "", "" },
    { 1.024e3, 1e3, "Ki", "K" },
    { 1.048576e6, 1e6, "Mi", "M" },
    { 1.073741824e9, 1e9, "Gi", "G" },
    { 1.099511627776e12, 1e12, "Ti", "T" },
    { 1.125899906842624e15, 1e15, "Pi", "P" },
};

static const char *textcontext_get_formatter_name(void *p)
{
    AVTextFormatContext *tctx = p;
    return tctx->formatter->name;
}

#define OFFSET(x) offsetof(AVTextFormatContext, x)

static const AVOption textcontext_options[] = {
    { "string_validation", "set string validation mode",
      OFFSET(string_validation), AV_OPT_TYPE_INT, { .i64 = AV_TEXTFORMAT_STRING_VALIDATION_REPLACE }, 0, AV_TEXTFORMAT_STRING_VALIDATION_NB - 1, .unit = "sv" },
    { "sv", "set string validation mode",
      OFFSET(string_validation), AV_OPT_TYPE_INT, { .i64 = AV_TEXTFORMAT_STRING_VALIDATION_REPLACE }, 0, AV_TEXTFORMAT_STRING_VALIDATION_NB - 1, .unit = "sv" },
        { "ignore",  NULL, 0, AV_OPT_TYPE_CONST,  { .i64 = AV_TEXTFORMAT_STRING_VALIDATION_IGNORE },  .unit = "sv" },
        { "replace", NULL, 0, AV_OPT_TYPE_CONST,  { .i64 = AV_TEXTFORMAT_STRING_VALIDATION_REPLACE }, .unit = "sv" },
        { "fail",    NULL, 0, AV_OPT_TYPE_CONST,  { .i64 = AV_TEXTFORMAT_STRING_VALIDATION_FAIL },    .unit = "sv" },
    { "string_validation_replacement", "set string validation replacement string", OFFSET(string_validation_replacement), AV_OPT_TYPE_STRING, { .str = "" } },
    { "svr", "set string validation replacement string", OFFSET(string_validation_replacement), AV_OPT_TYPE_STRING, { .str = "\xEF\xBF\xBD" } },
    { NULL }
};

static void *textcontext_child_next(void *obj, void *prev)
{
    AVTextFormatContext *ctx = obj;
    if (!prev && ctx->formatter && ctx->formatter->priv_class && ctx->priv)
        return ctx->priv;
    return NULL;
}

static const AVClass textcontext_class = {
    .class_name = "AVTextContext",
    .item_name  = textcontext_get_formatter_name,
    .option     = textcontext_options,
    .version    = LIBAVUTIL_VERSION_INT,
    .child_next = textcontext_child_next,
};

static void bprint_bytes(AVBPrint *bp, const uint8_t *ubuf, size_t ubuf_size)
{
    av_bprintf(bp, "0X");
    for (unsigned i = 0; i < ubuf_size; i++)
        av_bprintf(bp, "%02X", ubuf[i]);
}

int avtext_context_close(AVTextFormatContext **ptctx)
{
    AVTextFormatContext *tctx = *ptctx;
    int ret = 0;

    if (!tctx)
        return AVERROR(EINVAL);

    av_hash_freep(&tctx->hash);

    if (tctx->formatter) {
        if (tctx->formatter->uninit)
            ret = tctx->formatter->uninit(tctx);
        if (tctx->formatter->priv_class)
            av_opt_free(tctx->priv);
    }
    for (int i = 0; i < SECTION_MAX_NB_LEVELS; i++)
        av_bprint_finalize(&tctx->section_pbuf[i], NULL);
    av_freep(&tctx->priv);
    av_opt_free(tctx);
    av_freep(ptctx);
    return ret;
}


int avtext_context_open(AVTextFormatContext **ptctx, const AVTextFormatter *formatter, AVTextWriterContext *writer_context, const char *args,
                        const AVTextFormatSection *sections, int nb_sections, AVTextFormatOptions options, char *show_data_hash)
{
    AVTextFormatContext *tctx;
    int ret = 0;

    av_assert0(ptctx && formatter);

    if (!(tctx = av_mallocz(sizeof(AVTextFormatContext)))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    for (int i = 0; i < SECTION_MAX_NB_LEVELS; i++)
        av_bprint_init(&tctx->section_pbuf[i], 1, AV_BPRINT_SIZE_UNLIMITED);

    tctx->class = &textcontext_class;
    av_opt_set_defaults(tctx);

    if (!(tctx->priv = av_mallocz(formatter->priv_size))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    tctx->show_value_unit = options.show_value_unit;
    tctx->use_value_prefix = options.use_value_prefix;
    tctx->use_byte_value_binary_prefix = options.use_byte_value_binary_prefix;
    tctx->use_value_sexagesimal_format = options.use_value_sexagesimal_format;
    tctx->show_optional_fields = options.show_optional_fields;

    if (nb_sections > SECTION_MAX_NB_SECTIONS) {
        av_log(tctx, AV_LOG_ERROR, "The number of section definitions (%d) is larger than the maximum allowed (%d)\n", nb_sections, SECTION_MAX_NB_SECTIONS);
        ret = AVERROR(EINVAL);
        goto fail;
    }

    tctx->formatter = formatter;
    tctx->level = -1;
    tctx->sections = sections;
    tctx->nb_sections = nb_sections;
    tctx->writer = writer_context;

    if (formatter->priv_class) {
        void *priv_ctx = tctx->priv;
        *(const AVClass **)priv_ctx = formatter->priv_class;
        av_opt_set_defaults(priv_ctx);
    }

    /* convert options to dictionary */
    if (args) {
        AVDictionary *opts = NULL;
        const AVDictionaryEntry *opt = NULL;

        if ((ret = av_dict_parse_string(&opts, args, "=", ":", 0)) < 0) {
            av_log(tctx, AV_LOG_ERROR, "Failed to parse option string '%s' provided to textformat context\n", args);
            av_dict_free(&opts);
            goto fail;
        }

        while ((opt = av_dict_iterate(opts, opt))) {
            if ((ret = av_opt_set(tctx, opt->key, opt->value, AV_OPT_SEARCH_CHILDREN)) < 0) {
                av_log(tctx, AV_LOG_ERROR, "Failed to set option '%s' with value '%s' provided to textformat context\n",
                       opt->key, opt->value);
                av_dict_free(&opts);
                goto fail;
            }
        }

        av_dict_free(&opts);
    }

    if (show_data_hash) {
        if ((ret = av_hash_alloc(&tctx->hash, show_data_hash)) < 0) {
            if (ret == AVERROR(EINVAL)) {
                const char *n;
                av_log(NULL, AV_LOG_ERROR, "Unknown hash algorithm '%s'\nKnown algorithms:", show_data_hash);
                for (unsigned i = 0; (n = av_hash_names(i)); i++)
                    av_log(NULL, AV_LOG_ERROR, " %s", n);
                av_log(NULL, AV_LOG_ERROR, "\n");
            }
            goto fail;
        }
    }

    /* validate replace string */
    {
        const uint8_t *p = (uint8_t *)tctx->string_validation_replacement;
        const uint8_t *endp = p + strlen((const char *)p);
        while (*p) {
            const uint8_t *p0 = p;
            int32_t code;
            ret = av_utf8_decode(&code, &p, endp, tctx->string_validation_utf8_flags);
            if (ret < 0) {
                AVBPrint bp;
                av_bprint_init(&bp, 0, AV_BPRINT_SIZE_AUTOMATIC);
                bprint_bytes(&bp, p0, p - p0);
                av_log(tctx, AV_LOG_ERROR,
                       "Invalid UTF8 sequence %s found in string validation replace '%s'\n",
                       bp.str, tctx->string_validation_replacement);
                goto fail;
            }
        }
    }

    if (tctx->formatter->init)
        ret = tctx->formatter->init(tctx);
    if (ret < 0)
        goto fail;

    *ptctx = tctx;

    return 0;

fail:
    avtext_context_close(&tctx);
    return ret;
}

/* Temporary definitions during refactoring */
static const char unit_second_str[]         = "s";
static const char unit_hertz_str[]          = "Hz";
static const char unit_byte_str[]           = "byte";
static const char unit_bit_per_second_str[] = "bit/s";


void avtext_print_section_header(AVTextFormatContext *tctx, const void *data, int section_id)
{
    if (section_id < 0 || section_id >= tctx->nb_sections) {
        av_log(tctx, AV_LOG_ERROR, "Invalid section_id for section_header: %d\n", section_id);
        return;
    }

    tctx->level++;
    av_assert0(tctx->level < SECTION_MAX_NB_LEVELS);

    tctx->nb_item[tctx->level] = 0;
    memset(tctx->nb_item_type[tctx->level], 0, sizeof(tctx->nb_item_type[tctx->level]));
    tctx->section[tctx->level] = &tctx->sections[section_id];

    if (tctx->formatter->print_section_header)
        tctx->formatter->print_section_header(tctx, data);
}

void avtext_print_section_footer(AVTextFormatContext *tctx)
{
    if (tctx->level < 0 || tctx->level >= SECTION_MAX_NB_LEVELS) {
        av_log(tctx, AV_LOG_ERROR, "Invalid level for section_footer: %d\n", tctx->level);
        return;
    }

    int section_id = tctx->section[tctx->level]->id;
    int parent_section_id = tctx->level ?
        tctx->section[tctx->level - 1]->id : SECTION_ID_NONE;

    if (parent_section_id != SECTION_ID_NONE) {
        tctx->nb_item[tctx->level - 1]++;
        tctx->nb_item_type[tctx->level - 1][section_id]++;
    }

    if (tctx->formatter->print_section_footer)
        tctx->formatter->print_section_footer(tctx);
    tctx->level--;
}

void avtext_print_integer(AVTextFormatContext *tctx, const char *key, int64_t val, int flags)
{
    const AVTextFormatSection *section;

    av_assert0(tctx);

    if (tctx->show_optional_fields == SHOW_OPTIONAL_FIELDS_NEVER)
        return;

    if (tctx->show_optional_fields == SHOW_OPTIONAL_FIELDS_AUTO
        && (flags & AV_TEXTFORMAT_PRINT_STRING_OPTIONAL)
        && !(tctx->formatter->flags & AV_TEXTFORMAT_FLAG_SUPPORTS_OPTIONAL_FIELDS))
        return;

    av_assert0(key && tctx->level >= 0 && tctx->level < SECTION_MAX_NB_LEVELS);

    section = tctx->section[tctx->level];

    if (section->show_all_entries || av_dict_get(section->entries_to_show, key, NULL, 0)) {
        tctx->formatter->print_integer(tctx, key, val);
        tctx->nb_item[tctx->level]++;
    }
}

static inline int validate_string(AVTextFormatContext *tctx, char **dstp, const char *src)
{
    const uint8_t *p, *endp, *srcp = (const uint8_t *)src;
    AVBPrint dstbuf;
    AVBPrint invalid_seq;
    int invalid_chars_nb = 0, ret = 0;

    *dstp = NULL;
    av_bprint_init(&dstbuf, 0, AV_BPRINT_SIZE_UNLIMITED);
    av_bprint_init(&invalid_seq, 0, AV_BPRINT_SIZE_UNLIMITED);

    endp = srcp + strlen(src);
    for (p = srcp; *p;) {
        int32_t code;
        int invalid = 0;
        const uint8_t *p0 = p;

        if (av_utf8_decode(&code, &p, endp, tctx->string_validation_utf8_flags) < 0) {

            av_bprint_clear(&invalid_seq);

            bprint_bytes(&invalid_seq, p0, p - p0);

            av_log(tctx, AV_LOG_DEBUG, "Invalid UTF-8 sequence '%s' found in string '%s'\n", invalid_seq.str, src);
            invalid = 1;
        }

        if (invalid) {
            invalid_chars_nb++;

            switch (tctx->string_validation) {
            case AV_TEXTFORMAT_STRING_VALIDATION_FAIL:
                av_log(tctx, AV_LOG_ERROR, "Invalid UTF-8 sequence found in string '%s'\n", src);
                ret = AVERROR_INVALIDDATA;
                goto end;

            case AV_TEXTFORMAT_STRING_VALIDATION_REPLACE:
                av_bprintf(&dstbuf, "%s", tctx->string_validation_replacement);
                break;
            }
        }

        if (!invalid || tctx->string_validation == AV_TEXTFORMAT_STRING_VALIDATION_IGNORE)
            av_bprint_append_data(&dstbuf, p0, p-p0);
    }

    if (invalid_chars_nb && tctx->string_validation == AV_TEXTFORMAT_STRING_VALIDATION_REPLACE)
        av_log(tctx, AV_LOG_WARNING,
               "%d invalid UTF-8 sequence(s) found in string '%s', replaced with '%s'\n",
               invalid_chars_nb, src, tctx->string_validation_replacement);

end:
    av_bprint_finalize(&dstbuf, dstp);
    av_bprint_finalize(&invalid_seq, NULL);
    return ret;
}

struct unit_value {
    union {
        double  d;
        int64_t i;
    } val;

    const char *unit;
};

static char *value_string(const AVTextFormatContext *tctx, char *buf, int buf_size, struct unit_value uv)
{
    double vald;
    int64_t vali = 0;
    int show_float = 0;

    if (uv.unit == unit_second_str) {
        vald = uv.val.d;
        show_float = 1;
    } else {
        vald = (double)uv.val.i;
        vali = uv.val.i;
    }

    if (uv.unit == unit_second_str && tctx->use_value_sexagesimal_format) {
        double secs;
        int hours, mins;
        secs  = vald;
        mins  = (int)secs / 60;
        secs  = secs - mins * 60;
        hours = mins / 60;
        mins %= 60;
        snprintf(buf, buf_size, "%d:%02d:%09.6f", hours, mins, secs);
    } else {
        const char *prefix_string = "";

        if (tctx->use_value_prefix && vald > 1) {
            int64_t index;

            if (uv.unit == unit_byte_str && tctx->use_byte_value_binary_prefix) {
                index = (int64_t)(log2(vald) / 10);
                index = av_clip64(index, 0, FF_ARRAY_ELEMS(si_prefixes) - 1);
                vald /= si_prefixes[index].bin_val;
                prefix_string = si_prefixes[index].bin_str;
            } else {
                index = (int64_t)(log10(vald) / 3);
                index = av_clip64(index, 0, FF_ARRAY_ELEMS(si_prefixes) - 1);
                vald /= si_prefixes[index].dec_val;
                prefix_string = si_prefixes[index].dec_str;
            }
            vali = (int64_t)vald;
        }

        if (show_float || (tctx->use_value_prefix && vald != (int64_t)vald))
            snprintf(buf, buf_size, "%f", vald);
        else
            snprintf(buf, buf_size, "%"PRId64, vali);

        av_strlcatf(buf, buf_size, "%s%s%s", *prefix_string || tctx->show_value_unit ? " " : "",
                    prefix_string, tctx->show_value_unit ? uv.unit : "");
    }

    return buf;
}


void avtext_print_unit_integer(AVTextFormatContext *tctx, const char *key, int64_t val, const char *unit)
{
    char val_str[128];
    struct unit_value uv;
    uv.val.i = val;
    uv.unit = unit;
    avtext_print_string(tctx, key, value_string(tctx, val_str, sizeof(val_str), uv), 0);
}


int avtext_print_string(AVTextFormatContext *tctx, const char *key, const char *val, int flags)
{
    const AVTextFormatSection *section;
    int ret = 0;

    av_assert0(key && val && tctx->level >= 0 && tctx->level < SECTION_MAX_NB_LEVELS);

    section = tctx->section[tctx->level];

    if (tctx->show_optional_fields == SHOW_OPTIONAL_FIELDS_NEVER)
        return 0;

    if (tctx->show_optional_fields == SHOW_OPTIONAL_FIELDS_AUTO
        && (flags & AV_TEXTFORMAT_PRINT_STRING_OPTIONAL)
        && !(tctx->formatter->flags & AV_TEXTFORMAT_FLAG_SUPPORTS_OPTIONAL_FIELDS))
        return 0;

    if (section->show_all_entries || av_dict_get(section->entries_to_show, key, NULL, 0)) {
        if (flags & AV_TEXTFORMAT_PRINT_STRING_VALIDATE) {
            char *key1 = NULL, *val1 = NULL;
            ret = validate_string(tctx, &key1, key);
            if (ret < 0) goto end;
            ret = validate_string(tctx, &val1, val);
            if (ret < 0) goto end;
            tctx->formatter->print_string(tctx, key1, val1);
        end:
            if (ret < 0)
                av_log(tctx, AV_LOG_ERROR,
                       "Invalid key=value string combination %s=%s in section %s\n",
                       key, val, section->unique_name);
            av_free(key1);
            av_free(val1);
        } else {
            tctx->formatter->print_string(tctx, key, val);
        }

        tctx->nb_item[tctx->level]++;
    }

    return ret;
}

void avtext_print_rational(AVTextFormatContext *tctx, const char *key, AVRational q, char sep)
{
    char buf[44];
    snprintf(buf, sizeof(buf), "%d%c%d", q.num, sep, q.den);
    avtext_print_string(tctx, key, buf, 0);
}

void avtext_print_time(AVTextFormatContext *tctx, const char *key,
                       int64_t ts, const AVRational *time_base, int is_duration)
{
    if ((!is_duration && ts == AV_NOPTS_VALUE) || (is_duration && ts == 0)) {
        avtext_print_string(tctx, key, "N/A", AV_TEXTFORMAT_PRINT_STRING_OPTIONAL);
    } else {
        char buf[128];
        double d = av_q2d(*time_base) * ts;
        struct unit_value uv;
        uv.val.d = d;
        uv.unit = unit_second_str;
        value_string(tctx, buf, sizeof(buf), uv);
        avtext_print_string(tctx, key, buf, 0);
    }
}

void avtext_print_ts(AVTextFormatContext *tctx, const char *key, int64_t ts, int is_duration)
{
    if ((!is_duration && ts == AV_NOPTS_VALUE) || (is_duration && ts == 0))
        avtext_print_string(tctx, key, "N/A", AV_TEXTFORMAT_PRINT_STRING_OPTIONAL);
    else
        avtext_print_integer(tctx, key, ts, 0);
}

void avtext_print_data(AVTextFormatContext *tctx, const char *key,
                       const uint8_t *data, int size)
{
    AVBPrint bp;
    unsigned offset = 0;
    int i;

    av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
    av_bprintf(&bp, "\n");
    while (size) {
        av_bprintf(&bp, "%08x: ", offset);
        int l = FFMIN(size, 16);
        for (i = 0; i < l; i++) {
            av_bprintf(&bp, "%02x", data[i]);
            if (i & 1)
                av_bprintf(&bp, " ");
        }
        av_bprint_chars(&bp, ' ', 41 - 2 * i - i / 2);
        for (i = 0; i < l; i++)
            av_bprint_chars(&bp, data[i] - 32U < 95 ? data[i] : '.', 1);
        av_bprintf(&bp, "\n");
        offset += l;
        data   += l;
        size   -= l;
    }
    avtext_print_string(tctx, key, bp.str, 0);
    av_bprint_finalize(&bp, NULL);
}

void avtext_print_data_hash(AVTextFormatContext *tctx, const char *key,
                            const uint8_t *data, int size)
{
    char buf[AV_HASH_MAX_SIZE * 2 + 64] = { 0 };
    int len;

    if (!tctx->hash)
        return;

    av_hash_init(tctx->hash);
    av_hash_update(tctx->hash, data, size);
    len = snprintf(buf, sizeof(buf), "%s:", av_hash_get_name(tctx->hash));
    av_hash_final_hex(tctx->hash, (uint8_t *)&buf[len], (int)sizeof(buf) - len);
    avtext_print_string(tctx, key, buf, 0);
}

void avtext_print_integers(AVTextFormatContext *tctx, const char *key,
                           uint8_t *data, int size, const char *format,
                           int columns, int bytes, int offset_add)
{
    AVBPrint bp;
    unsigned offset = 0;

    if (!key || !data || !format || columns <= 0 || bytes <= 0)
        return;

    av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
    av_bprintf(&bp, "\n");
    while (size) {
        av_bprintf(&bp, "%08x: ", offset);
        for (int i = 0, l = FFMIN(size, columns); i < l; i++) {
            if      (bytes == 1) av_bprintf(&bp, format, *data);
            else if (bytes == 2) av_bprintf(&bp, format, AV_RN16(data));
            else if (bytes == 4) av_bprintf(&bp, format, AV_RN32(data));
            data += bytes;
            size--;
        }
        av_bprintf(&bp, "\n");
        offset += offset_add;
    }
    avtext_print_string(tctx, key, bp.str, 0);
    av_bprint_finalize(&bp, NULL);
}

static const char *writercontext_get_writer_name(void *p)
{
    AVTextWriterContext *wctx = p;
    return wctx->writer->name;
}

static void *writercontext_child_next(void *obj, void *prev)
{
    AVTextFormatContext *ctx = obj;
    if (!prev && ctx->formatter && ctx->formatter->priv_class && ctx->priv)
        return ctx->priv;
    return NULL;
}

static const AVClass textwriter_class = {
    .class_name = "AVTextWriterContext",
    .item_name  = writercontext_get_writer_name,
    .version    = LIBAVUTIL_VERSION_INT,
    .child_next = writercontext_child_next,
};


int avtextwriter_context_close(AVTextWriterContext **pwctx)
{
    AVTextWriterContext *wctx = *pwctx;
    int ret = 0;

    if (!wctx)
        return AVERROR(EINVAL);

    if (wctx->writer) {
        if (wctx->writer->uninit)
            ret = wctx->writer->uninit(wctx);
        if (wctx->writer->priv_class)
            av_opt_free(wctx->priv);
    }
    av_freep(&wctx->priv);
    av_freep(pwctx);
    return ret;
}


int avtextwriter_context_open(AVTextWriterContext **pwctx, const AVTextWriter *writer)
{
    AVTextWriterContext *wctx;
    int ret = 0;

    if (!pwctx || !writer)
        return AVERROR(EINVAL);

    if (!((wctx = av_mallocz(sizeof(AVTextWriterContext))))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    if (writer->priv_size && !((wctx->priv = av_mallocz(writer->priv_size)))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    if (writer->priv_class) {
        void *priv_ctx = wctx->priv;
        *(const AVClass **)priv_ctx = writer->priv_class;
        av_opt_set_defaults(priv_ctx);
    }

    wctx->class = &textwriter_class;
    wctx->writer = writer;

    av_opt_set_defaults(wctx);


    if (wctx->writer->init)
        ret = wctx->writer->init(wctx);
    if (ret < 0)
        goto fail;

    *pwctx = wctx;

    return 0;

fail:
    avtextwriter_context_close(&wctx);
    return ret;
}

static const AVTextFormatter *const registered_formatters[] =
{
    &avtextformatter_default,
    &avtextformatter_compact,
    &avtextformatter_csv,
    &avtextformatter_flat,
    &avtextformatter_ini,
    &avtextformatter_json,
    &avtextformatter_xml,
    &avtextformatter_mermaid,
    &avtextformatter_mermaidhtml,
    NULL
};

const AVTextFormatter *avtext_get_formatter_by_name(const char *name)
{
    for (int i = 0; registered_formatters[i]; i++) {
        const char *end;
        if (av_strstart(name, registered_formatters[i]->name, &end) &&
            (*end == '\0' || *end == '='))
            return registered_formatters[i];
    }

    return NULL;
}
