/*****************************************************************************
 * taglib.cpp: Taglib tag parser/writer
 *****************************************************************************
 * Copyright (C) 2003-2016 VLC authors and VideoLAN
 * $Id$
 *
 * Authors: Clément Stenac <zorglub@videolan.org>
 *          Rafaël Carré <funman@videolanorg>
 *          Rémi Duraffort <ivoire@videolan.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser 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.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_demux.h>              /* demux_meta_t */
#include <vlc_strings.h>            /* vlc_b64_decode_binary */
#include <vlc_input.h>              /* for attachment_new */
#include <vlc_url.h>                /* vlc_uri2path */
#include <vlc_mime.h>               /* mime type */
#include <vlc_fs.h>
#include <vlc_charset.h>

#include <sys/stat.h>

#ifdef _WIN32
# include <vlc_charset.h>           /* ToWide */
# include <io.h>
#else
# include <unistd.h>
#endif

// Taglib headers
#ifdef _WIN32
# define TAGLIB_STATIC
#endif
#include <taglib.h>

#define VERSION_INT(a, b, c) ((a)<<16 | (b)<<8 | (c))
#define TAGLIB_VERSION VERSION_INT(TAGLIB_MAJOR_VERSION, \
                                   TAGLIB_MINOR_VERSION, \
                                   TAGLIB_PATCH_VERSION)

#define TAGLIB_VERSION_1_11 VERSION_INT(1,11,0)

#if TAGLIB_VERSION >= TAGLIB_VERSION_1_11
#include <tfilestream.h>
#endif
#include <fileref.h>
#include <tag.h>
#include <tbytevector.h>

/* Support for stream-based metadata */
#if TAGLIB_VERSION >= TAGLIB_VERSION_1_11
# include <vlc_access.h>
# include <tiostream.h>
#endif

#include <apefile.h>
#include <asffile.h>
#include <apetag.h>
#include <flacfile.h>
#include <flacpicture.h>
#include <mpcfile.h>
#include <mpegfile.h>
#include <mp4file.h>
#include <oggfile.h>
#include <oggflacfile.h>
#include <opusfile.h>
#include "ID3Pictures.h"

#include <aifffile.h>
#include <wavfile.h>

#include <speexfile.h>
#include <trueaudiofile.h>
#include <vorbisfile.h>
#include <wavpackfile.h>

#include <attachedpictureframe.h>
#include <textidentificationframe.h>
#include <uniquefileidentifierframe.h>

using namespace TagLib;


#include <algorithm>
#include <limits>

#if TAGLIB_VERSION >= VERSION_INT(1, 13, 0)
#define USE_IOSTREAM_RESOLVER 1
#endif

namespace VLCTagLib
{
    template <class T>
#ifdef USE_IOSTREAM_RESOLVER
    class ExtResolver : public FileRef::StreamTypeResolver
#else
    class ExtResolver : public FileRef::FileTypeResolver
#endif
    {
        public:
            ExtResolver(const std::string &);
            ~ExtResolver() {}
            virtual File *createFile(FileName, bool, AudioProperties::ReadStyle) const;
#ifdef USE_IOSTREAM_RESOLVER
            virtual File *createFileFromStream(IOStream*, bool, AudioProperties::ReadStyle) const;
#endif

        protected:
            std::string ext;
    };
}

template <class T>
VLCTagLib::ExtResolver<T>::ExtResolver(const std::string & ext)
{
    this->ext = ext;
    std::transform(this->ext.begin(), this->ext.end(), this->ext.begin(), ::toupper);
}

template <class T>
File *VLCTagLib::ExtResolver<T>::createFile(FileName fileName, bool, AudioProperties::ReadStyle) const
{
#if defined(_WIN32) && TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    std::string filename = fileName.toString().to8Bit(true);
#else
    std::string filename = std::string(fileName);
#endif
    std::size_t namesize = filename.size();

    if (namesize > ext.length())
    {
        std::string fext = filename.substr(namesize - ext.length(), ext.length());
        std::transform(fext.begin(), fext.end(), fext.begin(), ::toupper);
        if(fext == ext)
            return new T(fileName, false, AudioProperties::Fast);
    }

    return nullptr;
}

#ifdef USE_IOSTREAM_RESOLVER
template<class T>
File* VLCTagLib::ExtResolver<T>::createFileFromStream(IOStream* s, bool, AudioProperties::ReadStyle) const
{
#if defined(_WIN32) && TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    std::string filename = s->name().toString().to8Bit(true);
#else
    std::string filename = std::string(s->name());
#endif
    std::size_t namesize = filename.size();

    if (namesize > ext.length())
    {
        std::string fext = filename.substr(namesize - ext.length(), ext.length());
        std::transform(fext.begin(), fext.end(), fext.begin(), ::toupper);
        if(fext == ext)
#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
            return new T(s, false, AudioProperties::Fast, ID3v2::FrameFactory::instance());
#else
            return new T(s, ID3v2::FrameFactory::instance(), false, AudioProperties::Fast);
#endif
    }

    return nullptr;
}
#endif

#if TAGLIB_VERSION >= TAGLIB_VERSION_1_11
static VLCTagLib::ExtResolver<MPEG::File> aacresolver(".aac");
#endif
static bool b_extensions_registered = false;

// taglib is not thread safe
static vlc_mutex_t taglib_lock = VLC_STATIC_MUTEX;

// Local functions
static int ReadMeta    ( vlc_object_t * );
static int WriteMeta   ( vlc_object_t * );

vlc_module_begin ()
    set_capability( "meta reader", 1000 )
    set_callbacks( ReadMeta, NULL )
    add_submodule ()
        set_capability( "meta writer", 50 )
        set_callbacks( WriteMeta, NULL )
vlc_module_end ()

#if TAGLIB_VERSION >= TAGLIB_VERSION_1_11
class VlcIostream : public IOStream
{
public:
    VlcIostream(stream_t* p_stream)
        : m_stream( p_stream )
        , m_previousPos( 0 )
        , m_borked( false )
        , m_seqReadLength( 0 )
        , m_seqReadLimit( std::numeric_limits<long>::max() )
    {
    }

    ~VlcIostream()
    {
        vlc_stream_Delete( m_stream );
    }

    FileName name() const
    {
        // Taglib only cares about the file name part, so it doesn't matter
        // whether we include the mrl scheme or not
        return m_stream->psz_url;
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    ByteVector readBlock(size_t length)
#else
    ByteVector readBlock(ulong length)
#endif
    {
        if(m_borked || m_seqReadLength >= m_seqReadLimit)
            return {};
        ByteVector res(length, 0);
        ssize_t i_read = vlc_stream_Read( m_stream, res.data(), length);
        if (i_read < 0)
            return {};
        else if ((size_t)i_read != length)
            res.resize(i_read);
        m_previousPos += i_read;
        m_seqReadLength += i_read;
        return res;
    }

    void writeBlock(const ByteVector&)
    {
        // Let's stay Read-Only for now
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    void insert(const ByteVector&, offset_t, size_t)
#else
    void insert(const ByteVector&, ulong, ulong)
#endif
    {
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    void removeBlock(offset_t, size_t)
#else
    void removeBlock(ulong, ulong)
#endif
    {
    }

    bool readOnly() const
    {
        return true;
    }

    bool isOpen() const
    {
        return true;
    }

    void setMaxSequentialRead(long s)
    {
        m_seqReadLimit = s;
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    void seek(offset_t offset, Position p)
#else
    void seek(long offset, Position p)
#endif
    {
        uint64_t pos = 0;
        long len;
        switch (p)
        {
            case Current:
                pos = m_previousPos;
                break;
            case End:
                len = length();
                if(len > -1)
                {
                    pos = len;
                }
                else
                {
                    m_borked = true;
                    return;
                }
                break;
            default:
                break;
        }
        m_borked = (vlc_stream_Seek( m_stream, pos + offset ) != 0);
        if(!m_borked)
            m_previousPos = pos + offset;
        m_seqReadLength = 0;
    }

    void clear()
    {
        return;
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    offset_t tell() const
#else
    long tell() const
#endif
    {
        return m_previousPos;
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    offset_t length()
#else
    long length()
#endif
    {
        uint64_t i_size;
        if (vlc_stream_GetSize( m_stream, &i_size ) != VLC_SUCCESS)
            return -1;
        return i_size;
    }

#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
    void truncate(offset_t)
#else
    void truncate(long)
#endif
    {
    }

private:
    stream_t* m_stream;
    int64_t m_previousPos;
    bool m_borked;
    long m_seqReadLength;
    long m_seqReadLimit;
};
#endif /* TAGLIB_VERSION_1_11 */

static int ExtractCoupleNumberValues( vlc_meta_t* p_meta, const char *psz_value,
        vlc_meta_type_t first, vlc_meta_type_t second)
{
    unsigned int i_trknum, i_trktot;

    int i_ret = sscanf( psz_value, "%u/%u", &i_trknum, &i_trktot );
    char psz_trck[11];
    if( i_ret >= 1 )
    {
        snprintf( psz_trck, sizeof( psz_trck ), "%u", i_trknum );
        vlc_meta_Set( p_meta, first, psz_trck );
    }
    if( i_ret == 2)
    {
        snprintf( psz_trck, sizeof( psz_trck ), "%u", i_trktot );
        vlc_meta_Set( p_meta, second, psz_trck );
    }
    return i_ret;
}

/**
 * Read meta information from APE tags
 * @param tag: the APE tag
 * @param p_demux_meta: the demuxer meta
 * @param p_meta: the meta
 */
static void ReadMetaFromAPE( APE::Tag* tag, demux_meta_t* p_demux_meta, vlc_meta_t* p_meta )
{
    APE::ItemListMap fields ( tag->itemListMap() );
    APE::ItemListMap::Iterator iter;

    iter = fields.find("COVER ART (FRONT)");
    if( iter != fields.end()
        && !iter->second.isEmpty()
        && iter->second.type() == APE::Item::Binary)
    {
        input_attachment_t *p_attachment;

        const ByteVector picture = iter->second.binaryData();
        const char *p_data = picture.data();
        unsigned i_data = picture.size();

        /* Null terminated filename followed by the image data */
        size_t desc_len = strnlen(p_data, i_data);
        if( desc_len < i_data && IsUTF8( p_data ) )
        {
            const char *psz_name = p_data;
            const char *psz_mime = vlc_mime_Ext2Mime( psz_name );
            p_data += desc_len + 1; /* '\0' */
            i_data -= desc_len + 1;

            msg_Dbg( p_demux_meta, "Found embedded art: %s (%s) is %u bytes",
                     psz_name, psz_mime, i_data );

            p_attachment = vlc_input_attachment_New( psz_name, psz_mime,
                                    psz_name, p_data, i_data );
            if( p_attachment )
            {
                TAB_APPEND_CAST( (input_attachment_t**),
                                 p_demux_meta->i_attachments, p_demux_meta->attachments,
                                 p_attachment );

                char *psz_url;
                if( asprintf( &psz_url, "attachment://%s", p_attachment->psz_name ) != -1 )
                {
                    vlc_meta_SetArtURL( p_meta, psz_url );
                    free( psz_url );
                }
            }
        }

        fields.erase(iter);
    }

#define SET( keyName, metaName ) \
    iter = fields.find(keyName); \
    if( iter != fields.end() && !iter->second.isEmpty() ) { \
        vlc_meta_Set##metaName( p_meta, iter->second.toString().toCString( true ) ); \
        fields.erase(iter); \
    }

#define SET_EXTRA( keyName, metaName ) \
    iter = fields.find( keyName ); \
    if( iter != fields.end() && !iter->second.isEmpty() ) { \
        vlc_meta_AddExtra( p_meta, metaName, iter->second.toString().toCString( true ) ); \
        fields.erase(iter); \
    }

    SET( "ALBUM", Album );
    SET( "ARTIST", Artist );
    SET( "COMMENT", Description );
    SET( "GENRE", Genre );
    SET( "TITLE", Title );
    SET( "COPYRIGHT", Copyright );
    SET( "LANGUAGE", Language );
    SET( "PUBLISHER", Publisher );
    SET( "MUSICBRAINZ_TRACKID", TrackID );

    SET_EXTRA( "MUSICBRAINZ_ALBUMID", VLC_META_EXTRA_MB_ALBUMID );

#undef SET
#undef SET_EXTRA

    /* */
    iter = fields.find( "TRACK" );
    if( iter != fields.end() && !iter->second.isEmpty() )
    {
        ExtractCoupleNumberValues( p_meta, iter->second.toString().toCString( true ),
                vlc_meta_TrackNumber, vlc_meta_TrackTotal );
        fields.erase( iter );
    }

    /* Remainings */
    for( iter = fields.begin(); iter != fields.end(); ++iter )
    {
        if( iter->second.isEmpty() )
            continue;

        if( iter->second.type() != APE::Item::Text )
            continue;

        vlc_meta_AddExtra( p_meta,
                           iter->first.toCString( true ),
                           iter->second.toString().toCString( true ) );
    }
}


/**
 * Read meta information from ASF tags
 * @param tag: the ASF tag
 * @param p_demux_meta: the demuxer meta
 * @param p_meta: the meta
 */
static void ReadMetaFromASF( ASF::Tag* tag, demux_meta_t* p_demux_meta, vlc_meta_t* p_meta )
{

    ASF::AttributeList list;
#define SET( keyName, metaName )                                                     \
    if( tag->attributeListMap().contains(keyName) )                                  \
    {                                                                                \
        list = tag->attributeListMap()[keyName];                                     \
        vlc_meta_Set##metaName( p_meta, list.front().toString().toCString( true ) ); \
    }

#define SET_EXTRA( keyName, metaName )                                                     \
    if( tag->attributeListMap().contains(keyName) )                                  \
    {                                                                                \
        list = tag->attributeListMap()[keyName];                                     \
        vlc_meta_AddExtra( p_meta, metaName, list.front().toString().toCString( true ) ); \
    }

    SET("MusicBrainz/Track Id", TrackID );
    SET_EXTRA("MusicBrainz/Album Id", VLC_META_EXTRA_MB_ALBUMID );

#undef SET
#undef SET_EXTRA

    // List the pictures
    list = tag->attributeListMap()["WM/Picture"];
    ASF::AttributeList::Iterator iter;
    for( iter = list.begin(); iter != list.end(); iter++ )
    {
        const ASF::Picture asfPicture = (*iter).toPicture();
        const ByteVector picture = asfPicture.picture();
        const char *psz_mime = asfPicture.mimeType().toCString();
        const char *p_data = picture.data();
        const unsigned i_data = picture.size();
        char *psz_name;
        input_attachment_t *p_attachment;

        if( asfPicture.description().size() > 0 )
            psz_name = strdup( asfPicture.description().toCString( true ) );
        else
        {
            if( asprintf( &psz_name, "%i", asfPicture.type() ) == -1 )
                psz_name = NULL;
        }

        if( unlikely(psz_name == NULL) )
            continue;

        msg_Dbg( p_demux_meta, "Found embedded art: %s (%s) is %u bytes",
                 psz_name, psz_mime, i_data );

        p_attachment = vlc_input_attachment_New( psz_name, psz_mime,
                                psz_name, p_data, i_data );
        if( p_attachment )
            TAB_APPEND_CAST( (input_attachment_t**),
                             p_demux_meta->i_attachments, p_demux_meta->attachments,
                             p_attachment );
        char *psz_url;
        if( asprintf( &psz_url, "attachment://%s", psz_name ) != -1 )
        {
            vlc_meta_SetArtURL( p_meta, psz_url );
            free( psz_url );
        }
        free( psz_name );
    }
}

static void AddAPICToAttachments( demux_meta_t* p_demux_meta,
                                  vlc_meta_t* p_meta,
                                  const String &mimeType,
                                  const String &description,
                                  const char *p_data,
                                  size_t i_data,
                                  bool b_default = false )
{
    char *psz_name;
    if( asprintf( &psz_name, "%i", p_demux_meta->i_attachments ) == -1 )
        return;

    input_attachment_t *p_attachment =
        vlc_input_attachment_New( psz_name,
                                  mimeType.toCString(),
                                  description.toCString(),
                                  p_data, i_data );
    free( psz_name );
    if( !p_attachment )
        return;

    msg_Dbg( p_demux_meta, "Found embedded art: %s (%zu bytes)",
             p_attachment->psz_mime, p_attachment->i_data );

    TAB_APPEND_CAST( (input_attachment_t**),
                     p_demux_meta->i_attachments, p_demux_meta->attachments,
                     p_attachment );

    if( b_default )
    {
        char *psz_url;
        if( asprintf( &psz_url, "attachment://%s",
                      p_attachment->psz_name ) == -1 )
            return;
        vlc_meta_SetArtURL( p_meta, psz_url );
        free( psz_url );
    }
}

/**
 * Fills attachments list from ID3 APIC tags
 * @param tag: the APIC tags list
 * @param p_demux_meta: the demuxer meta
 * @param p_meta: the meta
 */

template<class T, class L>
    const T * getDefaultPic(const L &list)
{
    const T *defaultPic = nullptr;
    int bestscore = 0;
    for( auto iter = list.begin(); iter != list.end(); ++iter )
    {
        const T* p = static_cast<const T*>(*iter);
        if( !p )
            continue;
        int score = p->type() >= ARRAY_SIZE(ID3v2_cover_scores) ? 0 : ID3v2_cover_scores[p->type()];
        if(defaultPic == nullptr || score > bestscore)
        {
            defaultPic = p;
            bestscore = score;
        }
    }
    return defaultPic;
}

static void ProcessAPICListFromId3v2( const ID3v2::FrameList &list,
                                      demux_meta_t* p_demux_meta, vlc_meta_t* p_meta )
{
    const ID3v2::AttachedPictureFrame *defaultPic =
        getDefaultPic<ID3v2::AttachedPictureFrame, ID3v2::FrameList>(list);

    for( auto iter = list.begin(); iter != list.end(); ++iter )
    {
        const ID3v2::AttachedPictureFrame* p =
                dynamic_cast<const ID3v2::AttachedPictureFrame*>(*iter);
        if( !p )
            continue;
        // Get the mime and description of the image.
        String description = p->description();
        String mimeType = p->mimeType();

        /* some old iTunes version not only sets incorrectly the mime type
         * or the description of the image,
         * but also embeds incorrectly the image.
         * Recent versions seem to behave correctly */
        if( mimeType == "PNG" || description == "\xC2\x89PNG" )
        {
            msg_Warn( p_demux_meta, "Invalid picture embedded by broken iTunes version" );
            continue;
        }

        AddAPICToAttachments( p_demux_meta, p_meta,
                              mimeType, description,
                              p->picture().data(), p->picture().size(),
                              p == defaultPic );
    }
}

static void ProcessAPICListFromFLAC( const List< FLAC::Picture * > &list,
                                     demux_meta_t* p_demux_meta, vlc_meta_t* p_meta )
{
    const FLAC::Picture *defaultPic =
        getDefaultPic<FLAC::Picture, List< FLAC::Picture * >>(list);

    for( auto iter = list.begin(); iter != list.end(); ++iter )
    {
        const FLAC::Picture* p = static_cast<const FLAC::Picture*>(*iter);
        if( !p )
            continue;
        AddAPICToAttachments( p_demux_meta, p_meta,
                              p->mimeType(), p->description(),
                              p->data().data(), p->data().size(),
                              p == defaultPic );
    }
}

static void ReadMetaFromBasicTag(const Tag* tag, vlc_meta_t *dest)
{
#define SET( accessor, meta )                                                  \
    if( !tag->accessor().isEmpty() )                                           \
        vlc_meta_Set##meta( dest, tag->accessor().toCString(true) )
#define SETINT( accessor, meta )                                               \
    if( tag->accessor() )                                                      \
    {                                                                          \
        char tmp[10];                                                          \
        snprintf( tmp, 10, "%d", tag->accessor() );                            \
        vlc_meta_Set##meta( dest, tmp );                                       \
    }

    SET( title, Title );
    SET( artist, Artist );
    SET( album, Album );
    SET( comment, Description );
    SET( genre, Genre );
    SETINT( year, Date );
    SETINT( track, TrackNum );

#undef SETINT
#undef SET
}

/**
 * Read meta information from id3v2 tags
 * @param tag: the id3v2 tag
 * @param p_demux_meta: the demuxer meta
 * @param p_meta: the meta
 */
static void ReadMetaFromId3v2( ID3v2::Tag* tag, demux_meta_t* p_demux_meta, vlc_meta_t* p_meta )
{
    // Get the unique file identifier
    ID3v2::FrameList list = tag->frameListMap()["UFID"];
    ID3v2::FrameList::Iterator iter;
    for( iter = list.begin(); iter != list.end(); iter++ )
    {
        ID3v2::UniqueFileIdentifierFrame* p_ufid =
                dynamic_cast<ID3v2::UniqueFileIdentifierFrame*>(*iter);
        if( !p_ufid )
            continue;
        const char *owner = p_ufid->owner().toCString();
        if (!strcmp( owner, "http://musicbrainz.org" ))
        {
            /* ID3v2 UFID contains up to 64 bytes binary data
             * but in our case it will be a '\0'
             * terminated string */
            char psz_ufid[64];
            int max_size = std::min<unsigned>( p_ufid->identifier().size(), 63);
            strncpy( psz_ufid, p_ufid->identifier().data(), max_size );
            psz_ufid[max_size] = '\0';
            vlc_meta_SetTrackID( p_meta, psz_ufid );
        }
    }

    // Get the use text
    list = tag->frameListMap()["TXXX"];
    for( iter = list.begin(); iter != list.end(); iter++ )
    {
        ID3v2::UserTextIdentificationFrame* p_txxx =
                dynamic_cast<ID3v2::UserTextIdentificationFrame*>(*iter);
        if( !p_txxx )
            continue;
        if( !strcmp( p_txxx->description().toCString( true ), "TRACKTOTAL" ) )
        {
            vlc_meta_Set( p_meta, vlc_meta_TrackTotal, p_txxx->fieldList().back().toCString( true ) );
            continue;
        }
        if( !strcmp( p_txxx->description().toCString( true ), "MusicBrainz Album Id" ) )
        {
            vlc_meta_AddExtra( p_meta, VLC_META_EXTRA_MB_ALBUMID, p_txxx->fieldList().back().toCString( true ) );
            continue;
        }
        vlc_meta_AddExtra( p_meta, p_txxx->description().toCString( true ),
                           p_txxx->fieldList().back().toCString( true ) );
    }

    // Get some more information
#define SET( tagName, metaName )                                               \
    list = tag->frameListMap()[tagName];                                       \
    if( !list.isEmpty() )                                                      \
        vlc_meta_Set##metaName( p_meta,                                        \
                                (*list.begin())->toString().toCString( true ) );

#define SET_EXTRA( tagName, metaName )\
    list = tag->frameListMap()[tagName];\
    if( !list.isEmpty() )\
        vlc_meta_AddExtra( p_meta, metaName,\
                           (*list.begin())->toString().toCString( true ) );


    SET( "TCOP", Copyright );
    SET( "TENC", EncodedBy );
    SET( "TLAN", Language );
    SET( "TPUB", Publisher );
    SET( "TPE2", AlbumArtist );
    SET_EXTRA( "USLT", "Lyrics" );

#undef SET_EXTRA
#undef SET

    /* */
    list = tag->frameListMap()["TRCK"];
    if( !list.isEmpty() )
    {
        ExtractCoupleNumberValues( p_meta, (*list.begin())->toString().toCString( true ),
                vlc_meta_TrackNumber, vlc_meta_TrackTotal );
    }

    /* */
    list = tag->frameListMap()["TPOS"];
    if( !list.isEmpty() )
    {
        ExtractCoupleNumberValues( p_meta, (*list.begin())->toString().toCString( true ),
                vlc_meta_DiscNumber, vlc_meta_DiscTotal );
    }

    // Try now to get embedded art
    list = tag->frameListMap()[ "APIC" ];
    if( !list.isEmpty() )
        ProcessAPICListFromId3v2( list, p_demux_meta, p_meta );
}

/**
 * Read the meta information from XiphComments
 * @param tag: the Xiph Comment
 * @param p_demux_meta: the demuxer meta
 * @param p_meta: the meta
 */
static void ReadMetaFromXiph( Ogg::XiphComment* tag, demux_meta_t* p_demux_meta, vlc_meta_t* p_meta )
{
    bool hasTrackTotal = false;
#define SET( keyName, metaName ) \
    { \
        StringList tmp_list { tag->fieldListMap()[keyName] }; \
        if( !tmp_list.isEmpty() ) \
            vlc_meta_Set##metaName( p_meta, (*tmp_list.begin()).toCString( true ) ); \
    }

#define SET_EXTRA( keyName, metaName ) \
    { \
        StringList tmp_list = tag->fieldListMap()[keyName]; \
        if( !tmp_list.isEmpty() ) \
            vlc_meta_AddExtra( p_meta, keyName, (*tmp_list.begin()).toCString( true ) ); \
    }

    SET( "COPYRIGHT", Copyright );
    SET( "ORGANIZATION", Publisher );
    SET( "DATE", Date );
    SET( "ENCODER", EncodedBy );
    SET( "RATING", Rating );
    SET( "LANGUAGE", Language );
    SET( "MUSICBRAINZ_TRACKID", TrackID );
    SET( "ALBUMARTIST", AlbumArtist );
    SET( "DISCNUMBER", DiscNumber );

    SET_EXTRA( "MUSICBRAINZ_ALBUMID", VLC_META_EXTRA_MB_ALBUMID );
#undef SET
#undef SET_EXTRA

    StringList track_number_list = tag->fieldListMap()["TRACKNUMBER"];
    if( !track_number_list.isEmpty() )
    {
        int i_values = ExtractCoupleNumberValues( p_meta, (*track_number_list.begin()).toCString( true ),
                 vlc_meta_TrackNumber, vlc_meta_TrackTotal );
        hasTrackTotal = i_values == 2;
    }
    if( !hasTrackTotal )
    {
        StringList track_total_list { tag->fieldListMap()["TRACKTOTAL"] };
        if( track_total_list.isEmpty() )
        {
            StringList total_tracks_list { tag->fieldListMap()["TOTALTRACKS"] };
            if( !total_tracks_list.isEmpty() )
                vlc_meta_SetTrackTotal( p_meta, (*total_tracks_list.begin()).toCString( true ) );
        }
        else
        {
            vlc_meta_SetTrackTotal( p_meta, (*track_total_list.begin()).toCString( true ) );
        }
    }

    // Taglib extracts if(key == "METADATA_BLOCK_PICTURE" || key == "COVERART")
    ProcessAPICListFromFLAC( tag->pictureList(), p_demux_meta, p_meta );
}

/**
 * Read the meta information from mp4 specific tags
 * @param tag: the mp4 tag
 * @param p_demux_meta: the demuxer meta
 * @param p_meta: the meta
 */
static void ReadMetaFromMP4( MP4::Tag* tag, demux_meta_t *p_demux_meta, vlc_meta_t* p_meta )
{
    MP4::Item list;
#define SET( keyName, metaName )                                                             \
    if( tag->contains(keyName) )                                                             \
    {                                                                                        \
        list = tag->item(keyName);                                                           \
        vlc_meta_Set##metaName( p_meta, list.toStringList().front().toCString( true ) );     \
    }
#define SET_EXTRA( keyName, metaName )                                                   \
    if( tag->contains(keyName) )                                                         \
    {                                                                                    \
        list = tag->item(keyName);                                                       \
        vlc_meta_AddExtra( p_meta, metaName, list.toStringList().front().toCString( true ) ); \
    }

    SET("----:com.apple.iTunes:MusicBrainz Track Id", TrackID );
    SET_EXTRA("----:com.apple.iTunes:MusicBrainz Album Id", VLC_META_EXTRA_MB_ALBUMID );

#undef SET
#undef SET_EXTRA

    if( tag->contains("covr") )
    {
        MP4::CoverArtList cover_list = tag->item("covr").toCoverArtList();
        const char *psz_format = cover_list[0].format() == MP4::CoverArt::PNG ? "image/png" : "image/jpeg";

        msg_Dbg( p_demux_meta, "Found embedded art (%s) is %i bytes",
                 psz_format, cover_list[0].data().size() );

        input_attachment_t *p_attachment =
                vlc_input_attachment_New( "cover", psz_format, "cover",
                                          cover_list[0].data().data(), cover_list[0].data().size() );
        if( p_attachment )
        {
            TAB_APPEND_CAST( (input_attachment_t**),
                             p_demux_meta->i_attachments, p_demux_meta->attachments,
                             p_attachment );
            vlc_meta_SetArtURL( p_meta, "attachment://cover" );
        }
    }
}

static int ReadWAVMeta( const RIFF::WAV::File *wav, demux_meta_t *demux_meta )
{
    if( !wav->hasID3v2Tag() && !wav->hasInfoTag() )
        return VLC_EGENERIC;

    demux_meta->p_meta = vlc_meta_New();
    if( !demux_meta->p_meta )
        return VLC_ENOMEM;

    TAB_INIT( demux_meta->i_attachments, demux_meta->attachments );

    if( wav->hasInfoTag() )
        ReadMetaFromBasicTag( wav->InfoTag(), demux_meta->p_meta );
    if( wav->hasID3v2Tag() )
    {
        // Re-read basic tags from id3 to prioritize it against INFO tags.
        ReadMetaFromBasicTag( wav->ID3v2Tag(), demux_meta->p_meta );
        ReadMetaFromId3v2( wav->ID3v2Tag(), demux_meta, demux_meta->p_meta );
    }
    return VLC_SUCCESS;
}

static bool isSchemeCompatible( const char *psz_uri )
{
    const char *p = strstr( psz_uri, "://" );
    if( p == NULL )
        return false;

    size_t i_len = p - psz_uri;
    const char * compatibleschemes[] =
    {
        "file", "smb",
    };
    for( size_t i=0; i<ARRAY_SIZE(compatibleschemes); i++ )
        if( !strncasecmp( psz_uri, compatibleschemes[i], i_len ) )
            return true;
    return false;
}

/**
 * Get the tags from the file using TagLib
 * @param p_this: the demux object
 * @return VLC_SUCCESS if the operation success
 */
static int ReadMeta( vlc_object_t* p_this)
{
    vlc_mutex_locker locker (&taglib_lock);
    demux_meta_t*   p_demux_meta = (demux_meta_t *)p_this;
    vlc_meta_t*     p_meta;
    FileRef f;

    p_demux_meta->p_meta = NULL;

    char *psz_uri = input_item_GetURI( p_demux_meta->p_item );
    if( unlikely(psz_uri == NULL) )
        return VLC_ENOMEM;

    if( !isSchemeCompatible( psz_uri ) )
    {
        free( psz_uri );
        return VLC_EGENERIC;
    }

    if( !b_extensions_registered )
    {
#if TAGLIB_VERSION >= TAGLIB_VERSION_1_11
        FileRef::addFileTypeResolver( &aacresolver );
#endif
        b_extensions_registered = true;
    }

#if TAGLIB_VERSION >= TAGLIB_VERSION_1_11
    stream_t *p_stream = vlc_access_NewMRL( p_this, psz_uri );
    free( psz_uri );
    if( p_stream == NULL )
        return VLC_EGENERIC;
    stream_t* p_filter = vlc_stream_FilterNew( p_stream, "prefetch,cache" );
    if( p_filter )
        p_stream = p_filter;

    VlcIostream s( p_stream );
#ifndef VLC_PATCHED_TAGLIB_ID3V2_READSTYLE
    uint64_t dummy;
    if( vlc_stream_GetSize( p_stream, &dummy ) != VLC_SUCCESS )
        s.setMaxSequentialRead( 2048 );
    else
        s.setMaxSequentialRead( 1024 * 2048 );
#endif
    f = FileRef( &s, false, AudioProperties::ReadStyle::Fast );
#else // !TAGLIB_VERSION_1_11
    char *psz_path = vlc_uri2path( psz_uri );
    free( psz_uri );
    if( psz_path == NULL )
        return VLC_EGENERIC;

#if defined(_WIN32)
    wchar_t *wpath = ToWide( psz_path );
    if( wpath == NULL )
    {
        free( psz_path );
        return VLC_EGENERIC;
    }
    f = FileRef( wpath );
    free( wpath );
#else /* _WIN32 */
    f = FileRef( psz_path );
#endif /* _WIN32 */
    free( psz_path );
#endif // !TAGLIB_VERSION_1_11

    if( f.isNull() )
        return VLC_EGENERIC;

    // XXX: Workaround a quirk in TagLib that doesn't merge id3 tags and RIFF
    // INFO tags in `Wav::File::tag()`'s return value.
    // This forces us to parse WAV separately for now.
    const auto* riff_wav = dynamic_cast<RIFF::WAV::File*>(f.file());
    if (riff_wav != nullptr)
        return ReadWAVMeta(riff_wav, p_demux_meta);

    if( !f.tag() || f.tag()->isEmpty() )
        return VLC_EGENERIC;

    p_demux_meta->p_meta = p_meta = vlc_meta_New();
    if( !p_meta )
        return VLC_ENOMEM;

    // Read the tags from the file
    ReadMetaFromBasicTag(f.tag(), p_meta);

    TAB_INIT( p_demux_meta->i_attachments, p_demux_meta->attachments );

    if( APE::File* ape = dynamic_cast<APE::File*>(f.file()) )
    {
        if( ape->APETag() )
            ReadMetaFromAPE( ape->APETag(), p_demux_meta, p_meta );
    }
    else
    if( ASF::File* asf = dynamic_cast<ASF::File*>(f.file()) )
    {
        if( asf->tag() )
            ReadMetaFromASF( asf->tag(), p_demux_meta, p_meta );
    }
    else
    if( FLAC::File* flac = dynamic_cast<FLAC::File*>(f.file()) )
    {
        if( flac->ID3v2Tag() )
            ReadMetaFromId3v2( flac->ID3v2Tag(), p_demux_meta, p_meta );
        else if( flac->xiphComment() )
            ReadMetaFromXiph( flac->xiphComment(), p_demux_meta, p_meta );
        ProcessAPICListFromFLAC( flac->pictureList(), p_demux_meta, p_meta );
    }
    else if( MP4::File *mp4 = dynamic_cast<MP4::File*>(f.file()) )
    {
        if( mp4->tag() )
            ReadMetaFromMP4( mp4->tag(), p_demux_meta, p_meta );
    }
    else if( MPC::File* mpc = dynamic_cast<MPC::File*>(f.file()) )
    {
        if( mpc->APETag() )
            ReadMetaFromAPE( mpc->APETag(), p_demux_meta, p_meta );
    }
    else if( MPEG::File* mpeg = dynamic_cast<MPEG::File*>(f.file()) )
    {
        if( mpeg->APETag() )
            ReadMetaFromAPE( mpeg->APETag(), p_demux_meta, p_meta );
        if( mpeg->ID3v2Tag() )
            ReadMetaFromId3v2( mpeg->ID3v2Tag(), p_demux_meta, p_meta );
    }
    else if( dynamic_cast<Ogg::File*>(f.file()) )
    {
        if( Ogg::FLAC::File* ogg_flac = dynamic_cast<Ogg::FLAC::File*>(f.file()))
            ReadMetaFromXiph( ogg_flac->tag(), p_demux_meta, p_meta );
        else if( Ogg::Speex::File* ogg_speex = dynamic_cast<Ogg::Speex::File*>(f.file()) )
            ReadMetaFromXiph( ogg_speex->tag(), p_demux_meta, p_meta );
        else if( Ogg::Vorbis::File* ogg_vorbis = dynamic_cast<Ogg::Vorbis::File*>(f.file()) )
            ReadMetaFromXiph( ogg_vorbis->tag(), p_demux_meta, p_meta );
#if defined(TAGLIB_OPUSFILE_H)
        else if( Ogg::Opus::File* ogg_opus = dynamic_cast<Ogg::Opus::File*>(f.file()) )
            ReadMetaFromXiph( ogg_opus->tag(), p_demux_meta, p_meta );
#endif
    }
    else if( RIFF::AIFF::File* riff_aiff = dynamic_cast<RIFF::AIFF::File*>(f.file()) )
    {
        ReadMetaFromId3v2( riff_aiff->tag(), p_demux_meta, p_meta );
    }
    else if( TrueAudio::File* trueaudio = dynamic_cast<TrueAudio::File*>(f.file()) )
    {
        if( trueaudio->ID3v2Tag() )
            ReadMetaFromId3v2( trueaudio->ID3v2Tag(), p_demux_meta, p_meta );
    }
    else if( WavPack::File* wavpack = dynamic_cast<WavPack::File*>(f.file()) )
    {
        if( wavpack->APETag() )
            ReadMetaFromAPE( wavpack->APETag(), p_demux_meta, p_meta );
    }

    return VLC_SUCCESS;
}


/**
 * Write meta information to APE tags
 * @param tag: the APE tag
 * @param p_item: the input item
 */
static void WriteMetaToAPE( APE::Tag* tag, input_item_t* p_item )
{
    char* psz_meta;
#define WRITE( metaName, keyName )                      \
    psz_meta = input_item_Get##metaName( p_item );      \
    if( psz_meta )                                      \
    {                                                   \
        String key( keyName, String::UTF8 );            \
        String value( psz_meta, String::UTF8 );         \
        tag->addValue( key, value, true );              \
    }                                                   \
    free( psz_meta );

    WRITE( Copyright, "COPYRIGHT" );
    WRITE( Language, "LANGUAGE" );
    WRITE( Publisher, "PUBLISHER" );
    WRITE( TrackID, "MUSICBRAINZ_TRACKID" );
#undef WRITE
}


/**
 * Write meta information to id3v2 tags
 * @param tag: the id3v2 tag
 * @param p_input: the input item
 */
static void WriteMetaToId3v2( ID3v2::Tag* tag, input_item_t* p_item )
{
    char* psz_meta;
#define WRITE( metaName, tagName )                                            \
    psz_meta = input_item_Get##metaName( p_item );                            \
    if( psz_meta )                                                            \
    {                                                                         \
        ByteVector p_byte( tagName, 4 );                                      \
        tag->removeFrames( p_byte );                                         \
        ID3v2::TextIdentificationFrame* p_frame =                             \
            new ID3v2::TextIdentificationFrame( p_byte, String::UTF8 );       \
        p_frame->setText( psz_meta );                                         \
        tag->addFrame( p_frame );                                             \
    }                                                                         \
    free( psz_meta );

    WRITE( Copyright, "TCOP" );
    WRITE( EncodedBy, "TENC" );
    WRITE( Language,  "TLAN" );
    WRITE( Publisher, "TPUB" );

#undef WRITE
    /* Known TXXX frames */
    ID3v2::FrameList list = tag->frameListMap()["TXXX"];

#define WRITETXXX( metaName, txxName )\
    psz_meta = input_item_Get##metaName( p_item );                                       \
    if ( psz_meta )                                                                      \
    {                                                                                    \
        ID3v2::UserTextIdentificationFrame *p_txxx;                                      \
        for( ID3v2::FrameList::Iterator iter = list.begin(); iter != list.end(); iter++ )\
        {                                                                                \
            p_txxx = dynamic_cast<ID3v2::UserTextIdentificationFrame*>(*iter);           \
            if( !p_txxx )                                                                \
                continue;                                                                \
            if( !strcmp( p_txxx->description().toCString( true ), txxName ) )            \
            {                                                                            \
                p_txxx->setText( psz_meta );                                             \
                FREENULL( psz_meta );                                                    \
                break;                                                                   \
            }                                                                            \
        }                                                                                \
        if( psz_meta ) /* not found in existing custom fields */                         \
        {                                                                                \
            ByteVector p_byte( "TXXX", 4 );                                              \
            p_txxx = new ID3v2::UserTextIdentificationFrame( p_byte );                   \
            p_txxx->setDescription( txxName );                                           \
            p_txxx->setText( psz_meta );                                                 \
            free( psz_meta );                                                            \
            tag->addFrame( p_txxx );                                                     \
        }                                                                                \
    }

    WRITETXXX( TrackTotal, "TRACKTOTAL" );

#undef WRITETXXX

    /* Write album art */
    char *psz_url = input_item_GetArtworkURL( p_item );
    if( psz_url == NULL )
        return;

    char *psz_path = vlc_uri2path( psz_url );
    free( psz_url );
    if( psz_path == NULL )
        return;

    const char *psz_mime = vlc_mime_Ext2Mime( psz_path );

    FILE *p_file = vlc_fopen( psz_path, "rb" );
    if( p_file == NULL )
    {
        free( psz_path );
        return;
    }

    struct stat st;
    if( vlc_stat( psz_path, &st ) == -1 )
    {
        free( psz_path );
        fclose( p_file );
        return;
    }
    auto file_size = st.st_size;

    free( psz_path );

    /* Limit picture size to 10MiB */
    if( file_size > 10485760 )
    {
      fclose( p_file );
      return;
    }

    char *p_buffer = new (std::nothrow) char[file_size];
    if( p_buffer == NULL )
    {
        fclose( p_file );
        return;
    }

    if( fread( p_buffer, 1, file_size, p_file ) != (unsigned)file_size )
    {
        fclose( p_file );
        delete[] p_buffer;
        return;
    }
    fclose( p_file );

    ByteVector data( p_buffer, file_size );
    delete[] p_buffer;

    ID3v2::FrameList frames = tag->frameList( "APIC" );
    ID3v2::AttachedPictureFrame *frame = NULL;
    if( frames.isEmpty() )
    {
        frame = new TagLib::ID3v2::AttachedPictureFrame;
        tag->addFrame( frame );
    }
    else
    {
        frame = static_cast<ID3v2::AttachedPictureFrame *>( frames.back() );
    }

    frame->setPicture( data );
    frame->setMimeType( psz_mime );
}


/**
 * Write the meta information to XiphComments
 * @param tag: the Xiph Comment
 * @param p_input: the input item
 */
static void WriteMetaToXiph( Ogg::XiphComment* tag, input_item_t* p_item )
{
    char* psz_meta;
#define WRITE( metaName, keyName )                      \
    psz_meta = input_item_Get##metaName( p_item );      \
    if( psz_meta )                                      \
    {                                                   \
        String key( keyName, String::UTF8 );            \
        String value( psz_meta, String::UTF8 );         \
        tag->addField( key, value, true );              \
    }                                                   \
    free( psz_meta );

    WRITE( TrackNum, "TRACKNUMBER" );
    WRITE( TrackTotal, "TRACKTOTAL" );
    WRITE( Copyright, "COPYRIGHT" );
    WRITE( Publisher, "ORGANIZATION" );
    WRITE( Date, "DATE" );
    WRITE( EncodedBy, "ENCODER" );
    WRITE( Rating, "RATING" );
    WRITE( Language, "LANGUAGE" );
    WRITE( TrackID, "MUSICBRAINZ_TRACKID" );
#undef WRITE
}


/**
 * Set the tags to the file using TagLib
 * @param p_this: the demux object
 * @return VLC_SUCCESS if the operation success
 */

static int WriteMeta( vlc_object_t *p_this )
{
    vlc_mutex_locker locker (&taglib_lock);
    meta_export_t *p_export = (meta_export_t *)p_this;
    input_item_t *p_item = p_export->p_item;
    FileRef f;

    if( !p_item )
    {
        msg_Err( p_this, "Can't save meta data of an empty input" );
        return VLC_EGENERIC;
    }

#if defined(_WIN32)
    wchar_t *wpath = ToWide( p_export->psz_file );
    if( wpath == NULL )
        return VLC_EGENERIC;
    f = FileRef( wpath, false );
    free( wpath );
#else
    f = FileRef( p_export->psz_file, false );
#endif

    if( f.isNull() || !f.tag() || f.file()->readOnly() )
    {
        msg_Err( p_this, "File %s can't be opened for tag writing",
                 p_export->psz_file );
        return VLC_EGENERIC;
    }

    msg_Dbg( p_this, "Writing metadata for %s", p_export->psz_file );

    Tag *p_tag = f.tag();

    char *psz_meta;

#define SET( a, b )                                             \
    psz_meta = input_item_Get ## a( p_item );                   \
    if( psz_meta )                                              \
    {                                                           \
        String tmp( psz_meta, String::UTF8 );                   \
        p_tag->set##b( tmp );                                   \
    }                                                           \
    free( psz_meta );

    // Saving all common fields
    // If the title is empty, use the name
    SET( TitleFbName, Title );
    SET( Artist, Artist );
    SET( Album, Album );
    SET( Description, Comment );
    SET( Genre, Genre );

#undef SET

    psz_meta = input_item_GetDate( p_item );
    if( !EMPTY_STR(psz_meta) ) p_tag->setYear( atoi( psz_meta ) );
    else p_tag->setYear( 0 );
    free( psz_meta );

    psz_meta = input_item_GetTrackNum( p_item );
    if( !EMPTY_STR(psz_meta) ) p_tag->setTrack( atoi( psz_meta ) );
    else p_tag->setTrack( 0 );
    free( psz_meta );


    // Try now to write special tags
    if( APE::File* ape = dynamic_cast<APE::File*>(f.file()) )
    {
        if( ape->APETag() )
            WriteMetaToAPE( ape->APETag(), p_item );
    }
    else
    if( FLAC::File* flac = dynamic_cast<FLAC::File*>(f.file()) )
    {
        if( flac->ID3v2Tag() )
            WriteMetaToId3v2( flac->ID3v2Tag(), p_item );
        else if( flac->xiphComment() )
            WriteMetaToXiph( flac->xiphComment(), p_item );
    }
    else if( MPC::File* mpc = dynamic_cast<MPC::File*>(f.file()) )
    {
        if( mpc->APETag() )
            WriteMetaToAPE( mpc->APETag(), p_item );
    }
    else if( MPEG::File* mpeg = dynamic_cast<MPEG::File*>(f.file()) )
    {
        if( mpeg->ID3v2Tag() )
            WriteMetaToId3v2( mpeg->ID3v2Tag(), p_item );
        else if( mpeg->APETag() )
            WriteMetaToAPE( mpeg->APETag(), p_item );
    }
    else if( dynamic_cast<Ogg::File*>(f.file()) )
    {
        if( Ogg::FLAC::File* ogg_flac = dynamic_cast<Ogg::FLAC::File*>(f.file()))
            WriteMetaToXiph( ogg_flac->tag(), p_item );
        else if( Ogg::Speex::File* ogg_speex = dynamic_cast<Ogg::Speex::File*>(f.file()) )
            WriteMetaToXiph( ogg_speex->tag(), p_item );
        else if( Ogg::Vorbis::File* ogg_vorbis = dynamic_cast<Ogg::Vorbis::File*>(f.file()) )
            WriteMetaToXiph( ogg_vorbis->tag(), p_item );
#if defined(TAGLIB_OPUSFILE_H)
        else if( Ogg::Opus::File* ogg_opus = dynamic_cast<Ogg::Opus::File*>(f.file()) )
            WriteMetaToXiph( ogg_opus->tag(), p_item );
#endif
    }
    else if( dynamic_cast<RIFF::File*>(f.file()) )
    {
        if( RIFF::AIFF::File* riff_aiff = dynamic_cast<RIFF::AIFF::File*>(f.file()) )
            WriteMetaToId3v2( riff_aiff->tag(), p_item );
        else if( RIFF::WAV::File* riff_wav = dynamic_cast<RIFF::WAV::File*>(f.file()) )
#if TAGLIB_VERSION >= VERSION_INT(2, 0, 0)
            WriteMetaToId3v2( riff_wav->ID3v2Tag(), p_item );
#else
            WriteMetaToId3v2( riff_wav->tag(), p_item );
#endif
    }
    else if( TrueAudio::File* trueaudio = dynamic_cast<TrueAudio::File*>(f.file()) )
    {
        if( trueaudio->ID3v2Tag() )
            WriteMetaToId3v2( trueaudio->ID3v2Tag(), p_item );
    }
    else if( WavPack::File* wavpack = dynamic_cast<WavPack::File*>(f.file()) )
    {
        if( wavpack->APETag() )
            WriteMetaToAPE( wavpack->APETag(), p_item );
    }

    // Save the meta data
    f.save();

    return VLC_SUCCESS;
}
