/* This file is part of Clementine.
   Copyright 2010, David Sansome <me@davidsansome.com>

   Clementine 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.

   Clementine 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 Clementine.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "librarybackend.h"

#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
#include <QVariant>
#include <QtDebug>

#include "core/application.h"
#include "core/database.h"
#include "core/scopedtransaction.h"
#include "core/tagreaderclient.h"
#include "core/utilities.h"
#include "libraryquery.h"
#include "smartplaylists/search.h"
#include "sqlrow.h"

const char* LibraryBackend::kSettingsGroup = "LibraryBackend";

const char* LibraryBackend::kNewScoreSql =
    "case when playcount <= 0 then (%1 * 100 + score) / 2"
    "     else (score * (playcount + skipcount) + %1 * 100) / (playcount + "
    "skipcount + 1)"
    " end";

LibraryBackend::LibraryBackend(QObject* parent)
    : LibraryBackendInterface(parent),
      save_statistics_in_file_(false),
      save_ratings_in_file_(false) {}

void LibraryBackend::Init(Database* db, const QString& songs_table,
                          const QString& fts_table) {
  db_ = db;
  songs_table_ = songs_table;
  fts_table_ = fts_table;
}

void LibraryBackend::Init(Database* db, const QString& songs_table,
                          const QString& dirs_table,
                          const QString& subdirs_table,
                          const QString& fts_table) {
  Init(db, songs_table, fts_table);
  dirs_table_ = dirs_table;
  subdirs_table_ = subdirs_table;
}

void LibraryBackend::LoadDirectoriesAsync() {
  metaObject()->invokeMethod(this, "LoadDirectories", Qt::QueuedConnection);
}

void LibraryBackend::UpdateTotalSongCountAsync() {
  metaObject()->invokeMethod(this, "UpdateTotalSongCount",
                             Qt::QueuedConnection);
}

void LibraryBackend::IncrementPlayCountAsync(int id) {
  metaObject()->invokeMethod(this, "IncrementPlayCount", Qt::QueuedConnection,
                             Q_ARG(int, id));
}

void LibraryBackend::IncrementSkipCountAsync(int id, float progress) {
  metaObject()->invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection,
                             Q_ARG(int, id), Q_ARG(float, progress));
}

void LibraryBackend::ResetStatisticsAsync(int id) {
  metaObject()->invokeMethod(this, "ResetStatistics", Qt::QueuedConnection,
                             Q_ARG(int, id));
}

void LibraryBackend::UpdateSongRatingAsync(int id, float rating) {
  metaObject()->invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection,
                             Q_ARG(int, id), Q_ARG(float, rating));
}

void LibraryBackend::UpdateSongsRatingAsync(const QList<int>& ids,
                                            float rating) {
  metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection,
                             Q_ARG(const QList<int>&, ids),
                             Q_ARG(float, rating));
}

void LibraryBackend::LoadDirectories() {
  DirectoryList dirs = GetAllDirectories();

  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  for (const Directory& dir : dirs) {
    emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db));
  }
}

void LibraryBackend::ChangeDirPath(int id, const QString& old_path,
                                   const QString& new_path) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());
  ScopedTransaction t(&db);

  // Do the dirs table
  QSqlQuery q(db);
  q.prepare(
      QString("UPDATE %1 SET path=:path WHERE ROWID=:id").arg(dirs_table_));
  q.bindValue(":path", new_path);
  q.bindValue(":id", id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  const QByteArray old_url = QUrl::fromLocalFile(old_path).toEncoded();
  const QByteArray new_url = QUrl::fromLocalFile(new_path).toEncoded();

  const int path_len = old_url.length();

  // Do the subdirs table
  q = QSqlQuery(db);
  q.prepare(QString("UPDATE %1 SET path=:path || substr(path, %2)"
                    " WHERE directory=:id")
                .arg(subdirs_table_)
                .arg(path_len));
  q.bindValue(":path", new_url);
  q.bindValue(":id", id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  // Do the songs table
  q = QSqlQuery(db);
  q.prepare(QString("UPDATE %1 SET filename=:path || substr(filename, %2)"
                    " WHERE directory=:id")
                .arg(songs_table_)
                .arg(path_len));
  q.bindValue(":path", new_url);
  q.bindValue(":id", id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  t.Commit();
}

DirectoryList LibraryBackend::GetAllDirectories() {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  DirectoryList ret;

  QSqlQuery q(db);
  q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_));
  q.exec();
  if (db_->CheckErrors(q)) return ret;

  while (q.next()) {
    Directory dir;
    dir.id = q.value(0).toInt();
    dir.path = q.value(1).toString();

    ret << dir;
  }
  return ret;
}

SubdirectoryList LibraryBackend::SubdirsInDirectory(int id) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db = db_->Connect();
  return SubdirsInDirectory(id, db);
}

SubdirectoryList LibraryBackend::SubdirsInDirectory(int id, QSqlDatabase& db) {
  QSqlQuery q(db);
  q.prepare(QString("SELECT path, mtime FROM %1"
                    " WHERE directory = :dir")
                .arg(subdirs_table_));
  q.bindValue(":dir", id);
  q.exec();
  if (db_->CheckErrors(q)) return SubdirectoryList();

  SubdirectoryList subdirs;
  while (q.next()) {
    Subdirectory subdir;
    subdir.directory_id = id;
    subdir.path = q.value(0).toString();
    subdir.mtime = q.value(1).toUInt();
    subdirs << subdir;
  }

  return subdirs;
}

void LibraryBackend::UpdateTotalSongCount() {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("SELECT COUNT(*) FROM %1 WHERE unavailable = 0")
                .arg(songs_table_));
  q.exec();
  if (db_->CheckErrors(q)) return;
  if (!q.next()) return;

  emit TotalSongCountUpdated(q.value(0).toInt());
}

void LibraryBackend::AddDirectory(const QString& path) {
  QString canonical_path = QFileInfo(path).canonicalFilePath();
  QString db_path = canonical_path;

  if (Application::kIsPortable && Utilities::UrlOnSameDriveAsClementine(
                                      QUrl::fromLocalFile(canonical_path))) {
    db_path = Utilities::GetRelativePathToClementineBin(db_path);
    qLog(Debug) << "db_path" << db_path;
  }

  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("INSERT INTO %1 (path, subdirs)"
                    " VALUES (:path, 1)")
                .arg(dirs_table_));
  q.bindValue(":path", db_path);
  q.exec();
  if (db_->CheckErrors(q)) return;

  Directory dir;
  dir.path = canonical_path;
  dir.id = q.lastInsertId().toInt();

  emit DirectoryDiscovered(dir, SubdirectoryList());
}

void LibraryBackend::RemoveDirectory(int dir_id) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  // Remove songs first
  DeleteSongs(FindSongsInDirectory(dir_id));

  ScopedTransaction transaction(&db);

  // Delete the subdirs that were in this directory
  QSqlQuery q(db);
  q.prepare(
      QString("DELETE FROM %1 WHERE directory = :id").arg(subdirs_table_));
  q.bindValue(":id", dir_id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  // Now remove the directory itself
  q = QSqlQuery(db);
  q.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(dirs_table_));
  q.bindValue(":id", dir_id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  emit DirectoryDeleted(dir_id);

  transaction.Commit();
}

SongList LibraryBackend::FindSongsInDirectory(int id) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec +
                    " FROM %1 WHERE directory = :directory")
                .arg(songs_table_));
  q.bindValue(":directory", id);
  q.exec();
  if (db_->CheckErrors(q)) return SongList();

  SongList ret;
  while (q.next()) {
    Song song;
    song.InitFromQuery(q, true);
    ret << song;
  }
  return ret;
}

void LibraryBackend::SongPathChanged(const Song& song,
                                     const QFileInfo& new_file) {
  // Take a song and update its path
  Song updated_song = song;
  updated_song.InitFromFilePartial(new_file.absoluteFilePath());
  SongList updated_songs;
  updated_songs << updated_song;
  AddOrUpdateSongs(updated_songs);
}

void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());
  QSqlQuery find_query(db);
  find_query.prepare(QString("SELECT ROWID FROM %1"
                             " WHERE directory = :id AND path = :path")
                         .arg(subdirs_table_));
  QSqlQuery add_query(db);
  add_query.prepare(QString("INSERT INTO %1 (directory, path, mtime)"
                            " VALUES (:id, :path, :mtime)")
                        .arg(subdirs_table_));
  QSqlQuery update_query(db);
  update_query.prepare(QString("UPDATE %1 SET mtime = :mtime"
                               " WHERE directory = :id AND path = :path")
                           .arg(subdirs_table_));
  QSqlQuery delete_query(db);
  delete_query.prepare(QString("DELETE FROM %1"
                               " WHERE directory = :id AND path = :path")
                           .arg(subdirs_table_));

  ScopedTransaction transaction(&db);
  for (const Subdirectory& subdir : subdirs) {
    if (subdir.mtime == 0) {
      // Delete the subdirectory
      delete_query.bindValue(":id", subdir.directory_id);
      delete_query.bindValue(":path", subdir.path);
      delete_query.exec();
      db_->CheckErrors(delete_query);
    } else {
      // See if this subdirectory already exists in the database
      find_query.bindValue(":id", subdir.directory_id);
      find_query.bindValue(":path", subdir.path);
      find_query.exec();
      if (db_->CheckErrors(find_query)) continue;

      if (find_query.next()) {
        update_query.bindValue(":mtime", subdir.mtime);
        update_query.bindValue(":id", subdir.directory_id);
        update_query.bindValue(":path", subdir.path);
        update_query.exec();
        db_->CheckErrors(update_query);
      } else {
        add_query.bindValue(":id", subdir.directory_id);
        add_query.bindValue(":path", subdir.path);
        add_query.bindValue(":mtime", subdir.mtime);
        add_query.exec();
        db_->CheckErrors(add_query);
      }
    }
  }
  transaction.Commit();
}

void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery check_dir(db);
  check_dir.prepare(
      QString("SELECT ROWID FROM %1 WHERE ROWID = :id").arg(dirs_table_));
  QSqlQuery add_song(db);
  add_song.prepare(QString("INSERT INTO %1 (" + Song::kColumnSpec +
                           ")"
                           " VALUES (" +
                           Song::kBindSpec + ")")
                       .arg(songs_table_));
  QSqlQuery update_song(db);
  update_song.prepare(
      QString("UPDATE %1 SET " + Song::kUpdateSpec + " WHERE ROWID = :id")
          .arg(songs_table_));
  QSqlQuery add_song_fts(db);
  add_song_fts.prepare(QString("INSERT INTO %1 (ROWID, " +
                               Song::kFtsColumnSpec +
                               ")"
                               " VALUES (:id, " +
                               Song::kFtsBindSpec + ")")
                           .arg(fts_table_));
  QSqlQuery update_song_fts(db);
  update_song_fts.prepare(
      QString("UPDATE %1 SET " + Song::kFtsUpdateSpec + " WHERE ROWID = :id")
          .arg(fts_table_));

  ScopedTransaction transaction(&db);

  SongList added_songs;
  SongList deleted_songs;

  for (const Song& song : songs) {
    // Do a sanity check first - make sure the song's directory still exists
    // This is to fix a possible race condition when a directory is removed
    // while LibraryWatcher is scanning it.
    if (!dirs_table_.isEmpty()) {
      check_dir.bindValue(":id", song.directory_id());
      check_dir.exec();
      if (db_->CheckErrors(check_dir)) continue;

      if (!check_dir.next()) continue;  // Directory didn't exist
    }

    if (song.id() == -1) {
      // Create

      // Insert the row and create a new ID
      song.BindToQuery(&add_song);
      add_song.exec();
      if (db_->CheckErrors(add_song)) continue;

      // Get the new ID
      const int id = add_song.lastInsertId().toInt();

      // Add to the FTS index
      add_song_fts.bindValue(":id", id);
      song.BindToFtsQuery(&add_song_fts);
      add_song_fts.exec();
      if (db_->CheckErrors(add_song_fts)) continue;

      Song copy(song);
      copy.set_id(id);
      added_songs << copy;
    } else {
      // Get the previous song data first
      Song old_song(GetSongById(song.id()));
      if (!old_song.is_valid()) continue;

      // Update
      song.BindToQuery(&update_song);
      update_song.bindValue(":id", song.id());
      update_song.exec();
      if (db_->CheckErrors(update_song)) continue;

      song.BindToFtsQuery(&update_song_fts);
      update_song_fts.bindValue(":id", song.id());
      update_song_fts.exec();
      if (db_->CheckErrors(update_song_fts)) continue;

      deleted_songs << old_song;
      added_songs << song;
    }
  }

  transaction.Commit();

  if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);

  if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);

  UpdateTotalSongCountAsync();
}

void LibraryBackend::UpdateMTimesOnly(const SongList& songs) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id")
                .arg(songs_table_));

  ScopedTransaction transaction(&db);
  for (const Song& song : songs) {
    q.bindValue(":mtime", song.mtime());
    q.bindValue(":id", song.id());
    q.exec();
    db_->CheckErrors(q);
  }
  transaction.Commit();
}

void LibraryBackend::DeleteSongs(const SongList& songs) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery remove(db);
  remove.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
  QSqlQuery remove_fts(db);
  remove_fts.prepare(
      QString("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));

  ScopedTransaction transaction(&db);
  for (const Song& song : songs) {
    remove.bindValue(":id", song.id());
    remove.exec();
    db_->CheckErrors(remove);

    remove_fts.bindValue(":id", song.id());
    remove_fts.exec();
    db_->CheckErrors(remove_fts);
  }
  transaction.Commit();

  emit SongsDeleted(songs);

  UpdateTotalSongCountAsync();
}

void LibraryBackend::MarkSongsUnavailable(const SongList& songs,
                                          bool unavailable) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery remove(db);
  remove.prepare(QString("UPDATE %1 SET unavailable = %2 WHERE ROWID = :id")
                     .arg(songs_table_)
                     .arg(int(unavailable)));

  ScopedTransaction transaction(&db);
  for (const Song& song : songs) {
    remove.bindValue(":id", song.id());
    remove.exec();
    db_->CheckErrors(remove);
  }
  transaction.Commit();

  emit SongsDeleted(songs);
  UpdateTotalSongCountAsync();
}

QStringList LibraryBackend::GetAll(const QString& column,
                                   const QueryOptions& opt) {
  LibraryQuery query(opt);
  query.SetColumnSpec("DISTINCT " + column);
  query.AddCompilationRequirement(false);

  QMutexLocker l(db_->Mutex());
  if (!ExecQuery(&query)) return QStringList();

  QStringList ret;
  while (query.Next()) {
    ret << query.Value(0).toString();
  }
  return ret;
}

QStringList LibraryBackend::GetAllArtists(const QueryOptions& opt) {
  return GetAll("artist", opt);
}

QStringList LibraryBackend::GetAllArtistsWithAlbums(const QueryOptions& opt) {
  // Albums with 'albumartist' field set:
  LibraryQuery query(opt);
  query.SetColumnSpec("DISTINCT albumartist");
  query.AddCompilationRequirement(false);
  query.AddWhere("album", "", "!=");

  // Albums with no 'albumartist' (extract 'artist'):
  LibraryQuery query2(opt);
  query2.SetColumnSpec("DISTINCT artist");
  query2.AddCompilationRequirement(false);
  query2.AddWhere("album", "", "!=");
  query2.AddWhere("albumartist", "", "=");

  {
    QMutexLocker l(db_->Mutex());
    if (!ExecQuery(&query) || !ExecQuery(&query2)) {
      return QStringList();
    }
  }

  QSet<QString> artists;
  while (query.Next()) {
    artists << query.Value(0).toString();
  }
  while (query2.Next()) {
    artists << query2.Value(0).toString();
  }

  return QStringList(artists.values());
}

LibraryBackend::AlbumList LibraryBackend::GetAllAlbums(
    const QueryOptions& opt) {
  return GetAlbums(QString(), QString(), false, opt);
}

LibraryBackend::AlbumList LibraryBackend::GetAlbumsByArtist(
    const QString& artist, const QueryOptions& opt) {
  return GetAlbums(artist, QString(), false, opt);
}

LibraryBackend::AlbumList LibraryBackend::GetAlbumsByAlbumArtist(
    const QString& album_artist, const QueryOptions& opt) {
  return GetAlbums(QString(), album_artist, false, opt);
}

SongList LibraryBackend::GetSongsByAlbum(const QString& album,
                                         const QueryOptions& opt) {
  LibraryQuery query(opt);
  query.AddCompilationRequirement(false);
  query.AddWhere("album", album);
  return ExecLibraryQuery(&query);
}

SongList LibraryBackend::GetSongs(const QString& artist, const QString& album,
                                  const QueryOptions& opt) {
  LibraryQuery query(opt);
  query.AddCompilationRequirement(false);
  query.AddWhere("artist", artist);
  query.AddWhere("album", album);
  return ExecLibraryQuery(&query);
}

SongList LibraryBackend::ExecLibraryQuery(LibraryQuery* query) {
  query->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
  QMutexLocker l(db_->Mutex());
  if (!ExecQuery(query)) return SongList();

  SongList ret;
  while (query->Next()) {
    Song song;
    song.InitFromQuery(*query, true);
    ret << song;
  }
  return ret;
}

Song LibraryBackend::GetSongById(int id) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());
  return GetSongById(id, db);
}

SongList LibraryBackend::GetSongsById(const QList<int>& ids) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QStringList str_ids;
  for (int id : ids) {
    str_ids << QString::number(id);
  }

  return GetSongsById(str_ids, db);
}

SongList LibraryBackend::GetSongsById(const QStringList& ids) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  return GetSongsById(ids, db);
}

SongList LibraryBackend::GetSongsByForeignId(const QStringList& ids,
                                             const QString& table,
                                             const QString& column) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QString in = ids.join(",");

  QSqlQuery q(db);
  q.prepare(
      QString(
          "SELECT %2.ROWID, " + Song::kColumnSpec +
          ", %2.%3"
          " FROM %2, %1"
          " WHERE %2.%3 IN (%4) AND %1.ROWID = %2.ROWID AND unavailable = 0")
          .arg(songs_table_, table, column, in));
  q.exec();
  if (db_->CheckErrors(q)) return SongList();

  QVector<Song> ret(ids.count());
  while (q.next()) {
    const QString foreign_id = q.value(Song::kColumns.count() + 1).toString();
    const int index = ids.indexOf(foreign_id);
    if (index == -1) continue;

    ret[index].InitFromQuery(q, true);
  }
  return ret.toList();
}

Song LibraryBackend::GetSongById(int id, QSqlDatabase& db) {
  SongList list = GetSongsById(QStringList() << QString::number(id), db);
  if (list.isEmpty()) return Song();
  return list.first();
}

SongList LibraryBackend::GetSongsById(const QStringList& ids,
                                      QSqlDatabase& db) {
  QString in = ids.join(",");

  QSqlQuery q(db);
  q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec +
                    " FROM %1"
                    " WHERE ROWID IN (%2)")
                .arg(songs_table_, in));
  q.exec();
  if (db_->CheckErrors(q)) return SongList();

  SongList ret;
  while (q.next()) {
    Song song;
    song.InitFromQuery(q, true);
    ret << song;
  }
  return ret;
}

Song LibraryBackend::GetSongByUrl(const QUrl& url, qint64 beginning) {
  LibraryQuery query;
  query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
  query.AddWhere("filename", url.toEncoded());
  query.AddWhere("beginning", beginning);

  Song song;
  if (ExecQuery(&query) && query.Next()) {
    song.InitFromQuery(query, true);
  }
  return song;
}

SongList LibraryBackend::GetSongsByUrl(const QUrl& url) {
  LibraryQuery query;
  query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
  query.AddWhere("filename", url.toEncoded());

  SongList songlist;
  if (ExecQuery(&query)) {
    while (query.Next()) {
      Song song;
      song.InitFromQuery(query, true);

      songlist << song;
    }
  }
  return songlist;
}

LibraryBackend::AlbumList LibraryBackend::GetCompilationAlbums(
    const QueryOptions& opt) {
  return GetAlbums(QString(), QString(), true, opt);
}

SongList LibraryBackend::GetCompilationSongs(const QString& album,
                                             const QueryOptions& opt) {
  LibraryQuery query(opt);
  query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
  query.AddCompilationRequirement(true);
  query.AddWhere("album", album);

  QMutexLocker l(db_->Mutex());
  if (!ExecQuery(&query)) return SongList();

  SongList ret;
  while (query.Next()) {
    Song song;
    song.InitFromQuery(query, true);
    ret << song;
  }
  return ret;
}

void LibraryBackend::UpdateCompilations() {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  // Look for albums that have songs by more than one 'effective album artist'
  // in the same
  // directory

  QSqlQuery q(db);
  q.prepare(QString("SELECT effective_albumartist, album, filename, sampler "
                    "FROM %1 WHERE unavailable = 0 ORDER BY album")
                .arg(songs_table_));
  q.exec();
  if (db_->CheckErrors(q)) return;

  QMap<QString, CompilationInfo> compilation_info;
  while (q.next()) {
    QString artist = q.value(0).toString();
    QString album = q.value(1).toString();
    QString filename = q.value(2).toString();
    bool sampler = q.value(3).toBool();

    // Ignore songs that don't have an album field set
    if (album.isEmpty()) continue;

    // Find the directory the song is in
    QUrl url = QUrl::fromEncoded(filename.toUtf8());
    QString directory =
        url.toString(QUrl::PreferLocalFile | QUrl::RemoveFilename);

    CompilationInfo& info = compilation_info[directory + album];
    info.urls << url;
    if (!info.artists.contains(artist)) info.artists << artist;
    if (sampler)
      ++info.has_samplers;
    else
      ++info.has_not_samplers;
  }

  // Now mark the songs that we think are in compilations

  SongList deleted_songs;
  SongList added_songs;

  ScopedTransaction transaction(&db);

  QMap<QString, CompilationInfo>::const_iterator it =
      compilation_info.constBegin();
  for (; it != compilation_info.constEnd(); ++it) {
    const CompilationInfo& info = it.value();

    // If there were more then one 'effective album artist' for this album
    // directory then it's a compilation

    for (const QUrl& url : info.urls) {
      if (info.artists.count() > 1) {  // This directory+album is a compilation.
        if (info.has_not_samplers > 0)
          UpdateCompilations(db, deleted_songs, added_songs, url, true);
      } else {
        if (info.has_samplers > 0)
          UpdateCompilations(db, deleted_songs, added_songs, url, false);
      }
    }
  }

  transaction.Commit();

  if (!deleted_songs.isEmpty()) {
    emit SongsDeleted(deleted_songs);
    emit SongsDiscovered(added_songs);
  }
}

void LibraryBackend::UpdateCompilations(const QSqlDatabase& db,
                                        SongList& deleted_songs,
                                        SongList& added_songs, const QUrl& url,
                                        const bool sampler) {
  QSqlQuery find_song(db);
  find_song.prepare(QString("SELECT ROWID, " + Song::kColumnSpec +
                            " FROM %1"
                            " WHERE filename = :filename AND unavailable = 0")
                        .arg(songs_table_));

  QSqlQuery update_song(db);
  update_song.prepare(
      QString("UPDATE %1"
              " SET sampler = :sampler,"
              "     effective_compilation = ((compilation OR :sampler OR "
              "forced_compilation_on) AND NOT forced_compilation_off) + 0"
              " WHERE filename = :filename AND unavailable = 0")
          .arg(songs_table_));

  // Get song, so we can tell the model its updated
  find_song.bindValue(":filename", url.toEncoded());
  find_song.exec();
  while (find_song.next()) {
    Song song;
    song.InitFromQuery(find_song, true);
    deleted_songs << song;
    song.set_sampler(sampler);
    added_songs << song;
  }

  // Update the song
  update_song.bindValue(":sampler", int(sampler));
  update_song.bindValue(":filename", url.toEncoded());
  update_song.exec();
  db_->CheckErrors(update_song);
}

LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
                                                    const QString& album_artist,
                                                    bool compilation,
                                                    const QueryOptions& opt) {
  AlbumList ret;

  LibraryQuery query(opt);
  query.SetColumnSpec(
      "album, artist, albumartist, compilation, sampler, art_automatic, "
      "art_manual, filename");
  query.SetOrderBy("album");

  if (compilation) {
    query.AddCompilationRequirement(true);
  } else if (!album_artist.isNull()) {
    query.AddCompilationRequirement(false);
    query.AddWhere("albumartist", album_artist);
  } else if (!artist.isNull()) {
    query.AddCompilationRequirement(false);
    query.AddWhere("artist", artist);
  }

  {
    QMutexLocker l(db_->Mutex());
    if (!ExecQuery(&query)) return ret;
  }

  QString last_album;
  QString last_artist;
  QString last_album_artist;
  while (query.Next()) {
    bool compilation = query.Value(3).toBool() | query.Value(4).toBool();

    Album info;
    info.artist = compilation ? QString() : query.Value(1).toString();
    info.album_artist = compilation ? QString() : query.Value(2).toString();
    info.album_name = query.Value(0).toString();
    info.art_automatic = query.Value(5).toString();
    info.art_manual = query.Value(6).toString();
    info.first_url = QUrl::fromEncoded(query.Value(7).toByteArray());

    if ((info.artist == last_artist ||
         info.album_artist == last_album_artist) &&
        info.album_name == last_album)
      continue;

    ret << info;

    last_album = info.album_name;
    last_artist = info.artist;
    last_album_artist = info.album_artist;
  }

  return ret;
}

LibraryBackend::Album LibraryBackend::GetAlbumArt(const QString& artist,
                                                  const QString& albumartist,
                                                  const QString& album) {
  Album ret;
  ret.album_name = album;
  ret.artist = artist;
  ret.album_artist = albumartist;

  LibraryQuery query = LibraryQuery(QueryOptions());
  query.SetColumnSpec("art_automatic, art_manual, filename");
  if (!albumartist.isEmpty()) {
    query.AddWhere("albumartist", albumartist);
  } else if (!artist.isEmpty()) {
    query.AddWhere("artist", artist);
  }
  query.AddWhere("album", album);

  QMutexLocker l(db_->Mutex());
  if (!ExecQuery(&query)) return ret;

  if (query.Next()) {
    ret.art_automatic = query.Value(0).toString();
    ret.art_manual = query.Value(1).toString();
    ret.first_url = QUrl::fromEncoded(query.Value(2).toByteArray());
  }

  return ret;
}

void LibraryBackend::UpdateManualAlbumArtAsync(const QString& artist,
                                               const QString& albumartist,
                                               const QString& album,
                                               const QString& art) {
  metaObject()->invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection,
                             Q_ARG(QString, artist),
                             Q_ARG(QString, albumartist), Q_ARG(QString, album),
                             Q_ARG(QString, art));
}

void LibraryBackend::UpdateManualAlbumArt(const QString& artist,
                                          const QString& albumartist,
                                          const QString& album,
                                          const QString& art) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  // Get the songs before they're updated
  LibraryQuery query;
  query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
  query.AddWhere("album", album);

  if (!albumartist.isNull() && !albumartist.isEmpty()) {
    query.AddWhere("albumartist", albumartist);
  } else if (!artist.isNull()) {
    query.AddWhere("artist", artist);
  }

  if (!ExecQuery(&query)) return;

  SongList deleted_songs;
  while (query.Next()) {
    Song song;
    song.InitFromQuery(query, true);
    deleted_songs << song;
  }

  // Update the songs
  QString sql(QString("UPDATE %1 SET art_manual = :art"
                      " WHERE album = :album AND unavailable = 0")
                  .arg(songs_table_));
  if (!albumartist.isNull() && !albumartist.isEmpty()) {
    sql += " AND albumartist = :albumartist";
  } else if (!artist.isNull()) {
    sql += " AND artist = :artist";
  }

  QSqlQuery q(db);
  q.prepare(sql);
  q.bindValue(":art", art);
  q.bindValue(":album", album);
  if (!albumartist.isNull() && !albumartist.isEmpty()) {
    q.bindValue(":albumartist", albumartist);
  } else if (!artist.isNull()) {
    q.bindValue(":artist", artist);
  }

  q.exec();
  db_->CheckErrors(q);

  // Now get the updated songs
  if (!ExecQuery(&query)) return;

  SongList added_songs;
  while (query.Next()) {
    Song song;
    song.InitFromQuery(query, true);
    added_songs << song;
  }

  if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
    emit SongsDeleted(deleted_songs);
    emit SongsDiscovered(added_songs);
  }
}

void LibraryBackend::ForceCompilation(const QString& album,
                                      const QList<QString>& artists, bool on) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());
  SongList deleted_songs, added_songs;

  for (const QString& artist : artists) {
    // Get the songs before they're updated
    LibraryQuery query;
    query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
    query.AddWhere("album", album);
    if (!artist.isNull()) query.AddWhere("artist", artist);

    if (!ExecQuery(&query)) return;

    while (query.Next()) {
      Song song;
      song.InitFromQuery(query, true);
      deleted_songs << song;
    }

    // Update the songs
    QString sql(
        QString(
            "UPDATE %1 SET forced_compilation_on = :forced_compilation_on,"
            "              forced_compilation_off = :forced_compilation_off,"
            "              effective_compilation = ((compilation OR sampler OR "
            ":forced_compilation_on) AND NOT :forced_compilation_off) + 0"
            " WHERE album = :album AND unavailable = 0")
            .arg(songs_table_));
    if (!artist.isEmpty()) sql += " AND artist = :artist";

    QSqlQuery q(db);
    q.prepare(sql);
    q.bindValue(":forced_compilation_on", on ? 1 : 0);
    q.bindValue(":forced_compilation_off", on ? 0 : 1);
    q.bindValue(":album", album);
    if (!artist.isEmpty()) q.bindValue(":artist", artist);

    q.exec();
    db_->CheckErrors(q);

    // Now get the updated songs
    if (!ExecQuery(&query)) return;

    while (query.Next()) {
      Song song;
      song.InitFromQuery(query, true);
      added_songs << song;
    }
  }

  if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
    emit SongsDeleted(deleted_songs);
    emit SongsDiscovered(added_songs);
  }
}

bool LibraryBackend::ExecQuery(LibraryQuery* q) {
  return !db_->CheckErrors(q->Exec(db_->Connect(), songs_table_, fts_table_));
}

SongList LibraryBackend::FindSongs(const smart_playlists::Search& search) {
  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  // Build the query
  QString sql = search.ToSql(songs_table());

  // Run the query
  SongList ret;
  QSqlQuery query(db);
  query.prepare(sql);
  query.exec();
  if (db_->CheckErrors(query)) return ret;

  // Read the results
  while (query.next()) {
    Song song;
    song.InitFromQuery(query, true);
    ret << song;
  }
  return ret;
}

SongList LibraryBackend::GetAllSongs() {
  // Get all the songs!
  return FindSongs(smart_playlists::Search(
      smart_playlists::Search::Type_All, smart_playlists::Search::TermList(),
      smart_playlists::Search::Sort_FieldAsc,
      smart_playlists::SearchTerm::Field_Artist, -1));
}

void LibraryBackend::IncrementPlayCount(int id) {
  if (id == -1) return;

  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("UPDATE %1 SET playcount = playcount + 1,"
                    "              lastplayed = :now,"
                    "              score = " +
                    QString(kNewScoreSql).arg("1.0") + " WHERE ROWID = :id")
                .arg(songs_table_));
  q.bindValue(":now", QDateTime::currentDateTime().toTime_t());
  q.bindValue(":id", id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  Song new_song = GetSongById(id, db);
  emit SongsStatisticsChanged(SongList() << new_song);
}

void LibraryBackend::IncrementSkipCount(int id, float progress) {
  if (id == -1) return;
  progress = qBound(0.0f, progress, 1.0f);

  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("UPDATE %1 SET skipcount = skipcount + 1,"
                    "              score = " +
                    QString(kNewScoreSql).arg(progress) + " WHERE ROWID = :id")
                .arg(songs_table_));
  q.bindValue(":id", id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  Song new_song = GetSongById(id, db);
  emit SongsStatisticsChanged(SongList() << new_song);
}

void LibraryBackend::ResetStatistics(int id) {
  if (id == -1) return;

  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QSqlQuery q(db);
  q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0,"
                    "              lastplayed = -1, score = 0"
                    " WHERE ROWID = :id")
                .arg(songs_table_));
  q.bindValue(":id", id);
  q.exec();
  if (db_->CheckErrors(q)) return;

  Song new_song = GetSongById(id, db);
  emit SongsStatisticsChanged(SongList() << new_song);
}

void LibraryBackend::UpdateSongRating(int id, float rating) {
  if (id == -1) return;

  QList<int> id_list;
  id_list << id;
  UpdateSongsRating(id_list, rating);
}

void LibraryBackend::UpdateSongsRating(const QList<int>& id_list,
                                       float rating) {
  if (id_list.isEmpty()) return;

  QMutexLocker l(db_->Mutex());
  QSqlDatabase db(db_->Connect());

  QStringList id_str_list;
  for (int i : id_list) {
    id_str_list << QString::number(i);
  }
  QString ids = id_str_list.join(",");
  QSqlQuery q(db);
  q.prepare(QString("UPDATE %1 SET rating = :rating"
                    " WHERE ROWID IN (%2)")
                .arg(songs_table_, ids));
  q.bindValue(":rating", rating);
  q.exec();
  if (db_->CheckErrors(q)) return;
  SongList new_song_list = GetSongsById(id_str_list, db);
  emit SongsRatingChanged(new_song_list);
}

void LibraryBackend::DeleteAll() {
  {
    QMutexLocker l(db_->Mutex());
    QSqlDatabase db(db_->Connect());
    ScopedTransaction t(&db);

    QSqlQuery q("DELETE FROM " + songs_table_, db);
    q.exec();
    if (db_->CheckErrors(q)) return;

    q = QSqlQuery("DELETE FROM " + fts_table_, db);
    q.exec();
    if (db_->CheckErrors(q)) return;

    t.Commit();
  }

  emit DatabaseReset();
}
