/*
 * Strawberry Music Player
 * This file was part of Clementine.
 * Copyright 2010, David Sansome <me@davidsansome.com>
 * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
 *
 * Strawberry 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.
 *
 * Strawberry 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 Strawberry.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <QtGlobal>

#include <memory>

#include <glib.h>
#include <glib/gtypes.h>
#include <glib-object.h>

#include <gst/gst.h>
#include <gst/tag/tag.h>

#include <QObject>
#include <QMutex>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QtConcurrentRun>
#include <QScopeGuard>

#include "cddasongloader.h"
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "constants/timeconstants.h"

using std::make_shared;

using namespace Qt::Literals::StringLiterals;

CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
    : QObject(parent),
      url_(url),
      network_(make_shared<NetworkAccessManager>()),
#ifdef HAVE_MUSICBRAINZ
      musicbrainz_client_(new MusicBrainzClient(network_, this)),
#endif
      whatever_(false) {

#ifdef HAVE_MUSICBRAINZ
  QObject::connect(this, &CDDASongLoader::LoadTagsFromMusicBrainz, this, &CDDASongLoader::LoadTagsFromMusicBrainzSlot);
  QObject::connect(musicbrainz_client_, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::LoadTagsFromMusicBrainzFinished);
#endif  // HAVE_MUSICBRAINZ

}

CDDASongLoader::~CDDASongLoader() {
  loading_future_.waitForFinished();
}

QUrl CDDASongLoader::GetUrlFromTrack(int track_number) const {

  if (url_.isEmpty()) {
    return QUrl(QStringLiteral("cdda://%1a").arg(track_number));
  }

  return QUrl(QStringLiteral("cdda://%1/%2").arg(url_.path()).arg(track_number));

}

void CDDASongLoader::LoadSongs() {

  if (IsActive()) {
    return;
  }

  loading_future_ = QtConcurrent::run(&CDDASongLoader::LoadSongsFromCDDA, this);

}

void CDDASongLoader::LoadSongsFromCDDA() {

  QMutexLocker l(&mutex_load_);

  GstElement *cdda = gst_element_factory_make("cdiocddasrc", nullptr);
  if (!cdda) {
    Error(tr("Could not create cdiocddasrc"));
    return;
  }

  if (!url_.isEmpty()) {
    g_object_set(cdda, "device", url_.path().toLocal8Bit().constData(), nullptr);
  }
  if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda), "paranoia-mode")) {
    g_object_set(cdda, "paranoia-mode", 0, nullptr);
  }

  // Change the element's state to ready and paused, to be able to query it
  if (gst_element_set_state(cdda, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
    gst_element_set_state(cdda, GST_STATE_NULL);
    gst_object_unref(GST_OBJECT(cdda));
    cdda = nullptr;
    Error(tr("Error while setting CDDA device to ready state."));
    return;
  }

  if (gst_element_set_state(cdda, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
    gst_element_set_state(cdda, GST_STATE_NULL);
    gst_object_unref(GST_OBJECT(cdda));
    cdda = nullptr;
    Error(tr("Error while setting CDDA device to pause state."));
    return;
  }

  // Get number of tracks
  GstFormat format_track = gst_format_get_by_nick("track");
  if (format_track == GST_FORMAT_UNDEFINED) {
    gst_element_set_state(cdda, GST_STATE_NULL);
    gst_object_unref(GST_OBJECT(cdda));
    cdda = nullptr;
    Error(tr("The 'track' format is not supported by GStreamer."));
    return;
  }
  GstFormat format_duration = format_track;
  gint64 total_tracks = 0;
  if (!gst_element_query_duration(cdda, format_duration, &total_tracks)) {
    gst_element_set_state(cdda, GST_STATE_NULL);
    gst_object_unref(GST_OBJECT(cdda));
    cdda = nullptr;
    Error(tr("Error while querying CDDA tracks."));
    return;
  }

  if (format_duration != format_track) {
    qLog(Error) << "Error while querying CDDA GstElement (2).";
    gst_element_set_state(cdda, GST_STATE_NULL);
    gst_object_unref(GST_OBJECT(cdda));
    cdda = nullptr;
    Error(tr("Error while querying CDDA tracks."));
    return;
  }

  QMap<int, Song> songs;
  for (int track_number = 1; track_number <= total_tracks; ++track_number) {
    Song song(Song::Source::CDDA);
    song.set_id(track_number);
    song.set_valid(true);
    song.set_filetype(Song::FileType::CDDA);
    song.set_url(GetUrlFromTrack(track_number));
    song.set_title(QStringLiteral("Track %1").arg(track_number));
    song.set_track(track_number);
    songs.insert(track_number, song);
  }

  Q_EMIT SongsLoaded(songs.values());

#ifdef HAVE_MUSICBRAINZ
  gst_tag_register_musicbrainz_tags();
#endif  // HAVE_MUSICBRAINZ

  GstElement *pipeline = gst_pipeline_new("pipeline");
  GstElement *sink = gst_element_factory_make("fakesink", nullptr);
  gst_bin_add_many(GST_BIN(pipeline), cdda, sink, nullptr);
  gst_element_link(cdda, sink);
  gst_element_set_state(pipeline, GST_STATE_READY);
  gst_element_set_state(pipeline, GST_STATE_PAUSED);

  GstMessage *msg = nullptr;
  int track_artist_tags = 0;
  int track_album_tags = 0;
  int track_title_tags = 0;
#ifdef HAVE_MUSICBRAINZ
  QString musicbrainz_discid;
#endif  // HAVE_MUSICBRAINZ
  GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
  while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {

    const QScopeGuard scopeguard_msg = qScopeGuard([msg]() {
      gst_message_unref(msg);
    });

    if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
      GstToc *toc = nullptr;
      gst_message_parse_toc(msg, &toc, nullptr);
      const QScopeGuard scopeguard_toc = qScopeGuard([toc]() {
        gst_toc_unref(toc);
      });
      GList *entries = gst_toc_get_entries(toc);
      int track_number = 0;
      for (GList *entry_node = entries; entry_node != nullptr; entry_node = entry_node->next) {
        ++track_number;
        if (songs.contains(track_number)) {
          Song &song = songs[track_number];
          GstTocEntry *entry = static_cast<GstTocEntry*>(entry_node->data);
          gint64 start = 0, stop = 0;
          if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) {
            song.set_length_nanosec(static_cast<qint64>(stop - start));
          }
        }
      }
      msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
    }

    else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {

      GstTagList *tags = nullptr;
      gst_message_parse_tag(msg, &tags);
      const QScopeGuard scopeguard_tags = qScopeGuard([tags]() {
        gst_tag_list_free(tags);
      });

      gint64 track_index = 0;
      gst_element_query_position(cdda, format_track, &track_index);

      char *tag = nullptr;

#ifdef HAVE_MUSICBRAINZ
      if (musicbrainz_discid.isEmpty()) {
        if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &tag)) {
          musicbrainz_discid = QString::fromUtf8(tag);
          g_free(tag);
          tag = nullptr;
        }
      }
#endif

      guint track_number = 0;
      if (!gst_tag_list_get_uint(tags, GST_TAG_TRACK_NUMBER, &track_number)) {
        qLog(Error) << "Could not get track number";
        msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
        continue;
      }

      if (!songs.contains(track_number)) {
        qLog(Error) << "Got invalid track number" << track_number;
        msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
        continue;
      }

      Song &song = songs[track_number];
      guint64 duration = 0;
      if (gst_tag_list_get_uint64(tags, GST_TAG_DURATION, &duration)) {
        song.set_length_nanosec(static_cast<qint64>(duration));
      }
      if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST, &tag)) {
        song.set_albumartist(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST_SORTNAME, &tag)) {
        song.set_albumartistsort(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_ARTIST, &tag)) {
        song.set_artist(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
        ++track_artist_tags;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_ARTIST_SORTNAME, &tag)) {
        song.set_artistsort(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &tag)) {
        song.set_album(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
        ++track_album_tags;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_SORTNAME, &tag)) {
        song.set_albumsort(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_TITLE, &tag)) {
        song.set_title(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
        ++track_title_tags;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_TITLE_SORTNAME, &tag)) {
        song.set_titlesort(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_GENRE, &tag)) {
        song.set_genre(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER, &tag)) {
        song.set_composer(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER_SORTNAME, &tag)) {
        song.set_composersort(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_PERFORMER, &tag)) {
        song.set_performer(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      if (gst_tag_list_get_string(tags, GST_TAG_COMMENT, &tag)) {
        song.set_comment(QString::fromUtf8(tag));
        g_free(tag);
        tag = nullptr;
      }
      guint bitrate = 0;
      if (gst_tag_list_get_uint(tags, GST_TAG_BITRATE, &bitrate)) {
        song.set_bitrate(static_cast<int>(bitrate));
      }

      if (track_number >= total_tracks) {
        msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
        continue;
      }

      const gint64 next_track_index = track_index + 1;
      if (!gst_element_seek_simple(pipeline, format_track, static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_TRICKMODE), next_track_index)) {
        qLog(Error) << "Failed to seek to next track index" << next_track_index;
        msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
      }

    }
  }

  gst_element_set_state(pipeline, GST_STATE_NULL);
  // This will also cause cdda to be unref'd.
  gst_object_unref(pipeline);

  if ((track_artist_tags >= total_tracks && track_album_tags >= total_tracks && track_title_tags >= total_tracks)) {
    qLog(Info) << "Songs loaded from CD-Text";
    Q_EMIT SongsUpdated(songs.values());
    Q_EMIT LoadingFinished();
  }
  else {
#ifdef HAVE_MUSICBRAINZ
    if (musicbrainz_discid.isEmpty()) {
      qLog(Info) << "CD is missing tags";
      Q_EMIT LoadingFinished();
    }
    else {
      qLog(Info) << "MusicBrainz Disc ID:" << musicbrainz_discid;
      Q_EMIT LoadTagsFromMusicBrainz(musicbrainz_discid, songs);
    }
#else
    Q_EMIT LoadingFinished();
#endif  // HAVE_MUSICBRAINZ
  }

}

#ifdef HAVE_MUSICBRAINZ

void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid, const QMap<int, Song> &songs) {

  musicbrainz_discid_ = musicbrainz_discid;
  musicbrainz_songs_ = songs;
  musicbrainz_client_->StartDiscIdRequest(musicbrainz_discid);

}

void CDDASongLoader::LoadTagsFromMusicBrainzFinished(const QString &musicbrainz_discid, const MusicBrainzClient::ResultList &results, const QString &error) {

  if (musicbrainz_discid != musicbrainz_discid_) {
    return;
  }

  QMap<int, Song> songs = musicbrainz_songs_;
  musicbrainz_discid_.clear();
  musicbrainz_songs_.clear();

  if (!error.isEmpty()) {
    Error(error);
    return;
  }

  if (results.empty()) {
    Q_EMIT LoadingFinished();
    return;
  }

  for (const MusicBrainzClient::Result &result : results) {
    if (songs.contains(result.track_)) {
      Song &song = songs[result.track_];
      song.set_valid(true);
      song.set_id(result.track_);
      song.set_track(result.track_);
      song.set_artist(result.artist_);
      song.set_artistsort(result.sort_artist_);
      song.set_album(result.album_);
      song.set_title(result.title_);
      song.set_track(result.track_);
      song.set_year(result.year_);
      song.set_url(GetUrlFromTrack(song.track()));
      if (song.length_nanosec() <= 0) {
        song.set_length_nanosec(result.duration_msec_ * kNsecPerMsec);
      }
      if (!result.album_artist_.isEmpty() && result.album_artist_ != result.artist_) {
        song.set_albumartist(result.album_artist_);
      }
      if (!result.sort_album_artist_.isEmpty() && result.sort_album_artist_ != result.sort_artist_) {
        song.set_albumartistsort(result.sort_album_artist_);
      }
    }
  }

  Q_EMIT SongsUpdated(songs.values());
  Q_EMIT LoadingFinished();

}

#endif  // HAVE_MUSICBRAINZ

void CDDASongLoader::Error(const QString &error) {

  qLog(Error) << error;

  Q_EMIT LoadError(error);
  Q_EMIT LoadingFinished();

}
