/*
 * Copyright (C) 2012 Intel Corporation.
 *
 * Authors: Jens Georg <jensg@openismus.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:gupnp-cds-last-change-parser
 * @short_description: LastChange parser for the format used in
 * CDS:3
 *
 * #GUPnPCDSLastChangeParser parses XML strings from
 * CDS's LastChange state variable.
 *
 */

#include "xml-util.h"
#include "gupnp-cds-last-change-parser.h"

/**
 * GUPnPCDSLastChangeEntry:
 *
 * Opaque struct which contains information about the event.
 **/
struct _GUPnPCDSLastChangeEntry {
        int ref_count;
        GUPnPCDSLastChangeEvent event;
        char *object_id;
        char *parent_id;
        char *class;
        guint32 update_id;
        gboolean is_subtree_update;
};

G_DEFINE_TYPE (GUPnPCDSLastChangeParser,
               gupnp_cds_last_change_parser,
               G_TYPE_OBJECT)

G_DEFINE_BOXED_TYPE (GUPnPCDSLastChangeEntry,
                     gupnp_cds_last_change_entry,
                     gupnp_cds_last_change_entry_ref,
                     gupnp_cds_last_change_entry_unref);

static void
gupnp_cds_last_change_parser_init (G_GNUC_UNUSED GUPnPCDSLastChangeParser *parser)
{
}

static void
gupnp_cds_last_change_parser_class_init (G_GNUC_UNUSED GUPnPCDSLastChangeParserClass *klass)
{
}

/**
 * gupnp_cds_last_change_parser_new:
 *
 * Create a new #GUPnPCDSLastChangeParser.
 *
 * This parser is able to parse LastChange as defined in the
 * ContentDirectory:3 specification.
 *
 * Returns:(transfer full): A new instance of #GUPnPCDSLastChangeParser.
 **/
GUPnPCDSLastChangeParser *
gupnp_cds_last_change_parser_new (void)
{
        return g_object_new (GUPNP_TYPE_CDS_LAST_CHANGE_PARSER,
                             NULL);
}

/**
 * gupnp_cds_last_change_parser_parse:
 * @parser: #GUPnPCDSLastChangeParser
 * @last_change: XML string to parse
 * @error: Return value for parser error or %NULL to ingore
 *
 * Parse a LastChange XML document in the flavor defined by the
 * ContentDirectory:3 specification.
 *
 * Returns: (element-type GUPnPCDSLastChangeEntry)(transfer full):
 * List of #GUPnPCDSLastChangeEntry<!-- -->s
 **/
GList *
gupnp_cds_last_change_parser_parse (GUPnPCDSLastChangeParser *parser,
                                    const char               *last_change,
                                    GError                  **error)

{
        xmlDoc *doc;
        xmlNode *state_event, *it;
        GList *result = NULL;
        GUPnPCDSLastChangeEntry *entry;

        g_return_val_if_fail (GUPNP_IS_CDS_LAST_CHANGE_PARSER (parser),
                              NULL);

        doc = xmlParseDoc ((const xmlChar *) last_change);
        if (doc == NULL) {
                g_set_error (error,
                             G_MARKUP_ERROR,
                             G_MARKUP_ERROR_PARSE,
                             "Could not parse LastChange XML");

                goto out;
        }

        state_event = xml_util_get_element ((xmlNode *) doc,
                                            "StateEvent",
                                            NULL);
        if (state_event == NULL) {
                g_set_error (error,
                             G_MARKUP_ERROR,
                             G_MARKUP_ERROR_PARSE,
                             "Missing StateEvent node");

                goto out;
        }

        for (it = state_event->children; it != NULL; it = it->next) {
                if (it->type == XML_TEXT_NODE)
                        continue;
                else if (g_ascii_strcasecmp ((const char *) it->name,
                                             "objAdd") == 0) {
                        const char *tmp;

                        entry = g_slice_new0 (GUPnPCDSLastChangeEntry);
                        entry->ref_count = 1;
                        entry->event = GUPNP_CDS_LAST_CHANGE_EVENT_OBJECT_ADDED;

                        tmp = xml_util_get_attribute_content (it, "objID");
                        entry->object_id = g_strdup (tmp);

                        tmp = xml_util_get_attribute_content (it,
                                                              "objParentID");
                        entry->parent_id = g_strdup (tmp);

                        tmp = xml_util_get_attribute_content (it, "objClass");
                        entry->class = g_strdup (tmp);

                        entry->update_id = (guint32) xml_util_get_uint_attribute
                                        (it,
                                         "updateID",
                                         0);
                        entry->is_subtree_update =
                                        xml_util_get_boolean_attribute
                                                (it,
                                                 "stUpdate");
                } else if (g_ascii_strcasecmp ((const char *) it->name,
                                               "objMod") == 0) {
                        const char *tmp;

                        entry = g_slice_new0 (GUPnPCDSLastChangeEntry);
                        entry->ref_count = 1;
                        entry->event = GUPNP_CDS_LAST_CHANGE_EVENT_OBJECT_MODIFIED;

                        tmp = xml_util_get_attribute_content (it, "objID");
                        entry->object_id = g_strdup (tmp);

                        entry->update_id = (guint32) xml_util_get_uint_attribute
                                        (it,
                                         "updateID",
                                         0);
                        entry->is_subtree_update =
                                        xml_util_get_boolean_attribute
                                                (it,
                                                 "stUpdate");
                } else if (g_ascii_strcasecmp ((const char *) it->name,
                                               "objDel") == 0) {
                        const char *tmp;

                        entry = g_slice_new0 (GUPnPCDSLastChangeEntry);
                        entry->ref_count = 1;
                        entry->event = GUPNP_CDS_LAST_CHANGE_EVENT_OBJECT_REMOVED;

                        tmp = xml_util_get_attribute_content (it, "objID");
                        entry->object_id = g_strdup (tmp);

                        entry->update_id = (guint32) xml_util_get_uint_attribute
                                        (it,
                                         "updateID",
                                         0);
                        entry->is_subtree_update =
                                        xml_util_get_boolean_attribute
                                                (it,
                                                 "stUpdate");
                } else if (g_ascii_strcasecmp ((const char *) it->name,
                                               "stDone") == 0) {
                        const char *tmp;

                        entry = g_slice_new0 (GUPnPCDSLastChangeEntry);
                        entry->ref_count = 1;
                        entry->event = GUPNP_CDS_LAST_CHANGE_EVENT_ST_DONE;

                        tmp = xml_util_get_attribute_content (it, "objID");
                        entry->object_id = g_strdup (tmp);

                        entry->update_id = (guint32) xml_util_get_uint_attribute
                                        (it,
                                         "updateID",
                                         0);
                } else {
                        g_warning ("Skipping invalid LastChange entry: %s",
                                   (const char *) it->name);
                        continue;
                }

                result = g_list_prepend (result, entry);
        }

        result = g_list_reverse (result);
out:
        if (doc != NULL) {
                xmlFreeDoc (doc);
        }

        return result;

}

/**
 * gupnp_cds_last_change_entry_ref:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Increase reference count of a #GUPnPCDSLastChangeEntry.
 *
 * Returns:(transfer full): The object passed in @entry.
 **/
GUPnPCDSLastChangeEntry *
gupnp_cds_last_change_entry_ref (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL, NULL);
        g_return_val_if_fail (entry->ref_count > 0, NULL);

        g_atomic_int_inc (&entry->ref_count);

        return entry;
}

/**
 * gupnp_cds_last_change_entry_unref:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Decrease reference count of a #GUPnPCDSLastChangeEntry. If the reference
 * count drops to 0, @entry is freed.
 **/
void
gupnp_cds_last_change_entry_unref (GUPnPCDSLastChangeEntry *entry)
{
        g_return_if_fail (entry != NULL);
        g_return_if_fail (entry->ref_count > 0);

        if (g_atomic_int_dec_and_test (&entry->ref_count)) {
                g_free (entry->class);
                g_free (entry->object_id);
                g_free (entry->parent_id);

                g_slice_free (GUPnPCDSLastChangeEntry, entry);
        }
}

/**
 * gupnp_cds_last_change_entry_get_event:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Get the type of the last change entry as defined in
 * #GUPnPCDSLastChangeEvent.
 *
 * Returns: An event from the #GUPnPCDSLastChangeEvent or
 * %GUPNP_CDS_LAST_CHANGE_EVENT_INVALID if the entry is not valid.
 **/
GUPnPCDSLastChangeEvent
gupnp_cds_last_change_entry_get_event (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL,
                              GUPNP_CDS_LAST_CHANGE_EVENT_INVALID);

        return entry->event;
}

/**
 * gupnp_cds_last_change_entry_get_object_id:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Get the ID of the object in this change entry.
 *
 * Returns: (transfer none): The id of the object of this entry.
 **/
const char *
gupnp_cds_last_change_entry_get_object_id (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL, NULL);

        return entry->object_id;
}

/**
 * gupnp_cds_last_change_entry_get_parent_id:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Get the parent object id of the object in this change entry. This is only
 * valid if gupnp_cds_last_change_entry_get_event() returns
 * %GUPNP_CDS_LAST_CHANGE_EVENT_OBJECT_ADDED.
 *
 * Returns: (transfer none): The id of the object's parent of this entry.
 **/
const char *
gupnp_cds_last_change_entry_get_parent_id (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL, NULL);

        return entry->parent_id;
}

/**
 * gupnp_cds_last_change_entry_get_class:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Get the class of the object in this change entry. This is only
 * valid if gupnp_cds_last_change_entry_get_event() returns
 * %GUPNP_CDS_LAST_CHANGE_EVENT_OBJECT_ADDED.
 *
 * Returns: (transfer none): The upnp class of the object of this entry.
 **/
const char *
gupnp_cds_last_change_entry_get_class (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL, NULL);

        return entry->class;
}

/**
 * gupnp_cds_last_change_entry_is_subtree_update:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Returns whether this entry is part of a subtree update.
 *
 * Returns: %TRUE, if the entry is part of a subtree update, %FALSE otherwise.
 **/
gboolean
gupnp_cds_last_change_entry_is_subtree_update (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL, FALSE);

        return entry->is_subtree_update;
}

/**
 * gupnp_cds_last_change_entry_get_update_id:
 * @entry: A #GUPnPCDSLastChangeEntry
 *
 * Get the update id of the last change entry.
 *
 * Returns: update id of the entry or 0 if the entry is not valid.
 **/
guint32
gupnp_cds_last_change_entry_get_update_id (GUPnPCDSLastChangeEntry *entry)
{
        g_return_val_if_fail (entry != NULL, 0);

        return entry->update_id;
}
