/*******************************************************************************
*                         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 <errno.h>

#include <tag.h>
#include <fileref.h>
#include <tstring.h>
#include <id3v1genres.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)
#define TAGLIB_HAVE_MP4 1
#endif

#ifdef TAGLIB_HAVE_MP4
#include "mp4file.h"
#include "mp4tag.h"
#include "mp4coverart.h"
#endif


#ifndef TAGLIB_MAJOR_VERSION
#error "missing taglib_major_version"
#endif
#define MKVERSION(major,minor,release) ((release)+(minor*1000)+(major*1000000))
#define TAGLIB_VERSION MKVERSION(TAGLIB_MAJOR_VERSION,TAGLIB_MINOR_VERSION,TAGLIB_PATCH_VERSION)

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

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


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;
  }

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

class GMFileTag {
public:
  TagLib::File              * file;
  TagLib::Tag               * tag;
#ifdef TAGLIB_HAVE_MP4
  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 xiph_update_field(const FXchar * field,const FXStringList & value);
  void id3v2_update_field(const FXchar * field,const FXString & value);
  void id3v2_update_field(const FXchar * field,const FXStringList & value);
  void mp4_update_field(const FXchar * field,const FXString & value);
  void mp4_update_field(const FXchar * field,const FXStringList & value);
  void ape_update_field(const FXchar * field,const FXStringList & value);
  void xiph_get_field(const FXchar * field,FXString &);
  void xiph_get_field(const FXchar * field,FXStringList &);
  void id3v2_get_field(const FXchar * field,FXString &);
  void id3v2_get_field(const FXchar * field,FXStringList &);
  void mp4_get_field(const FXchar * field,FXString &);
  void mp4_get_field(const FXchar * field,FXStringList &);
  void ape_get_field(const FXchar * field,FXString &);
  void ape_get_field(const FXchar * field,FXStringList &);
  void parse_tagids(FXStringList&);
public:
  GMFileTag(const FXString & filename);
  GMFileTag(TagLib::File*);
  void setComposer(const FXString & value);
  void setConductor(const FXString & value);
  void setAlbumArtist(const FXString & value);
  void setTags(const FXStringList & value);
  void setDiscNumber(FXushort disc);
  void getComposer(FXString &);
  void getConductor(FXString &);
  void getAlbumArtist(FXString &);
  void getTags(FXStringList&);
  void getTitle(FXString&);
  FXushort getDiscNumber();
  void getGain(FXdouble & track_gain,FXdouble & track_peak,FXdouble & album_gain,FXdouble & album_peak);
  };


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

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

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

  TagLib::MPEG::File        * mpgfile   = NULL;
  TagLib::Ogg::Vorbis::File * oggfile   = NULL;
  TagLib::FLAC::File        * flacfile  = NULL;
#ifdef TAGLIB_HAVE_MP4
  TagLib::MP4::File         * mp4file   = NULL;
#endif

  tag = file->tag();

  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();
    }
#ifdef TAGLIB_HAVE_MP4
  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_update_field(const FXchar * field,const FXStringList & list) {
  FXASSERT(field);
  FXASSERT(xiph);
  xiph->removeField(field);
  for (FXint i=0;i<list.no();i++) {
    xiph->addField(field,TagLib::String(list[i].text(),TagLib::String::UTF8),false);
    }
  }


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::xiph_get_field(const FXchar * field,FXStringList & list) {
  FXASSERT(field);
  FXASSERT(xiph);
  if (xiph->contains(field)) {
    const TagLib::StringList & fieldlist = xiph->fieldListMap()[field];
    for(TagLib::StringList::ConstIterator it = fieldlist.begin(); it != fieldlist.end(); it++) {
      list.append(it->toCString(true));
      }
    }
  else {
    list.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::ape_get_field(const FXchar * field,FXStringList & list) {
  FXASSERT(field);
  FXASSERT(ape);
  if (ape->itemListMap().contains(field)) {
    TagLib::StringList fieldlist = ape->itemListMap()[field].toStringList();
    for(TagLib::StringList::ConstIterator it = fieldlist.begin(); it != fieldlist.end(); it++) {
      list.append(it->toCString(true));
      }
    }
  else {
    list.clear();
    }
  }

void GMFileTag::ape_update_field(const FXchar * field,const FXStringList & list) {
  FXASSERT(field);
  FXASSERT(ape);
  ape->removeItem(field);

  TagLib::StringList values;
  for (FXint i=0;i<list.no();i++) {
    values.append(TagLib::String(list[i].text(),TagLib::String::UTF8));
    }

  ape->setItem(field,TagLib::APE::Item(field,values));
  }


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_update_field(const FXchar * field,const FXStringList & list) {
  FXASSERT(field);
  FXASSERT(id3v2);
  if (list.no()==0) {
    id3v2->removeFrames(field);
    }
  else {
    TagLib::ID3v2::TextIdentificationFrame * frame = NULL;
    if (id3v2->frameListMap().contains(field) && !id3v2->frameListMap()[field].isEmpty()) {
      frame = dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(id3v2->frameListMap()[field].front());
      }
    else {
      frame = new TagLib::ID3v2::TextIdentificationFrame(field,TagLib::ID3v2::FrameFactory::instance()->defaultTextEncoding());
      id3v2->addFrame(frame);
      }
    FXASSERT(frame);

    TagLib::StringList values;
    for (FXint i=0;i<list.no();i++) {
      values.append(TagLib::String(list[i].text(),TagLib::String::UTF8));
      }
    frame->setText(values);
    }
  }

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::id3v2_get_field(const FXchar * field,FXStringList & list) {
  FXASSERT(field);
  FXASSERT(id3v2);
  if (id3v2->frameListMap().contains(field) && !id3v2->frameListMap()[field].isEmpty() ) {
    TagLib::ID3v2::TextIdentificationFrame * frame = dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(id3v2->frameListMap()[field].front());
    TagLib::StringList fieldlist = frame->fieldList();
    for(TagLib::StringList::ConstIterator it = fieldlist.begin(); it != fieldlist.end(); it++) {
      list.append(it->toCString(true));
      }
    }
  else {
    list.clear();
    }
  }


void GMFileTag::mp4_update_field(const FXchar * field,const FXString & value) {
#ifdef TAGLIB_HAVE_MP4
  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_update_field(const FXchar * field,const FXStringList & list) {
#ifdef TAGLIB_HAVE_MP4
  FXASSERT(field);
  FXASSERT(mp4);
  if (list.no()==0) {
    mp4->itemListMap().erase(field);
    }
  else {
    TagLib::StringList values;
    for (FXint i=0;i<list.no();i++) {
      values.append(TagLib::String(list[i].text(),TagLib::String::UTF8));
      }
    mp4->itemListMap()[field]=values;
    }
#endif
  }


void GMFileTag::mp4_get_field(const FXchar * field,FXString & value) {
#ifdef TAGLIB_HAVE_MP4
  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::mp4_get_field(const FXchar * field,FXStringList & list) {
#ifdef TAGLIB_HAVE_MP4
  FXASSERT(field);
  FXASSERT(mp4);
  if (mp4->itemListMap().contains(field) && !mp4->itemListMap().isEmpty()) {
    TagLib::StringList fieldlist = mp4->itemListMap()[field].toStringList();
    for(TagLib::StringList::ConstIterator it = fieldlist.begin(); it != fieldlist.end(); it++) {
      list.append(it->toCString(true));
      }
    }
  else
    list.clear();
#else
  list.clear();
#endif
  }



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

void GMFileTag::setComposer(const FXString & composer) {
  if (xiph)
    xiph_update_field("COMPOSER",composer);
  else if (id3v2)
    id3v2_update_field("TCOM",composer);
  else if (mp4)
    mp4_update_field("\251wrt",composer);
  }

void GMFileTag::getComposer(FXString & composer) {
  if (xiph)
    xiph_get_field("COMPOSER",composer);
  else if (id3v2)
    id3v2_get_field("TCOM",composer);
  else if (mp4)
    mp4_get_field("\251wrt",composer);
  else
    composer.clear();
  }

void GMFileTag::setConductor(const FXString & conductor) {
  if (xiph)
    xiph_update_field("COMPOSER",conductor);
  else if (id3v2)
    id3v2_update_field("TPE3",conductor);
  else if (mp4)
    mp4_update_field("----:com.apple.iTunes:CONDUCTOR",conductor);
  }

void GMFileTag::getConductor(FXString & conductor) {
  if (xiph)
    xiph_get_field("COMPOSER",conductor);
  else if (id3v2)
    id3v2_get_field("TPE3",conductor);
  else if (mp4)
    mp4_get_field("----:com.apple.iTunes:CONDUCTOR",conductor);
  else
    conductor.clear();
  }


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::setTags(const FXStringList & tags){
  if (xiph)
    xiph_update_field("GENRE",tags);
  else if (id3v2)
    id3v2_update_field("TCON",tags);
  else if (mp4)
    mp4_update_field("\251gen",tags);
  else if (ape)
    ape_update_field("GENRE",tags);
  else {
    if (tags.no())
      tag->setGenre(TagLib::String(tags[0].text(),TagLib::String::UTF8));
    else
      tag->setGenre(TagLib::String("",TagLib::String::UTF8));
    }
  }

void GMFileTag::getTags(FXStringList & tags) {
  if (xiph)
    xiph_get_field("GENRE",tags);
  else if (id3v2) {
    id3v2_get_field("TCON",tags);
    parse_tagids(tags);
    }
  else if (mp4)
    mp4_get_field("\251gen",tags);
  else if (ape)
    ape_get_field("GENRE",tags);
  else
    tags.append(FXString(tag->genre().toCString(true)));
  }

void GMFileTag::getTitle(FXString & title){
  if (xiph) {
    FXStringList titles;
    xiph_get_field("TITLE",titles);
    title.clear();
    if (titles.no()) {
      for (FXint i=0;i<titles.no();i++) {
        titles[i].trim();
        if (!title.empty()) title+=" - ";
        title+=titles[i];
        }
      }
    }
  else {
    title=tag->title().toCString(true);
    title.trim();
    }
  }

static FXbool to_int(const FXString & str,FXint & val){
  char * endptr=NULL;
  errno=0;
  val=strtol(str.text(),&endptr,10);
  if (errno==0) {
    if (endptr==str.text())
      return false;
    return true;
    }
  return false;
  }


void GMFileTag::parse_tagids(FXStringList & tags){
  FXint id;
  for (FXint i=tags.no()-1;i>=0;i--){
    if (to_int(tags[i],id)) {
      tags[i]=TagLib::ID3v1::genre(id).toCString(true);
      }
    }
  }


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);
    }
#ifdef TAGLIB_HAVE_MP4
  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);
    }
#ifdef TAGLIB_HAVE_MP4
  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 IMPLEMENTATION                                                     */
/******************************************************************************/

FXbool GMTrack::saveTag(const FXString & filename,FXuint/*opts=0*/) {

  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->setYear(year);
  tag->setTrack(getTrackNumber());
  tag->setGenre(TagLib::String(genre.text(),TagLib::String::UTF8));

  GMFileTag filetags(file.file());

  filetags.setDiscNumber(getDiscNumber());
  //filetags.setComposer(composer);
  //filetags.setConductor(conductor);
  //filetags.setTags(tags);

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

  return file.save();
  }


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

  FXString disc,value;

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

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

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

  artist.trim();
  album.trim();

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

  GMFileTag filetags(file.file());

  filetags.getAlbumArtist(album_artist);
//  filetags.getComposer(composer);
//  filetags.getConductor(conductor);
  filetags.getGain(track_gain,track_peak,album_gain,album_peak);
//  filetags.getTags(tags);

  filetags.getTitle(title);

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

  setDiscNumber(filetags.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;
  }












/******************************************************************************/
/*            FLAC PICTURE LOADING                                            */
/******************************************************************************/

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;
    }
  };


#if TAGLIB_VERSION < MKVERSION(1,7,0)

#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
#define FLAC_LAST_BLOCK      0x80000000
#define FLAC_BLOCK_TYPE_MASK 0x7F000000
#define FLAC_BLOCK_TYPE(h)   ((h&0x7F000000)>>24)
#define FLAC_BLOCK_SIZE(h)   (h&0xFFFFFF)
#define FLAC_BLOCK_SET_TYPE(h,type) (h|=((type<<24)&FLAC_BLOCK_TYPE_MASK))
#define FLAC_BLOCK_SET_SIZE(h,size) (h|=(size&0xFFFFFF))
#endif


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


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_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;
  }

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;
  }

#endif




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 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;
  }



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;
    }
  }

#if TAGLIB_VERSION >= MKVERSION(1,7,0)
GMCover* flac_load_cover_from_taglib(const TagLib::FLAC::Picture * picture,FXint scale) {
  GMCover * cover=NULL;
  if (picture) {
    if (picture->type()==TagLib::FLAC::Picture::FileIcon ||
        picture->type()==TagLib::FLAC::Picture::OtherFileIcon ||
        picture->type()==TagLib::FLAC::Picture::ColouredFish) {
        return NULL;
        }

    FXImage * image = gm_load_image_from_data(picture->data().data(),picture->data().size(),picture->mimeType().toCString(true),scale);
    if (image) {
      cover = new GMCover(image,picture->type(),picture->description().toCString(true));
      }
    }
  return cover;
  }

GMCover* flac_load_frontcover_from_taglib(const TagLib::FLAC::Picture * picture,FXint scale) {
  GMCover * cover=NULL;
  if (picture && picture->type()==TagLib::FLAC::Picture::FrontCover) {
    FXImage * image = gm_load_image_from_data(picture->data().data(),picture->data().size(),picture->mimeType().toCString(true),scale);
    if (image) {
      cover = new GMCover(image,picture->type(),picture->description().toCString(true));
      }
    }
  return cover;
  }
#endif



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

#if TAGLIB_VERSION < MKVERSION(1,7,0)
  if (comparecase(extension,"flac")==0){
    flac_load_covers(mrl,covers,scale);
    if (covers.no()) return (covers.no());
    }
#endif

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

#if TAGLIB_VERSION >= MKVERSION(1,7,0)
  TagLib::FLAC::File * flacfile = dynamic_cast<TagLib::FLAC::File*>(file.file());
  if (flacfile) {
    const TagLib::List<TagLib::FLAC::Picture*> picturelist = flacfile->pictureList();
    for(TagLib::List<TagLib::FLAC::Picture*>::ConstIterator it = picturelist.begin(); it != picturelist.end(); it++) {
      GMCover * cover = flac_load_cover_from_taglib((*it),scale);
      if (cover) covers.append(cover);
      }
    if (covers.no()) {
      return (covers.no());
      }
    }
#endif

  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);
        }
      }
    }
#ifdef TAGLIB_HAVE_MP4
  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 TAGLIB_VERSION < MKVERSION(1,7,0)
  if (comparecase(extension,"flac")==0){
    GMCover * cover = flac_load_cover(mrl,scale);
    if (cover) { return cover;}
    }
#endif
  TagLib::FileRef file(mrl.text(),false);
  if (file.isNull() || !file.tag()) {
    return NULL;
    }

  GMFileTag tags(file.file());

  FXIconSource src(FXApp::instance());

#if TAGLIB_VERSION >= MKVERSION(1,7,0)
  TagLib::FLAC::File * flacfile = dynamic_cast<TagLib::FLAC::File*>(file.file());
  if (flacfile) {
    const TagLib::List<TagLib::FLAC::Picture*> picturelist = flacfile->pictureList();
    for(TagLib::List<TagLib::FLAC::Picture*>::ConstIterator it = picturelist.begin(); it != picturelist.end(); it++) {
      GMCover * cover = flac_load_frontcover_from_taglib((*it),scale);
      if (cover) { return cover;}
      }
    }
#endif
  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;}
        }
      }
    }
#ifdef TAGLIB_HAVE_MP4
  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;
  }


GMTrack::GMTrack() :
  year(0),
  no(0),
  time(0),
  bitrate(0),
  album_gain(NAN),
  album_peak(NAN),
  track_gain(NAN),
  track_peak(NAN){
  }

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;
  }



