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

#define TAGLIB_STATIC
#include <apeproperties.h>
#include <apetag.h>
#include <aifffile.h>
#include <asffile.h>
#include <dsffile.h>
#include <fileref.h>
#include <flacfile.h>
#include <id3v2tag.h>
#include <unsynchronizedlyricsframe.h>
#include <synchronizedlyricsframe.h>
#include <mp4file.h>
#include <mpegfile.h>
#include <opusfile.h>
#include <tpropertymap.h>
#include <vorbisfile.h>
#include <wavfile.h>
#include <wavfile.h>
#include <wavpackfile.h>

#include "taglib_wrapper.h"

char has_cover(const TagLib::FileRef f);

static char TAGLIB_VERSION[16];

char* taglib_version() {
    snprintf((char *)TAGLIB_VERSION, 16, "%d.%d.%d", TAGLIB_MAJOR_VERSION, TAGLIB_MINOR_VERSION, TAGLIB_PATCH_VERSION);
    return (char *)TAGLIB_VERSION;
}

int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) {
  TagLib::FileRef f(filename, true, TagLib::AudioProperties::Fast);

  if (f.isNull()) {
    return TAGLIB_ERR_PARSE;
  }

  if (!f.audioProperties()) {
    return TAGLIB_ERR_AUDIO_PROPS;
  }

  // Add audio properties to the tags
  const TagLib::AudioProperties *props(f.audioProperties());
  goPutInt(id, (char *)"__lengthinmilliseconds", props->lengthInMilliseconds());
  goPutInt(id, (char *)"__bitrate", props->bitrate());
  goPutInt(id, (char *)"__channels", props->channels());
  goPutInt(id, (char *)"__samplerate", props->sampleRate());

  // Extract bits per sample for supported formats
  int bitsPerSample = 0;
  if (const auto* apeProperties{ dynamic_cast<const TagLib::APE::Properties*>(props) })
      bitsPerSample = apeProperties->bitsPerSample();
  else if (const auto* asfProperties{ dynamic_cast<const TagLib::ASF::Properties*>(props) })
      bitsPerSample = asfProperties->bitsPerSample();
  else if (const auto* flacProperties{ dynamic_cast<const TagLib::FLAC::Properties*>(props) })
      bitsPerSample = flacProperties->bitsPerSample();
  else if (const auto* mp4Properties{ dynamic_cast<const TagLib::MP4::Properties*>(props) })
      bitsPerSample = mp4Properties->bitsPerSample();
  else if (const auto* wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(props) })
      bitsPerSample = wavePackProperties->bitsPerSample();
  else if (const auto* aiffProperties{ dynamic_cast<const TagLib::RIFF::AIFF::Properties*>(props) })
      bitsPerSample = aiffProperties->bitsPerSample();
  else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(props) })
      bitsPerSample = wavProperties->bitsPerSample();
  else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(props) })
      bitsPerSample = dsfProperties->bitsPerSample();
  
  if (bitsPerSample > 0) {
      goPutInt(id, (char *)"__bitspersample", bitsPerSample);
  }

  // Send all properties to the Go map
  TagLib::PropertyMap tags = f.file()->properties();

  // Make sure at least the basic properties are extracted
  TagLib::Tag *basic = f.file()->tag();
  if (!basic->isEmpty()) {
    if (!basic->title().isEmpty()) {
      tags.insert("__title", basic->title());
    }
    if (!basic->artist().isEmpty()) {
      tags.insert("__artist", basic->artist());
    }
    if (!basic->album().isEmpty()) {
      tags.insert("__album", basic->album());
    }
    if (!basic->comment().isEmpty()) {
      tags.insert("__comment", basic->comment());
    }
    if (!basic->genre().isEmpty()) {
      tags.insert("__genre", basic->genre());
    }
    if (basic->year() > 0) {
      tags.insert("__year", TagLib::String::number(basic->year()));
    }
    if (basic->track() > 0) {
      tags.insert("__track", TagLib::String::number(basic->track()));
    }
  }

  TagLib::ID3v2::Tag *id3Tags = NULL;

  // Get some extended/non-standard ID3-only tags (ex: iTunes extended frames)
  TagLib::MPEG::File *mp3File(dynamic_cast<TagLib::MPEG::File *>(f.file()));
  if (mp3File != NULL) {
    id3Tags = mp3File->ID3v2Tag();
  }

  if (id3Tags == NULL) {
    TagLib::RIFF::WAV::File *wavFile(dynamic_cast<TagLib::RIFF::WAV::File *>(f.file()));
    if (wavFile != NULL && wavFile->hasID3v2Tag()) {
      id3Tags = wavFile->ID3v2Tag();
    }
  }

  if (id3Tags == NULL) {
    TagLib::RIFF::AIFF::File *aiffFile(dynamic_cast<TagLib::RIFF::AIFF::File *>(f.file()));
    if (aiffFile && aiffFile->hasID3v2Tag()) {
      id3Tags = aiffFile->tag();
    }
  }

  // Yes, it is possible to have ID3v2 tags in FLAC. However, that can cause problems
  // with many players, so they will not be parsed

  if (id3Tags != NULL) {
    const auto &frames = id3Tags->frameListMap();

    for (const auto &kv: frames) {
      if (kv.first == "USLT") {
        for (const auto &tag: kv.second) {
          TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = dynamic_cast<TagLib::ID3v2::UnsynchronizedLyricsFrame *>(tag);
          if (frame == NULL) continue;

          tags.erase("LYRICS");

          const auto bv = frame->language();
          char language[4] = {'x', 'x', 'x', '\0'};
          if (bv.size() == 3) {
            strncpy(language, bv.data(), 3);
          }

          char *val = const_cast<char*>(frame->text().toCString(true));

          goPutLyrics(id, language, val);
        }
      } else if (kv.first == "SYLT") {
        for (const auto &tag: kv.second) {
          TagLib::ID3v2::SynchronizedLyricsFrame *frame = dynamic_cast<TagLib::ID3v2::SynchronizedLyricsFrame *>(tag);
          if (frame == NULL) continue;

          const auto bv = frame->language();
          char language[4] = {'x', 'x', 'x', '\0'};
          if (bv.size() == 3) {
            strncpy(language, bv.data(), 3);
          }

          const auto format = frame->timestampFormat();
          if (format == TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds) {

            for (const auto &line: frame->synchedText()) {
              char *text = const_cast<char*>(line.text.toCString(true));
              goPutLyricLine(id, language, text, line.time);
            }
          } else if (format == TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames) {
            const int sampleRate = props->sampleRate();

            if (sampleRate != 0) {
              for (const auto &line: frame->synchedText()) {
                const int timeInMs = (line.time * 1000) / sampleRate;
                char *text = const_cast<char*>(line.text.toCString(true));
                goPutLyricLine(id, language, text, timeInMs);
              }
            }
          }
        }
      } else if (kv.first == "TIPL"){
        if (!kv.second.isEmpty()) {
          tags.insert(kv.first, kv.second.front()->toString());
        }
      }
    }
  }

  // M4A may have some iTunes specific tags not captured by the PropertyMap interface
  TagLib::MP4::File *m4afile(dynamic_cast<TagLib::MP4::File *>(f.file()));
  if (m4afile != NULL) {
    const auto itemListMap = m4afile->tag()->itemMap();
    for (const auto item: itemListMap) {
      char *key = const_cast<char*>(item.first.toCString(true));
      for (const auto value: item.second.toStringList()) {
        char *val = const_cast<char*>(value.toCString(true));
        goPutM4AStr(id, key, val);
      }
    }
  }

  // WMA/ASF files may have additional tags not captured by the PropertyMap interface
  TagLib::ASF::File *asfFile(dynamic_cast<TagLib::ASF::File *>(f.file()));
  if (asfFile != NULL) {
    const TagLib::ASF::Tag *asfTags{asfFile->tag()};
    const auto itemListMap = asfTags->attributeListMap();
    for (const auto item : itemListMap) {
      char *key = const_cast<char*>(item.first.toCString(true));

      for (auto j = item.second.begin();
           j != item.second.end(); ++j) {

        char *val = const_cast<char*>(j->toString().toCString(true));
        goPutStr(id, key, val);
      }
    }
  }

  // Send all collected tags to the Go map
  for (TagLib::PropertyMap::ConstIterator i = tags.begin(); i != tags.end();
       ++i) {
    char *key = const_cast<char*>(i->first.toCString(true));
    for (TagLib::StringList::ConstIterator j = i->second.begin();
         j != i->second.end(); ++j) {
      char *val = const_cast<char*>((*j).toCString(true));
      goPutStr(id, key, val);
    }
  }

  // Cover art has to be handled separately
  if (has_cover(f)) {
    goPutStr(id, (char *)"has_picture", (char *)"true");
  }

  return 0;
}

// Detect if the file has cover art. Returns 1 if the file has cover art, 0 otherwise.
char has_cover(const TagLib::FileRef f) {
  char hasCover = 0;
  // ----- MP3
  if (TagLib::MPEG::File * mp3File{dynamic_cast<TagLib::MPEG::File *>(f.file())}) {
    if (mp3File->ID3v2Tag()) {
      const auto &frameListMap{mp3File->ID3v2Tag()->frameListMap()};
      hasCover = !frameListMap["APIC"].isEmpty();
    }
  }
  // ----- FLAC
  else if (TagLib::FLAC::File * flacFile{dynamic_cast<TagLib::FLAC::File *>(f.file())}) {
    hasCover = !flacFile->pictureList().isEmpty();
  }
  // ----- MP4
  else if (TagLib::MP4::File * mp4File{dynamic_cast<TagLib::MP4::File *>(f.file())}) {
    auto &coverItem{mp4File->tag()->itemMap()["covr"]};
    TagLib::MP4::CoverArtList coverArtList{coverItem.toCoverArtList()};
    hasCover = !coverArtList.isEmpty();
  }
  // ----- Ogg
  else if (TagLib::Ogg::Vorbis::File * vorbisFile{dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file())}) {
    hasCover = !vorbisFile->tag()->pictureList().isEmpty();
  }
  // ----- Opus
  else if (TagLib::Ogg::Opus::File * opusFile{dynamic_cast<TagLib::Ogg::Opus::File *>(f.file())}) {
    hasCover = !opusFile->tag()->pictureList().isEmpty();
  }
  // ----- WAV
  else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(f.file()) }) {
    if (wavFile->hasID3v2Tag()) {
      const auto& frameListMap{ wavFile->ID3v2Tag()->frameListMap() };
      hasCover = !frameListMap["APIC"].isEmpty();
    }
  }
  // ----- AIFF 
  else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File *>(f.file())}) {
    if (aiffFile->hasID3v2Tag()) {
      const auto& frameListMap{ aiffFile->tag()->frameListMap() };
      hasCover = !frameListMap["APIC"].isEmpty();
    }
  }
  // ----- WMA
  else if (TagLib::ASF::File * asfFile{dynamic_cast<TagLib::ASF::File *>(f.file())}) {
    const TagLib::ASF::Tag *tag{ asfFile->tag() };
    hasCover = tag && tag->attributeListMap().contains("WM/Picture");
  }
  // ----- DSF
  else if (TagLib::DSF::File * dsffile{ dynamic_cast<TagLib::DSF::File *>(f.file())}) {
    const TagLib::ID3v2::Tag *tag { dsffile->tag() };
    hasCover = tag && !tag->frameListMap()["APIC"].isEmpty();
  }
  // ----- WAVPAK (APE tag)
  else if (TagLib::WavPack::File * wvFile{dynamic_cast<TagLib::WavPack::File *>(f.file())}) {
    if (wvFile->hasAPETag()) {
      // This is the particular string that Picard uses
      hasCover = !wvFile->APETag()->itemListMap()["COVER ART (FRONT)"].isEmpty();
    }
  }

  return hasCover;
}
