#include "kmparseappdata.h"

#include "common/KMUtil.h"

static void componentFree(gpointer data)
{
    Component *component = static_cast<Component *>(data);

    g_hash_table_unref(component->names);
    g_hash_table_unref(component->comments);
    g_free(component->id);
    g_free(component->version);
    g_free(component->license);
    g_free(component->contentRatingType);
    g_clear_pointer(&component->contentRating, g_hash_table_unref);

    g_free(component);
}

static Component *componentNew(void)
{
    Component *component = g_new0(Component, 1);

    component->names = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    component->comments = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

    return component;
}

static void parserDataFree(ParserData *data)
{
    g_ptr_array_unref(data->components);
    g_string_free(data->text, TRUE);
    g_free(data->lang);

    g_free(data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(ParserData, parserDataFree)

static ParserData *parserDataNew(void)
{
    ParserData *data = g_new0(ParserData, 1);

    data->components = g_ptr_array_new_with_free_func(componentFree);
    data->text = g_string_new("");

    return data;
}

static void startElement(GMarkupParseContext *context, const char *elementName, const char **attributeNames, const char **attributeValues, gpointer userData, GError **error)
{
    ParserData *data = static_cast<ParserData *>(userData);

    g_assert(data->text->len == 0);
    g_assert(data->lang == NULL);

    if (g_str_equal(elementName, "component"))
    {
        g_ptr_array_add(data->components, componentNew());
    }
    else if (g_str_equal(elementName, "id"))
    {
        data->inText = TRUE;
    }
    else if (g_str_equal(elementName, "name") || g_str_equal(elementName, "summary"))
    {
        const char *lang = NULL;

        if (g_markup_collect_attributes(elementName, attributeNames, attributeValues, error, (GMarkupCollectType)(G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL), "xml:lang", &lang, (GMarkupCollectType)G_MARKUP_COLLECT_INVALID))
        {
            if (lang)
            {
                data->lang = g_strdup(lang);
            }
            else
            {
                data->lang = g_strdup("C");
            }
            data->inText = TRUE;
        }
    }
    else if (g_str_equal(elementName, "project_license"))
    {
        data->inText = TRUE;
    }
    else if (g_str_equal(elementName, "release"))
    {
        const char *timestamp;
        const char *date;
        const char *version;
        Component *component = NULL;

        g_assert(data->components->len > 0);

        component = (Component *)g_ptr_array_index(data->components, data->components->len - 1);

        if (g_markup_collect_attributes(elementName,
                                        attributeNames,
                                        attributeValues,
                                        error,
                                        G_MARKUP_COLLECT_STRING,
                                        "version",
                                        &version,
                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL,
                                        "timestamp",
                                        &timestamp,
                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL,
                                        "date",
                                        &date,
                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL,
                                        "date_eol",
                                        NULL,
                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL,
                                        "urgency",
                                        NULL,
                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL,
                                        "type",
                                        NULL,
                                        G_MARKUP_COLLECT_INVALID))
        {
            guint64 ts = 0;

            if (timestamp)
            {
                ts = g_ascii_strtoull(timestamp, NULL, 10);
            }
            else if (date)
            {
                g_autoptr(GTimeZone) tz = g_time_zone_new_utc();
                g_autoptr(GDateTime) dt = g_date_time_new_from_iso8601(date, tz);
                if (!dt)
                {
                    int d, m, y;

                    if (sscanf(date, "%u-%u-%u", &d, &m, &y) == 3)
                    {
                        dt = g_date_time_new_utc(d, m, y, 0, 0, 0);
                    }
                }
                if (dt)
                {
                    ts = g_date_time_to_unix(dt);
                }
            }
            else
                kmlogger.warn("Ignoring release element without timestamp or date");

            if (ts > data->timestamp)
            {
                data->timestamp = ts;
                g_free(component->version);
                component->version = g_strdup(version);
            }
        }
    }
    else if (g_str_equal(elementName, "contentRating"))
    {
        Component *component = NULL;

        g_assert(data->components->len > 0);

        component = (Component *)g_ptr_array_index(data->components, data->components->len - 1);

        if (component->contentRating == NULL)
        {
            const gchar *type = NULL;

            if (g_markup_collect_attributes(elementName, attributeNames, attributeValues, error, G_MARKUP_COLLECT_STRING, "type", &type, G_MARKUP_COLLECT_INVALID))
            {
                component->contentRatingType = g_strdup(type);
                component->contentRating = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);
                data->inContentRating = TRUE;
            }
            else
            {
                kmlogger.warn("Ignoring content rating missing type attribute");
            }
        }
        else
        {
            kmlogger.warn("Ignoring duplicate content rating");
        }
    }
    else if (data->inContentRating && g_str_equal(elementName, "content_attribute"))
    {
        Component *component = NULL;
        const gchar *id = NULL;

        g_assert(data->components->len > 0);

        component = (Component *)g_ptr_array_index(data->components, data->components->len - 1);
        g_assert(component->contentRating != NULL);

        if (g_markup_collect_attributes(elementName, attributeNames, attributeValues, error, G_MARKUP_COLLECT_STRING, "id", &id, G_MARKUP_COLLECT_INVALID))
        {
            data->id = g_intern_string(id);
            data->inText = TRUE;
        }
        else
        {
            kmlogger.warn("Ignoring content attribute missing id attribute");
        }
    }
}

static void endElement(GMarkupParseContext *context, const char *elementName, gpointer userData, GError **error)
{
    ParserData *data = static_cast<ParserData *>(userData);
    g_autofree char *text = NULL;
    Component *component = NULL;
    const char *parent = NULL;
    const GSList *elements;

    elements = g_markup_parse_context_get_element_stack(context);
    if (elements->next)
    {
        parent = (const char *)elements->next->data;
    }

    g_assert(data->components->len > 0);

    component = (Component *)g_ptr_array_index(data->components, data->components->len - 1);

    if (data->inText)
    {
        text = g_strdup(data->text->str);
        g_string_truncate(data->text, 0);
        data->inText = FALSE;
    }

    /* 避免从 <provides> 中选取 <id> 元素 */
    if (g_str_equal(elementName, "id") && g_str_equal(parent, "component"))
    {
        component->id = KMUtil::stealPointer(&text);
    }
    else if (g_str_equal(elementName, "name"))
    {
        g_hash_table_insert(component->names, KMUtil::stealPointer(&data->lang), KMUtil::stealPointer(&text));
    }
    else if (g_str_equal(elementName, "summary"))
    {
        g_hash_table_insert(component->comments, KMUtil::stealPointer(&data->lang), KMUtil::stealPointer(&text));
    }
    else if (g_str_equal(elementName, "project_license"))
    {
        component->license = KMUtil::stealPointer(&text);
    }
    else if (g_str_equal(elementName, "contentRating"))
    {
        data->inContentRating = FALSE;
    }
    else if (data->inContentRating && g_str_equal(elementName, "content_attribute"))
    {
        /* https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-contentRating */
        g_assert(component->contentRating != NULL);
        g_hash_table_insert(component->contentRating, (gpointer)data->id, (gpointer)g_intern_string(text));
    }
}

static void text(GMarkupParseContext *context, const char *text, gsize textLen, gpointer userData, GError **error)
{
    ParserData *data = static_cast<ParserData *>(userData);

    if (data->inText)
    {
        g_string_append_len(data->text, text, textLen);
    }
}

bool parseAppdata(const char *appdataXml, const char *appDataId, GHashTable **names, GHashTable **comments, char **version, char **license, char **contentRatingType, GHashTable **contentRating)
{
    g_autoptr(GMarkupParseContext) context = NULL;
    GMarkupParser parser = { startElement, endElement, text, NULL, NULL };
    g_autoptr(ParserData) data = parserDataNew();
    g_autoptr(GError) error = NULL;
    int i;
    g_autofree char *legacyId = NULL;

    context = g_markup_parse_context_new(&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, data, NULL);

    if (!g_markup_parse_context_parse(context, appdataXml, -1, &error))
    {
        kmlogger.warn("Failed to parse appdata: %s", error->message);
        return false;
    }

    legacyId = g_strconcat(appDataId, ".desktop", NULL);

    for (i = 0; i < data->components->len; i++)
    {
        Component *component = (Component *)g_ptr_array_index(data->components, i);

        if (g_str_equal(component->id, appDataId) || g_str_equal(component->id, legacyId))
        {
            *names = g_hash_table_ref(component->names);
            *comments = g_hash_table_ref(component->comments);
            *version = KMUtil::stealPointer(&component->version);
            *license = KMUtil::stealPointer(&component->license);
            if (contentRatingType != NULL)
            {
                *contentRatingType = KMUtil::stealPointer(&component->contentRatingType);
            }
            if (contentRating != NULL)
            {
                *contentRating = KMUtil::stealPointer(&component->contentRating);
            }
            return true;
        }
    }

    kmlogger.warn("No matching appdata for %s", appDataId);
    return false;
}
