////////////////////////////////////////////////////////////////////////////
//                           **** WAVPACK ****                            //
//                  Hybrid Lossless Wavefile Compressor                   //
//              Copyright (c) 1998 - 2013 Conifer Software.               //
//                          All Rights Reserved.                          //
//      Distributed under the BSD Software License (see license.txt)      //
////////////////////////////////////////////////////////////////////////////

// tag_utils.c

// This module provides the high-level API for creating, reading and editing
// APEv2 tags on WavPack files. Read-only support is also provided for ID3v1
// tags, but their use is not recommended.

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

#include "wavpack_local.h"

#ifdef _WIN32
#define stricmp(x,y) _stricmp(x,y)
#else
#define stricmp strcasecmp
#endif

static int get_ape_tag_item (M_Tag *m_tag, const char *item, char *value, int size, int type);
static int get_id3_tag_item (M_Tag *m_tag, const char *item, char *value, int size);
static int get_ape_tag_item_indexed (M_Tag *m_tag, int index, char *item, int size, int type);
static int get_id3_tag_item_indexed (M_Tag *m_tag, int index, char *item, int size);
static int append_ape_tag_item (WavpackContext *wpc, const char *item, const char *value, int vsize, int type);
static int write_tag_blockout (WavpackContext *wpc);
static int write_tag_reader (WavpackContext *wpc);
static void tagcpy (char *dest, char *src, int tag_size);
static int tagdata (char *src, int tag_size);

//////////////////// Global functions part of external API /////////////////////////

// Count and return the total number of tag items in the specified file.

int WavpackGetNumTagItems (WavpackContext *wpc)
{
    int i = 0;

    while (WavpackGetTagItemIndexed (wpc, i, NULL, 0))
        ++i;

    return i;
}

// Count and return the total number of binary tag items in the specified file. This applies
// only to APEv2 tags and was implemented as a separate function to avoid breaking the old API.

int WavpackGetNumBinaryTagItems (WavpackContext *wpc)
{
    int i = 0;

    while (WavpackGetBinaryTagItemIndexed (wpc, i, NULL, 0))
        ++i;

    return i;
}

// Attempt to get the specified item from the specified file's ID3v1 or APEv2
// tag. The "size" parameter specifies the amount of space available at "value",
// if the desired item will not fit in this space then ellipses (...) will
// be appended and the string terminated. Only text data are supported. The
// actual length of the string is returned (or 0 if no matching value found).
// Note that with APEv2 tags the length might not be the same as the number of
// characters because UTF-8 encoding is used. Also, APEv2 tags can have multiple
// (NULL separated) strings for a single value (this is why the length is
// returned). If this function is called with a NULL "value" pointer (or a
// zero "length") then only the actual length of the value data is returned
// (not counting the terminating NULL). This can be used to determine the
// actual memory to be allocated beforehand.

int WavpackGetTagItem (WavpackContext *wpc, const char *item, char *value, int size)
{
    M_Tag *m_tag = &wpc->m_tag;

    if (value && size)
        *value = 0;

    if (m_tag->ape_tag_hdr.ID [0] == 'A')
        return get_ape_tag_item (m_tag, item, value, size, APE_TAG_TYPE_TEXT);
    else if (m_tag->id3_tag.tag_id [0] == 'T')
        return get_id3_tag_item (m_tag, item, value, size);
    else
        return 0;
}

// Attempt to get the specified binary item from the specified file's APEv2
// tag. The "size" parameter specifies the amount of space available at "value".
// If the desired item will not fit in this space then nothing will be copied
// and 0 will be returned, otherwise the actual size will be returned. If this
// function is called with a NULL "value" pointer (or a zero "length") then only
// the actual length of the value data is returned and can be used to determine
// the actual memory to be allocated beforehand.

int WavpackGetBinaryTagItem (WavpackContext *wpc, const char *item, char *value, int size)
{
    M_Tag *m_tag = &wpc->m_tag;

    if (value && size)
        *value = 0;

    if (m_tag->ape_tag_hdr.ID [0] == 'A')
        return get_ape_tag_item (m_tag, item, value, size, APE_TAG_TYPE_BINARY);
    else
        return 0;
}

// This function looks up the tag item name by index and is used when the
// application wants to access all the items in the file's ID3v1 or APEv2 tag.
// Note that this function accesses only the item's name; WavpackGetTagItem()
// still must be called to get the actual value. The "size" parameter specifies
// the amount of space available at "item", if the desired item will not fit in
// this space then ellipses (...) will be appended and the string terminated.
// The actual length of the string is returned (or 0 if no item exists for
// index). If this function is called with a NULL "value" pointer (or a
// zero "length") then only the actual length of the item name is returned
// (not counting the terminating NULL). This can be used to determine the
// actual memory to be allocated beforehand. For binary tag values use the
// otherwise identical WavpackGetBinaryTagItemIndexed ();

int WavpackGetTagItemIndexed (WavpackContext *wpc, int index, char *item, int size)
{
    M_Tag *m_tag = &wpc->m_tag;

    if (item && size)
        *item = 0;

    if (m_tag->ape_tag_hdr.ID [0] == 'A')
        return get_ape_tag_item_indexed (m_tag, index, item, size, APE_TAG_TYPE_TEXT);
    else if (m_tag->id3_tag.tag_id [0] == 'T')
        return get_id3_tag_item_indexed (m_tag, index, item, size);
    else
        return 0;
}

int WavpackGetBinaryTagItemIndexed (WavpackContext *wpc, int index, char *item, int size)
{
    M_Tag *m_tag = &wpc->m_tag;

    if (item && size)
        *item = 0;

    if (m_tag->ape_tag_hdr.ID [0] == 'A')
        return get_ape_tag_item_indexed (m_tag, index, item, size, APE_TAG_TYPE_BINARY);
    else
        return 0;
}

// These two functions are used to append APEv2 tags to WavPack files; one is
// for text values (UTF-8 encoded) and the other is for binary values. If no tag
// has been started, then an empty one will be allocated first. When finished,
// use WavpackWriteTag() to write the completed tag to the file. The purpose of
// the passed size parameter is obvious for binary values, but might not be for
// text values. Keep in mind that APEv2 text values can have multiple values
// that are NULL separated, so the size is required to know the extent of the
// value (although the final terminating NULL is not included in the passed
// size). If the specified item already exists, it will be replaced with the
// new value. ID3v1 tags are not supported.

int WavpackAppendTagItem (WavpackContext *wpc, const char *item, const char *value, int vsize)
{
    while (WavpackDeleteTagItem (wpc, item));
    return append_ape_tag_item (wpc, item, value, vsize, APE_TAG_TYPE_TEXT);
}

int WavpackAppendBinaryTagItem (WavpackContext *wpc, const char *item, const char *value, int vsize)
{
    while (WavpackDeleteTagItem (wpc, item));
    return append_ape_tag_item (wpc, item, value, vsize, APE_TAG_TYPE_BINARY);
}

// Delete the specified tag item from the APEv2 tag on the specified WavPack file
// (fields cannot be deleted from ID3v1 tags). A return value of TRUE indicates
// that the item was found and successfully deleted.

int WavpackDeleteTagItem (WavpackContext *wpc, const char *item)
{
    M_Tag *m_tag = &wpc->m_tag;

    if (m_tag->ape_tag_hdr.ID [0] == 'A') {
        unsigned char *p = m_tag->ape_tag_data;
        unsigned char *q = p + m_tag->ape_tag_hdr.length - sizeof (APE_Tag_Hdr);
        int i;

        for (i = 0; i < m_tag->ape_tag_hdr.item_count && q - p > 8; ++i) {
            int vsize, isize;

            vsize = p[0] + (p[1] << 8) + (p[2] << 16) + ((uint32_t) p[3] << 24); p += 8;   // skip flags because we don't need them
            for (isize = 0; p + isize < q && p[isize]; ++isize);

            if (vsize < 0 || vsize > m_tag->ape_tag_hdr.length || p + isize + vsize + 1 > q)
                break;

            if (isize && vsize && !stricmp (item, (char *) p)) {
                unsigned char *d = p - 8;

                p += isize + vsize + 1;

                while (p < q)
                    *d++ = *p++;

                m_tag->ape_tag_hdr.length = (int32_t)(d - m_tag->ape_tag_data) + sizeof (APE_Tag_Hdr);
                m_tag->ape_tag_hdr.item_count--;
                return 1;
            }
            else
                p += isize + vsize + 1;
        }
    }

    return 0;
}

// Once a APEv2 tag has been created with WavpackAppendTag(), this function is
// used to write the completed tag to the end of the WavPack file. Note that
// this function uses the same "blockout" function that is used to write
// regular WavPack blocks, although that's where the similarity ends. It is also
// used to write tags that have been edited on existing files.

int WavpackWriteTag (WavpackContext *wpc)
{
    if (wpc->blockout)      // this is the case for creating fresh WavPack files
        return write_tag_blockout (wpc);
    else                    // otherwise we are editing existing tags (OPEN_EDIT_TAGS)
        return write_tag_reader (wpc);
}

////////////////////////// local static functions /////////////////////////////

static int get_ape_tag_item (M_Tag *m_tag, const char *item, char *value, int size, int type)
{
    unsigned char *p = m_tag->ape_tag_data;
    unsigned char *q = p + m_tag->ape_tag_hdr.length - sizeof (APE_Tag_Hdr);
    int i;

    for (i = 0; i < m_tag->ape_tag_hdr.item_count && q - p > 8; ++i) {
        int vsize, flags, isize;

        vsize = p[0] + (p[1] << 8) + (p[2] << 16) + ((uint32_t) p[3] << 24); p += 4;
        flags = p[0] + (p[1] << 8) + (p[2] << 16) + ((uint32_t) p[3] << 24); p += 4;
        for (isize = 0; p + isize < q && p[isize]; ++isize);

        if (vsize < 0 || vsize > m_tag->ape_tag_hdr.length || p + isize + vsize + 1 > q)
            break;

        if (isize && vsize && !stricmp (item, (char *) p) && ((flags & 6) >> 1) == type) {

            if (!value || !size)
                return vsize;

            if (type == APE_TAG_TYPE_BINARY) {
                if (vsize <= size) {
                    memcpy (value, p + isize + 1, vsize);
                    return vsize;
                }
                else
                    return 0;
            }
            else if (vsize < size) {
                memcpy (value, p + isize + 1, vsize);
                value [vsize] = 0;
                return vsize;
            }
            else if (size >= 4) {
                memcpy (value, p + isize + 1, size - 1);
                value [size - 4] = value [size - 3] = value [size - 2] = '.';
                value [size - 1] = 0;
                return size - 1;
            }
            else
                return 0;
        }
        else
            p += isize + vsize + 1;
    }

    return 0;
}

static int get_id3_tag_item (M_Tag *m_tag, const char *item, char *value, int size)
{
    char lvalue [64];
    int len;

    lvalue [0] = 0;

    if (!stricmp (item, "title"))
        tagcpy (lvalue, m_tag->id3_tag.title, sizeof (m_tag->id3_tag.title));
    else if (!stricmp (item, "artist"))
        tagcpy (lvalue, m_tag->id3_tag.artist, sizeof (m_tag->id3_tag.artist));
    else if (!stricmp (item, "album"))
        tagcpy (lvalue, m_tag->id3_tag.album, sizeof (m_tag->id3_tag.album));
    else if (!stricmp (item, "year"))
        tagcpy (lvalue, m_tag->id3_tag.year, sizeof (m_tag->id3_tag.year));
    else if (!stricmp (item, "comment"))
        tagcpy (lvalue, m_tag->id3_tag.comment, sizeof (m_tag->id3_tag.comment));
    else if (!stricmp (item, "track") && m_tag->id3_tag.comment [29] && !m_tag->id3_tag.comment [28])
        sprintf (lvalue, "%d", m_tag->id3_tag.comment [29]);
    else
        return 0;

    len = (int) strlen (lvalue);

    if (!value || !size)
        return len;

    if (len < size) {
        strcpy (value, lvalue);
        return len;
    }
    else if (size >= 4) {
        strncpy (value, lvalue, size - 1);
        value [size - 4] = value [size - 3] = value [size - 2] = '.';
        value [size - 1] = 0;
        return size - 1;
    }
    else
        return 0;
}

static int get_ape_tag_item_indexed (M_Tag *m_tag, int index, char *item, int size, int type)
{
    unsigned char *p = m_tag->ape_tag_data;
    unsigned char *q = p + m_tag->ape_tag_hdr.length - sizeof (APE_Tag_Hdr);
    int i;

    for (i = 0; i < m_tag->ape_tag_hdr.item_count && index >= 0 && q - p > 8; ++i) {
        int vsize, flags, isize;

        vsize = p[0] + (p[1] << 8) + (p[2] << 16) + ((uint32_t) p[3] << 24); p += 4;
        flags = p[0] + (p[1] << 8) + (p[2] << 16) + ((uint32_t) p[3] << 24); p += 4;
        for (isize = 0; p + isize < q && p[isize]; ++isize);

        if (vsize < 0 || vsize > m_tag->ape_tag_hdr.length || p + isize + vsize + 1 > q)
            break;

        if (isize && vsize && ((flags & 6) >> 1) == type && !index--) {

            if (!item || !size)
                return isize;

            if (isize < size) {
                memcpy (item, p, isize);
                item [isize] = 0;
                return isize;
            }
            else if (size >= 4) {
                memcpy (item, p, size - 1);
                item [size - 4] = item [size - 3] = item [size - 2] = '.';
                item [size - 1] = 0;
                return size - 1;
            }
            else
                return 0;
        }
        else
            p += isize + vsize + 1;
    }

    return 0;
}

static int get_id3_tag_item_indexed (M_Tag *m_tag, int index, char *item, int size)
{
    char lvalue [16];
    int len;

    lvalue [0] = 0;

    if (tagdata (m_tag->id3_tag.title, sizeof (m_tag->id3_tag.title)) && !index--)
        strcpy (lvalue, "Title");
    else if (tagdata (m_tag->id3_tag.artist, sizeof (m_tag->id3_tag.artist)) && !index--)
        strcpy (lvalue, "Artist");
    else if (tagdata (m_tag->id3_tag.album, sizeof (m_tag->id3_tag.album)) && !index--)
        strcpy (lvalue, "Album");
    else if (tagdata (m_tag->id3_tag.year, sizeof (m_tag->id3_tag.year)) && !index--)
        strcpy (lvalue, "Year");
    else if (tagdata (m_tag->id3_tag.comment, sizeof (m_tag->id3_tag.comment)) && !index--)
        strcpy (lvalue, "Comment");
    else if (m_tag->id3_tag.comment [29] && !m_tag->id3_tag.comment [28] && !index--)
        strcpy (lvalue, "Track");
    else
        return 0;

    len = (int) strlen (lvalue);

    if (!item || !size)
        return len;

    if (len < size) {
        strcpy (item, lvalue);
        return len;
    }
    else if (size >= 4) {
        strncpy (item, lvalue, size - 1);
        item [size - 4] = item [size - 3] = item [size - 2] = '.';
        item [size - 1] = 0;
        return size - 1;
    }
    else
        return 0;
}

static int append_ape_tag_item (WavpackContext *wpc, const char *item, const char *value, int vsize, int type)
{
    M_Tag *m_tag = &wpc->m_tag;
    int isize = (int) strlen (item);

    if (!m_tag->ape_tag_hdr.ID [0]) {
        memcpy (m_tag->ape_tag_hdr.ID, "APETAGEX", sizeof (m_tag->ape_tag_hdr.ID));
        m_tag->ape_tag_hdr.version = 2000;
        m_tag->ape_tag_hdr.length = sizeof (m_tag->ape_tag_hdr);
        m_tag->ape_tag_hdr.item_count = 0;
        m_tag->ape_tag_hdr.flags = APE_TAG_CONTAINS_HEADER;  // we will include header on tags we originate
    }

    if (m_tag->ape_tag_hdr.ID [0] == 'A') {
        int new_item_len = vsize + isize + 9, flags = type << 1;
        unsigned char *p;

        if (m_tag->ape_tag_hdr.length + new_item_len > APE_TAG_MAX_LENGTH) {
            strcpy (wpc->error_message, "APEv2 tag exceeds maximum allowed length!");
            return FALSE;
        }

        m_tag->ape_tag_hdr.item_count++;
        m_tag->ape_tag_hdr.length += new_item_len;
        p = m_tag->ape_tag_data = (unsigned char*)realloc (m_tag->ape_tag_data, m_tag->ape_tag_hdr.length);
        p += m_tag->ape_tag_hdr.length - sizeof (APE_Tag_Hdr) - new_item_len;

        *p++ = (unsigned char) vsize;
        *p++ = (unsigned char) (vsize >> 8);
        *p++ = (unsigned char) (vsize >> 16);
        *p++ = (unsigned char) (vsize >> 24);

        *p++ = (unsigned char) flags;
        *p++ = (unsigned char) (flags >> 8);
        *p++ = (unsigned char) (flags >> 16);
        *p++ = (unsigned char) (flags >> 24);

        strcpy ((char *) p, item);
        p += isize + 1;
        memcpy (p, value, vsize);

        return TRUE;
    }
    else
        return FALSE;
}

// Append the stored APEv2 tag to the file being created using the "blockout" function callback.

static int write_tag_blockout (WavpackContext *wpc)
{
    M_Tag *m_tag = &wpc->m_tag;
    int result = TRUE;

    if (m_tag->ape_tag_hdr.ID [0] == 'A' && m_tag->ape_tag_hdr.item_count) {

        // only write header if it's specified in the flags

        if (m_tag->ape_tag_hdr.flags & APE_TAG_CONTAINS_HEADER) {
            m_tag->ape_tag_hdr.flags |= APE_TAG_THIS_IS_HEADER;
            WavpackNativeToLittleEndian (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
            result = wpc->blockout (wpc->wv_out, &m_tag->ape_tag_hdr, sizeof (m_tag->ape_tag_hdr));
            WavpackLittleEndianToNative (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
        }

        if (m_tag->ape_tag_hdr.length > sizeof (m_tag->ape_tag_hdr))
            result = wpc->blockout (wpc->wv_out, m_tag->ape_tag_data, m_tag->ape_tag_hdr.length - sizeof (m_tag->ape_tag_hdr));

        m_tag->ape_tag_hdr.flags &= ~APE_TAG_THIS_IS_HEADER;    // this is NOT header
        WavpackNativeToLittleEndian (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
        result = wpc->blockout (wpc->wv_out, &m_tag->ape_tag_hdr, sizeof (m_tag->ape_tag_hdr));
        WavpackLittleEndianToNative (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
    }

    if (!result)
        strcpy (wpc->error_message, "can't write WavPack data, disk probably full!");

    return result;
}

// Write the [potentially] edited tag to the existing WavPack file using the reader callback functions.

static int write_tag_reader (WavpackContext *wpc)
{
    M_Tag *m_tag = &wpc->m_tag;
    int32_t tag_size = 0;
    int result;

    // before we write an edited (or new) tag into an existing file, make sure it's safe and possible

    if (m_tag->tag_begins_file) {
        strcpy (wpc->error_message, "can't edit tags located at the beginning of files!");
        return FALSE;
    }

    if (!wpc->reader->can_seek (wpc->wv_in)) {
        strcpy (wpc->error_message, "can't edit tags on pipes or unseekable files!");
        return FALSE;
    }

    if (!(wpc->open_flags & OPEN_EDIT_TAGS)) {
        strcpy (wpc->error_message, "can't edit tags without OPEN_EDIT_TAGS flag!");
        return FALSE;
    }

    if (m_tag->ape_tag_hdr.ID [0] == 'A' && m_tag->ape_tag_hdr.item_count &&
        m_tag->ape_tag_hdr.length > sizeof (m_tag->ape_tag_hdr))
            tag_size = m_tag->ape_tag_hdr.length;

    // only write header if it's specified in the flags

    if (tag_size && (m_tag->ape_tag_hdr.flags & APE_TAG_CONTAINS_HEADER))
        tag_size += sizeof (m_tag->ape_tag_hdr);

    result = !wpc->reader->set_pos_rel (wpc->wv_in, m_tag->tag_file_pos, SEEK_END);

    if (result && tag_size < -m_tag->tag_file_pos && !wpc->reader->truncate_here) {
        int nullcnt = (int) (-m_tag->tag_file_pos - tag_size);
        char zero [1] = { 0 };

        while (nullcnt--)
            wpc->reader->write_bytes (wpc->wv_in, &zero, 1);
    }

    if (result && tag_size) {
        if (m_tag->ape_tag_hdr.flags & APE_TAG_CONTAINS_HEADER) {
            m_tag->ape_tag_hdr.flags |= APE_TAG_THIS_IS_HEADER;
            WavpackNativeToLittleEndian (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
            result = (wpc->reader->write_bytes (wpc->wv_in, &m_tag->ape_tag_hdr, sizeof (m_tag->ape_tag_hdr)) == sizeof (m_tag->ape_tag_hdr));
            WavpackLittleEndianToNative (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
        }

        result = (wpc->reader->write_bytes (wpc->wv_in, m_tag->ape_tag_data, m_tag->ape_tag_hdr.length - sizeof (m_tag->ape_tag_hdr)) == sizeof (m_tag->ape_tag_hdr));
        m_tag->ape_tag_hdr.flags &= ~APE_TAG_THIS_IS_HEADER;    // this is NOT header
        WavpackNativeToLittleEndian (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
        result = (wpc->reader->write_bytes (wpc->wv_in, &m_tag->ape_tag_hdr, sizeof (m_tag->ape_tag_hdr)) == sizeof (m_tag->ape_tag_hdr));
        WavpackLittleEndianToNative (&m_tag->ape_tag_hdr, APE_Tag_Hdr_Format);
    }

    if (result && tag_size < -m_tag->tag_file_pos && wpc->reader->truncate_here)
        result = !wpc->reader->truncate_here (wpc->wv_in);

    if (!result)
        strcpy (wpc->error_message, "can't write WavPack data, disk probably full!");

    return result;
}

// Copy the specified ID3v1 tag value (with specified field size) from the
// source pointer to the destination, eliminating leading spaces and trailing
// spaces and nulls.

static void tagcpy (char *dest, char *src, int tag_size)
{
    char *s1 = src, *s2 = src + tag_size - 1;

    if (*s2 && !s2 [-1])
        s2--;

    while (s1 <= s2)
        if (*s1 == ' ')
            ++s1;
        else if (!*s2 || *s2 == ' ')
            --s2;
        else
            break;

    while (*s1 && s1 <= s2)
        *dest++ = *s1++;

    *dest = 0;
}

static int tagdata (char *src, int tag_size)
{
    char *s1 = src, *s2 = src + tag_size - 1;

    if (*s2 && !s2 [-1])
        s2--;

    while (s1 <= s2)
        if (*s1 == ' ')
            ++s1;
        else if (!*s2 || *s2 == ' ')
            --s2;
        else
            break;

    return (*s1 && s1 <= s2);
}
