/*
 * Audacious: A cross-platform multimedia player
 * Copyright (c) 2006 William Pitcock, Tony Vroon, George Averill,
 *                    Giacomo Lozito, Derek Pomery, Yoshiki Yazawa
 *                    and Matti Hämäläinen.
 *
 * 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 2 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 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.
 */

/* #define AUD_DEBUG 1 */

#include <config.h>

#include <glib.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>

#include <audacious/plugin.h>

#include <libxml/tree.h>
#include <libxml/parser.h>
#include <libxml/xmlreader.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
#include <libxml/uri.h>

#define XSPF_ROOT_NODE_NAME "playlist"
#define XSPF_XMLNS "http://xspf.org/ns/0/"

enum {
    CMP_DEF = 0,
    CMP_GT,
    CMP_NULL
} xspf_compare;

typedef struct {
    gint tupleField;
    gchar *xspfName;
    TupleValueType type;
    gboolean isMeta;
    gint compare;
} xspf_entry_t;


static const xspf_entry_t xspf_entries[] = {
    { FIELD_ARTIST,       "creator",      TUPLE_STRING,   FALSE,  CMP_DEF },
    { FIELD_TITLE,        "title",        TUPLE_STRING,   FALSE,  CMP_DEF },
    { FIELD_ALBUM,        "album",        TUPLE_STRING,   FALSE,  CMP_DEF },
    { FIELD_COMMENT,      "annotation",   TUPLE_STRING,   FALSE,  CMP_DEF },
    { FIELD_GENRE,        "genre",        TUPLE_STRING,   TRUE,   CMP_DEF },

    { FIELD_TRACK_NUMBER, "trackNum",     TUPLE_INT,      FALSE,  CMP_DEF },
    { FIELD_LENGTH,       "duration",     TUPLE_INT,      FALSE,  CMP_GT },
    { FIELD_YEAR,         "year",         TUPLE_INT,      TRUE,   CMP_DEF },
    { FIELD_QUALITY,      "quality",      TUPLE_STRING,   TRUE,   CMP_DEF },

    { FIELD_CODEC,        "codec",        TUPLE_STRING,   TRUE,   CMP_DEF },
    { FIELD_SONG_ARTIST,  "song-artist",  TUPLE_STRING,   TRUE,   CMP_DEF },

    { FIELD_MTIME,        "mtime",        TUPLE_INT,      TRUE,   CMP_DEF },
    { FIELD_FORMATTER,    "formatter",    TUPLE_STRING,   TRUE,   CMP_DEF },
    { FIELD_PERFORMER,    "performer",    TUPLE_STRING,   TRUE,   CMP_DEF },
    { FIELD_COPYRIGHT,    "copyright",    TUPLE_STRING,   TRUE,   CMP_DEF },
    { FIELD_DATE,         "date",         TUPLE_STRING,   TRUE,   CMP_DEF },

    { FIELD_SUBSONG_ID,   "subsong-id",   TUPLE_INT,      TRUE,   CMP_DEF },
    { FIELD_SUBSONG_NUM,  "subsong-num",  TUPLE_INT,      TRUE,   CMP_DEF },
    { FIELD_MIMETYPE,     "mime-type",    TUPLE_STRING,   TRUE,   CMP_DEF },
    { FIELD_BITRATE,      "bitrate",      TUPLE_INT,      TRUE,   CMP_DEF },
};

static const gint xspf_nentries = (sizeof(xspf_entries) / sizeof(xspf_entries[0]));

static void xspf_add_file (xmlNode * track, const gchar * filename, const gchar
 * base, struct index * filenames, struct index * tuples)
{
    xmlNode *nptr;
    Tuple *tuple;
    gchar *location = NULL;

    tuple = aud_tuple_new();
    aud_tuple_associate_int(tuple, FIELD_LENGTH, NULL, -1);
    aud_tuple_associate_int(tuple, FIELD_MTIME, NULL, -1);


    for (nptr = track->children; nptr != NULL; nptr = nptr->next) {
        if (nptr->type == XML_ELEMENT_NODE) {
            if (!xmlStrcmp(nptr->name, (xmlChar *)"location")) {
                /* Location is a special case */
                gchar *str = (gchar *)xmlNodeGetContent(nptr);
                if (!strstr(str, "://") && base)
                    location = g_strdup_printf("%s/%s", base, str);
                else
                    location = g_strdup(str);
                xmlFree(str);
            } else {
                /* Rest of the nodes are handled here */
                gint i;
                gboolean isMeta;
                xmlChar *findName;

                if (!xmlStrcmp(nptr->name, (xmlChar *)"meta")) {
                    isMeta = TRUE;
                    findName = xmlGetProp(nptr, (xmlChar *)"rel");
                } else {
                    isMeta = FALSE;
                    findName = xmlStrdup(nptr->name);
                }

                for (i = 0; i < xspf_nentries; i++)
                if ((xspf_entries[i].isMeta == isMeta) &&
                    !xmlStrcmp(findName, (xmlChar *)xspf_entries[i].xspfName)) {
                    xmlChar *str = xmlNodeGetContent(nptr);
                    switch (xspf_entries[i].type) {
                        case TUPLE_STRING:
                            aud_tuple_associate_string(tuple, xspf_entries[i].tupleField, NULL, (gchar *)str);
                            break;

                        case TUPLE_INT:
                            AUDDBG("field=%s val=%s\n", xspf_entries[i].xspfName, str);
                            aud_tuple_associate_int(tuple, xspf_entries[i].tupleField, NULL, atol((char *)str));
                            break;

                        default:
                            break;
                    }
                    xmlFree(str);
                    break;
                }

                xmlFree(findName);
            }
        }
    }

    if (location != NULL)
    {
        tuple_set_filename(tuple, location);

        index_append(filenames, location);
        index_append(tuples, tuple);
    }
}


static void xspf_find_track (xmlNode * tracklist, const gchar * filename, const
 gchar * base, struct index * filenames, struct index * tuples)
{
    xmlNode *nptr;

    for (nptr = tracklist->children; nptr != NULL; nptr = nptr->next) {
        if (nptr->type == XML_ELEMENT_NODE &&
         ! xmlStrcmp (nptr->name, (xmlChar *) "track"))
            xspf_add_file (nptr, filename, base, filenames, tuples);
    }
}

#if 0
static void xspf_find_audoptions(xmlNode *tracklist, const gchar *filename, gint pos)
{
    xmlNode *nptr;
    Playlist *playlist = aud_playlist_get_active();

    for (nptr = tracklist->children; nptr != NULL; nptr = nptr->next) {
        if (nptr->type == XML_ELEMENT_NODE &&
            !xmlStrcmp(nptr->name, (xmlChar *)"options")) {
            xmlChar *opt = NULL;

            opt = xmlGetProp(nptr, (xmlChar *)"staticlist");
            if (!g_strcasecmp((char *)opt, "true"))
                playlist->attribute |= PLAYLIST_STATIC;
            else
                playlist->attribute ^= PLAYLIST_STATIC;

            xmlFree(opt);
        }
    }
}
#endif

static void xspf_playlist_load(const gchar *filename, gint pos)
{
    xmlDocPtr doc;
    xmlNode *nptr, *nptr2;
    struct index * filenames, * tuples;

    g_return_if_fail(filename != NULL);

    AUDDBG("filename='%s', pos=%d\n", filename, pos);

    doc = xmlRecoverFile(filename);
    if (doc == NULL)
        return;

    filenames = index_new ();
    tuples = index_new ();

    // find trackList
    for (nptr = doc->children; nptr != NULL; nptr = nptr->next) {
        if (nptr->type == XML_ELEMENT_NODE &&
            !xmlStrcmp(nptr->name, (xmlChar *)"playlist")) {
            gchar *tmp, *base;

            base = (gchar *)xmlNodeGetBase(doc, nptr);

            AUDDBG("base #1 = %s\n", base);

            // if filename is specified as a base, ignore it.
            tmp = xmlURIUnescapeString(base, -1, NULL);
            if (tmp) {
                if (!strcmp(tmp, filename)) {
                    xmlFree(base);
                    base = NULL;
                }
                g_free(tmp);
            }

            AUDDBG("base #2 = %s\n", base);

            if (!base)
                base = g_path_get_dirname(filename);

            AUDDBG("base #3 = %s\n", base);

            for (nptr2 = nptr->children; nptr2 != NULL; nptr2 = nptr2->next) {
#if 0
                if (nptr2->type == XML_ELEMENT_NODE &&
                    !xmlStrcmp(nptr2->name, (xmlChar *)"extension")) {
                    //check if application is audacious
                    xmlChar *app = NULL;
                    app = xmlGetProp(nptr2, (xmlChar *)"application");
                    if (!xmlStrcmp(app, (xmlChar *)"audacious"))
                        xspf_find_audoptions(nptr2, filename, pos);
                    xmlFree(app);
                } else
#endif
                if (nptr2->type == XML_ELEMENT_NODE &&
                    !xmlStrcmp(nptr2->name, (xmlChar *)"title")) {
                    xmlChar *title = xmlNodeGetContent(nptr2);

                    if (title && *title) {
                        aud_playlist_set_title (aud_playlist_get_active (),
                         (const gchar *) title);
                    }
                    xmlFree(title);
                } else
                if (nptr2->type == XML_ELEMENT_NODE &&
                    !xmlStrcmp(nptr2->name, (xmlChar *)"trackList")) {
                    xspf_find_track (nptr2, filename, base, filenames, tuples);
                }
            }
        }
    }
    xmlFreeDoc(doc);

    aud_playlist_entry_insert_batch (aud_playlist_get_active (), pos, filenames,
     tuples);
}


static void xspf_add_node(xmlNodePtr node, TupleValueType type,
        gboolean isMeta, const gchar *xspfName, const gchar *strVal,
        const gint intVal)
{
    gchar tmps[64];
    xmlNodePtr tmp;

    if (isMeta) {
        tmp = xmlNewNode(NULL, (xmlChar *) "meta");
        xmlSetProp(tmp, (xmlChar *) "rel", (xmlChar *) xspfName);
    } else
        tmp = xmlNewNode(NULL, (xmlChar *) xspfName);

    switch (type) {
        case TUPLE_STRING:
            xmlAddChild(tmp, xmlNewText((xmlChar *) strVal));
            break;

        case TUPLE_INT:
            g_snprintf(tmps, sizeof(tmps), "%d", intVal);
            xmlAddChild(tmp, xmlNewText((xmlChar *) tmps));
            break;

        default:
            break;
    }

    xmlAddChild(node, tmp);
}


static void xspf_playlist_save(const gchar *filename, gint pos)
{
    gint playlist = aud_playlist_get_active ();
    const gchar * title = aud_playlist_get_title (playlist);
    gint entries = aud_playlist_entry_count (playlist);
    xmlDocPtr doc;
    xmlNodePtr rootnode, tracklist;
#if 0
    gint baselen = 0;
    gchar *base = NULL;
#endif
    gint count;

    AUDDBG("filename='%s', pos=%d\n", filename, pos);

    doc = xmlNewDoc((xmlChar *)"1.0");
    doc->charset = XML_CHAR_ENCODING_UTF8;
    doc->encoding = xmlStrdup((xmlChar *)"UTF-8");

    rootnode = xmlNewNode(NULL, (xmlChar *)XSPF_ROOT_NODE_NAME);
    xmlSetProp(rootnode, (xmlChar *)"version", (xmlChar *)"1");
    xmlSetProp(rootnode, (xmlChar *)"xmlns", (xmlChar *)XSPF_XMLNS);

#if 0
    /* relative */
    if (playlist->attribute & PLAYLIST_USE_RELATIVE) {
        /* prescan to determine base uri */
        for (node = playlist->entries; node != NULL; node = g_list_next(node)) {
            gchar *ptr1, *ptr2, *ptrslash;
            PlaylistEntry *entry = PLAYLIST_ENTRY(node->data);
            gchar *tmp;
            gint tmplen = 0;

            if (!is_uri(entry->filename)) { //obsolete
                gchar *tmp2 = g_path_get_dirname(entry->filename);
                tmp = g_strdup_printf("%s/", tmp2);
                g_free(tmp2);
            } else
                tmp = g_strdup(entry->filename);

            if (!base) {
                base = strdup(tmp);
                baselen = strlen(base);
            }

            ptr1 = base;
            ptrslash = ptr2 = tmp;

            while(ptr1 && ptr2 && *ptr1 && *ptr2 && *ptr1 == *ptr2) {
                if (*ptr2 == '/') ptrslash = ptr2 + 1;

                ptr1++;
                ptr2++;
            }

            if (!(*ptrslash)) ptrslash--;
            *ptrslash = '\0';       //terminate
            tmplen = ptrslash - tmp;

            if (tmplen <= baselen) {
                g_free(base);
                base = tmp;
                baselen = tmplen;
                AUDDBG("base='%s', baselen=%d\n", base, baselen);
            } else
                g_free(tmp);
        }

        /* set base URI */
        if (base) {
            gchar *tmp;
            if (!is_uri(base)) {
                tmp = (gchar *) xspf_path_to_uri((xmlChar *)base);
                if (tmp) {
                    g_free(base);
                    base = tmp;
                }
            }

            if (!is_uri(base)) {
                AUDDBG("base is not uri. something is wrong.\n");
                tmp = g_strdup_printf("file://%s", base);
                xmlSetProp(rootnode, (xmlChar *)"xml:base", (xmlChar *)tmp);
                g_free(tmp);
            } else
                xmlSetProp(rootnode, (xmlChar *)"xml:base", (xmlChar *)base);
        }
    }                           /* USE_RELATIVE */
#endif

    /* common */
    xmlDocSetRootElement(doc, rootnode);
    xspf_add_node(rootnode, TUPLE_STRING, FALSE, "creator", PACKAGE "-" VERSION, 0);

#if 0
    /* add staticlist marker */
    if (playlist->attribute & PLAYLIST_STATIC) {
        xmlNodePtr extension, options;

        extension = xmlNewNode(NULL, (xmlChar *)"extension");
        xmlSetProp(extension, (xmlChar *)"application", (xmlChar *)"audacious");

        options = xmlNewNode(NULL, (xmlChar *)"options");
        xmlSetProp(options, (xmlChar *)"staticlist", (xmlChar *)"true");

        xmlAddChild(extension, options);
        xmlAddChild(rootnode, extension);
    }
#endif

    if (title != NULL)
        xspf_add_node (rootnode, TUPLE_STRING, FALSE, "title", title, 0);

    tracklist = xmlNewNode(NULL, (xmlChar *)"trackList");
    xmlAddChild(rootnode, tracklist);

    for (count = pos; count < entries; count ++)
    {
        const gchar * filename = aud_playlist_entry_get_filename (playlist,
         count);
        const Tuple * tuple = aud_playlist_entry_get_tuple (playlist, count);
        xmlNodePtr track, location;
        const gchar *scratch = NULL;
        gint scratchi = 0;

        track = xmlNewNode(NULL, (xmlChar *)"track");
        location = xmlNewNode(NULL, (xmlChar *)"location");

        xmlAddChild(location, xmlNewText((xmlChar *)filename));
        xmlAddChild(track, location);
        xmlAddChild(tracklist, track);

        if (tuple != NULL)
        {
            gint i;
            for (i = 0; i < xspf_nentries; i++) {
                const xspf_entry_t *xs = &xspf_entries[i];
                gboolean isOK = FALSE;

                switch (xs->type) {
                    case TUPLE_STRING:
                        scratch = aud_tuple_get_string ((Tuple *) tuple,
                         xs->tupleField, NULL);

                        switch (xs->compare) {
                            case CMP_DEF: isOK = (scratch != NULL); break;
                            case CMP_NULL: isOK = (scratch == NULL); break;
                        }
                        if (scratch != NULL && !g_utf8_validate(scratch, -1, NULL))
                            isOK = FALSE;
                        break;

                    case TUPLE_INT:
                        scratchi = aud_tuple_get_int ((Tuple *) tuple,
                         xs->tupleField, NULL);

                        switch (xs->compare) {
                            case CMP_DEF: isOK = (scratchi != 0); break;
                            case CMP_GT:  isOK = (scratchi > 0); break;
                        }
                        break;

                    default:
                        break;
                }

                if (isOK)
                    xspf_add_node(track, xs->type, xs->isMeta, xs->xspfName, scratch, scratchi);
            }
        }
    }

    xmlSaveFormatFile(filename, doc, 1);
    xmlFreeDoc(doc);
#if 0
    xmlFree(base);
#endif
}


PlaylistContainer plc_xspf = {
    .name = "XSPF Playlist Format",
    .ext = "xspf",
    .plc_read = xspf_playlist_load,
    .plc_write = xspf_playlist_save,
};


static void xspf_init(void)
{
    aud_playlist_container_register(&plc_xspf);
}


static void xspf_cleanup(void)
{
    aud_playlist_container_unregister(&plc_xspf);
}


DECLARE_PLUGIN(xspf, xspf_init, xspf_cleanup, NULL, NULL, NULL, NULL, NULL, NULL);
