/* contentdir.c - Implementation of UPnP ContentDirectory
 *
 * Copyright (C) 2005, 2006, 2007  Oskar Liljeblad
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License along
 * with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <config.h>
#include <stdint.h>		/* Gnulib/C99 */
#include <inttypes.h>		/* ? */
#include <sys/stat.h>		/* POSIX */
#include <assert.h>
#include "gettext.h"		/* Gnulib/gettext */
#define _(s) gettext(s)
#define N_(s) gettext_noop(s)
#include "xvasprintf.h"		/* Gnulib */
#include "minmax.h"		/* Gnulib */
#include "xalloc.h"		/* Gnulib */
#include "quotearg.h"		/* Gnulib */
#include "dirname.h"		/* Gnulib */
#include "c-ctype.h"		/* Gnulib */
#include "strbuf.h"
#include "intutil.h"
#include "strutil.h"
#include "tmap.h"
#include "gmediaserver.h"
#include "search-parser.h"
#include "search-lexer.h"

extern int yyparse(yyscan_t scanner, SearchCriteriaParseData *data);

static uint32_t update_id = 0;

static ServiceVariable *
find_variable(const char *name)
{
    int c;
    for (c = 0; contentdir_service_variables[c].name != NULL; c++) {
        if (strcmp(contentdir_service_variables[c].name, name) == 0)
            return &contentdir_service_variables[c];
    }
    assert(0); /* Shouldn't get here */
}

void
bump_update_id(void)
{
    ServiceVariable *var;

    var = find_variable("SystemUpdateID");
    update_id++;
    free(var->value);
    var->value = xstrdup(uint32_str(update_id));
    notify_change(CONTENTDIR_SERVICE_ID, var);
}

static char *
get_entry_property(const Entry *entry, char *property)
{
    EntryDetail *detail;

    detail = get_entry_detail(entry, DETAIL_TAG);

    /* XXX: should we allow namespace-less property names,
     * such as "title" rather than "dc:title"?
     * XXX: should we allow attribute properties,
     * such as @refID, @id, @parentID or @protocolInfo?
     */

    /* See the ContentDirectory spec for an example where @id is used. */
    if (strcmp(property, "id") == 0 || strcmp(property, "@id") == 0) {
        char *t1, *t2;
        t1 = xasprintf("%" PRId32, entry->id); /* " */
        t2 = convert_string_to_device(t1);
        free(t1);
        return t2;
    }

    if (strcmp(property, "dc:title") == 0) {
	if (detail != NULL && detail->data.tag.title != NULL && strcmp(detail->data.tag.title, "") != 0)
	    return convert_string_to_device(detail->data.tag.title);
        return convert_string_to_device(entry->name);
    }

    if (strcmp(property, "upnp:class") == 0) {
	if (has_entry_detail(entry, DETAIL_CHILDREN))
	    return xstrdup("object.container.storageFolder");
        /*if (has_entry_detail(entry, DETAIL_URL))
            return xstrdup("object.item.audioItem.audioBroadcast");*/
        
        detail = get_entry_detail(entry, DETAIL_FILE);
        switch (detail->data.file.item_class) {
        case ITEM_AUDIO:
            return xstrdup("object.item.audioItem.musicTrack");
        case ITEM_IMAGE:
            return xstrdup("object.item.imageItem.photo");
        case ITEM_VIDEO:
            return xstrdup("object.item.videoItem.movie");
        case ITEM_PLAYLIST:
        case ITEM_TEXT:
        case ITEM_UNKNOWN:
        default:
            /* FIXME */
            return NULL;
        }
    }

    if (detail != NULL) {
        /* XXX: Accept dc:creator as a synonym to upnp:artist? */
	if (strcmp(property, "upnp:artist") == 0 && detail->data.tag.artist != NULL)
	    return convert_string_to_device(detail->data.tag.artist);
	if (strcmp(property, "upnp:album") == 0 && detail->data.tag.album != NULL)
	    return convert_string_to_device(detail->data.tag.album);
	if (strcmp(property, "upnp:genre") == 0 && detail->data.tag.genre != NULL)
	    return convert_string_to_device(detail->data.tag.genre);
        if (strcmp(property, "upnp:originalTrackNumber") == 0 && detail->data.tag.track_no != -1) {
            char *t1, *t2;
            t1 = xasprintf("%" PRId32, detail->data.tag.track_no);
            t2 = convert_string_to_device(t1);
            free(t1);
            return t2;
        }
    }

    return NULL;
}

int
sort_criteria_comparision(const void *k1, const void *k2, void *data)
{
    SortCriteria *criteria = data;
    SortCriteriaEntry *sort_entry;
    const Entry *e1 = k1;
    const Entry *e2 = k2;

    sort_entry = criteria->first;
    while (sort_entry != NULL) {
	char *p1 = get_entry_property(e1, sort_entry->property);
	char *p2 = get_entry_property(e2, sort_entry->property);

	if (p1 == NULL && p2 != NULL) {
	    free(p2);
	    return (sort_entry->ascending ? -1 : 1);
	}
	if (p1 != NULL && p2 == NULL) {
	    free(p1);
	    return (sort_entry->ascending ? 1 : -1);
	}
	if (p1 != NULL && p2 != NULL) {
	    int compare;

	    compare = strcmp(p1, p2);
	    free(p1);
	    free(p2);
	    if (compare != 0)
		return (sort_entry->ascending ? compare : -compare);
	}
    }

    return e1-e2;
}

void
free_sort_criteria(SortCriteria *criteria)
{
    SortCriteriaEntry *entry;

    for (entry = criteria->first; entry != NULL; ) {
	SortCriteriaEntry *next = entry->next;
	free(entry->property);
	free(entry);
	entry = next;
    }

    free(criteria);
}

void
free_search_criteria(SearchCriteria *criteria)
{
    if (criteria->expr != NULL)
	free_search_expr(criteria->expr);

    free(criteria);
}

void
free_search_expr(SearchExpr *expr)
{
    switch (expr->type) {
    case T_AND:
    case T_OR:
	free_search_expr(expr->u.logical.expr1);
	free_search_expr(expr->u.logical.expr2);
	break;
    case T_EXISTS:
	free(expr->u.exists.property);
	break;
    default:
	free(expr->u.binary.property);
	free(expr->u.binary.value);
	break;
    }

    free(expr);
}

static bool
contentdir_get_search_capabilities(ActionEvent *event)
{
    upnp_add_response(event, "SearchCaps", "*");
    return event->status;
}

static bool
contentdir_get_sort_capabilities(ActionEvent *event)
{
    upnp_add_response(event, "SortCaps", "*");
    return event->status;
}

static bool
contentdir_get_system_update_id(ActionEvent *event)
{
    upnp_add_response(event, "Id", uint32_str(update_id));
    return event->status;
}

static void
append_escaped_xml(StrBuf *out, const char *entity, const char *value)
{
    char *str;

    str = xsgmlescape(value);
    strbuf_appendf(out, "<%s>%s</%s>", entity, str, entity);
    free(str);
}

TMap *
new_result_set(SortCriteria *criteria)
{
    TMap *results;

    results = tmap_new();
    tmap_set_complex_compare_fn(results, sort_criteria_comparision, criteria);

    return results;
}

void
free_result_set(TMap *results)
{
    tmap_foreach_value(results, free);
    tmap_free(results);
}

char *
result_set_to_string(TMap *results)
{
    StrBuf *out;
    TMapIterator it;
    
    out = strbuf_new();
    strbuf_append(out, 
            "<DIDL-Lite xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\""
            " xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
            " xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\">");

    tmap_iterator(results, &it);
    while (it.has_next(&it)) {
	char *entry = it.next(&it);
	strbuf_append(out, entry);
    }

    strbuf_append(out, "</DIDL-Lite>");
    return strbuf_free_to_string(out);
}

static bool
filter_includes_any(const char *filter, ...)
{
    va_list args;
    const char *field;
    bool result = false;

    if (strcmp(filter, "*") == 0)
        return true;

    va_start(args, filter);
    for (;;) {
        field = va_arg(args, const char *);
        if (field == NULL)
            break;
        if (string_in_csv(filter, ',', field)) {
            result = true;
            break;
        }
    }
    va_end(args);

    return result;
}

static bool
filter_includes(const char *filter, const char *field)
{
    if (strcmp(filter, "*") == 0)
        return true;

    if (string_in_csv(filter, ',', field))
	return true;

    return false;
}

static void
add_result_entry(TMap *results, Entry *entry, const char *filter)
{
    EntryDetail *detail;
    char *value;
    StrBuf *result;

    result = strbuf_new();
    if (has_entry_detail(entry, DETAIL_CHILDREN)) {
        detail = get_entry_detail(entry, DETAIL_CHILDREN);
	strbuf_appendf(result,
		"<container id=\"%" PRIi32 "\" parentID=\"%" PRIi32 "\" childCount=\"%" PRIi32 "\" restricted=\"1\" searchable=\"1\">",
		entry->id,
		entry->parent,
		detail->data.children.count);
    } else {
	strbuf_appendf(result,
		"<item id=\"%" PRIi32 "\" parentID=\"%" PRIi32 "\" restricted=\"1\">",
		entry->id,
		entry->parent);
        if (filter_includes_any(filter, "res", "@protocolInfo", "@size", "res@protocolInfo", "res@size", NULL)) {
            detail = get_entry_detail(entry, DETAIL_FILE);
            if (detail != NULL) {
                strbuf_append(result, "<res");

                if (filter_includes_any(filter, "res", "@protocolInfo", "res@protocolInfo", NULL))
                    strbuf_appendf(result, " protocolInfo=\"http-get:*:%s:%s\"", detail->data.file.mime_type, detail->data.file.ext_info);

		if (S_ISREG(detail->data.file.mode)) { /*if (detail->data.file.size != 0)*/
	            if (filter_includes_any(filter, "res", "@size", "res@size", NULL))
                        strbuf_appendf(result, " size=\"%zu\"", detail->data.file.size);

                    if (has_entry_detail(entry, DETAIL_TAG)) {
                        EntryDetail *tag_detail = get_entry_detail(entry, DETAIL_TAG);
                        if (filter_includes_any(filter, "res", "@duration", "res@duration", NULL))
                            strbuf_appendf(result, " duration=\"%u:%02u:%02u.%02u\"", SPLIT_DURATION(tag_detail->data.tag.duration));
                    }
#if 0
                    if (filter_includes_any(filter, "res", "@bitrate", "res@bitrate", NULL))
                        strbuf_appendf(result, " bitrate=\"%u\"", 0);
                    
                    if (filter_includes_any(filter, "res", "@bitsPerSample", "res@bitsPerSample", NULL))
                        strbuf_appendf(result, " bitsPerSample=\"%u\"", 0);
                    
                    if (filter_includes_any(filter, "res", "@sampleFrequency", "res@sampleFrequency", NULL))
                        strbuf_appendf(result, " sampleFrequency=\"%u\"", 0);
#endif
                }

                strbuf_appendf(result, ">http://%s:%d/files/%d</res>",
                        UpnpGetServerIpAddress(),
                        UpnpGetServerPort(),
                        entry->id);
            } else {
                detail = get_entry_detail(entry, DETAIL_URL);
                strbuf_append(result, "<res");
                if (filter_includes(filter, "res") || filter_includes(filter, "@protocolInfo"))
                    strbuf_appendf(result, " protocolInfo=\"http-get:*:audio/mpeg:*\""); /* FIXME: application/octet-stream? */
                strbuf_appendf(result, ">http://%s:%d/files/%d</res>",
                        UpnpGetServerIpAddress(),
                        UpnpGetServerPort(),
                        entry->id);
            }
        }
    }

    /* upnp:class is required and cannot be filtered out. */
    value = get_entry_property(entry, "upnp:class");
    strbuf_appendf(result, "<upnp:class>%s</upnp:class>", value);
    free(value);

    /* dc:title is required and cannot be filtered out. */
    value = get_entry_property(entry, "dc:title");
    if (value != NULL)
        append_escaped_xml(result, "dc:title", value);
    free(value);
    /* XXX: dc:creator instead or in combination with upnp:artist? */
    if (filter_includes(filter, "upnp:artist")) {
        value = get_entry_property(entry, "upnp:artist");
        if (value != NULL)
	    append_escaped_xml(result, "upnp:artist", value);
        free(value);
    }
    if (filter_includes(filter, "upnp:album")) {
        value = get_entry_property(entry, "upnp:album");
        if (value != NULL)
	    append_escaped_xml(result, "upnp:album", value);
        free(value);
    }
    if (filter_includes(filter, "upnp:genre")) {
        value = get_entry_property(entry, "upnp:genre");
        if (value != NULL)
	    append_escaped_xml(result, "upnp:genre", value);
        free(value);
    }
    if (filter_includes(filter, "upnp:originalTrackNumber")) {
        value = get_entry_property(entry, "upnp:originalTrackNumber");
        if (value != NULL)
            append_escaped_xml(result, "upnp:originalTrackNumber", value);
        free(value);
    }

    if (has_entry_detail(entry, DETAIL_CHILDREN)) {
	strbuf_append(result, "</container>");
    } else {
	strbuf_append(result, "</item>");
    }

    tmap_put(results, entry, strbuf_free_to_string(result));
}

static char *
operator_name(int type)
{
    switch (type) {
    case T_AND: return _("Logical and");
    case T_OR: return _("Logical or");
    case T_EXISTS: return _("Exists");
    case T_EQ: return _("Binary =");
    case T_NE: return _("Binary !=");
    case T_LT: return _("Binary <");
    case T_LE: return _("Binary <=");
    case T_GT: return _("Binary >");
    case T_GE: return _("Binary >=");
    case T_CONTAINS: return _("Binary contains");
    case T_DOES_NOT_CONTAIN: return _("Binary doesNotContain");
    case T_DERIVED_FROM: return _("Binary derivedFrom");
    }

    return NULL;
}

static void
dump_sort_criteria(SortCriteria *criteria)
{
    SortCriteriaEntry *entry;

    say(4, _("Sort criteria:\n"));
    for (entry = criteria->first; entry != NULL; entry = entry->next) {
	say(4, "  %s (%s)\n",
		entry->property,
		entry->ascending ? _("ascending") : _("descending"));
    }
    if (criteria->first == NULL)
	say(4, _("  (empty)\n"));
}

static void
dump_search_expr(SearchExpr *e, int indent_size)
{
    char *indent;

    indent = xstrdupn(" ", indent_size);
    if (e == NULL) {
	say(4, _("%sMatch anything\n"), indent);
    } else {
	say(4, _("%s%s operator\n"), indent, operator_name(e->type));
	if (e->type == T_AND || e->type == T_OR) {
	    say(4, _("%sExpression 1:\n"), indent);
	    dump_search_expr(e->u.logical.expr1, indent_size+2);
	    say(4, _("%sExpression 2:\n"), indent);
	    dump_search_expr(e->u.logical.expr2, indent_size+2);
	} else if (e->type == T_EXISTS) {
	    say(4, _("%sProperty: %s\n"), indent, e->u.exists.property);
	    say(4, _("%sExistence: %s\n"), indent, e->u.exists.must_exist ? _("true") : _("false"));
	} else {
	    say(4, _("%sProperty: %s\n"), indent, e->u.binary.property);
	    say(4, _("%sValue: %s\n"), indent, e->u.binary.value);
	}
    }
    free(indent);
}

static SortCriteria *
parse_sort_criteria(const char *str, const char **error)
{
    SortCriteria *criteria;
    SortCriteriaEntry *previous_entry = NULL;

    criteria = xmalloc(sizeof(SortCriteria));
    criteria->first = NULL;

    for (;;) {
	SortCriteriaEntry *entry;
	bool ascending;
	const char *end;

	for (; c_isspace(*str); str++);
	if (*str == '\0')
	    break;
	if (*str == '+') {
	    ascending = true;
	} else if (*str == '-') {
	    ascending = false;
	} else {
	    free_sort_criteria(criteria);
	    *error = _("invalid sort sign (should be `+' or `-')");
	    return NULL;
	}
	str++;
	for (end = str; *end != '\0' && *end != ',' && !c_isspace(*end); end++);
	if (str == end) {
	    free_sort_criteria(criteria);
	    *error = _("missing property name");
	    return NULL;
	}
	entry = xmalloc(sizeof(SortCriteriaEntry));
	entry->property = xmemdup(str, end-str+1);
	entry->property[end-str] = 0;
	entry->ascending = ascending;
	entry->next = NULL;
	if (criteria->first == NULL)
	    criteria->first = entry;
	else
	    previous_entry->next = entry;
	previous_entry = entry;
	str = end;
	if (*str == ',')
	    str++;
    }

    return criteria;
}

static SearchCriteria *
parse_search_criteria(const char *str, const char **error)
{
    SearchCriteriaParseData data;
    yyscan_t scanner;
    YY_BUFFER_STATE buffer;
    int result;

    if (yylex_init(&scanner) != 0) {
	*error = errstr;
	return NULL;
    }
    yyset_extra(&data, scanner);
    buffer = yy_scan_string(str, scanner);
    result = yyparse(scanner, &data);
    if (result != 0) {
	*error = data.error;
	return NULL;
    }
    yy_delete_buffer(buffer, scanner);
    yylex_destroy(scanner);

    return data.criteria;
}

static bool
match_search_expr(SearchExpr *expr, Entry *entry)
{
    char *value;
    bool result;
    
    switch (expr->type) {
    case T_AND:
	return match_search_expr(expr->u.logical.expr1, entry)
		&& match_search_expr(expr->u.logical.expr2, entry);
    case T_OR:
	return match_search_expr(expr->u.logical.expr1, entry)
		|| match_search_expr(expr->u.logical.expr2, entry);
    case T_EXISTS:
        value = get_entry_property(entry, expr->u.exists.property);
        result = (value != NULL) == (expr->u.exists.must_exist);
        free(value);
        return result;
    case T_EQ:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strcmp(value, expr->u.binary.value) == 0;
	free(value);
	return result;
    case T_NE:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strcmp(value, expr->u.binary.value) != 0;
	free(value);
	return result;
    case T_LT:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strcmp(value, expr->u.binary.value) > 0;
	free(value);
	return result;
    case T_LE:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strcmp(value, expr->u.binary.value) >= 0;
	free(value);
	return result;
    case T_GT:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strcmp(value, expr->u.binary.value) < 0;
	free(value);
	return result;
    case T_GE:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strcmp(value, expr->u.binary.value) <= 0;
	free(value);
	return result;
    case T_CONTAINS:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strstr(value, expr->u.binary.value) != NULL;
	free(value);
	return result;
    case T_DOES_NOT_CONTAIN:
	value = get_entry_property(entry, expr->u.binary.property);
	result = value != NULL && strstr(value, expr->u.binary.value) == NULL;
	free(value);
	return result;
    case T_DERIVED_FROM:
	/* XXX: clean up these comparisons, can be made simpler! */
	if (strcmp(expr->u.binary.value, "object") == 0)
	    return true;
	if (has_entry_detail(entry, DETAIL_CHILDREN)) {
	    if (strcmp(expr->u.binary.value, "object.container") == 0)
		return true;
	    if (strcmp(expr->u.binary.value, "object.container.storageFolder") == 0)
		return true;
	} else {
	    EntryDetail *detail;

	    if (strcmp(expr->u.binary.value, "object.item") == 0)
		return true;

            detail = get_entry_detail(entry, DETAIL_FILE);
            switch (detail->data.file.item_class) {
            case ITEM_AUDIO:
	        if (strcmp(expr->u.binary.value, "object.item.audioItem") == 0)
		    return true;
                if (strcmp(expr->u.binary.value, "object.item.audioItem.musicTrack") == 0)
		    return true;
                return false;
            case ITEM_IMAGE:
	        if (strcmp(expr->u.binary.value, "object.item.imageItem") == 0)
		    return true;
                if (strcmp(expr->u.binary.value, "object.item.imageItem.photo") == 0)
		    return true;
                return false;
            case ITEM_VIDEO:
	        if (strcmp(expr->u.binary.value, "object.item.videoItem") == 0)
		    return true;
                if (strcmp(expr->u.binary.value, "object.item.videoItem.movie") == 0)
		    return true;
                return false;
            case ITEM_PLAYLIST:
            case ITEM_TEXT:
            case ITEM_UNKNOWN:
            default:
                /* FIXME */
                return false;
            }            
	}
	break;
    }

    /* Shouldn't get here */
    return false;
}

static bool
match_search_criteria(SearchCriteria *criteria, Entry *entry)
{
    if (criteria->expr == NULL)
	return true; /* match any */

    return match_search_expr(criteria->expr, entry);
}

static uint32_t
search_container(SearchCriteria *criteria, Entry *entry,
		 uint32_t *skip, uint32_t max_count,
		 const char *filter, TMap *results)
{
    EntryDetail *detail;
    uint32_t c;
    uint32_t match_count = 0;

    /* It has already been checked whether entry has children. */
    detail = get_entry_detail(entry, DETAIL_CHILDREN);

    for (c = 0; c < detail->data.children.count; c++) {
	Entry *child;

	child = get_entry_by_id(detail->data.children.list[c]);
	if (match_search_criteria(criteria, child)) {
	    if (*skip > 0) {
		(*skip)--;
	    } else {
	        /*if (result != NULL)*/
                add_result_entry(results, child, filter);
		match_count++;
		if (match_count > max_count)
		    return match_count;
	    }
	}

	if (has_entry_detail(child, DETAIL_CHILDREN)) {
	    /* Even if max_count is UINT32_MAX (for RequestedCount=0)
	     * we allow it to be decreased. This is because we never
	     * can return more than UINT32_MAX results anyway
	     * (the return value would overflow).
	     */
	    match_count += search_container(criteria, child, skip, max_count-match_count, filter, results);
	    if (match_count > max_count)
		return match_count;
	}
    }

    return match_count;
}

static bool
contentdir_search(ActionEvent *event)
{
    int32_t id;
    uint32_t index;
    uint32_t count;
    uint32_t match_count;
    char *search_criteria_str;
    char *sort_criteria_str;
    char *filter;
    const char *error = NULL;
    Entry *entry;
    EntryDetail *detail;
    SearchCriteria *search_criteria;
    SortCriteria *sort_criteria;
    TMap *results;
    char *results_str;

    /* Retrieve arguments */
    id = upnp_get_i4(event, "ContainerID"); /* See comment for ObjectID in browse */
    search_criteria_str = upnp_get_string(event, "SearchCriteria");
    sort_criteria_str = upnp_get_string(event, "SortCriteria");
    filter = upnp_get_string(event, "Filter");
    strreplace(filter, ' ', ','); /* XXX: This is a hack. Better remove all spaces. */
    index = upnp_get_ui4(event, "StartingIndex");
    count = upnp_get_ui4(event, "RequestedCount");
    if (!event->status)
	return false;

    /* A RequestedCount of 0 means that all results are to be returned. */
    if (count == 0)
        count = UINT32_MAX;

    /* Check SortCriteria. */
    sort_criteria = parse_sort_criteria(sort_criteria_str, &error);
    if (sort_criteria == NULL) {
        upnp_set_error(event, UPNP_CONTENTDIR_E_BAD_SORT_CRITERIA,
            _("Invalid sort criteria: %s"), error);
	return false;
    }
    if (verbosity >= 4)
	dump_sort_criteria(sort_criteria);

    /* Check SearchCriteria. */
    search_criteria = parse_search_criteria(search_criteria_str, &error);
    if (search_criteria == NULL) {
        upnp_set_error(event, UPNP_CONTENTDIR_E_BAD_SEARCH_CRITERIA,
            _("Invalid search criteria: %s"), error);
	free_sort_criteria(sort_criteria);
	return false;
    }
    if (verbosity >= 4) {
	say(4, _("Search criteria:\n"));
	dump_search_expr(search_criteria->expr, 2);
    }

    /* Check ContainerID argument */
    lock_metadata();
    entry = get_entry_by_id(id);
    if (entry == NULL) {
        upnp_set_error(event, UPNP_CONTENTDIR_E_NO_CONTAINER,
            _("No such container"));
	free_sort_criteria(sort_criteria);
	free_search_criteria(search_criteria);
        unlock_metadata();
	return false;
    }
    detail = get_entry_detail(entry, DETAIL_CHILDREN);
    if (detail == NULL) {
        upnp_set_error(event, UPNP_CONTENTDIR_E_NO_CONTAINER,
            _("Not a container"));
	free_sort_criteria(sort_criteria);
	free_search_criteria(search_criteria);
        unlock_metadata();
	return false;
    }

    /* Do the actual searching. */
    results = new_result_set(sort_criteria);
    match_count = search_container(search_criteria, entry, &index, count, filter, results);

    /* Make response. */
    results_str = result_set_to_string(results);
    free_result_set(results);
    upnp_add_response(event, "Result", results_str);
    free(results_str);
    upnp_add_response(event, "NumberReturned", int32_str(match_count));
    upnp_add_response(event, "TotalMatches", "0");
    upnp_add_response(event, "UpdateID", uint32_str(id == 0 ? update_id : 0));
    free_sort_criteria(sort_criteria);
    free_search_criteria(search_criteria);
    unlock_metadata();

    return true;
}

static bool
contentdir_browse(ActionEvent *event)
{
    uint32_t index;
    uint32_t count;
    char *flag;
    char *filter;
    const char *error = NULL;
    int32_t id;
    char *sort_criteria_str;
    SortCriteria *sort_criteria;
    bool metadata;
    Entry *entry;
    TMap *results;
    char *results_str;

    /* Retrieve arguments */
    index = upnp_get_ui4(event, "StartingIndex");
    count = upnp_get_ui4(event, "RequestedCount");
    /* ObjectID is a string according to ContentDir specification, but we use int32. */
    /* XXX: is this OK? maybe we should use a more appropriate error response if not int32. */
    id = upnp_get_i4(event, "ObjectID"); 
    filter = upnp_get_string(event, "Filter");
    strreplace(filter, ' ', ','); /* XXX: This is a hack. Better remove all spaces. */
    flag = upnp_get_string(event, "BrowseFlag");
    sort_criteria_str = upnp_get_string(event, "SortCriteria");
    if (!event->status)
	return false;

    /* A RequestedCount of 0 means that all results are to be returned. */
    if (count == 0)
        count = UINT32_MAX;

    /* Check SortCriteria. */
    sort_criteria = parse_sort_criteria(sort_criteria_str, &error);
    if (sort_criteria == NULL) {
        upnp_set_error(event, UPNP_CONTENTDIR_E_BAD_SORT_CRITERIA,
            _("Invalid sort criteria: %s"), error);
	return false;
    }
    if (verbosity >= 4)
	dump_sort_criteria(sort_criteria);

    if (strcmp(flag, "BrowseMetadata") == 0) {
	if (index != 0) {
	    upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
	        _("StartingIndex must be 0 when BrowseFlag is BrowseMetaData."));
	    free_sort_criteria(sort_criteria);
	    return false;
	}
	metadata = true;
    } else if (strcmp(flag, "BrowseDirectChildren") == 0) {
	metadata = false;
    } else {
	upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
	    _("Invalid BrowseFlag argument value (%s)"), quotearg(flag));
	free_sort_criteria(sort_criteria);
	return false;
    }

    lock_metadata();
    if (metadata) {
        entry = get_entry_by_id(id);
	if (entry == NULL) {
            upnp_set_error(event, UPNP_CONTENTDIR_E_NO_OBJECT,
                _("No such object"));
	    free_sort_criteria(sort_criteria);
            unlock_metadata();
	    return false;
	}

	results = new_result_set(sort_criteria);
	add_result_entry(results, entry, filter);
	results_str = result_set_to_string(results);
        free_result_set(results);
        upnp_add_response(event, "Result", results_str);
	free(results_str);
        upnp_add_response(event, "NumberReturned", "1");
        upnp_add_response(event, "TotalMatches", "1");
    } else {
        Entry *entry;
	EntryDetail *detail;
	uint32_t c;
	uint32_t end_index;
	uint32_t result_count = 0;

        entry = get_entry_by_id(id);
	if (entry == NULL) {
            upnp_set_error(event, UPNP_CONTENTDIR_E_NO_OBJECT,
                _("No such object"));
	    free_sort_criteria(sort_criteria);
            unlock_metadata();
	    return false;
	}

	detail = get_entry_detail(entry, DETAIL_CHILDREN);
        if (detail == NULL) {
            upnp_set_error(event, UPNP_SOAP_E_INVALID_ARGS,
                _("BrowseDirectChildren only possible on containers"));
	    free_sort_criteria(sort_criteria);
            unlock_metadata();
            return false;
        }

	results = new_result_set(sort_criteria);
	end_index = detail->data.children.count;
	if (count != UINT32_MAX) /* hmm.. should figure out a better way to detect if index+count overflows (>UINT32_MAX) */
	    end_index = MIN(index+count, end_index);
        for (c = index; c < end_index; c++) {
	    Entry *child = get_entry_by_id(detail->data.children.list[c]);
	    add_result_entry(results, child, filter);
	    result_count++;
        }

	results_str = result_set_to_string(results);
        free_result_set(results);
        upnp_add_response(event, "Result", results_str);
	free(results_str);
        upnp_add_response(event, "NumberReturned", int32_str(result_count));
        upnp_add_response(event, "TotalMatches", int32_str(detail->data.children.count));
    }

    upnp_add_response(event, "UpdateID", uint32_str(id == 0 ? update_id : 0));
    free_sort_criteria(sort_criteria);
    unlock_metadata();

    return event->status;
}

void
init_contentdir(void)
{
    find_variable("SystemUpdateID")->value = xstrdup(uint32_str(update_id));
}

void
finish_contentdir(void)
{
    free(find_variable("SystemUpdateID")->value);
}

/* XXX: In the future, ServiceVariable should allow different
 * data types (and point to real variables, e.g. {"SystemUpdateID", UINT32_TYPE, &update_id}).
 */
ServiceVariable contentdir_service_variables[] = {
  /*{ "TransferIDs", NULL },*/
  { "SystemUpdateID", NULL },
  /*{ "ContainerUpdateIDs", NULL },*/
  { 0, }
};

ServiceAction contentdir_service_actions[] = {
  { "GetSearchCapabilities", contentdir_get_search_capabilities },
  { "GetSortCapabilities", contentdir_get_sort_capabilities },
  { "GetSystemUpdateID", contentdir_get_system_update_id },
  { "Browse", contentdir_browse },
  { "Search", contentdir_search },
/*{ "CreateObject", contentdir_create_object },*/
/*{ "DestroyObject", contentdir_destroy_object },*/
/*{ "UpdateObject", contentdir_update_object },*/
/*{ "ImportResource", contentdir_import_resource },*/
/*{ "GetTransferProgress", contentdir_get_transfer_progress },*/
/*{ "DeleteResource", contentdir_delete_resource },*/
/*{ "CreateReference", contentdir_create_reference },*/
  { 0, }
};
