#include "sonic.h"

#include "config.h"
#include "log.h"
#include "link.h"
#include "memcache.h"
#include "util.h"

#include <expat.h>

#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct {
    char *server;
    char *username;
    char *password;
    char *client;
    char *api_version;
} SonicConfigStruct;

static SonicConfigStruct SONIC_CONFIG;

/**
 * \brief initialise Sonic configuration struct
 */
void sonic_config_init(const char *server, const char *username,
                       const char *password)
{
    SONIC_CONFIG.server = strndup(server, MAX_PATH_LEN);
    /*
     * Correct for the extra '/'
     */
    size_t server_url_len = strnlen(SONIC_CONFIG.server, MAX_PATH_LEN) - 1;
    if (SONIC_CONFIG.server[server_url_len] == '/') {
        SONIC_CONFIG.server[server_url_len] = '\0';
    }
    SONIC_CONFIG.username = strndup(username, MAX_FILENAME_LEN);
    SONIC_CONFIG.password = strndup(password, MAX_FILENAME_LEN);
    SONIC_CONFIG.client = DEFAULT_USER_AGENT;

    if (!CONFIG.sonic_insecure) {
        /*
         * API 1.13.0 is the minimum version that supports
         * salt authentication scheme
         */
        SONIC_CONFIG.api_version = "1.13.0";
    } else {
        /*
         * API 1.8.0 is the minimum version that supports ID3 mode
         */
        SONIC_CONFIG.api_version = "1.8.0";
    }
}

/**
 * \brief generate authentication string
 */
static char *sonic_gen_auth_str(void)
{
    if (!CONFIG.sonic_insecure) {
        char *salt = generate_salt();
        size_t pwd_len = strnlen(SONIC_CONFIG.password, MAX_FILENAME_LEN);
        size_t pwd_salt_len = pwd_len + strnlen(salt, MAX_FILENAME_LEN);
        char *pwd_salt = CALLOC(pwd_salt_len + 1, sizeof(char));
        strncat(pwd_salt, SONIC_CONFIG.password, MAX_FILENAME_LEN);
        strncat(pwd_salt + pwd_len, salt, MAX_FILENAME_LEN);
        char *token = generate_md5sum(pwd_salt);
        char *auth_str = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
        snprintf(auth_str, MAX_PATH_LEN, ".view?u=%s&t=%s&s=%s&v=%s&c=%s",
                 SONIC_CONFIG.username, token, salt, SONIC_CONFIG.api_version,
                 SONIC_CONFIG.client);
        FREE(salt);
        FREE(token);
        return auth_str;
    } else {
        char *pwd_hex = str_to_hex(SONIC_CONFIG.password);
        char *auth_str = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
        snprintf(auth_str, MAX_PATH_LEN, ".view?u=%s&p=enc:%s&v=%s&c=%s",
                 SONIC_CONFIG.username, pwd_hex, SONIC_CONFIG.api_version,
                 SONIC_CONFIG.client);
        FREE(pwd_hex);
        return auth_str;
    }
}

/**
 * \brief generate the first half of the request URL
 */
static char *sonic_gen_url_first_part(char *method)
{
    char *auth_str = sonic_gen_auth_str();
    char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
    snprintf(url, MAX_PATH_LEN, "%s/rest/%s%s", SONIC_CONFIG.server, method,
             auth_str);
    FREE(auth_str);
    return url;
}

/**
 * \brief generate a getMusicDirectory request URL
 */
static char *sonic_getMusicDirectory_link(const char *id)
{
    char *first_part = sonic_gen_url_first_part("getMusicDirectory");
    char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
    snprintf(url, MAX_PATH_LEN, "%s&id=%s", first_part, id);
    FREE(first_part);
    return url;
}

/**
 * \brief generate a getArtist request URL
 */
static char *sonic_getArtist_link(const char *id)
{
    char *first_part = sonic_gen_url_first_part("getArtist");
    char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
    snprintf(url, MAX_PATH_LEN, "%s&id=%s", first_part, id);
    FREE(first_part);
    return url;
}

/**
 * \brief generate a getAlbum request URL
 */
static char *sonic_getAlbum_link(const char *id)
{
    char *first_part = sonic_gen_url_first_part("getAlbum");
    char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
    snprintf(url, MAX_PATH_LEN, "%s&id=%s", first_part, id);
    FREE(first_part);
    return url;
}

/**
 * \brief generate a download request URL
 */
static char *sonic_stream_link(const char *id)
{
    char *first_part = sonic_gen_url_first_part("stream");
    char *url = CALLOC(MAX_PATH_LEN + 1, sizeof(char));
    snprintf(url, MAX_PATH_LEN, "%s&format=raw&id=%s", first_part, id);
    FREE(first_part);
    return url;
}

/**
 * \brief The parser for Sonic index mode
 * \details This is the callback function called by the the XML parser.
 * \param[in] data user supplied data, in this case it is the pointer to the
 * LinkTable.
 * \param[in] elem the name of this element, it should be either "child" or
 * "artist"
 * \param[in] attr Each attribute seen in a start (or empty) tag occupies
 * 2 consecutive places in this vector: the attribute name followed by the
 * attribute value. These pairs are terminated by a null pointer.
 * \note we are using strcmp rather than strncmp, because we are assuming the
 * parser terminates the strings properly, which is a fair assumption,
 * considering how mature expat is.
 */
static void XMLCALL XML_parser_general(void *data, const char *elem,
                                       const char **attr)
{
    /*
     * Error checking
     */
    if (!strcmp(elem, "error")) {
        lprintf(error, "error:\n");
        for (int i = 0; attr[i]; i += 2) {
            lprintf(error, "%s: %s\n", attr[i], attr[i + 1]);
        }
    }

    LinkTable *linktbl = (LinkTable *)data;
    Link *link;

    /*
     * Please refer to the documentation at the function prototype of
     * sonic_LinkTable_new_id3()
     */
    if (!strcmp(elem, "child")
        || (!strcmp(elem, "artist") && linktbl->links[0]->sonic.depth != 3)) {
        link = CALLOC(1, sizeof(Link));
        link->type = LINK_DIR;
    } else if (!strcmp(elem, "album") && linktbl->links[0]->sonic.depth == 3) {
        link = CALLOC(1, sizeof(Link));
        link->type = LINK_DIR;
        /*
         * The new table should be a level 4 song table
         */
        link->sonic.depth = 4;
    } else if (!strcmp(elem, "song") && linktbl->links[0]->sonic.depth == 4) {
        link = CALLOC(1, sizeof(Link));
        link->type = LINK_FILE;
    } else {
        /*
         * The element does not contain directory structural information
         */
        return;
    }

    int id_set = 0;
    int linkname_set = 0;

    int track = 0;
    char *title = "";
    char *suffix = "";
    for (int i = 0; attr[i]; i += 2) {
        if (!strcmp("id", attr[i])) {
            link->sonic.id = CALLOC(MAX_FILENAME_LEN + 1, sizeof(char));
            strncpy(link->sonic.id, attr[i + 1], MAX_FILENAME_LEN);
            id_set = 1;
            continue;
        }

        if (!strcmp("path", attr[i])) {
            memset(link->linkname, 0, MAX_FILENAME_LEN);
            /*
             * Skip to the last '/' if it exists
             */
            const char *s = strrchr(attr[i + 1], '/');
            if (s) {
                strncpy(link->linkname, s + 1, MAX_FILENAME_LEN);
            } else {
                strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN);
            }
            linkname_set = 1;
            continue;
        }

        /*
         * "title" is used for directory name,
         * "name" is for top level directories
         * N.B. "path" attribute is given the preference
         */
        if (!linkname_set
            && (!strcmp("title", attr[i]) || !strcmp("name", attr[i]))) {
            strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN);
            linkname_set = 1;
            continue;
        }

        if (!strcmp("isDir", attr[i])) {
            if (!strcmp("false", attr[i + 1])) {
                link->type = LINK_FILE;
            }
            continue;
        }

        if (!strcmp("created", attr[i])) {
            struct tm *tm = CALLOC(1, sizeof(struct tm));
            strptime(attr[i + 1], "%Y-%m-%dT%H:%M:%S.000Z", tm);
            link->time = mktime(tm);
            FREE(tm);
            continue;
        }

        if (!strcmp("size", attr[i])) {
            link->content_length = strtoll(attr[i + 1], NULL, 10);
            continue;
        }

        if (!strcmp("track", attr[i])) {
            track = (int)strtol(attr[i + 1], NULL, 10);
            continue;
        }

        if (!strcmp("title", attr[i])) {
            title = (char *)attr[i + 1];
            continue;
        }

        if (!strcmp("suffix", attr[i])) {
            suffix = (char *)attr[i + 1];
            continue;
        }
    }

    if (!linkname_set && strnlen(title, MAX_PATH_LEN) > 0
        && strnlen(suffix, MAX_PATH_LEN) > 0) {
        snprintf(link->linkname, MAX_FILENAME_LEN, "%02d - %s.%s", track, title,
                 suffix);
        linkname_set = 1;
    }

    /*
     * Clean up if linkname or id is not set
     */
    if (!linkname_set || !id_set) {
        FREE(link);
        return;
    }

    if (link->type == LINK_FILE) {
        char *url = sonic_stream_link(link->sonic.id);
        strncpy(link->f_url, url, MAX_PATH_LEN);
        FREE(url);
    }

    LinkTable_add(linktbl, link);
}

static void sanitise_LinkTable(LinkTable *linktbl)
{
    for (int i = 0; i < linktbl->num; i++) {
        if (!strcmp(linktbl->links[i]->linkname, ".")) {
            /* Note the super long sanitised name to avoid collision */
            strcpy(linktbl->links[i]->linkname, "__DOT__");
        }

        if (!strcmp(linktbl->links[i]->linkname, "/")) {
            /* Ditto */
            strcpy(linktbl->links[i]->linkname, "__FORWARD-SLASH__");
        }

        for (size_t j = 0; j < strlen(linktbl->links[i]->linkname); j++) {
            if (linktbl->links[i]->linkname[j] == '/') {
                linktbl->links[i]->linkname[j] = '-';
            }
        }

        if (linktbl->links[i]->next_table != NULL) {
            sanitise_LinkTable(linktbl->links[i]->next_table);
        }
    }
}

/**
 * \brief parse a XML string in order to fill in the LinkTable
 */
static LinkTable *sonic_url_to_LinkTable(const char *url,
                                         XML_StartElementHandler handler,
                                         int depth)
{
    LinkTable *linktbl = LinkTable_alloc(url);
    linktbl->links[0]->sonic.depth = depth;

    /*
     * start downloading the base URL
     */
    TransferStruct xml = Link_download_full(linktbl->links[0]);
    if (xml.curr_size == 0) {
        LinkTable_free(linktbl);
        return NULL;
    }

    XML_Parser parser = XML_ParserCreate(NULL);
    XML_SetUserData(parser, linktbl);

    XML_SetStartElementHandler(parser, handler);

    if (XML_Parse(parser, xml.data, xml.curr_size, 1) == XML_STATUS_ERROR) {
        lprintf(error, "Parse error at line %lu: %s\n",
                XML_GetCurrentLineNumber(parser),
                XML_ErrorString(XML_GetErrorCode(parser)));
    }

    XML_ParserFree(parser);

    FREE(xml.data);

    LinkTable_print(linktbl);

    sanitise_LinkTable(linktbl);

    return linktbl;
}

LinkTable *sonic_LinkTable_new_index(const char *id)
{
    char *url;
    if (strcmp(id, "0") != 0) {
        url = sonic_getMusicDirectory_link(id);
    } else {
        url = sonic_gen_url_first_part("getIndexes");
    }
    LinkTable *linktbl = sonic_url_to_LinkTable(url, XML_parser_general, 0);
    FREE(url);
    return linktbl;
}

static void XMLCALL XML_parser_id3_root(void *data, const char *elem,
                                        const char **attr)
{
    if (!strcmp(elem, "error")) {
        lprintf(error, "\n");
        for (int i = 0; attr[i]; i += 2) {
            lprintf(error, "%s: %s\n", attr[i], attr[i + 1]);
        }
    }

    LinkTable *root_linktbl = (LinkTable *)data;
    LinkTable *this_linktbl = NULL;

    /*
     * Set the current linktbl, if we have more than head link.
     */
    if (root_linktbl->num > 1) {
        this_linktbl = root_linktbl->links[root_linktbl->num - 1]->next_table;
    }

    int id_set = 0;
    int linkname_set = 0;
    Link *link;
    if (!strcmp(elem, "index")) {
        /*
         * Add a subdirectory
         */
        link = CALLOC(1, sizeof(Link));
        link->type = LINK_DIR;
        for (int i = 0; attr[i]; i += 2) {
            if (!strcmp("name", attr[i])) {
                strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN);
                linkname_set = 1;
                /*
                 * Allocate a new LinkTable
                 */
                link->next_table = LinkTable_alloc("/");
            }
        }
        /*
         * Make sure we don't add an empty directory
         */
        if (linkname_set) {
            LinkTable_add(root_linktbl, link);
        } else {
            FREE(link);
        }
        return;
    } else if (!strcmp(elem, "artist")) {
        link = CALLOC(1, sizeof(Link));
        link->type = LINK_DIR;
        /*
         * The new table should be a level 3 album table
         */
        link->sonic.depth = 3;
        for (int i = 0; attr[i]; i += 2) {
            if (!strcmp("name", attr[i])) {
                strncpy(link->linkname, attr[i + 1], MAX_FILENAME_LEN);
                linkname_set = 1;
                continue;
            }

            if (!strcmp("id", attr[i])) {
                link->sonic.id = CALLOC(MAX_FILENAME_LEN + 1, sizeof(char));
                strncpy(link->sonic.id, attr[i + 1], MAX_FILENAME_LEN);
                id_set = 1;
                continue;
            }
        }

        /*
         * Clean up if linkname is not set
         */
        if (!linkname_set || !id_set) {
            FREE(link);
            return;
        }

        LinkTable_add(this_linktbl, link);
    }
    /*
     * If we reach here, then this element does not contain directory structural
     * information
     */
}

LinkTable *sonic_LinkTable_new_id3(int depth, const char *id)
{
    char *url;
    LinkTable *linktbl;
    switch (depth) {
    /*
     * Root table
     */
    case 0:
        url = sonic_gen_url_first_part("getArtists");
        linktbl = sonic_url_to_LinkTable(url, XML_parser_id3_root, 0);
        FREE(url);
        break;
    /*
     * Album table - get all the albums of an artist
     */
    case 3:
        url = sonic_getArtist_link(id);
        linktbl = sonic_url_to_LinkTable(url, XML_parser_general, depth);
        FREE(url);
        break;
    /*
     * Song table - get all the songs of an album
     */
    case 4:
        url = sonic_getAlbum_link(id);
        linktbl = sonic_url_to_LinkTable(url, XML_parser_general, depth);
        FREE(url);
        break;
    default:
        /*
         * We shouldn't reach here.
         */
        lprintf(fatal, "case %d.\n", depth);
        break;
    }

    return linktbl;
}
