/*
 * 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 <inttypes.h>
#include <stddef.h>

#include "avtextformat.h"
#include "libavutil/bprint.h"
#include "libavutil/error.h"
#include "libavutil/opt.h"
#include "tf_internal.h"

/* XML output */

typedef struct XMLContext {
    const AVClass *class;
    int within_tag;
    int indent_level;
    int fully_qualified;
    int xsd_strict;
} XMLContext;

#undef OFFSET
#define OFFSET(x) offsetof(XMLContext, x)

static const AVOption xml_options[] = {
    { "fully_qualified", "specify if the output should be fully qualified", OFFSET(fully_qualified), AV_OPT_TYPE_BOOL, { .i64 = 0 },  0, 1 },
    { "q",               "specify if the output should be fully qualified", OFFSET(fully_qualified), AV_OPT_TYPE_BOOL, { .i64 = 0 },  0, 1 },
    { "xsd_strict",      "ensure that the output is XSD compliant",         OFFSET(xsd_strict),      AV_OPT_TYPE_BOOL, { .i64 = 0 },  0, 1 },
    { "x",               "ensure that the output is XSD compliant",         OFFSET(xsd_strict),      AV_OPT_TYPE_BOOL, { .i64 = 0 },  0, 1 },
    { NULL },
};

DEFINE_FORMATTER_CLASS(xml);

static av_cold int xml_init(AVTextFormatContext *wctx)
{
    XMLContext *xml = wctx->priv;

    if (xml->xsd_strict) {
        xml->fully_qualified = 1;
#define CHECK_COMPLIANCE(opt, opt_name)                                 \
        if (opt) {                                                      \
            av_log(wctx, AV_LOG_ERROR,                                  \
                   "XSD-compliant output selected but option '%s' was selected, XML output may be non-compliant.\n" \
                   "You need to disable such option with '-no%s'\n", opt_name, opt_name); \
            return AVERROR(EINVAL);                                     \
        }
        ////CHECK_COMPLIANCE(show_private_data, "private");
        CHECK_COMPLIANCE(wctx->opts.show_value_unit,   "unit");
        CHECK_COMPLIANCE(wctx->opts.use_value_prefix,  "prefix");
    }

    return 0;
}

#define XML_INDENT() writer_printf(wctx, "%*c", xml->indent_level * 4, ' ')

static void xml_print_section_header(AVTextFormatContext *wctx, const void *data)
{
    XMLContext *xml = wctx->priv;
    const AVTextFormatSection *section = tf_get_section(wctx, wctx->level);
    const AVTextFormatSection *parent_section = tf_get_parent_section(wctx, wctx->level);

    if (!section)
        return;

    if (wctx->level == 0) {
        const char *qual = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
            "xmlns:ffprobe=\"http://www.ffmpeg.org/schema/ffprobe\" "
            "xsi:schemaLocation=\"http://www.ffmpeg.org/schema/ffprobe ffprobe.xsd\"";

        writer_put_str(wctx, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        writer_printf(wctx, "<%sffprobe%s>\n",
                      xml->fully_qualified ? "ffprobe:" : "",
                      xml->fully_qualified ? qual : "");
        return;
    }

    if (xml->within_tag) {
        xml->within_tag = 0;
        writer_put_str(wctx, ">\n");
    }

    if (parent_section && (parent_section->flags & AV_TEXTFORMAT_SECTION_FLAG_IS_WRAPPER) &&
        wctx->level && wctx->nb_item[wctx->level - 1])
        writer_w8(wctx, '\n');
    xml->indent_level++;

    if (section->flags & (AV_TEXTFORMAT_SECTION_FLAG_IS_ARRAY | AV_TEXTFORMAT_SECTION_FLAG_HAS_VARIABLE_FIELDS)) {
        XML_INDENT();
        writer_printf(wctx, "<%s", section->name);

        if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_HAS_TYPE) {
            AVBPrint buf;
            av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
            av_bprint_escape(&buf, section->get_type(data), NULL,
                             AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
            writer_printf(wctx, " type=\"%s\"", buf.str);
        }
        writer_printf(wctx, ">\n", section->name);
    } else {
        XML_INDENT();
        writer_printf(wctx, "<%s ", section->name);
        xml->within_tag = 1;
    }
}

static void xml_print_section_footer(AVTextFormatContext *wctx)
{
    XMLContext *xml = wctx->priv;
    const AVTextFormatSection *section = tf_get_section(wctx, wctx->level);

    if (!section)
        return;

    if (wctx->level == 0) {
        writer_printf(wctx, "</%sffprobe>\n", xml->fully_qualified ? "ffprobe:" : "");
    } else if (xml->within_tag) {
        xml->within_tag = 0;
        writer_put_str(wctx, "/>\n");
        xml->indent_level--;
    } else {
        XML_INDENT();
        writer_printf(wctx, "</%s>\n", section->name);
        xml->indent_level--;
    }
}

static void xml_print_value(AVTextFormatContext *wctx, const char *key,
                            const char *str, int64_t num, const int is_int)
{
    AVBPrint buf;
    XMLContext *xml = wctx->priv;
    const AVTextFormatSection *section = tf_get_section(wctx, wctx->level);

    if (!section)
        return;

    av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);

    if (section->flags & AV_TEXTFORMAT_SECTION_FLAG_HAS_VARIABLE_FIELDS) {
        xml->indent_level++;
        XML_INDENT();
        av_bprint_escape(&buf, key, NULL,
                         AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
        writer_printf(wctx, "<%s key=\"%s\"",
                      section->element_name, buf.str);
        av_bprint_clear(&buf);

        if (is_int) {
            writer_printf(wctx, " value=\"%"PRId64"\"/>\n", num);
        } else {
            av_bprint_escape(&buf, str, NULL,
                             AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
            writer_printf(wctx, " value=\"%s\"/>\n", buf.str);
        }
        xml->indent_level--;
    } else {
        if (wctx->nb_item[wctx->level])
            writer_w8(wctx, ' ');

        if (is_int) {
            writer_printf(wctx, "%s=\"%"PRId64"\"", key, num);
        } else {
            av_bprint_escape(&buf, str, NULL,
                             AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
            writer_printf(wctx, "%s=\"%s\"", key, buf.str);
        }
    }

    av_bprint_finalize(&buf, NULL);
}

static inline void xml_print_str(AVTextFormatContext *wctx, const char *key, const char *value)
{
    xml_print_value(wctx, key, value, 0, 0);
}

static void xml_print_int(AVTextFormatContext *wctx, const char *key, int64_t value)
{
    xml_print_value(wctx, key, NULL, value, 1);
}

const AVTextFormatter avtextformatter_xml = {
    .name                 = "xml",
    .priv_size            = sizeof(XMLContext),
    .init                 = xml_init,
    .print_section_header = xml_print_section_header,
    .print_section_footer = xml_print_section_footer,
    .print_integer        = xml_print_int,
    .print_string         = xml_print_str,
    .flags = AV_TEXTFORMAT_FLAG_SUPPORTS_MIXED_ARRAY_CONTENT,
    .priv_class           = &xml_class,
};
