/*
 * Cantata
 *
 * Copyright (c) 2017-2022 Craig Drummond <craig.p.drummond@gmail.com>
 *
 * ----
 *
 * 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 2 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "sqllibrarymodel.h"
#include "playqueuemodel.h"
#include "gui/settings.h"
#include "widgets/icons.h"
#include "support/configuration.h"
#include "support/utils.h"
#include "roles.h"
#include <QMimeData>
#include <time.h>

static QString parentData(const SqlLibraryModel::Item *i)
{
    QString data;
    const SqlLibraryModel::Item *itm=i;

    while (itm->getParent()) {
        if (!itm->getParent()->getText().isEmpty()) {
            if (SqlLibraryModel::T_Root==itm->getParent()->getType()) {
                data="<b>"+itm->getParent()->getText()+"</b><br/>"+data;
            } else {
                data=itm->getParent()->getText()+"<br/>"+data;
            }
        }
        itm=itm->getParent();
    }
    return data;
}

SqlLibraryModel::Type SqlLibraryModel::toGrouping(const QString &str)
{
    for (int i=T_Genre; i<T_Track; ++i) {
        if (groupingStr((Type)i)==str) {
            return (Type)i;
        }
    }
    return T_Artist;
}

QString SqlLibraryModel::groupingStr(Type m)
{
    switch(m) {
    default:
    case T_Artist: return "artist";
    case T_Album:  return "album";
    case T_Genre:  return "genre";
    }
}

SqlLibraryModel::SqlLibraryModel(LibraryDb *d, QObject *p, Type top)
    : ActionModel(p)
    , tl(top)
    , root(nullptr)
    , db(d)
    , librarySort(LibraryDb::AS_YrAlAr)
    , albumSort(LibraryDb::AS_AlArYr)
{
    connect(db, SIGNAL(libraryUpdated()), SLOT(libraryUpdated()));
    connect(db, SIGNAL(error(QString)), this, SIGNAL(error(QString)));
}

void SqlLibraryModel::clear()
{
    beginResetModel();
    delete root;
    root=nullptr;
    endResetModel();
}

void SqlLibraryModel::clearDb()
{
    db->clear();
}

void SqlLibraryModel::settings(Type top, LibraryDb::AlbumSort lib, LibraryDb::AlbumSort al)
{
    bool changed=top!=tl || (T_Album!=top && lib!=librarySort) || (T_Album==top && al!=albumSort);
    tl=top;
    librarySort=lib;
    albumSort=al;
    if (changed) {
        libraryUpdated();
    }
}

void SqlLibraryModel::setTopLevel(Type t)
{
    if (t!=tl) {
        tl=t;
        libraryUpdated();
    }
}

void SqlLibraryModel::setLibraryAlbumSort(LibraryDb::AlbumSort s)
{
    if (s!=librarySort) {
        librarySort=s;
        if (T_Album!=tl) {
            libraryUpdated();
        }
    }
}

void SqlLibraryModel::setAlbumAlbumSort(LibraryDb::AlbumSort s)
{
    if (s!=albumSort) {
        albumSort=s;
        if (T_Album==tl) {
            libraryUpdated();
        }
    }
}

static QLatin1String constGroupingKey("grouping");
static QLatin1String constAlbumSortKey("albumSort");
static QLatin1String constLibrarySortKey("librarySort");

void SqlLibraryModel::load(Configuration &config)
{
    tl=toGrouping(config.get(constGroupingKey, groupingStr(tl)));
    albumSort=LibraryDb::toAlbumSort(config.get(constAlbumSortKey, LibraryDb::albumSortStr(albumSort)));
    librarySort=LibraryDb::toAlbumSort(config.get(constLibrarySortKey, LibraryDb::albumSortStr(librarySort)));
}

void SqlLibraryModel::save(Configuration &config)
{
    config.set(constGroupingKey, groupingStr(tl));
    config.set(constAlbumSortKey, LibraryDb::albumSortStr(albumSort));
    config.set(constLibrarySortKey, LibraryDb::albumSortStr(librarySort));
}

void SqlLibraryModel::libraryUpdated()
{
    beginResetModel();
    delete root;
    root=new CollectionItem(T_Root, QString());
    switch (tl) {
    case T_Genre: {
        QList<LibraryDb::Genre> genres=db->getGenres();
        if (!genres.isEmpty())  {
            for (const LibraryDb::Genre &genre: genres) {
                root->add(new CollectionItem(T_Genre, genre.name, genre.name, tr("%n Artist(s)", "", genre.artistCount), root));
            }
        }
        break;
    }
    case T_Artist: {
        QList<LibraryDb::Artist> artists=db->getArtists();
        if (!artists.isEmpty())  {
            for (const LibraryDb::Artist &artist: artists) {
                root->add(new CollectionItem(T_Artist, artist.name, artist.name, tr("%n Album(s)", "", artist.albumCount), root));
            }
        }
        break;
    }
    case T_Album: {
        QList<LibraryDb::Album> albums=db->getAlbums(QString(), QString(), albumSort);
        categories.clear();
        if (!albums.isEmpty())  {
            time_t now = time(nullptr);
            const time_t aDay = 24 * 60 * 60;
            time_t week = now - (7 * aDay);
            time_t week2 = now - (14 * aDay);
            time_t days30 = now - (30 * aDay);
            time_t days60 = now - (60 * aDay);
            time_t days90 = now - (90 * aDay);
            time_t year = now - (365 * aDay);
            time_t year2 = now - (365 * 2 * aDay);
            time_t year3 = now - (35 * 3 * aDay);

            QMap<QString, int> knownCats;
            for (const LibraryDb::Album &album: albums) {
                QString name;
                switch(albumSort) {
                case LibraryDb::AS_AlArYr:
                case LibraryDb::AS_AlYrAr:
                    name=album.sort.isEmpty() ? album.name.at(0).toUpper() : album.sort.at(0).toUpper();
                    break;
                default:
                case LibraryDb::AS_ArAlYr:
                case LibraryDb::AS_ArYrAl:
                    name=album.artist;
                    break;
                case LibraryDb::AS_YrAlAr:
                case LibraryDb::AS_YrArAl:
                    name=QString::number(album.year);
                    break;
                case LibraryDb::AS_Modified:
                    if (album.lastModified>=week) {
                        name = tr("Modified in the last week");
                    } else if (album.lastModified>=week2) {
                        name = tr("Modified in the last 2 weeks");
                    } else if (album.lastModified>=days30) {
                        name = tr("Modified in the last 30 days");
                    } else if (album.lastModified>=days60) {
                        name = tr("Modified in the last 60 days");
                    } else if (album.lastModified>=days90) {
                        name = tr("Modified in the last 90 days");
                    } else if (album.lastModified>=year) {
                        name = tr("Modified in the last year");
                    } else if (album.lastModified>=year2) {
                        name = tr("Modified in the last 2 years");
                    } else if (album.lastModified>=year3) {
                        name = tr("Modified in the last 3 years");
                    } else {
                        name = tr("Modified more than 3 years ago");
                    }
                    break;
                }

                int cat = -1;
                if (!name.isEmpty()) {
                    const auto existing = knownCats.find(name);
                    if (knownCats.constEnd()==existing) {
                        cat=categories.size();
                        categories.append(name);
                        knownCats.insert(name, cat);
                    } else {
                        cat=existing.value();
                    }
                }

                QString trackInfo = tr("%n Tracks (%1)", "", album.trackCount).arg(Utils::formatTime(album.duration, true));
                root->add(new AlbumItem(T_Album==tl && album.identifyById ? QString() : album.artist,
                                        album.id, Song::displayAlbum(album.name, album.year),
                                        T_Album==tl ? album.artist : trackInfo, T_Album==tl ? trackInfo : QString(), root, cat));
            }
        }
        break;
    }
    default:
        break;
    }

    endResetModel();
}

void SqlLibraryModel::search(const QString &str, const QString &genre)
{
    if (db->setFilter(str, genre)) {
        libraryUpdated();
    }
}

Qt::ItemFlags SqlLibraryModel::flags(const QModelIndex &index) const
{
    if (index.isValid()) {
        return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled;
    }
    return Qt::ItemIsDropEnabled;
}

QModelIndex SqlLibraryModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent)) {
        return QModelIndex();
    }
    const CollectionItem * p = parent.isValid() ? static_cast<CollectionItem *>(parent.internalPointer()) : root;
    const Item * c = row<p->getChildCount() ? p->getChildren().at(row) : nullptr;
    return c ? createIndex(row, column, (void *)c) : QModelIndex();
}

QModelIndex SqlLibraryModel::parent(const QModelIndex &child) const
{
    if (!child.isValid()) {
        return QModelIndex();
    }

    const Item * const item = static_cast<Item *>(child.internalPointer());
    Item * const parentItem = item->getParent();
    if (parentItem == root || nullptr==parentItem) {
        return QModelIndex();
    }

    return createIndex(parentItem->getRow(), 0, parentItem);
}

int SqlLibraryModel::rowCount(const QModelIndex &parent) const
{
    if (parent.column() > 0) {
        return 0;
    }

    const CollectionItem *parentItem=parent.isValid() ? static_cast<CollectionItem *>(parent.internalPointer()) : root;
    return parentItem ? parentItem->getChildCount() : 0;
}

int SqlLibraryModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 1;
}

bool SqlLibraryModel::hasChildren(const QModelIndex &index) const
{
    Item *item=toItem(index);
    return item && T_Track!=item->getType();
}

bool SqlLibraryModel::canFetchMore(const QModelIndex &index) const
{
    if (index.isValid()) {
        Item *item = toItem(index);
        return item && T_Track!=item->getType() && 0==item->getChildCount();
    } else {
        return false;
    }
}

void SqlLibraryModel::fetchMore(const QModelIndex &index)
{
    if (!index.isValid()) {
        return;
    }

    CollectionItem *item = static_cast<CollectionItem *>(toItem(index));
    switch (item->getType()) {
    case T_Root:
        break;
    case T_Genre: {
        QList<LibraryDb::Artist> artists=db->getArtists(item->getId());
        if (!artists.isEmpty())  {
            beginInsertRows(index, 0, artists.count()-1);
            for (const LibraryDb::Artist &artist: artists) {
                item->add(new CollectionItem(T_Artist, artist.name, artist.name, tr("%n Album(s)", "", artist.albumCount), item));
            }
            endInsertRows();
        }
        break;
    }
    case T_Artist: {
        QList<LibraryDb::Album> albums=db->getAlbums(item->getId(), T_Genre==tl ? item->getParent()->getId() : QString(), librarySort);
        if (!albums.isEmpty())  {
            beginInsertRows(index, 0, albums.count()-1);
            for (const LibraryDb::Album &album: albums) {
                item->add(new CollectionItem(T_Album, album.id, Song::displayAlbum(album.name, album.year),
                                             tr("%n Tracks (%1)", "", album.trackCount).arg(Utils::formatTime(album.duration, true)), item));
            }
            endInsertRows();
        }
        break;
    }
    case T_Album: {
        QList<Song> songs=T_Album==tl
                            ? db->getTracks(static_cast<AlbumItem *>(item)->getArtistId(), item->getId(), QString(), albumSort)
                            : db->getTracks(item->getParent()->getId(), item->getId(),
                                            T_Genre==tl ? item->getParent()->getParent()->getId() : QString(), librarySort);

        if (!songs.isEmpty())  {
            beginInsertRows(index, 0, songs.count()-1);
            for (const Song &song: songs) {
                item->add(new TrackItem(song, item));
            }
            endInsertRows();
        }
        break;
    }
    default:
        break;
    }
}

QVariant SqlLibraryModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        if (Cantata::Role_CatergizedHasSubText==role) {
            return T_Album!=tl || LibraryDb::AS_ArAlYr==albumSort || LibraryDb::AS_ArYrAl==albumSort;
        }
        return QVariant();
    }

    Item *item = static_cast<Item *>(index.internalPointer());

    switch (role) {
    case Qt::DecorationRole:
        switch (item->getType()) {
        case T_Genre:
            return Icons::self()->genreIcon;
        case T_Artist:
            return Icons::self()->artistIcon;
        case T_Album:
            return Icons::self()->albumIcon(32);
        case T_Track:
            return Song::Playlist==item->getSong().type ? Icons::self()->playlistListIcon : Icons::self()->audioListIcon;
        default:
            return QVariant();
        }
    case Cantata::Role_LoadCoverInUIThread:
        return T_Album==item->getType() && T_Album!=tl;
    case Cantata::Role_BriefMainText:
        if (T_Album==item->getType()) {
            return item->getText();
        }
    case Cantata::Role_MainText:
    case Qt::DisplayRole:
        if (T_Track==item->getType()) {
            TrackItem *track = static_cast<TrackItem *>(item);
            if (Song::Playlist==track->getSong().type) {
                return track->getSong().isCueFile() ? tr("Cue Sheet") : tr("Playlist");
            }
        }
        return item->getText();
    case Qt::ToolTipRole:
        if (!Settings::self()->infoTooltips()) {
            return QVariant();
        }
        if (T_Track==item->getType()) {
            return static_cast<TrackItem *>(item)->getSong().toolTip();
        }
        return parentData(item)+
                (0==item->getChildCount()
                 ? item->getText()
                 : (item->getText()+"<br/>"+data(index, Cantata::Role_SubText).toString()));
    case Cantata::Role_TitleSubText:
        if (T_Album==tl && T_Album==item->getType()) {
            return static_cast<AlbumItem *>(item)->getTitleSub();
        }
    case Cantata::Role_SubText:
        return item->getSubText();
    case Cantata::Role_ListImage:
        return T_Album==item->getType();
    case Cantata::Role_TitleText:
        if (T_Album==item->getType()) {
            return tr("%1 by %2").arg(item->getText()).arg(T_Album==tl ? item->getSubText() : item->getParent()->getText());
        }
        return item->getText();
    case Cantata::Role_TitleActions:
        switch (item->getType()) {
        case T_Artist:
        case T_Album:
            return true;
        default:
            return false;
        }
    default:
        break;
    }
    return ActionModel::data(index, role);
}

QMimeData * SqlLibraryModel::mimeData(const QModelIndexList &indexes) const
{
    QMimeData *mimeData = new QMimeData();
    QStringList files=filenames(indexes, true);
    PlayQueueModel::encode(*mimeData, PlayQueueModel::constFileNameMimeType, files);
    return mimeData;
}

static bool containsParent(const QSet<QModelIndex> &set, const QModelIndex &idx)
{
    QModelIndex p=idx.parent();
    while (p.isValid()) {
        if (set.contains(p)) {
            return true;
        }
        p=p.parent();
    }
    return false;
}

QList<Song> SqlLibraryModel::songs(const QModelIndexList &list, bool allowPlaylists) const
{
    QList<Song> songList;
    QSet<QModelIndex> set=Utils::listToSet(list);
    populate(list);
    for (const QModelIndex &idx: list) {
        if (!containsParent(set, idx)) {
            songList+=songs(idx, allowPlaylists);
        }
    }

    return songList;
}

QStringList SqlLibraryModel::filenames(const QModelIndexList &list, bool allowPlaylists) const
{
    QStringList files;
    QList<Song> songList=songs(list, allowPlaylists);
    for (const Song &s: songList) {
        files.append(s.file);
    }
    return files;
}

QModelIndex SqlLibraryModel::findSongIndex(const Song &song)
{
    if (root) {
        QModelIndex albumIndex=findAlbumIndex(song.albumArtistOrComposer(), song.albumId());
        if (albumIndex.isValid()) {
            if (canFetchMore(albumIndex)) {
                fetchMore(albumIndex);
            }
            CollectionItem *al=static_cast<CollectionItem *>(albumIndex.internalPointer());
            for (Item *t: al->getChildren()) {
                if (static_cast<TrackItem *>(t)->getSong().title==song.title) {
                    return index(t->getRow(), 0, albumIndex);
                }
            }
        }
    }

    // Hmm... Find song details in db via file path - fixes SingleTracks songs
    if (!song.file.isEmpty()) {
        QList<Song> dbSongs=db->songs(QStringList() << song.file);
        if (!dbSongs.isEmpty() && dbSongs.first().albumId()!=song.albumId()) {
            Song dbSong=dbSongs.first();
            dbSong.file=QString(); // Prevent recursion!
            return findSongIndex(dbSong);
        }
    }
    return QModelIndex();
}

QModelIndex SqlLibraryModel::findAlbumIndex(const QString &artist, const QString &album)
{
    if (root) {
        if (T_Album==tl) {
            for (Item *a: root->getChildren()) {
                if (a->getId()==album && static_cast<AlbumItem *>(a)->getArtistId()==artist) {
                    return index(a->getRow(), 0, QModelIndex());
                }
            }
        } else {
            QModelIndex artistIndex=findArtistIndex(artist);
            if (artistIndex.isValid()) {
                if (canFetchMore(artistIndex)) {
                    fetchMore(artistIndex);
                }
                CollectionItem *ar=static_cast<CollectionItem *>(artistIndex.internalPointer());
                for (Item *al: ar->getChildren()) {
                    if (al->getId()==album) {
                        return index(al->getRow(), 0, artistIndex);
                    }
                }
            }
        }
    }
    return QModelIndex();
}

QModelIndex SqlLibraryModel::findArtistIndex(const QString &artist)
{
    if (root) {
        if (T_Genre==tl) {
            for (Item *g: root->getChildren()) {
                QModelIndex gIndex=index(g->getRow(), 0, QModelIndex());
                if (canFetchMore(gIndex)) {
                    fetchMore(gIndex);
                }
                for (Item *a: static_cast<CollectionItem *>(g)->getChildren()) {
                    if (a->getId()==artist) {
                        return index(a->getRow(), 0, gIndex);
                    }
                }
            }
        } else if (T_Artist==tl) {
            for (Item *a: root->getChildren()) {
                if (a->getId()==artist) {
                    return index(a->getRow(), 0, QModelIndex());
                }
            }
        }
    }
    return QModelIndex();
}

QSet<QString> SqlLibraryModel::getGenres() const
{
    return db->get("genre");
}

QSet<QString> SqlLibraryModel::getArtists() const
{
    return db->get("albumArtist");
}

QList<Song> SqlLibraryModel::getAlbumTracks(const QString &artistId, const QString &albumId, int maxTracks) const
{
    return db->getTracks(artistId, albumId, QString(), LibraryDb::AS_ArAlYr, false, maxTracks);
}

QList<Song> SqlLibraryModel::songs(const QStringList &files, bool allowPlaylists) const
{
    return db->songs(files, allowPlaylists);
}

QList<LibraryDb::Album> SqlLibraryModel::getArtistOrComposerAlbums(const QString &artist) const
{
    return db->getAlbumsWithArtistOrComposer(artist);
}

void SqlLibraryModel::getDetails(QSet<QString> &artists, QSet<QString> &albumArtists, QSet<QString> &composers, QSet<QString> &albums, QSet<QString> &genres)
{
    db->getDetails(artists, albumArtists, composers, albums, genres);
}

bool SqlLibraryModel::songExists(const Song &song)
{
    return db->songExists(song);
}

int SqlLibraryModel::trackCount() const
{
    return db->trackCount();
}

void SqlLibraryModel::populate(const QModelIndexList &list) const
{
    for (const QModelIndex &idx: list) {
        if (canFetchMore(idx)) {
            const_cast<SqlLibraryModel *>(this)->fetchMore(idx);
        }
        if (T_Track!=static_cast<Item *>(idx.internalPointer())->getType()) {
            populate(children(idx));
        }
    }
}

QModelIndexList SqlLibraryModel::children(const QModelIndex &parent) const
{
    QModelIndexList list;

    for(int r=0; r<rowCount(parent); ++r) {
        list.append(index(r, 0, parent));
    }
    return list;
}

QList<Song> SqlLibraryModel::songs(const QModelIndex &idx, bool allowPlaylists) const
{
    QList<Song> list;
    if (hasChildren(idx)) {
        for (const QModelIndex &c: children(idx)) {
            list+=songs(c, allowPlaylists);
        }
    } else {
        const Item *i=static_cast<const Item *>(idx.internalPointer());
        if (i && T_Track==i->getType() && (allowPlaylists || (Song::Playlist!=i->getSong().type && !i->getSong().isFromCue()))) {
            Song s(i->getSong());
            list.append(fixPath(s));
        }
    }
    return list;
}

void SqlLibraryModel::CollectionItem::add(Item *i)
{
    children.append(i);
    i->setRow(children.count()-1);
    childMap.insert(i->getUniqueId(), i);
}

const SqlLibraryModel::Item *SqlLibraryModel::CollectionItem::getChild(const QString &id) const
{
    QMap<QString, Item *>::ConstIterator it=childMap.find(id);
    return childMap.constEnd()==it ? nullptr : it.value();
}

#include "moc_sqllibrarymodel.cpp"
