/*******************************************************************************
*                         Goggles Music Manager                                *
********************************************************************************
*           Copyright (C) 2006-2010 by Sander Jansen. All Rights Reserved      *
*                               ---                                            *
* This program is free software: you can redistribute it and/or modify         *
* it under the terms of the GNU General Public License as published by         *
* the Free Software Foundation, either version 3 of the License, or            *
* (at your option) any later version.                                          *
*                                                                              *
* This program is distributed in the hope that it will be useful,              *
* but WITHOUT ANY WARRANTY; without even the implied warranty of               *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                *
* GNU General Public License for more details.                                 *
*                                                                              *
* You should have received a copy of the GNU General Public License            *
* along with this program.  If not, see http://www.gnu.org/licenses.           *
********************************************************************************/
#include <math.h>

#include <tag.h>
#include <fileref.h>
#include <tstring.h>
#include <id3v2tag.h>
#include <id3v2framefactory.h>

#include <mpegfile.h>
#include <vorbisfile.h>
#include <flacfile.h>
#include <apetag.h>
#include <textidentificationframe.h>
#include <attachedpictureframe.h>

#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
#include "mp4file.h"
#include "mp4tag.h"
#include "mp4coverart.h"
#endif

#include "gmdefs.h"
#include "gmutils.h"
#include "GMTag.h"

#include "FXPNGImage.h"
#include "FXJPGImage.h"


/******************************************************************************/
/* HELPER CLASS TO ACCESS ADDITIONAL TAGS FROM FILE                           */
/******************************************************************************/

class GMFileTag {
public:
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  TagLib::MP4::Tag          * mp4;
#else
  void                      * mp4;
#endif
  TagLib::Ogg::XiphComment  * xiph;
  TagLib::ID3v2::Tag        * id3v2;
  TagLib::APE::Tag          * ape;
protected:
  void xiph_update_field(const FXchar * field,const FXString & value);
  void id3v2_update_field(const FXchar * field,const FXString & value);
  void mp4_update_field(const FXchar * field,const FXString & value);
  void xiph_get_field(const FXchar * field,FXString &);
  void id3v2_get_field(const FXchar * field,FXString &);
  void mp4_get_field(const FXchar * field,FXString &);
  void ape_get_field(const FXchar * field,FXString &);
public:
  GMFileTag(const FXString & filename);
  GMFileTag(TagLib::File*);
  void setAlbumArtist(const FXString & value);
  void setDiscNumber(FXushort disc);
  void getAlbumArtist(FXString &);
  FXushort getDiscNumber();
  void getGain(FXdouble & track_gain,FXdouble & track_peak,FXdouble & album_gain,FXdouble & album_peak);
  };


/******************************************************************************/

GMFileTag::GMFileTag(const FXString &/* filename*/) :
    mp4(NULL),
    xiph(NULL),
    id3v2(NULL),
    ape(NULL) {
  /// TODO
  }

GMFileTag::GMFileTag(TagLib::File * file) :
    mp4(NULL),
    xiph(NULL),
    id3v2(NULL),
    ape(NULL) {

  TagLib::MPEG::File        * mpgfile   = NULL;
  TagLib::Ogg::Vorbis::File * oggfile   = NULL;
  TagLib::FLAC::File        * flacfile  = NULL;
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  TagLib::MP4::File         * mp4file   = NULL;
#endif

  if ((oggfile = dynamic_cast<TagLib::Ogg::Vorbis::File*>(file))) {
    xiph=oggfile->tag();
    }
  else if ((flacfile = dynamic_cast<TagLib::FLAC::File*>(file))){
    xiph=flacfile->xiphComment();
    id3v2=flacfile->ID3v2Tag();
    }
  else if ((mpgfile = dynamic_cast<TagLib::MPEG::File*>(file))){
    id3v2=mpgfile->ID3v2Tag();
    ape=mpgfile->APETag();
    }
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  else if ((mp4file = dynamic_cast<TagLib::MP4::File*>(file))){
    mp4=mp4file->tag();
    }
#endif
  }

void GMFileTag::xiph_update_field(const FXchar * field,const FXString & value) {
  FXASSERT(field);
  FXASSERT(xiph);
  if (!value.empty())
    xiph->addField(field,TagLib::String(value.text(),TagLib::String::UTF8),true);
  else
    xiph->removeField(field);
  }

void  GMFileTag::xiph_get_field(const FXchar * field,FXString & value) {
  FXASSERT(field);
  FXASSERT(xiph);
  if (xiph->contains(field))
    value=xiph->fieldListMap()[field].front().toCString(true);
  else
    value.clear();
  }

void GMFileTag::ape_get_field(const FXchar * field,FXString & value) {
  FXASSERT(field);
  FXASSERT(ape);
  if (ape->itemListMap().contains(field) && !ape->itemListMap()[field].isEmpty())
    value=ape->itemListMap()[field].toString().toCString(true);
  else
    value.clear();
  }

void GMFileTag::id3v2_update_field(const FXchar * field,const FXString & value) {
  FXASSERT(field);
  FXASSERT(id3v2);
  if (value.empty()) {
    id3v2->removeFrames(field);
    }
  else if (id3v2->frameListMap().contains(field) && !id3v2->frameListMap()[field].isEmpty()) {
    id3v2->frameListMap()[field].front()->setText(TagLib::String(value.text(),TagLib::String::UTF8));
    }
  else {
    TagLib::ID3v2::TextIdentificationFrame *frame = new TagLib::ID3v2::TextIdentificationFrame(field,TagLib::ID3v2::FrameFactory::instance()->defaultTextEncoding());
    frame->setText(TagLib::String(value.text(),TagLib::String::UTF8) );
    id3v2->addFrame(frame);
    }
  }

void  GMFileTag::id3v2_get_field(const FXchar * field,FXString & value) {
  FXASSERT(field);
  FXASSERT(id3v2);
  if (id3v2->frameListMap().contains(field) && !id3v2->frameListMap()[field].isEmpty() )
    value=id3v2->frameListMap()[field].front()->toString().toCString(true);
  else
    value.clear();
  }



void GMFileTag::mp4_update_field(const FXchar * field,const FXString & value) {
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  FXASSERT(field);
  FXASSERT(mp4);
  if (!value.empty())
    mp4->itemListMap()[field] = TagLib::StringList(TagLib::String(value.text(),TagLib::String::UTF8));
  else
    mp4->itemListMap().erase(field);
#endif
  }

void GMFileTag::mp4_get_field(const FXchar * field,FXString & value) {
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  FXASSERT(field);
  FXASSERT(mp4);
  if (mp4->itemListMap().contains(field) && !mp4->itemListMap().isEmpty())
    value=mp4->itemListMap()[field].toStringList().toString(", ").toCString(true);
  else
    value.clear();
#else
  value.clear();
#endif
  }

/******************************************************************************/

void GMFileTag::setAlbumArtist(const FXString & albumartist) {
  if (xiph)
    xiph_update_field("ALBUMARTIST",albumartist);
  else if (id3v2)
    id3v2_update_field("TPE2",albumartist);
  else if (mp4)
    mp4_update_field("aART",albumartist);
  }


void GMFileTag::getAlbumArtist(FXString & albumartist) {
  if (xiph)
    xiph_get_field("ALBUMARTIST",albumartist);
  else if (id3v2)
    id3v2_get_field("TPE2",albumartist);
  else if (mp4)
    mp4_get_field("aART",albumartist);
  else
    albumartist.clear();
  }


void GMFileTag::setDiscNumber(FXushort disc) {
  if (xiph) {
    if (disc>0)
      xiph_update_field("DISCNUMBER",GMStringFormat("%d",disc));
    else
      xiph_update_field("DISCNUMBER",FXString::null);
    }
  else if (id3v2) {
    if (disc>0)
      id3v2_update_field("TPOS",GMStringFormat("%d",disc));
    else
      id3v2_update_field("TPOS",FXString::null);
    }
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  else if (mp4) {
    if (disc>0)
      mp4->itemListMap()["disk"] = TagLib::MP4::Item(disc,0);
    else
      mp4->itemListMap().erase("disk");
    }
#endif
  }


static FXushort string_to_disc_number(const FXString & disc) {
  if (disc.empty())
    return 0;
#if FOXVERSION >= FXVERSION(1,7,12)
  return FXMIN(disc.before('/').toUInt(),0xFFFF);
#else
  return FXMIN(FXUIntVal(disc.before('/')),0xFFFF);
#endif
  }

FXushort GMFileTag::getDiscNumber() {
  FXString disc;
  if (xiph) {
    xiph_get_field("DISCNUMBER",disc);
    return string_to_disc_number(disc);
    }
  else if (id3v2) {
    id3v2_get_field("TPOS",disc);
    return string_to_disc_number(disc);
    }
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  else if (mp4) {
    if (mp4->itemListMap().contains("disk"))
      return FXMIN(mp4->itemListMap()["disk"].toIntPair().first,0xFFFF);
    }
#endif
  return 0;
  }

void GMFileTag::getGain(FXdouble & track_gain,FXdouble & track_peak,FXdouble & album_gain,FXdouble & album_peak) {
  track_gain=track_peak=album_gain=album_peak=NAN;
  FXString tmp;
  if (xiph) {
    xiph_get_field("REPLAYGAIN_ALBUM_GAIN",tmp);
    album_gain=gm_parse_number(tmp);

    xiph_get_field("REPLAYGAIN_ALBUM_PEAK",tmp);
    album_peak=gm_parse_number(tmp);

    xiph_get_field("REPLAYGAIN_TRACK_GAIN",tmp);
    track_gain=gm_parse_number(tmp);

    xiph_get_field("REPLAYGAIN_TRACK_PEAK",tmp);
    track_peak=gm_parse_number(tmp);

    if (isnan(track_peak) && isnan(album_gain)) {
      xiph_get_field("RG_RADIO",tmp);
      track_gain=gm_parse_number(tmp);

      xiph_get_field("RG_PEAK",tmp);
      track_peak=gm_parse_number(tmp);

      xiph_get_field("RG_AUDIOPHILE",tmp);
      album_gain=gm_parse_number(tmp);
      }
    }
  else if (ape) {
    ape_get_field("REPLAYGAIN_ALBUM_GAIN",tmp);
    album_gain=gm_parse_number(tmp);

    ape_get_field("REPLAYGAIN_ALBUM_PEAK",tmp);
    album_peak=gm_parse_number(tmp);

    ape_get_field("REPLAYGAIN_TRACK_GAIN",tmp);
    track_gain=gm_parse_number(tmp);

    ape_get_field("REPLAYGAIN_TRACK_PEAK",tmp);
    track_peak=gm_parse_number(tmp);
    }
  }







GMTrack::GMTrack(){
  }

void GMTrack::clear() {
  path.clear();
  title.clear();
  artist.clear();
  album.clear();
  album_artist.clear();
  genre.clear();
  year=0;
  no=0;
  time=0;
  bitrate=0;
  album_gain=NAN;
  album_peak=NAN;
  track_gain=NAN;
  track_peak=NAN;
  }






static GMCover * id3v2_load_cover(TagLib::ID3v2::AttachedPictureFrame * frame,FXint scale) {
  FXString mime = frame->mimeType().toCString(true);
  /// Skip File Icon
  if (frame->type()==TagLib::ID3v2::AttachedPictureFrame::FileIcon ||
      frame->type()==TagLib::ID3v2::AttachedPictureFrame::OtherFileIcon ||
      frame->type()==TagLib::ID3v2::AttachedPictureFrame::ColouredFish) {
    return NULL;
    }
  FXImage * image = gm_load_image_from_data(frame->picture().data(),frame->picture().size(),mime,scale);
  if (image) return new GMCover(image,frame->type());
  return NULL;
  }

static FXbool id3v2_is_front_cover(TagLib::ID3v2::AttachedPictureFrame * frame){
  if (frame->type()==TagLib::ID3v2::AttachedPictureFrame::FrontCover)
    return true;
  else
    return false;
  }

/******************************************************************************/
/*            FLAC PICTURE LOADING                                            */
/******************************************************************************/
#if FOX_BIGENDIAN == 0
#define FLAC_LAST_BLOCK   0x80

#define FLAC_BLOCK_TYPE_MASK 0x7f

#define FLAC_BLOCK_TYPE(h) (h&0x7f)
#define FLAC_BLOCK_SIZE(h) ( ((h&0xFF00)<<8) | ((h&0xFF0000)>>8) | ((h&0xFF000000)>>24) )

#define FLAC_BLOCK_SET_TYPE(h,type) (h|=(type&FLAC_BLOCK_TYPE_MASK))
#define FLAC_BLOCK_SET_SIZE(h,size) (h|=(((size&0xFF)<<24) | ((size&0xFF0000)>>16) | ((size&0xFF00)<<8)))

#else
#error "BUG: FLAC  macros not defined for Big Endian Architecture"
#endif

enum {
  FLAC_BLOCK_STREAMINFO     = 0,
  FLAC_BLOCK_PADDING        = 1,
  FLAC_BLOCK_VORBIS_COMMENT = 4,
  FLAC_BLOCK_PICTURE        = 6
  };






static FXbool gm_uint32_be(const FXuchar * block,FXuint & v) {
#if FOX_BIGENDIAN == 0
  ((FXuchar*)&v)[3]=block[0];
  ((FXuchar*)&v)[2]=block[1];
  ((FXuchar*)&v)[1]=block[2];
  ((FXuchar*)&v)[0]=block[3];
#else
  ((FXuchar*)&v)[3]=block[3];
  ((FXuchar*)&v)[2]=block[2];
  ((FXuchar*)&v)[1]=block[1];
  ((FXuchar*)&v)[0]=block[0];
#endif
  return true;
  }


static FXbool gm_read_uint32_be(FXIO & io,FXuint & v) {
#if FOX_BIGENDIAN == 0
  FXuchar block[4];
  if (io.readBlock(block,4)!=4) return false;
  ((FXuchar*)&v)[3]=block[0];
  ((FXuchar*)&v)[2]=block[1];
  ((FXuchar*)&v)[1]=block[2];
  ((FXuchar*)&v)[0]=block[3];
#else
  if (io.readBlock(&v,4)!=4) return false;
#endif
  return true;
  }


static FXbool gm_write_uint32_be(FXIO & io,FXuint v) {
#if FOX_BIGENDIAN == 0
  FXuchar block[4];
  block[0]=((const FXuchar*)&v)[3];
  block[1]=((const FXuchar*)&v)[2];
  block[2]=((const FXuchar*)&v)[1];
  block[3]=((const FXuchar*)&v)[0];
  if (io.writeBlock(block,4)!=4) return false;
#else
  if (io.writeBlock(&v,4)!=4) return false;
#endif
  return true;
  }


static FXbool gm_read_string_be(FXIO & io,FXString & v) {
  FXuint len=0;
  gm_read_uint32_be(io,len);
  if (len>0) {
    v.length(len);
    if (io.readBlock(&v[0],len)!=len)
      return false;
    }
  return true;
  }


static FXbool gm_write_string_be(FXIO & io,const FXString & v) {
  gm_write_uint32_be(io,v.length());
  if (v.length()>0) {
    if (io.writeBlock(v.text(),v.length())!=v.length())
      return false;
    }
  return true;
  }



struct FlacPictureBlock{
  FXString    mimetype;
  FXString    description;
  FXuint      type;
  FXuint      width;
  FXuint      height;
  FXuint      bps;
  FXuint      ncolors;
  FXuint      data_size;
  FXuchar*    data;

  FXuint size() const {
    return 64 + description.length() + mimetype.length() + data_size;
    }
  };


FXbool gm_flac_write_block_picture(FXIO & io,const FlacPictureBlock & picture,FXbool lastblock) {
  FXuint header=0;

  if (lastblock)
    header|=FLAC_LAST_BLOCK;

  FLAC_BLOCK_SET_TYPE(header,FLAC_BLOCK_PICTURE);
  FLAC_BLOCK_SET_SIZE(header,picture.size());

  gm_write_uint32_be(io,picture.type);
  gm_write_string_be(io,picture.mimetype);
  gm_write_string_be(io,picture.description);
  gm_write_uint32_be(io,picture.width);
  gm_write_uint32_be(io,picture.height);
  gm_write_uint32_be(io,picture.bps);
  gm_write_uint32_be(io,picture.ncolors);
  gm_write_uint32_be(io,picture.data_size);

  if (picture.data_size > 0) {
    io.writeBlock(picture.data,picture.data_size);
    }
  return true;
  }


static GMCover * gm_flac_parse_block_picture(const FXuchar * buffer,FXint len,FXint scale) {
  FlacPictureBlock picture;
  const FXuchar * p = buffer;
  FXuint sz;
  gm_uint32_be(p,picture.type);

  /// Skip useless icons
  if (picture.type==GMCover::FileIcon || picture.type==GMCover::OtherFileIcon ||
      picture.type==GMCover::Fish) {
    return NULL;
    }
  p+=4;

  gm_uint32_be(p,sz);
  picture.mimetype.length(sz);
  picture.mimetype.assign((const FXchar*)p+4,sz);

  p+=(4+sz);

  gm_uint32_be(p,sz);
  picture.description.length(sz);
  picture.description.assign((const FXchar*)p+4,sz);

  p+=(4+sz);

  gm_uint32_be(p+0,picture.width);
  gm_uint32_be(p+4,picture.height);
  gm_uint32_be(p+8,picture.bps);
  gm_uint32_be(p+12,picture.ncolors);
  gm_uint32_be(p+16,picture.data_size);

  if (picture.data_size>0) {
    picture.data = (FXuchar*) p+20;
    if (picture.data+picture.data_size>buffer+len)
      return NULL;

    FXImage * image = gm_load_image_from_data(picture.data,picture.data_size,picture.mimetype,scale);
    if (image) return new GMCover(image,picture.type,picture.description);
    }
  return NULL;
  }

FXbool gm_flac_is_front_cover(FXIO & io) {
  FlacPictureBlock picture;
  gm_read_uint32_be(io,picture.type);
  if (picture.type==GMCover::FrontCover)
    return true;
  else
    return false;
  }

GMCover * gm_flac_parse_block_picture(FXIO & io,FXint scale) {
  GMCover*  cover=NULL;
  FlacPictureBlock picture;

  gm_read_uint32_be(io,picture.type);

  /// Skip useless icons
  if (picture.type==GMCover::FileIcon || picture.type==GMCover::OtherFileIcon ||
      picture.type==GMCover::Fish) {
    return NULL;
    }

  gm_read_string_be(io,picture.mimetype);
  gm_read_string_be(io,picture.description);
  gm_read_uint32_be(io,picture.width);
  gm_read_uint32_be(io,picture.height);
  gm_read_uint32_be(io,picture.bps);
  gm_read_uint32_be(io,picture.ncolors);
  gm_read_uint32_be(io,picture.data_size);

  if (picture.data_size>0) {
    allocElms(picture.data,picture.data_size);
    if (io.readBlock(picture.data,picture.data_size)==picture.data_size) {
      FXImage * image = gm_load_image_from_data(picture.data,picture.data_size,picture.mimetype,scale);
      if (image) {
        cover = new GMCover(image,picture.type,picture.description);
        }
      }
    freeElms(picture.data);
    }
  return cover;
  }


static FXbool gm_flac_parse_header(FXIO & io,FXuint & header) {
  FXchar  marker[4];

  if (io.readBlock(marker,4)!=4 || compare(marker,"fLaC",4))
    return false;

  if (io.readBlock(&header,4)!=4 || FLAC_BLOCK_TYPE(header)!=FLAC_BLOCK_STREAMINFO || FLAC_BLOCK_SIZE(header)!=34  || (header&FLAC_LAST_BLOCK))
    return false;

  /// Go to beginning of meta data
  io.position(34,FXIO::Current);
  return true;
  }

static FXbool gm_flac_next_block(FXIO & io,FXuint & header) {
  if (!(header&FLAC_LAST_BLOCK) && (io.readBlock(&header,4)==4))
    return true;
  else
    return false;
  }

static FXint flac_load_covers(const FXString & mrl,GMCoverList & covers,FXint scale) {
  FXuint  header;
  FXFile io;

  if (io.open(mrl,FXIO::Reading)) {

    if (!gm_flac_parse_header(io,header))
      return 0;

    while(gm_flac_next_block(io,header)) {
      if (FLAC_BLOCK_TYPE(header)==FLAC_BLOCK_PICTURE) {
        GMCover * cover = gm_flac_parse_block_picture(io,scale);
        if (cover) covers.append(cover);
        }
      else if (!(header&FLAC_LAST_BLOCK)){
        io.position(FLAC_BLOCK_SIZE(header),FXIO::Current);
        }
      }
    }
  return covers.no();
  }

static GMCover * flac_load_cover(const FXString & mrl,FXint scale) {
  FXuint  header;
  FXlong  pos;
  FXFile io;

  if (io.open(mrl,FXIO::Reading)) {

    if (!gm_flac_parse_header(io,header))
      return 0;

    while(gm_flac_next_block(io,header)) {
      if (FLAC_BLOCK_TYPE(header)==FLAC_BLOCK_PICTURE) {
        pos=io.position();
        if (gm_flac_is_front_cover(io)) {
          io.position(pos,FXIO::Begin);
          GMCover * cover = gm_flac_parse_block_picture(io,scale);
          if (cover) return cover;
          }
        }
      else if (!(header&FLAC_LAST_BLOCK)){
        io.position(FLAC_BLOCK_SIZE(header),FXIO::Current);
        }
      }
    }
  return NULL;
  }


static GMCover * xiph_load_cover(const TagLib::ByteVector & tbuf,FXint scale) {
  GMCover * cover = NULL;
  if (tbuf.size()) {
    FXuchar * buffer=NULL;
    FXint   len=tbuf.size();

    allocElms(buffer,len);
    memcpy(buffer,tbuf.data(),len);
    if (gm_decode_base64(buffer,len)) {
      cover = gm_flac_parse_block_picture(buffer,len,scale);
      }
    freeElms(buffer);
    }
  return cover;
  }


namespace GMTag {

void init(){
  TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF16);
  }


FXbool length(GMTrack & info) {
  TagLib::FileRef file(info.mrl.text());
  if (file.isNull())
    return false;
  TagLib::AudioProperties *prop = file.audioProperties();
  if (prop)
    info.time = prop->length();
  else
    info.time = 0;
  return true;
  }


FXbool properties(const FXString & mrl,Properties & p) {
  p.bitrate=-1;
  p.samplerate=-1;
  p.channels=-1;

  TagLib::FileRef file(mrl.text());
  if (file.isNull())
    return false;

  TagLib::AudioProperties *prop = file.audioProperties();

  if (!prop)
    return false;

  p.bitrate    = prop->bitrate();
  p.samplerate = prop->sampleRate();
  p.channels   = prop->channels();
  return true;
  }
}




GMCover::GMCover() : image(NULL), type(0) {
  }

GMCover::GMCover(FXImage * img,FXuint t,const FXString & label) : image(img),description(label),type(t) {
  }

GMCover::~GMCover() {
  if (image) {
    delete image;
    image=NULL;
    }
  }


FXint GMCover::fromTag(const FXString & mrl,GMCoverList & covers,FXint scale/*=0*/) {
  FXString extension = FXPath::extension(mrl);

  if (comparecase(extension,"flac")==0){
    flac_load_covers(mrl,covers,scale);
    if (covers.no()) return (covers.no());
    }

  TagLib::FileRef file(mrl.text(),false);
  if (file.isNull() || !file.tag()) {
    return 0;
    }

  GMFileTag tags(file.file());
  FXIconSource src(FXApp::instance());
  if (tags.xiph) {
    if (tags.xiph->contains("METADATA_BLOCK_PICTURE")) {
      const TagLib::StringList & coverlist = tags.xiph->fieldListMap()["METADATA_BLOCK_PICTURE"];
      for(TagLib::StringList::ConstIterator it = coverlist.begin(); it != coverlist.end(); it++) {
        GMCover * cover = xiph_load_cover((*it).data(TagLib::String::UTF8),scale);
        if (cover) covers.append(cover);
        }
      }
    }
  if (tags.id3v2) {
    TagLib::ID3v2::FrameList framelist = tags.id3v2->frameListMap()["APIC"];
    if(!framelist.isEmpty()){
      for(TagLib::ID3v2::FrameList::Iterator it = framelist.begin(); it != framelist.end(); it++) {
        TagLib::ID3v2::AttachedPictureFrame * frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(*it);
        GMCover * cover = id3v2_load_cover(frame,scale);
        if (cover) covers.append(cover);
        }
      }
    }
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  else if (tags.mp4) {
    if (tags.mp4->itemListMap().contains("covr")) {
      TagLib::MP4::CoverArtList coverlist = tags.mp4->itemListMap()["covr"].toCoverArtList();
      for(TagLib::MP4::CoverArtList::Iterator it = coverlist.begin(); it != coverlist.end(); it++) {
        FXImage * img = NULL;

        if (it->format()==TagLib::MP4::CoverArt::PNG)
          img = gm_load_image_from_data(it->data().data(),it->data().size(),FXPNGImage::fileExt,scale);
        else if (it->format()==TagLib::MP4::CoverArt::JPEG)
          img = gm_load_image_from_data(it->data().data(),it->data().size(),FXJPGImage::fileExt,scale);

        if (img)
          covers.append(new GMCover(img,0));
        }
      }
    }
#endif
  return covers.no();
  }

FXint GMCover::fromPath(const FXString & path,GMCoverList & list,FXint scale/*=0*/) {
  FXString * files=NULL;
  FXImage * image;
  FXIconSource src(FXApp::instance());

  FXint nfiles = FXDir::listFiles(files,path,"*.(png,jpg,jpeg,bmp,gif)",FXDir::NoDirs|FXDir::NoParent|FXDir::CaseFold|FXDir::HiddenFiles);
  if (nfiles) {
    for (FXint i=0;i<nfiles;i++) {
      if (scale)
        image = src.loadScaledImageFile(path+PATHSEPSTRING+files[i],scale,1);
      else
        image = src.loadImageFile(path+PATHSEPSTRING+files[i]);

      if (image)
        list.append(new GMCover(image,0));
      }
    delete [] files;
    }
  return list.no();
  }




GMCover * GMCover::fromTag(const FXString & mrl,FXint scale/*=0*/) {
  FXString mime;
  FXString extension = FXPath::extension(mrl);

  if (comparecase(extension,"flac")==0){
    GMCover * cover = flac_load_cover(mrl,scale);
    if (cover) return cover;
    }

  TagLib::FileRef file(mrl.text(),false);
  if (file.isNull() || !file.tag()) {
    return NULL;
    }

  GMFileTag tags(file.file());

  FXIconSource src(FXApp::instance());

  if (tags.id3v2) {
    TagLib::ID3v2::FrameList framelist = tags.id3v2->frameListMap()["APIC"];
    if(!framelist.isEmpty()){
      /// First Try Front Cover
      for(TagLib::ID3v2::FrameList::Iterator it = framelist.begin(); it != framelist.end(); it++) {
        TagLib::ID3v2::AttachedPictureFrame * frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(*it);
        FXASSERT(frame);
        if (id3v2_is_front_cover(frame)) {
          GMCover * cover = id3v2_load_cover(frame,scale);
          if (cover) return cover;
          }
        }
      for(TagLib::ID3v2::FrameList::Iterator it = framelist.begin(); it != framelist.end(); it++) {
        TagLib::ID3v2::AttachedPictureFrame * frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(*it);
        FXASSERT(frame);
        GMCover * cover = id3v2_load_cover(frame,scale);
        if (cover) return cover;
        }
      }
    }
  else if (tags.xiph) {
    if (tags.xiph->contains("METADATA_BLOCK_PICTURE")) {
      const TagLib::StringList & coverlist = tags.xiph->fieldListMap()["METADATA_BLOCK_PICTURE"];
      for(TagLib::StringList::ConstIterator it = coverlist.begin(); it != coverlist.end(); it++) {
        GMCover * cover = xiph_load_cover((*it).data(TagLib::String::UTF8),scale);
        if (cover) return cover;
        }
      }
    }
#if defined(TAGLIB_WITH_MP4) && (TAGLIB_WITH_MP4==1)
  if (tags.mp4) { /// MP4
    if (tags.mp4->itemListMap().contains("covr")) {
      TagLib::MP4::CoverArtList coverlist = tags.mp4->itemListMap()["covr"].toCoverArtList();
      for(TagLib::MP4::CoverArtList::Iterator it = coverlist.begin(); it != coverlist.end(); it++) {
        FXImage * img = NULL;
        if (it->format()==TagLib::MP4::CoverArt::PNG)
          img = gm_load_image_from_data(it->data().data(),it->data().size(),FXPNGImage::fileExt,scale);
        else if (it->format()==TagLib::MP4::CoverArt::JPEG)
          img = gm_load_image_from_data(it->data().data(),it->data().size(),FXJPGImage::fileExt,scale);

        if (img) return new GMCover(img,0);
        }
      }
    }
#endif
  return NULL;
  }





GMCover * GMCover::fromPath(const FXString & path,FXint scale/*=0*/) {
  static const FXchar * covernames[]={"cover","album","front","albumart",".folder","folder",NULL};
  FXString * files=NULL;
  FXString * names=NULL;
  FXImage  * image=NULL;

  FXIconSource src(FXApp::instance());

  FXint nfiles = FXDir::listFiles(files,path,"*.(png,jpg,jpeg,bmp,gif)",FXDir::NoDirs|FXDir::NoParent|FXDir::CaseFold|FXDir::HiddenFiles);
  if (nfiles) {
    names = new FXString[nfiles];
    for (FXint i=0;i<nfiles;i++)
      names[i]=FXPath::title(files[i]);

    for (FXint c=0;covernames[c]!=NULL;c++) {
      for (FXint i=0;i<nfiles;i++){
        if (comparecase(names[i],covernames[c])==0) {
          if (scale)
            image = src.loadScaledImageFile(path+PATHSEPSTRING+files[i],scale,1);
          else
            image = src.loadImageFile(path+PATHSEPSTRING+files[i]);

          if (image) {
            delete [] names;
            delete [] files;
            return new GMCover(image,0);
            }
          }
        }
      }
    delete [] names;

    /// No matching cover name found. Just load the first file.
    if (scale)
      image = src.loadScaledImageFile(path+PATHSEPSTRING+files[0],scale,1);
    else
      image = src.loadImageFile(path+PATHSEPSTRING+files[0]);

    delete [] files;
    }

  if (image)
    return new GMCover(image,0);

  return NULL;
  }




FXImage * GMCover::toImage(GMCover * cover) {
  if (cover) {
    FXImage * image = cover->image;
    cover->image=NULL;
    delete cover;
    return image;
    }
  return NULL;
  }


FXIcon * GMCover::toIcon(GMCover * cover) {
  if (cover) {
    FXImage * image = GMCover::toImage(cover);
    if (image) {
      FXIcon * icon = new FXIcon(FXApp::instance(),image->getData(),0,IMAGE_OWNED,image->getWidth(),image->getHeight());
      FXImageOwner::clear(image);
      delete image;
      return icon;
      }
    }
  return NULL;
  }



FXbool GMTrack::saveTag(const FXString & filename) {

  if (!FXStat::isWritable(filename))
    return false;

  TagLib::FileRef file(filename.text(),false);
  if (file.isNull() || !file.tag()) {
    return false;
    }

  TagLib::Tag * tag = file.tag();

  tag->setTitle(TagLib::String(title.text(),TagLib::String::UTF8));
  tag->setArtist(TagLib::String(artist.text(),TagLib::String::UTF8));
  tag->setAlbum(TagLib::String(album.text(),TagLib::String::UTF8));
  tag->setGenre(TagLib::String(genre.text(),TagLib::String::UTF8));
  tag->setYear(year);
  tag->setTrack(getTrackNumber());

  GMFileTag tags(file.file());

  tags.setDiscNumber(getDiscNumber());

  if (album_artist!=artist && !album_artist.empty())
    tags.setAlbumArtist(album_artist);
  else
    tags.setAlbumArtist(FXString::null);

  return file.save();
  }


FXbool GMTrack::loadTag(const FXString & filename) {
  FXString disc,value;

  TagLib::FileRef file(mrl.text());
  if (file.isNull() || !file.tag()) {
    clear();
    return false;
    }

  /// This is a really dirty and quick hack that is not garanteed to work. We need
  /// it for proper floating point to string conversion.
  /// xine will set the LC_NUMERIC locale in its threads, so we're not sure,
  /// when xine will change it. Luckily xine is already shutdown when we call this.
  setlocale(LC_NUMERIC,"C");

  TagLib::Tag * tag = file.tag();
  TagLib::AudioProperties * properties = file.audioProperties();

  mrl          = filename;
  artist       = tag->artist().toCString(true);
  album        = tag->album().toCString(true);
  title        = tag->title().toCString(true);
  genre        = tag->genre().toCString(true);
  year         = tag->year();
  no           = FXMIN(tag->track(),0xFFFF);

  artist.trim();
  album.trim();
  title.trim();
  genre.trim();

  if (properties) {
    time     = properties->length();
    bitrate  = properties->bitrate();
    }
  else {
    bitrate  = 0;
    time     = 0;
    }

  GMFileTag tags(file.file());

  tags.getAlbumArtist(album_artist);
  tags.getGain(track_gain,track_peak,album_gain,album_peak);

  if (album_artist.empty())
    album_artist=artist;

  setDiscNumber(tags.getDiscNumber());
//  GM_DEBUG_PRINT("gain = (%lf %lf) (%lf %lf) track: %d/%d\n",album_gain,album_peak,track_gain,track_peak,getTrackNumber(),getDiscNumber());
  return true;
  }



