QString createPlaylistLabel(
        const QString& name,
        int count,
        int duration)

#include "library/trackset/playlistfeature.h"

#include <QFile>
#include <QMenu>
#include <QtDebug>

#include "controllers/keyboard/keyboardeventfilter.h"
#include "library/library.h"
#include "library/parser.h"
#include "library/playlisttablemodel.h"
#include "library/queryutil.h"
#include "library/trackcollection.h"
#include "library/trackcollectionmanager.h"
#include "library/treeitem.h"
#include "moc_playlistfeature.cpp"
#include "sources/soundsourceproxy.h"
#include "util/db/dbconnection.h"
#include "util/dnd.h"
#include "util/duration.h"
#include "widget/wlibrary.h"
#include "widget/wlibrarysidebar.h"
#include "widget/wlibrarytextbrowser.h"

namespace {

{
    return QStringLiteral("%1 (%2) %3")
            .arg(name,
                    QString::number(count),
                    mixxx::Duration::formatTime(
                            duration, mixxx::Duration::Precision::SECONDS));
}

} // anonymous namespace

PlaylistFeature::PlaylistFeature(Library* pLibrary, UserSettingsPointer pConfig)
        : BasePlaylistFeature(pLibrary,
                  pConfig,
                  new PlaylistTableModel(nullptr,
                          pLibrary->trackCollectionManager(),
                          "mixxx.db.model.playlist"),
                  QStringLiteral("PLAYLISTHOME"),
                  QStringLiteral("playlist")) {
    // construct child model
    std::unique_ptr<TreeItem> pRootItem = TreeItem::newRoot(this);
    m_pSidebarModel->setRootItem(std::move(pRootItem));
    constructChildModel(kInvalidPlaylistId);
}

QVariant PlaylistFeature::title() {
    return tr("Playlists");
}

void PlaylistFeature::onRightClick(const QPoint& globalPos) {
    m_lastRightClickedIndex = QModelIndex();
    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pCreateImportPlaylistAction);
    menu.exec(globalPos);
}

void PlaylistFeature::onRightClickChild(
        const QPoint& globalPos, const QModelIndex& index) {
    //Save the model index so we can get it in the action slots...
    m_lastRightClickedIndex = index;
    int playlistId = playlistIdFromIndex(index);

    bool locked = m_playlistDao.isPlaylistLocked(playlistId);
    m_pDeletePlaylistAction->setEnabled(!locked);
    m_pRenamePlaylistAction->setEnabled(!locked);

    m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock"));

    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pRenamePlaylistAction);
    menu.addAction(m_pDuplicatePlaylistAction);
    menu.addAction(m_pDeletePlaylistAction);
    menu.addAction(m_pLockPlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pAddToAutoDJAction);
    menu.addAction(m_pAddToAutoDJTopAction);
    menu.addAction(m_pAddToAutoDJReplaceAction);
    menu.addSeparator();
    menu.addAction(m_pAnalyzePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pImportPlaylistAction);
    menu.addAction(m_pExportPlaylistAction);
    menu.addAction(m_pExportTrackFilesAction);
    menu.exec(globalPos);
}

bool PlaylistFeature::dropAcceptChild(
        const QModelIndex& index, const QList<QUrl>& urls, QObject* pSource) {
    int playlistId = playlistIdFromIndex(index);
    VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) {
        return false;
    }
    // If a track is dropped onto a playlist's name, but the track isn't in the
    // library, then add the track to the library before adding it to the
    // pSource != nullptr it is a drop from inside Mixxx and indicates all
    // tracks already in the DB
    QList<TrackId> trackIds = m_pLibrary->trackCollectionManager()
                                      ->resolveTrackIdsFromUrls(urls, !pSource);
    if (!trackIds.size()) {
        return false;
    }

    // Return whether appendTracksToPlaylist succeeded.
    return m_playlistDao.appendTracksToPlaylist(trackIds, playlistId);
}

bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& url) {
    int playlistId = playlistIdFromIndex(index);
    bool locked = m_playlistDao.isPlaylistLocked(playlistId);

    bool formatSupported = SoundSourceProxy::isUrlSupported(url) ||
            Parser::isPlaylistFilenameSupported(url.toLocalFile());
    return !locked && formatSupported;
}

QList<BasePlaylistFeature::IdAndLabel> PlaylistFeature::createPlaylistLabels() {
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();

    QList<BasePlaylistFeature::IdAndLabel> playlistLabels;
    QString queryString = QStringLiteral(
            "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations "
            "AS SELECT "
            "  Playlists.id AS id, "
            "  Playlists.name AS name, "
            "  LOWER(Playlists.name) AS sort_name, "
            "  COUNT(case library.mixxx_deleted when 0 then 1 else null end) "
            "    AS count, "
            "  SUM(case library.mixxx_deleted "
            "    when 0 then library.duration else 0 end) AS durationSeconds "
            "FROM Playlists "
            "LEFT JOIN PlaylistTracks "
            "  ON PlaylistTracks.playlist_id = Playlists.id "
            "LEFT JOIN library "
            "  ON PlaylistTracks.track_id = library.id "
            "  WHERE Playlists.hidden = 0 "
            "  GROUP BY Playlists.id");
    queryString.append(
            mixxx::DbConnection::collateLexicographically(
                    " ORDER BY sort_name"));
    QSqlQuery query(database);
    if (!query.exec(queryString)) {
        LOG_FAILED_QUERY(query);
    }

    // Setup the sidebar playlist model
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int idColumn = record.indexOf("id");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    for (int row = 0; row < playlistTableModel.rowCount(); ++row) {
        int id =
                playlistTableModel
                        .data(playlistTableModel.index(row, idColumn))
                        .toInt();
        QString name =
                playlistTableModel
                        .data(playlistTableModel.index(row, nameColumn))
                        .toString();
        int count =
                playlistTableModel
                        .data(playlistTableModel.index(row, countColumn))
                        .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(row, durationColumn))
                        .toInt();
        BasePlaylistFeature::IdAndLabel idAndLabel;
        idAndLabel.id = id;
        idAndLabel.label = createPlaylistLabel(name, count, duration);
        playlistLabels.append(idAndLabel);
    }
    return playlistLabels;
}

QString PlaylistFeature::fetchPlaylistLabel(int playlistId) {
    // Setup the sidebar playlist model
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    QString filter = "id=" + QString::number(playlistId);
    playlistTableModel.setFilter(filter);
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    DEBUG_ASSERT(playlistTableModel.rowCount() <= 1);
    if (playlistTableModel.rowCount() > 0) {
        QString name =
                playlistTableModel.data(playlistTableModel.index(0, nameColumn))
                        .toString();
        int count = playlistTableModel
                            .data(playlistTableModel.index(0, countColumn))
                            .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(0, durationColumn))
                        .toInt();
        return createPlaylistLabel(name, count, duration);
    }
    return QString();
}

/// Purpose: When inserting or removing playlists,
/// we require the sidebar model not to reset.
/// This method queries the database and does dynamic insertion
/// @param selectedId entry which should be selected
QModelIndex PlaylistFeature::constructChildModel(int selectedId) {
    QList<TreeItem*> data_list;
    int selectedRow = -1;

    int row = 0;
    const QList<IdAndLabel> playlistLabels = createPlaylistLabels();
    for (const auto& idAndLabel : playlistLabels) {
        int playlistId = idAndLabel.id;
        QString playlistLabel = idAndLabel.label;

        if (selectedId == playlistId) {
            // save index for selection
            selectedRow = row;
        }

        // Create the TreeItem whose parent is the invisible root item
        TreeItem* item = new TreeItem(playlistLabel, playlistId);
        item->setBold(m_playlistIdsOfSelectedTrack.contains(playlistId));

        decorateChild(item, playlistId);
        data_list.append(item);

        ++row;
    }

    // Append all the newly created TreeItems in a dynamic way to the childmodel
    m_pSidebarModel->insertTreeItemRows(data_list, 0);
    if (selectedRow == -1) {
        return QModelIndex();
    }
    return m_pSidebarModel->index(selectedRow, 0);
}

void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) {
    if (m_playlistDao.isPlaylistLocked(playlistId)) {
        item->setIcon(
                QIcon(":/images/library/ic_library_locked_tracklist.svg"));
    } else {
        item->setIcon(QIcon());
    }
}

void PlaylistFeature::slotPlaylistTableChanged(int playlistId) {
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
    }
}

void PlaylistFeature::slotPlaylistContentChanged(QSet<int> playlistIds) {
    for (const auto playlistId : qAsConst(playlistIds)) {
        enum PlaylistDAO::HiddenType type =
                m_playlistDao.getHiddenType(playlistId);
        if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
                type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
            updateChildModel(playlistId);
        }
    }
}

void PlaylistFeature::slotPlaylistTableRenamed(
        int playlistId, const QString& newName) {
    Q_UNUSED(newName);
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
        if (type != PlaylistDAO::PLHT_UNKNOWN) {
            activatePlaylist(playlistId);
        }
    }
}

QString PlaylistFeature::getRootViewHtml() const {
    QString playlistsTitle = tr("Playlists");
    QString playlistsSummary =
            tr("Playlists are ordered lists of tracks that allow you to plan "
               "your DJ sets.");
    QString playlistsSummary2 =
            tr("Some DJs construct playlists before they perform live, but "
               "others prefer to build them on-the-fly.");
    QString playlistsSummary3 =
            tr("When using a playlist during a live DJ set, remember to always "
               "pay close attention to how your audience reacts to the music "
               "you've chosen to play.");
    QString playlistsSummary4 =
            tr("It may be necessary to skip some tracks in your prepared "
               "playlist or add some different tracks in order to maintain the "
               "energy of your audience.");
    QString createPlaylistLink = tr("Create New Playlist");

    QString html;
    html.append(QStringLiteral("
%1
").arg(playlistsTitle));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary2));
    html.append(QStringLiteral("
%1
%2
").arg(playlistsSummary3, playlistsSummary4));
    html.append(QStringLiteral("%1")
                        .arg(createPlaylistLink));
    return html;
}


#include "library/trackset/playlistfeature.h"

#include <QFile>
#include <QMenu>
#include <QtDebug>

#include "controllers/keyboard/keyboardeventfilter.h"
#include "library/library.h"
#include "library/parser.h"
#include "library/playlisttablemodel.h"
#include "library/queryutil.h"
#include "library/trackcollection.h"
#include "library/trackcollectionmanager.h"
#include "library/treeitem.h"
#include "moc_playlistfeature.cpp"
#include "sources/soundsourceproxy.h"
#include "util/db/dbconnection.h"
#include "util/dnd.h"
#include "util/duration.h"
#include "widget/wlibrary.h"
#include "widget/wlibrarysidebar.h"
#include "widget/wlibrarytextbrowser.h"

namespace {

QString createPlaylistLabel(
        const QString& name,
        int count,
        int duration) {
    return QStringLiteral("%1 (%2) %3")
            .arg(name,
                    QString::number(count),
                    mixxx::Duration::formatTime(
                            duration, mixxx::Duration::Precision::SECONDS));
}

} // anonymous namespace

PlaylistFeature::PlaylistFeature(Library* pLibrary, UserSettingsPointer pConfig)
        : BasePlaylistFeature(pLibrary,
                  pConfig,
                  new PlaylistTableModel(nullptr,
                          pLibrary->trackCollectionManager(),
                          "mixxx.db.model.playlist"),
                  QStringLiteral("PLAYLISTHOME"),
                  QStringLiteral("playlist")) {
    // construct child model
    std::unique_ptr<TreeItem> pRootItem = TreeItem::newRoot(this);
    m_pSidebarModel->setRootItem(std::move(pRootItem));
    constructChildModel(kInvalidPlaylistId);
}

QVariant PlaylistFeature::title()

void PlaylistFeature::onRightClick(const QPoint& globalPos) {
    m_lastRightClickedIndex = QModelIndex();
    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pCreateImportPlaylistAction);
    menu.exec(globalPos);
}

void PlaylistFeature::onRightClickChild(
        const QPoint& globalPos, const QModelIndex& index) {
    //Save the model index so we can get it in the action slots...
    m_lastRightClickedIndex = index;
    int playlistId = playlistIdFromIndex(index);

    bool locked = m_playlistDao.isPlaylistLocked(playlistId);
    m_pDeletePlaylistAction->setEnabled(!locked);
    m_pRenamePlaylistAction->setEnabled(!locked);

    m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock"));

    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pRenamePlaylistAction);
    menu.addAction(m_pDuplicatePlaylistAction);
    menu.addAction(m_pDeletePlaylistAction);
    menu.addAction(m_pLockPlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pAddToAutoDJAction);
    menu.addAction(m_pAddToAutoDJTopAction);
    menu.addAction(m_pAddToAutoDJReplaceAction);
    menu.addSeparator();
    menu.addAction(m_pAnalyzePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pImportPlaylistAction);
    menu.addAction(m_pExportPlaylistAction);
    menu.addAction(m_pExportTrackFilesAction);
    menu.exec(globalPos);
}

bool PlaylistFeature::dropAcceptChild(
        const QModelIndex& index, const QList<QUrl>& urls, QObject* pSource) {
    int playlistId = playlistIdFromIndex(index);
    VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) {
        return false;
    }
    // If a track is dropped onto a playlist's name, but the track isn't in the
    // library, then add the track to the library before adding it to the
    // pSource != nullptr it is a drop from inside Mixxx and indicates all
    // tracks already in the DB
    QList<TrackId> trackIds = m_pLibrary->trackCollectionManager()
                                      ->resolveTrackIdsFromUrls(urls, !pSource);
    if (!trackIds.size()) {
        return false;
    }

    // Return whether appendTracksToPlaylist succeeded.
    return m_playlistDao.appendTracksToPlaylist(trackIds, playlistId);
}

bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& url) {
    int playlistId = playlistIdFromIndex(index);
    bool locked = m_playlistDao.isPlaylistLocked(playlistId);

    bool formatSupported = SoundSourceProxy::isUrlSupported(url) ||
            Parser::isPlaylistFilenameSupported(url.toLocalFile());
    return !locked && formatSupported;
}

QList<BasePlaylistFeature::IdAndLabel> PlaylistFeature::createPlaylistLabels() {
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();

    QList<BasePlaylistFeature::IdAndLabel> playlistLabels;
    QString queryString = QStringLiteral(
            "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations "
            "AS SELECT "
            "  Playlists.id AS id, "
            "  Playlists.name AS name, "
            "  LOWER(Playlists.name) AS sort_name, "
            "  COUNT(case library.mixxx_deleted when 0 then 1 else null end) "
            "    AS count, "
            "  SUM(case library.mixxx_deleted "
            "    when 0 then library.duration else 0 end) AS durationSeconds "
            "FROM Playlists "
            "LEFT JOIN PlaylistTracks "
            "  ON PlaylistTracks.playlist_id = Playlists.id "
            "LEFT JOIN library "
            "  ON PlaylistTracks.track_id = library.id "
            "  WHERE Playlists.hidden = 0 "
            "  GROUP BY Playlists.id");
    queryString.append(
            mixxx::DbConnection::collateLexicographically(
                    " ORDER BY sort_name"));
    QSqlQuery query(database);
    if (!query.exec(queryString)) {
        LOG_FAILED_QUERY(query);
    }

    // Setup the sidebar playlist model
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int idColumn = record.indexOf("id");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    for (int row = 0; row < playlistTableModel.rowCount(); ++row) {
        int id =
                playlistTableModel
                        .data(playlistTableModel.index(row, idColumn))
                        .toInt();
        QString name =
                playlistTableModel
                        .data(playlistTableModel.index(row, nameColumn))
                        .toString();
        int count =
                playlistTableModel
                        .data(playlistTableModel.index(row, countColumn))
                        .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(row, durationColumn))
                        .toInt();
        BasePlaylistFeature::IdAndLabel idAndLabel;
        idAndLabel.id = id;
        idAndLabel.label = createPlaylistLabel(name, count, duration);
        playlistLabels.append(idAndLabel);
    }
    return playlistLabels;
}

QString PlaylistFeature::fetchPlaylistLabel(int playlistId) {
    // Setup the sidebar playlist model
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    QString filter = "id=" + QString::number(playlistId);
    playlistTableModel.setFilter(filter);
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    DEBUG_ASSERT(playlistTableModel.rowCount() <= 1);
    if (playlistTableModel.rowCount() > 0) {
        QString name =
                playlistTableModel.data(playlistTableModel.index(0, nameColumn))
                        .toString();
        int count = playlistTableModel
                            .data(playlistTableModel.index(0, countColumn))
                            .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(0, durationColumn))
                        .toInt();
        return createPlaylistLabel(name, count, duration);
    }
    return QString();
}

/// Purpose: When inserting or removing playlists,
/// we require the sidebar model not to reset.
/// This method queries the database and does dynamic insertion
/// @param selectedId entry which should be selected
QModelIndex PlaylistFeature::constructChildModel(int selectedId) {
    QList<TreeItem*> data_list;
    int selectedRow = -1;

    int row = 0;
    const QList<IdAndLabel> playlistLabels = createPlaylistLabels();
    for (const auto& idAndLabel : playlistLabels) {
        int playlistId = idAndLabel.id;
        QString playlistLabel = idAndLabel.label;

        if (selectedId == playlistId) {
            // save index for selection
            selectedRow = row;
        }

        // Create the TreeItem whose parent is the invisible root item
        TreeItem* item = new TreeItem(playlistLabel, playlistId);
        item->setBold(m_playlistIdsOfSelectedTrack.contains(playlistId));

        decorateChild(item, playlistId);
        data_list.append(item);

        ++row;
    }

    // Append all the newly created TreeItems in a dynamic way to the childmodel
    m_pSidebarModel->insertTreeItemRows(data_list, 0);
    if (selectedRow == -1) {
        return QModelIndex();
    }
    return m_pSidebarModel->index(selectedRow, 0);
}

void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) {
    if (m_playlistDao.isPlaylistLocked(playlistId)) {
        item->setIcon(
                QIcon(":/images/library/ic_library_locked_tracklist.svg"));
    } else {
        item->setIcon(QIcon());
    }
}

void PlaylistFeature::slotPlaylistTableChanged(int playlistId) {
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
    }
}

void PlaylistFeature::slotPlaylistContentChanged(QSet<int> playlistIds) {
    for (const auto playlistId : qAsConst(playlistIds)) {
        enum PlaylistDAO::HiddenType type =
                m_playlistDao.getHiddenType(playlistId);
        if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
                type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
            updateChildModel(playlistId);
        }
    }
}

void PlaylistFeature::slotPlaylistTableRenamed(
        int playlistId, const QString& newName) {
    Q_UNUSED(newName);
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
        if (type != PlaylistDAO::PLHT_UNKNOWN) {
            activatePlaylist(playlistId);
        }
    }
}

QString PlaylistFeature::getRootViewHtml() const {
    QString playlistsTitle = tr("Playlists");
    QString playlistsSummary =
            tr("Playlists are ordered lists of tracks that allow you to plan "
               "your DJ sets.");
    QString playlistsSummary2 =
            tr("Some DJs construct playlists before they perform live, but "
               "others prefer to build them on-the-fly.");
    QString playlistsSummary3 =
            tr("When using a playlist during a live DJ set, remember to always "
               "pay close attention to how your audience reacts to the music "
               "you've chosen to play.");
    QString playlistsSummary4 =
            tr("It may be necessary to skip some tracks in your prepared "
               "playlist or add some different tracks in order to maintain the "
               "energy of your audience.");
    QString createPlaylistLink = tr("Create New Playlist");

    QString html;
    html.append(QStringLiteral("
%1
").arg(playlistsTitle));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary2));
    html.append(QStringLiteral("
%1
%2
").arg(playlistsSummary3, playlistsSummary4));
    html.append(QStringLiteral("%1")
                        .arg(createPlaylistLink));
    return html;
}


#include "library/trackset/playlistfeature.h"

#include <QFile>
#include <QMenu>
#include <QtDebug>

#include "controllers/keyboard/keyboardeventfilter.h"
#include "library/library.h"
#include "library/parser.h"
#include "library/playlisttablemodel.h"
#include "library/queryutil.h"
#include "library/trackcollection.h"
#include "library/trackcollectionmanager.h"
#include "library/treeitem.h"
#include "moc_playlistfeature.cpp"
#include "sources/soundsourceproxy.h"
#include "util/db/dbconnection.h"
#include "util/dnd.h"
#include "util/duration.h"
#include "widget/wlibrary.h"
#include "widget/wlibrarysidebar.h"
#include "widget/wlibrarytextbrowser.h"

namespace {

QString createPlaylistLabel(
        const QString& name,
        int count,
        int duration) {
    return QStringLiteral("%1 (%2) %3")
            .arg(name,
                    QString::number(count),
                    mixxx::Duration::formatTime(
                            duration, mixxx::Duration::Precision::SECONDS));
}

} // anonymous namespace

PlaylistFeature::PlaylistFeature(Library* pLibrary, UserSettingsPointer pConfig)
        : BasePlaylistFeature(pLibrary,
                  pConfig,
                  new PlaylistTableModel(nullptr,
                          pLibrary->trackCollectionManager(),
                          "mixxx.db.model.playlist"),
                  QStringLiteral("PLAYLISTHOME"),
                  QStringLiteral("playlist")) {
    // construct child model
    std::unique_ptr<TreeItem> pRootItem = TreeItem::newRoot(this);
    m_pSidebarModel->setRootItem(std::move(pRootItem));
    constructChildModel(kInvalidPlaylistId);
}

QVariant PlaylistFeature::title() {
    return tr("Playlists");
}

void PlaylistFeature::onRightClick(const QPoint& globalPos) {
    m_lastRightClickedIndex = QModelIndex();
    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pCreateImportPlaylistAction);
    menu.exec(globalPos);
}

void PlaylistFeature::onRightClickChild(
        const QPoint& globalPos, const QModelIndex& index) {
    //Save the model index so we can get it in the action slots...
    m_lastRightClickedIndex = index;
    int playlistId = playlistIdFromIndex(index);

    bool locked = m_playlistDao.isPlaylistLocked(playlistId);
    m_pDeletePlaylistAction->setEnabled(!locked);
    m_pRenamePlaylistAction->setEnabled(!locked);

    m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock"));

    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pRenamePlaylistAction);
    menu.addAction(m_pDuplicatePlaylistAction);
    menu.addAction(m_pDeletePlaylistAction);
    menu.addAction(m_pLockPlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pAddToAutoDJAction);
    menu.addAction(m_pAddToAutoDJTopAction);
    menu.addAction(m_pAddToAutoDJReplaceAction);
    menu.addSeparator();
    menu.addAction(m_pAnalyzePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pImportPlaylistAction);
    menu.addAction(m_pExportPlaylistAction);
    menu.addAction(m_pExportTrackFilesAction);
    menu.exec(globalPos);
}

bool PlaylistFeature::dropAcceptChild(
        const QModelIndex& index, const QList<QUrl>& urls, QObject* pSource) {
    int playlistId = playlistIdFromIndex(index);
    VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) {
        return false;
    }
    // If a track is dropped onto a playlist's name, but the track isn't in the
    // library, then add the track to the library before adding it to the
    // pSource != nullptr it is a drop from inside Mixxx and indicates all
    // tracks already in the DB
    QList<TrackId> trackIds = m_pLibrary->trackCollectionManager()
                                      ->resolveTrackIdsFromUrls(urls, !pSource);
    if (!trackIds.size()) {
        return false;
    }

    // Return whether appendTracksToPlaylist succeeded.
    return m_playlistDao.appendTracksToPlaylist(trackIds, playlistId);
}

bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& url) {
    int playlistId = playlistIdFromIndex(index);
    bool locked = m_playlistDao.isPlaylistLocked(playlistId);

    bool formatSupported = SoundSourceProxy::isUrlSupported(url) ||
            Parser::isPlaylistFilenameSupported(url.toLocalFile());
    return !locked && formatSupported;
}

QList<BasePlaylistFeature::IdAndLabel> PlaylistFeature::createPlaylistLabels() {
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();

    QList<BasePlaylistFeature::IdAndLabel> playlistLabels;
    QString queryString = QStringLiteral(
            "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations "
            "AS SELECT "
            "  Playlists.id AS id, "
            "  Playlists.name AS name, "
            "  LOWER(Playlists.name) AS sort_name, "
            "  COUNT(case library.mixxx_deleted when 0 then 1 else null end) "
            "    AS count, "
            "  SUM(case library.mixxx_deleted "
            "    when 0 then library.duration else 0 end) AS durationSeconds "
            "FROM Playlists "
            "LEFT JOIN PlaylistTracks "
            "  ON PlaylistTracks.playlist_id = Playlists.id "
            "LEFT JOIN library "
            "  ON PlaylistTracks.track_id = library.id "
            "  WHERE Playlists.hidden = 0 "
            "  GROUP BY Playlists.id");
    queryString.append(
            mixxx::DbConnection::collateLexicographically(
                    " ORDER BY sort_name"));
    QSqlQuery

if (!query.exec(queryString)) {
        LOG_FAILED_QUERY(query);
    }

    // Setup the sidebar playlist model
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int idColumn = record.indexOf("id");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    for (int row = 0; row < playlistTableModel.rowCount(); ++row) {
        int id =
                playlistTableModel
                        .data(playlistTableModel.index(row, idColumn))
                        .toInt();
        QString name =
                playlistTableModel
                        .data(playlistTableModel.index(row, nameColumn))
                        .toString();
        int count =
                playlistTableModel
                        .data(playlistTableModel.index(row, countColumn))
                        .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(row, durationColumn))
                        .toInt();
        BasePlaylistFeature::IdAndLabel idAndLabel;
        idAndLabel.id = id;
        idAndLabel.label = createPlaylistLabel(name, count, duration);
        playlistLabels.append(idAndLabel);
    }
    return playlistLabels;
}

QString PlaylistFeature::fetchPlaylistLabel(int playlistId) {
    // Setup the sidebar playlist model
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    QString filter = "id=" + QString::number(playlistId);
    playlistTableModel.setFilter(filter);
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    DEBUG_ASSERT(playlistTableModel.rowCount() <= 1);
    if (playlistTableModel.rowCount() > 0) {
        QString name =
                playlistTableModel.data(playlistTableModel.index(0, nameColumn))
                        .toString();
        int count = playlistTableModel
                            .data(playlistTableModel.index(0, countColumn))
                            .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(0, durationColumn))
                        .toInt();
        return createPlaylistLabel(name, count, duration);
    }
    return QString();
}

/// Purpose: When inserting or removing playlists,
/// we require the sidebar model not to reset.
/// This method queries the database and does dynamic insertion
/// @param selectedId entry which should be selected
QModelIndex PlaylistFeature::constructChildModel(int selectedId) {
    QList<TreeItem*> data_list;
    int selectedRow = -1;

    int row = 0;
    const QList<IdAndLabel> playlistLabels = createPlaylistLabels();
    for (const auto& idAndLabel : playlistLabels) {
        int playlistId = idAndLabel.id;
        QString playlistLabel = idAndLabel.label;

        if (selectedId == playlistId) {
            // save index for selection
            selectedRow = row;
        }

        // Create the TreeItem whose parent is the invisible root item
        TreeItem* item = new TreeItem(playlistLabel, playlistId);
        item->setBold(m_playlistIdsOfSelectedTrack.contains(playlistId));

        decorateChild(item, playlistId);
        data_list.append(item);

        ++row;
    }

    // Append all the newly created TreeItems in a dynamic way to the childmodel
    m_pSidebarModel->insertTreeItemRows(data_list, 0);
    if (selectedRow == -1) {
        return QModelIndex();
    }
    return m_pSidebarModel->index(selectedRow, 0);
}

void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) {
    if (m_playlistDao.isPlaylistLocked(playlistId)) {
        item->setIcon(
                QIcon(":/images/library/ic_library_locked_tracklist.svg"));
    } else {
        item->setIcon(QIcon());
    }
}

void PlaylistFeature::slotPlaylistTableChanged(int playlistId) {
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
    }
}

void PlaylistFeature::slotPlaylistContentChanged(QSet<int> playlistIds) {
    for (const auto playlistId : qAsConst(playlistIds)) {
        enum PlaylistDAO::HiddenType type =
                m_playlistDao.getHiddenType(playlistId);
        if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
                type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
            updateChildModel(playlistId);
        }
    }
}

void PlaylistFeature::slotPlaylistTableRenamed(
        int playlistId, const QString& newName) {
    Q_UNUSED(newName);
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
        if (type != PlaylistDAO::PLHT_UNKNOWN) {
            activatePlaylist(playlistId);
        }
    }
}

QString PlaylistFeature::getRootViewHtml() const {
    QString playlistsTitle = tr("Playlists");
    QString playlistsSummary =
            tr("Playlists are ordered lists of tracks that allow you to plan "
               "your DJ sets.");
    QString playlistsSummary2 =
            tr("Some DJs construct playlists before they perform live, but "
               "others prefer to build them on-the-fly.");
    QString playlistsSummary3 =
            tr("When using a playlist during a live DJ set, remember to always "
               "pay close attention to how your audience reacts to the music "
               "you've chosen to play.");
    QString playlistsSummary4 =
            tr("It may be necessary to skip some tracks in your prepared "
               "playlist or add some different tracks in order to maintain the "
               "energy of your audience.");
    QString createPlaylistLink = tr("Create New Playlist");

    QString html;
    html.append(QStringLiteral("
%1
").arg(playlistsTitle));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary2));
    html.append(QStringLiteral("
%1
%2
").arg(playlistsSummary3, playlistsSummary4));
    html.append(QStringLiteral("%1")
                        .arg(createPlaylistLink));
    return html;
}


#include "library/trackset/playlistfeature.h"

#include <QFile>
#include <QMenu>
#include <QtDebug>

#include "controllers/keyboard/keyboardeventfilter.h"
#include "library/library.h"
#include "library/parser.h"
#include "library/playlisttablemodel.h"
#include "library/queryutil.h"
#include "library/trackcollection.h"
#include "library/trackcollectionmanager.h"
#include "library/treeitem.h"
#include "moc_playlistfeature.cpp"
#include "sources/soundsourceproxy.h"
#include "util/db/dbconnection.h"
#include "util/dnd.h"
#include "util/duration.h"
#include "widget/wlibrary.h"
#include "widget/wlibrarysidebar.h"
#include "widget/wlibrarytextbrowser.h"

namespace {

QString createPlaylistLabel(
        const QString& name,
        int count,
        int duration) {
    return QStringLiteral("%1 (%2) %3")
            .arg(name,
                    QString::number(count),
                    mixxx::Duration::formatTime(
                            duration, mixxx::Duration::Precision::SECONDS));
}

} // anonymous namespace

PlaylistFeature::PlaylistFeature(Library* pLibrary, UserSettingsPointer pConfig)
        : BasePlaylistFeature(pLibrary,
                  pConfig,
                  new PlaylistTableModel(nullptr,
                          pLibrary->trackCollectionManager(),
                          "mixxx.db.model.playlist"),
                  QStringLiteral("PLAYLISTHOME"),
                  QStringLiteral("playlist")) {
    // construct child model
    std::unique_ptr<TreeItem> pRootItem = TreeItem::newRoot(this);
    m_pSidebarModel->setRootItem(std::move(pRootItem));
    constructChildModel(kInvalidPlaylistId);
}

QVariant PlaylistFeature::title() {
    return tr("Playlists");
}

void PlaylistFeature::onRightClick(const QPoint& globalPos) {
    m_lastRightClickedIndex = QModelIndex();
    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pCreateImportPlaylistAction);
    menu.exec(globalPos);
}

void PlaylistFeature::onRightClickChild(
        const QPoint& globalPos, const QModelIndex& index) {
    //Save the model index so we can get it in the action slots...
    m_lastRightClickedIndex = index;
    int playlistId = playlistIdFromIndex(index);

    bool locked = m_playlistDao.isPlaylistLocked(playlistId);
    m_pDeletePlaylistAction->setEnabled(!locked);
    m_pRenamePlaylistAction->setEnabled(!locked);

    m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock"));

    QMenu menu(m_pSidebarWidget);
    menu.addAction(m_pCreatePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pRenamePlaylistAction);
    menu.addAction(m_pDuplicatePlaylistAction);
    menu.addAction(m_pDeletePlaylistAction);
    menu.addAction(m_pLockPlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pAddToAutoDJAction);
    menu.addAction(m_pAddToAutoDJTopAction);
    menu.addAction(m_pAddToAutoDJReplaceAction);
    menu.addSeparator();
    menu.addAction(m_pAnalyzePlaylistAction);
    menu.addSeparator();
    menu.addAction(m_pImportPlaylistAction);
    menu.addAction(m_pExportPlaylistAction);
    menu.addAction(m_pExportTrackFilesAction);
    menu.exec(globalPos);
}

bool PlaylistFeature::dropAcceptChild(
        const QModelIndex& index, const QList<QUrl>& urls, QObject* pSource) {
    int playlistId = playlistIdFromIndex(index);
    VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) {
        return false;
    }
    // If a track is dropped onto a playlist's name, but the track isn't in the
    // library, then add the track to the library before adding it to the
    // pSource != nullptr it is a drop from inside Mixxx and indicates all
    // tracks already in the DB
    QList<TrackId> trackIds = m_pLibrary->trackCollectionManager()
                                      ->resolveTrackIdsFromUrls(urls, !pSource);
    if (!trackIds.size()) {
        return false;
    }

    // Return whether appendTracksToPlaylist succeeded.
    return m_playlistDao.appendTracksToPlaylist(trackIds, playlistId);
}

bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& url) {
    int playlistId = playlistIdFromIndex(index);
    bool locked = m_playlistDao.isPlaylistLocked(playlistId);

    bool formatSupported = SoundSourceProxy::isUrlSupported(url) ||
            Parser::isPlaylistFilenameSupported(url.toLocalFile());
    return !locked && formatSupported;
}

QList<BasePlaylistFeature::IdAndLabel> PlaylistFeature::createPlaylistLabels() {
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();

    QList<BasePlaylistFeature::IdAndLabel> playlistLabels;
    QString queryString = QStringLiteral(
            "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations "
            "AS SELECT "
            "  Playlists.id AS id, "
            "  Playlists.name AS name, "
            "  LOWER(Playlists.name) AS sort_name, "
            "  COUNT(case library.mixxx_deleted when 0 then 1 else null end) "
            "    AS count, "
            "  SUM(case library.mixxx_deleted "
            "    when 0 then library.duration else 0 end) AS durationSeconds "
            "FROM Playlists "
            "LEFT JOIN PlaylistTracks "
            "  ON PlaylistTracks.playlist_id = Playlists.id "
            "LEFT JOIN library "
            "  ON PlaylistTracks.track_id = library.id "
            "  WHERE Playlists.hidden = 0 "
            "  GROUP BY Playlists.id");
    queryString.append(
            mixxx::DbConnection::collateLexicographically(
                    " ORDER BY sort_name"));
    QSqlQuery query(database);
    if (!query.exec(queryString)) {
        LOG_FAILED_QUERY(query);
    }

    // Setup the sidebar playlist model
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    playlistTableModel.select();

QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int idColumn = record.indexOf("id");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    for (int row = 0; row < playlistTableModel.rowCount(); ++row) {
        int id =
                playlistTableModel
                        .data(playlistTableModel.index(row, idColumn))
                        .toInt();
        QString name =
                playlistTableModel
                        .data(playlistTableModel.index(row, nameColumn))
                        .toString();
        int count =
                playlistTableModel
                        .data(playlistTableModel.index(row, countColumn))
                        .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(row, durationColumn))
                        .toInt();
        BasePlaylistFeature::IdAndLabel idAndLabel;
        idAndLabel.id = id;
        idAndLabel.label = createPlaylistLabel(name, count, duration);
        playlistLabels.append(idAndLabel);
    }
    return playlistLabels;
}

QString PlaylistFeature::fetchPlaylistLabel(int playlistId) {
    // Setup the sidebar playlist model
    QSqlDatabase database =
            m_pLibrary->trackCollectionManager()->internalCollection()->database();
    QSqlTableModel playlistTableModel(this, database);
    playlistTableModel.setTable("PlaylistsCountsDurations");
    QString filter = "id=" + QString::number(playlistId);
    playlistTableModel.setFilter(filter);
    playlistTableModel.select();
    while (playlistTableModel.canFetchMore()) {
        playlistTableModel.fetchMore();
    }
    QSqlRecord record = playlistTableModel.record();
    int nameColumn = record.indexOf("name");
    int countColumn = record.indexOf("count");
    int durationColumn = record.indexOf("durationSeconds");

    DEBUG_ASSERT(playlistTableModel.rowCount() <= 1);
    if (playlistTableModel.rowCount() > 0) {
        QString name =
                playlistTableModel.data(playlistTableModel.index(0, nameColumn))
                        .toString();
        int count = playlistTableModel
                            .data(playlistTableModel.index(0, countColumn))
                            .toInt();
        int duration =
                playlistTableModel
                        .data(playlistTableModel.index(0, durationColumn))
                        .toInt();
        return createPlaylistLabel(name, count, duration);
    }
    return QString();
}

/// Purpose: When inserting or removing playlists,
/// we require the sidebar model not to reset.
/// This method queries the database and does dynamic insertion
/// @param selectedId entry which should be selected
QModelIndex PlaylistFeature::constructChildModel(int selectedId) {
    QList<TreeItem*> data_list;
    int selectedRow = -1;

    int row = 0;
    const QList<IdAndLabel> playlistLabels = createPlaylistLabels();
    for (const auto& idAndLabel : playlistLabels) {
        int playlistId = idAndLabel.id;
        QString playlistLabel = idAndLabel.label;

        if (selectedId == playlistId) {
            // save index for selection
            selectedRow = row;
        }

        // Create the TreeItem whose parent is the invisible root item
        TreeItem* item = new TreeItem(playlistLabel, playlistId);
        item->setBold(m_playlistIdsOfSelectedTrack.contains(playlistId));

        decorateChild(item, playlistId);
        data_list.append(item);

        ++row;
    }

    // Append all the newly created TreeItems in a dynamic way to the childmodel
    m_pSidebarModel->insertTreeItemRows(data_list, 0);
    if (selectedRow == -1) {
        return QModelIndex();
    }
    return m_pSidebarModel->index(selectedRow, 0);
}

void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) {
    if (m_playlistDao.isPlaylistLocked(playlistId)) {
        item->setIcon(
                QIcon(":/images/library/ic_library_locked_tracklist.svg"));
    } else {
        item->setIcon(QIcon());
    }
}

void PlaylistFeature::slotPlaylistTableChanged(int playlistId) {
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
    }
}

void PlaylistFeature::slotPlaylistContentChanged(QSet<int> playlistIds) {
    for (const auto playlistId : qAsConst(playlistIds)) {
        enum PlaylistDAO::HiddenType type =
                m_playlistDao.getHiddenType(playlistId);
        if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
                type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
            updateChildModel(playlistId);
        }
    }
}

void PlaylistFeature::slotPlaylistTableRenamed(
        int playlistId, const QString& newName) {
    Q_UNUSED(newName);
    enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId);
    if (type == PlaylistDAO::PLHT_NOT_HIDDEN ||
            type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist
        clearChildModel();
        m_lastRightClickedIndex = constructChildModel(playlistId);
        if (type != PlaylistDAO::PLHT_UNKNOWN) {
            activatePlaylist(playlistId);
        }
    }
}

QString PlaylistFeature::getRootViewHtml() const {
    QString playlistsTitle = tr("Playlists");
    QString playlistsSummary =
            tr("Playlists are ordered lists of tracks that allow you to plan "
               "your DJ sets.");
    QString playlistsSummary2 =
            tr("Some DJs construct playlists before they perform live, but "
               "others prefer to build them on-the-fly.");
    QString playlistsSummary3 =
            tr("When using a playlist during a live DJ set, remember to always "
               "pay close attention to how your audience reacts to the music "
               "you've chosen to play.");
    QString playlistsSummary4 =
            tr("It may be necessary to skip some tracks in your prepared "
               "playlist or add some different tracks in order to maintain the "
               "energy of your audience.");
    QString createPlaylistLink = tr("Create New Playlist");

    QString html;
    html.append(QStringLiteral("
%1
").arg(playlistsTitle));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary));
    html.append(QStringLiteral("
%1
").arg(playlistsSummary2));
    html.append(QStringLiteral("
%1
%2
").arg(playlistsSummary3, playlistsSummary4));
    html.append(QStringLiteral("%1")
                        .arg(createPlaylistLink));
    return html;
}


// TODO: Make this test pass for all standards.

// Test that the __convert_to_integral functions properly converts Tp to the
// correct type and value for integral, enum and user defined types.

#include <limits>
#include <type_traits>
#include <cstdint>
#include <cassert>

#include "user_defined_integral.h"

#include "test_macros.h"

template <class T>
struct EnumType
{
  enum type : T {E_zero, E_one};
};

template <class From, class To>
void check_integral_types()
{
  typedef std::numeric_limits<From> Limits;
  const From max = Limits::max();
  const From min = Limits::min();
  {
  auto ret = std::__convert_to_integral((From)max);
  assert(ret == max);
  ret = std::__convert_to_integral((From)min);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
  {
  UserDefinedIntegral<From> f(max);
  auto ret = std::__convert_to_integral(f);
  assert(ret == max);
  f.value = min;
  ret = std::__convert_to_integral(f);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
  {
  typedef typename EnumType<From>::type Enum;
  Enum e(static_cast<Enum>(max));
  auto ret = std::__convert_to_integral(e);
  assert(ret == max);
  e = static_cast<Enum>(min);
  ret = std::__convert_to_integral(min);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
}

template <class From, class To>

{
  auto ret = std::__convert_to_integral((From)1);
  assert(ret == 1);
  static_assert(std::is_same<decltype(ret), To>::value, "");
}

enum enum1 { zero = 0, one = 1 };
enum enum2 : unsigned long {
  value = std::numeric_limits<unsigned long>::max()
};

int main(int, char**)
{
  check_integral_types<bool, int>();
  check_integral_types<char, int>();
  check_integral_types<signed char, int>();
  check_integral_types<unsigned char, int>();
  check_integral_types<wchar_t, decltype(((wchar_t)1) + 1)>();
  check_integral_types<char16_t, int>();
  // On some platforms, unsigned int and long are the same size.  These
  // platforms have a choice of making uint32_t an int or a long.  However
  // char32_t must promote to an unsigned int on these platforms [conv.prom].
  // Use the following logic to make the test work on such platforms.
  typedef std::conditional<sizeof(uint32_t) == sizeof(unsigned int),
                           unsigned int, uint32_t>::type char_integral;
  check_integral_types<char32_t, char_integral>();
  check_integral_types<short, int>();
  check_integral_types<unsigned short, int>();
  check_integral_types<int, int>();
  check_integral_types<unsigned, unsigned>();
  check_integral_types<long, long>();
  check_integral_types<unsigned long, unsigned long>();
  check_integral_types<long long, long long>();
  check_integral_types<unsigned long long, unsigned long long>();
#ifndef _LIBCUDACXX_HAS_NO_INT128
  check_integral_types<__int128_t, __int128_t>();
  check_integral_types<__uint128_t, __uint128_t>();
#endif
    // TODO(ericwf): Not standard
  typedef std::underlying_type<enum1>::type Enum1UT;
  check_enum_types<enum1, decltype(((Enum1UT)1) + 1)>();
  typedef std::underlying_type<enum2>::type Enum2UT;
  check_enum_types<enum2, decltype(((Enum2UT)1) + 1)>();

  return 0;
}


{
  auto ret = std::__convert_to_integral((From)1);
  assert(ret == 1);
  static_assert(std::is_same<decltype(ret), To>::value, "");
}

// TODO: Make this test pass for all standards.

// Test that the __convert_to_integral functions properly converts Tp to the
// correct type and value for integral, enum and user defined types.

#include <limits>
#include <type_traits>
#include <cstdint>
#include <cassert>

#include "user_defined_integral.h"

#include "test_macros.h"

template <class T>
struct EnumType
{
  enum type : T {E_zero, E_one};
};

template <class From, class To>
void check_integral_types()
{
  typedef std::numeric_limits<From> Limits;
  const From max = Limits::max();
  const From min = Limits::min();
  {
  auto ret = std::__convert_to_integral((From)max);
  assert(ret == max);
  ret = std::__convert_to_integral((From)min);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
  {
  UserDefinedIntegral<From> f(max);
  auto ret = std::__convert_to_integral(f);
  assert(ret == max);
  f.value = min;
  ret = std::__convert_to_integral(f);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
  {
  typedef typename EnumType<From>::type Enum;
  Enum e(static_cast<Enum>(max));
  auto ret = std::__convert_to_integral(e);
  assert(ret == max);
  e = static_cast<Enum>(min);
  ret = std::__convert_to_integral(min);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
}

template <class From, class To>
void check_enum_types()

enum enum1 { zero = 0, one = 1 };
enum enum2 : unsigned long {
  value = std::numeric_limits<unsigned long>::max()
};

int main(int, char**)
{
  check_integral_types<bool, int>();
  check_integral_types<char, int>();
  check_integral_types<signed char, int>();
  check_integral_types<unsigned char, int>();
  check_integral_types<wchar_t, decltype(((wchar_t)1) + 1)>();
  check_integral_types<char16_t, int>();
  // On some platforms, unsigned int and long are the same size.  These
  // platforms have a choice of making uint32_t an int or a long.  However
  // char32_t must promote to an unsigned int on these platforms [conv.prom].
  // Use the following logic to make the test work on such platforms.
  typedef std::conditional<sizeof(uint32_t) == sizeof(unsigned int),
                           unsigned int, uint32_t>::type char_integral;
  check_integral_types<char32_t, char_integral>();
  check_integral_types<short, int>();
  check_integral_types<unsigned short, int>();
  check_integral_types<int, int>();
  check_integral_types<unsigned, unsigned>();
  check_integral_types<long, long>();
  check_integral_types<unsigned long, unsigned long>();
  check_integral_types<long long, long long>();
  check_integral_types<unsigned long long, unsigned long long>();
#ifndef _LIBCUDACXX_HAS_NO_INT128
  check_integral_types<__int128_t, __int128_t>();
  check_integral_types<__uint128_t, __uint128_t>();
#endif
    // TODO(ericwf): Not standard
  typedef std::underlying_type<enum1>::type Enum1UT;
  check_enum_types<enum1, decltype(((Enum1UT)1) + 1)>();
  typedef std::underlying_type<enum2>::type Enum2UT;
  check_enum_types<enum2, decltype(((Enum2UT)1) + 1)>();

  return 0;
}


// TODO: Make this test pass for all standards.

// Test that the __convert_to_integral functions properly converts Tp to the
// correct type and value for integral, enum and user defined types.

#include <limits>
#include <type_traits>
#include <cstdint>
#include <cassert>

#include "user_defined_integral.h"

#include "test_macros.h"

template <class T>
struct EnumType
{
  enum type : T {E_zero, E_one};
};

template <class From, class To>
void check_integral_types()
{
  typedef

  const From max = Limits::max();
  const From min = Limits::min();
  {
  auto ret = std::__convert_to_integral((From)max);
  assert(ret == max);
  ret = std::__convert_to_integral((From)min);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
  {
  UserDefinedIntegral<From> f(max);
  auto ret = std::__convert_to_integral(f);
  assert(ret == max);
  f.value = min;
  ret = std::__convert_to_integral(f);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
  {
  typedef typename EnumType<From>::type Enum;
  Enum e(static_cast<Enum>(max));
  auto ret = std::__convert_to_integral(e);
  assert(ret == max);
  e = static_cast<Enum>(min);
  ret = std::__convert_to_integral(min);
  assert(ret == min);
  static_assert(std::is_same<decltype(ret), To>::value, "");
  }
}

template <class From, class To>
void check_enum_types()
{
  auto ret = std::__convert_to_integral((From)1);
  assert(ret == 1);
  static_assert(std::is_same<decltype(ret), To>::value, "");
}

enum enum1 { zero = 0, one = 1 };
enum enum2 : unsigned long {
  value = std::numeric_limits<unsigned long>::max()
};

int main(int, char**)
{
  check_integral_types<bool, int>();
  check_integral_types<char, int>();
  check_integral_types<signed char, int>();
  check_integral_types<unsigned char, int>();
  check_integral_types<wchar_t, decltype(((wchar_t)1) + 1)>();
  check_integral_types<char16_t, int>();
  // On some platforms, unsigned int and long are the same size.  These
  // platforms have a choice of making uint32_t an int or a long.  However
  // char32_t must promote to an unsigned int on these platforms [conv.prom].
  // Use the following logic to make the test work on such platforms.
  typedef std::conditional<sizeof(uint32_t) == sizeof(unsigned int),
                           unsigned int, uint32_t>::type char_integral;
  check_integral_types<char32_t, char_integral>();
  check_integral_types<short, int>();
  check_integral_types<unsigned short, int>();
  check_integral_types<int, int>();
  check_integral_types<unsigned, unsigned>();
  check_integral_types<long, long>();
  check_integral_types<unsigned long, unsigned long>();
  check_integral_types<long long, long long>();
  check_integral_types<unsigned long long, unsigned long long>();
#ifndef _LIBCUDACXX_HAS_NO_INT128
  check_integral_types<__int128_t, __int128_t>();
  check_integral_types<__uint128_t, __uint128_t>();
#endif
    // TODO(ericwf): Not standard
  typedef std::underlying_type<enum1>::type Enum1UT;
  check_enum_types<enum1, decltype(((Enum1UT)1) + 1)>();
  typedef std::underlying_type<enum2>::type Enum2UT;
  check_enum_types<enum2, decltype(((Enum2UT)1) + 1)>();

  return 0;
}


#include <experimental/type_traits>
#include <string>

#include "test_macros.h"

namespace ex = std::experimental;

template <typename T>
  using callFoo = decltype(std::declval<T&>().Foo());

struct yesFoo {
    int Foo() { return 0; }
};

struct noFoo {
};

struct wrongFoo {

{ return ""; }
};

struct convertibleFoo {
    long Foo() { return 0; }
};

template <typename T, bool b>
void test() {
    static_assert( b == ex::is_detected_convertible  <int, callFoo, T>::value, "" );
    static_assert( b == ex::is_detected_convertible_v<int, callFoo, T>, "" );
}

int main(int, char**) {
    test<yesFoo, true>();
    test<noFoo, false>();
    test<wrongFoo, false>();
    test<convertibleFoo, true>();

  return 0;
}


#include <experimental/type_traits>
#include <string>

#include "test_macros.h"

namespace ex = std::experimental;

template <typename T>
  using callFoo = decltype(std::declval<T&>().Foo());

struct yesFoo {
    int Foo()

};

struct noFoo {
};

struct wrongFoo {
    std::string Foo() { return ""; }
};

struct convertibleFoo {
    long Foo() { return 0; }
};

template <typename T, bool b>
void test() {
    static_assert( b == ex::is_detected_convertible  <int, callFoo, T>::value, "" );
    static_assert( b == ex::is_detected_convertible_v<int, callFoo, T>, "" );
}

int main(int, char**) {
    test<yesFoo, true>();
    test<noFoo, false>();
    test<wrongFoo, false>();
    test<convertibleFoo, true>();

  return 0;
}


convertible  <int, callFoo, T>::value, "" );

#include <experimental/type_traits>
#include <string>

#include "test_macros.h"

namespace ex = std::experimental;

template <typename T>
  using callFoo = decltype(std::declval<T&>().Foo());

struct yesFoo {
    int Foo() { return 0; }
};

struct noFoo {
};

struct wrongFoo {
    std::string Foo() { return ""; }
};

struct convertibleFoo {
    long Foo() { return 0; }
};

template <typename T, bool b>
void test() {
    static_assert( b == ex::is_detected_

    static_assert( b == ex::is_detected_convertible_v<int, callFoo, T>, "" );
}

int main(int, char**) {
    test<yesFoo, true>();
    test<noFoo, false>();
    test<wrongFoo, false>();
    test<convertibleFoo, true>();

  return 0;
}


/* BEGIN_COMMON_COPYRIGHT_HEADER
 *
 * TOra - An Oracle Toolkit for DBA's and developers
 *
 * Shared/mixed copyright is held throughout files in this product
 *
 *
 *
 *      with the Oracle Client libraries and distribute executables, as long
 *      software in the executable aside from Oracle client libraries.
 *
 * All trademarks belong to their respective owners.
 *
 * END_COMMON_COPYRIGHT_HEADER */

#include "tools/toresultlock.h"

#include "core/toconnection.h"
#include "core/toeventquery.h"
#include "core/toqvalue.h"
#include "core/tosql.h"
#include "core/totool.h"
#include "core/utils.h"

#include <map>

bool toResultLock::canHandle(const toConnection &conn)
{
    return conn.providerIs("Oracle");
}

toResultLock::toResultLock(QWidget *parent, const char *name)
    : toResultView(false, false, parent, name)
{
    setAllColumnsShowFocus(true);
    setSorting( -1);
    setRootIsDecorated(true);
    addColumn(tr("Session"));
    addColumn(tr("Schema"));
    addColumn(tr("Osuser"));
    addColumn(tr("Program"));
    addColumn(tr("Type"));
    addColumn(tr("Mode"));
    addColumn(tr("Request"));
    addColumn(tr("Object"));
    addColumn(tr("Grabbed"));
    addColumn(tr("Requested"));
    setSQLName(QString::fromLatin1("toResultLock"));

    Query = NULL;
    LastItem = NULL;
}

{
    if (Query)
        delete Query;
}

static toSQL SQLBlockingLock("toResultLock:BlockingLocks",
                             "select b.sid,\n"
                             "       b.schemaname,\n"
                             "       b.osuser,\n"
                             "       b.program,\n"
                             "       decode(a.type,\n"
                             "              'MR', 'Media Recovery',\n"
                             "              'RT', 'Redo Thread',\n"
                             "              'UN', 'User Name',\n"
                             "              'TX', 'Transaction',\n"
                             "              'TM', 'DML',\n"
                             "              'UL', 'PL/SQL User Lock',\n"
                             "              'DX', 'Distributed Xaction',\n"
                             "              'CF', 'Control File',\n"
                             "              'IS', 'Instance State',\n"
                             "              'FS', 'File Set',\n"
                             "              'IR', 'Instance Recovery',\n"
                             "              'ST', 'Disk Space Transaction',\n"
                             "              'TS', 'Temp Segment',\n"
                             "              'IV', 'Library Cache Invalidation',\n"
                             "              'LS', 'Log Start or Switch',\n"
                             "              'RW', 'Row Wait',\n"
                             "              'SQ', 'Sequence Number',\n"
                             "              'TE', 'Extend Table',\n"
                             "              'TT', 'Temp Table',\n"
                             "              'Internal ('||a.type||')'),\n"
                             "       DECODE(a.lmode,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(a.lmode)),\n"
                             "       DECODE(a.request,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(a.request)),\n"
                             "       d.object_name,\n"
                             "       ' ',\n"
                             "       TO_CHAR(SYSDATE-a.CTIME/3600/24)\n"
                             "  from v$lock a,v$session b,v$locked_object c,sys.all_objects d\n"
                             " where a.sid = b.sid\n"
                             "   and c.session_id = a.sid\n"
                             "   and exists (select 'X'\n"
                             "                 from v$locked_object bb,\n"
                             "                      v$lock cc\n"
                             "                where bb.session_id = cc.sid\n"
                             "                  and cc.sid != a.sid\n"
                             "                  and cc.id1 = a.id1\n"
                             "                  and cc.id2 = a.id2\n"
                             "                  and bb.object_id = c.object_id)\n"
                             "   and d.object_id = c.object_id\n"
                             "   and a.request != 0",
                             "List session blocked by a lock");

static toSQL SQLLock("toResultLock:Locks",
                     "select b.sid,\n"
                     "       b.schemaname,\n"
                     "       b.osuser,\n"
                     "       b.program,\n"
                     "       decode(a.type,\n"
                     "              'MR', 'Media Recovery',\n"
                     "              'RT', 'Redo Thread',\n"
                     "              'UN', 'User Name',\n"
                     "              'TX', 'Transaction',\n"
                     "              'TM', 'DML',\n"
                     "              'UL', 'PL/SQL User Lock',\n"
                     "              'DX', 'Distributed Xaction',\n"
                     "              'CF', 'Control File',\n"
                     "              'IS', 'Instance State',\n"
                     "              'FS', 'File Set',\n"
                     "              'IR', 'Instance Recovery',\n"
                     "              'ST', 'Disk Space Transaction',\n"
                     "              'TS', 'Temp Segment',\n"
                     "              'IV', 'Library Cache Invalidation',\n"
                     "              'LS', 'Log Start or Switch',\n"
                     "              'RW', 'Row Wait',\n"
                     "              'SQ', 'Sequence Number',\n"
                     "              'TE', 'Extend Table',\n"
                     "              'TT', 'Temp Table',\n"
                     "              'Internal ('||a.type||')'),\n"
                     "       DECODE(a.lmode,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(a.lmode)),\n"
                     "       DECODE(e.request,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(e.request)),\n"
                     "       d.object_name,\n"
                     "       TO_CHAR(SYSDATE-a.CTIME/3600/24),\n"
                     "       TO_CHAR(SYSDATE-e.CTIME/3600/24)\n"
                     "  from v$lock a, v$session b,v$locked_object c,sys.all_objects d,v$lock e\n"
                     " where a.sid = b.sid\n"
                     "   and a.lmode != 0\n"
                     "   and c.session_id = a.sid\n"
                     "   and c.object_id = d.object_id\n"
                     "   and exists (select 'X'\n"
                     "                 from v$locked_object bb,\n"
                     "                      v$lock cc\n"
                     "                where bb.session_id = cc.sid\n"
                     "                  and cc.sid != a.sid\n"
                     "                  and cc.id1 = a.id1\n"
                     "                  and cc.id2 = a.id2\n"
                     "                  and bb.object_id = c.object_id)\n"
                     "   and a.id1 = e.id1\n"
                     "   and a.id2 = e.id2\n"
                     "   and e.sid = :f1<char[101]>\n"
                     "   and e.lmode != e.request\n"
                     "   and e.request != 0",
                     "List locks in a session");

//        'Number of waiters: '||num_waiters waiters,
//        'Seconds in Wait: '||in_wait_secs Seconds,
//        'SecondsSince Last Wait: '||time_since_last_wait_secs sincelw,
//        'Blocking Wait Chain:'||decode(blocker_chain_id,null, '',blocker_chain_id) blocker_chain,
//FROM   V$WAIT_CHAINS  a
//LEFT OUTER JOIN DBA_OBJECTS o ON(o.object_id = a.ROW_WAIT_OBJ#)
//AND    PRIOR a.instance     = a.blocker_instance
//AND    PRIOR a.osid         = a.blocker_osid
//AND    PRIOR a.pid          = a.blocker_pid
//AND    PRIOR a.sid          = a.blocker_sid
//AND    PRIOR a.sess_serial# = a.blocker_sess_serial#
//START WITH a.blocker_is_valid='FALSE'
//ORDER  BY a.chain_id , LEVEL

void toResultLock::startQuery(void)
{
    connect(Query, SIGNAL(dataAvailable(toEventQuery*)), this, SLOT(poll()));
    connect(Query, SIGNAL(done(toEventQuery*, unsigned long)), this, SLOT(queryDone()));
    Query->start();
} // startQuery

void toResultLock::query(const QString &sql, const toQueryParams &param)
{
    Q_UNUSED(sql);

    QString sid;

    if (!handled())
        return ;

    if (!setSqlAndParams(sql, param))
        return ;

    if (Query)
    {
        delete Query;
        Query = NULL;
    }
    clear();
    Checked.clear();

    try
    {
        LastItem = NULL;
        if (!param.isEmpty())
        {
            Query = new toEventQuery(this, connection(), toSQL::string(SQLLock, connection()), param, toEventQuery::READ_ALL);
        }
        else
        {
            Query = new toEventQuery(this, connection(), toSQL::string(SQLBlockingLock, connection()), toQueryParams(), toEventQuery::READ_ALL);
        }
        startQuery();
    }
    TOCATCH
} // query

void toResultLock::poll(void)
{
    try
    {
        if (Query)
        {
            while (Query->hasMore())
            {
                toTreeWidgetItem *item;
                // Check if item has to be added to top level...
                if (!LastItem)
                    item = new toResultViewItem(this, NULL);
                // ...or attached as a child record to some parent.
                else
                    item = new toResultViewItem(LastItem, NULL);
                toQColumnDescriptionList desc = Query->describe();
                for (int pos = 0; pos < desc.size(); pos++)
                    item->setText(pos, (QString)Query->readValue());
            }
        }
    }
    catch (const QString &exc)
    {
        delete Query;
        Query = NULL;
        Utils::toStatusMessage(exc);
    }
} // poll

// Which column is used as a marker for records processed for children. This
// column will get value "Yes" so that it is not processed on on subsequent pass.
#define MARK_COL 20

void toResultLock::queryDone(void)
{
    if (Query)
    {
        delete Query;
        Query = NULL;
    }

    LastItem = NULL;
    toTreeWidgetItem *next = NULL;
    for (toTreeWidgetItem *item = firstChild(); item; item = next)
    {
        int sid = item->text(0).toInt();
        if (item->text(MARK_COL).isEmpty())
        {
            item->setText(MARK_COL, QString::fromLatin1("Yes"));
            item->setOpen(true);
            if (!Checked[sid])
            {
                Checked[sid] = true;
                LastItem = item;
                toQueryParams par = toQueryParams() << LastItem->text(0);
                Query = new toEventQuery(this, connection(), toSQL::string(SQLLock, connection()), par, toEventQuery::READ_ALL);
                startQuery();
            }
            else
            {
                toTreeWidgetItem *cn = NULL;
                for (toTreeWidgetItem *ci = firstChild(); ci; ci = cn)
                {
                    if (ci != item && ci->text(0) == item->text(0))
                    {
                        if (ci->firstChild())
                        {
                            ci = ci->firstChild();
                            cn = new toResultViewItem(item, NULL);
                            for (int i = 0; i < columns(); i++)
                                cn->setText(i, ci->text(i));
                        }
                        break;
                    }
                    if (ci->firstChild())
                    {
                        cn = ci->firstChild();
                    }
                    else if (ci->nextSibling())
                        cn = ci->nextSibling();
                    else
                    {
                        cn = ci;
                        do
                        {
                            cn = cn->parent();
                        }
                        while (cn && !cn->nextSibling());
                        if (cn)
                            cn = cn->nextSibling();
                    }
                }
            }
            break;
        }
        if (item->firstChild())
        {
            next = item->firstChild();
        }
        else if (item->nextSibling())
            next = item->nextSibling();
        else
        {
            next = item;
            do
            {
                next = next->parent();
            }
            while (next && !next->nextSibling());
            if (next)
                next = next->nextSibling();
        }
    }
} // queryDone


{
    if (Query)
        delete Query;
}

/* BEGIN_COMMON_COPYRIGHT_HEADER
 *
 * TOra - An Oracle Toolkit for DBA's and developers
 *
 * Shared/mixed copyright is held throughout files in this product
 *
 *
 *
 *      with the Oracle Client libraries and distribute executables, as long
 *      software in the executable aside from Oracle client libraries.
 *
 * All trademarks belong to their respective owners.
 *
 * END_COMMON_COPYRIGHT_HEADER */

#include "tools/toresultlock.h"

#include "core/toconnection.h"
#include "core/toeventquery.h"
#include "core/toqvalue.h"
#include "core/tosql.h"
#include "core/totool.h"
#include "core/utils.h"

#include <map>

bool toResultLock::canHandle(const toConnection &conn)
{
    return conn.providerIs("Oracle");
}

toResultLock::toResultLock(QWidget *parent, const char *name)
    : toResultView(false, false, parent, name)
{
    setAllColumnsShowFocus(true);
    setSorting( -1);
    setRootIsDecorated(true);
    addColumn(tr("Session"));
    addColumn(tr("Schema"));
    addColumn(tr("Osuser"));
    addColumn(tr("Program"));
    addColumn(tr("Type"));
    addColumn(tr("Mode"));
    addColumn(tr("Request"));
    addColumn(tr("Object"));
    addColumn(tr("Grabbed"));
    addColumn(tr("Requested"));
    setSQLName(QString::fromLatin1("toResultLock"));

    Query = NULL;
    LastItem = NULL;
}

toResultLock::~toResultLock()

static toSQL SQLBlockingLock("toResultLock:BlockingLocks",
                             "select b.sid,\n"
                             "       b.schemaname,\n"
                             "       b.osuser,\n"
                             "       b.program,\n"
                             "       decode(a.type,\n"
                             "              'MR', 'Media Recovery',\n"
                             "              'RT', 'Redo Thread',\n"
                             "              'UN', 'User Name',\n"
                             "              'TX', 'Transaction',\n"
                             "              'TM', 'DML',\n"
                             "              'UL', 'PL/SQL User Lock',\n"
                             "              'DX', 'Distributed Xaction',\n"
                             "              'CF', 'Control File',\n"
                             "              'IS', 'Instance State',\n"
                             "              'FS', 'File Set',\n"
                             "              'IR', 'Instance Recovery',\n"
                             "              'ST', 'Disk Space Transaction',\n"
                             "              'TS', 'Temp Segment',\n"
                             "              'IV', 'Library Cache Invalidation',\n"
                             "              'LS', 'Log Start or Switch',\n"
                             "              'RW', 'Row Wait',\n"
                             "              'SQ', 'Sequence Number',\n"
                             "              'TE', 'Extend Table',\n"
                             "              'TT', 'Temp Table',\n"
                             "              'Internal ('||a.type||')'),\n"
                             "       DECODE(a.lmode,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(a.lmode)),\n"
                             "       DECODE(a.request,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(a.request)),\n"
                             "       d.object_name,\n"
                             "       ' ',\n"
                             "       TO_CHAR(SYSDATE-a.CTIME/3600/24)\n"
                             "  from v$lock a,v$session b,v$locked_object c,sys.all_objects d\n"
                             " where a.sid = b.sid\n"
                             "   and c.session_id = a.sid\n"
                             "   and exists (select 'X'\n"
                             "                 from v$locked_object bb,\n"
                             "                      v$lock cc\n"
                             "                where bb.session_id = cc.sid\n"
                             "                  and cc.sid != a.sid\n"
                             "                  and cc.id1 = a.id1\n"
                             "                  and cc.id2 = a.id2\n"
                             "                  and bb.object_id = c.object_id)\n"
                             "   and d.object_id = c.object_id\n"
                             "   and a.request != 0",
                             "List session blocked by a lock");

static toSQL SQLLock("toResultLock:Locks",
                     "select b.sid,\n"
                     "       b.schemaname,\n"
                     "       b.osuser,\n"
                     "       b.program,\n"
                     "       decode(a.type,\n"
                     "              'MR', 'Media Recovery',\n"
                     "              'RT', 'Redo Thread',\n"
                     "              'UN', 'User Name',\n"
                     "              'TX', 'Transaction',\n"
                     "              'TM', 'DML',\n"
                     "              'UL', 'PL/SQL User Lock',\n"
                     "              'DX', 'Distributed Xaction',\n"
                     "              'CF', 'Control File',\n"
                     "              'IS', 'Instance State',\n"
                     "              'FS', 'File Set',\n"
                     "              'IR', 'Instance Recovery',\n"
                     "              'ST', 'Disk Space Transaction',\n"
                     "              'TS', 'Temp Segment',\n"
                     "              'IV', 'Library Cache Invalidation',\n"
                     "              'LS', 'Log Start or Switch',\n"
                     "              'RW', 'Row Wait',\n"
                     "              'SQ', 'Sequence Number',\n"
                     "              'TE', 'Extend Table',\n"
                     "              'TT', 'Temp Table',\n"
                     "              'Internal ('||a.type||')'),\n"
                     "       DECODE(a.lmode,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(a.lmode)),\n"
                     "       DECODE(e.request,0,'None',1,'Null',2,'Row-S',3,'Row-X',4,'Share',5,'S/Row-X',6,'Exclusive',TO_CHAR(e.request)),\n"
                     "       d.object_name,\n"
                     "       TO_CHAR(SYSDATE-a.CTIME/3600/24),\n"
                     "       TO_CHAR(SYSDATE-e.CTIME/3600/24)\n"
                     "  from v$lock a, v$session b,v$locked_object c,sys.all_objects d,v$lock e\n"
                     " where a.sid = b.sid\n"
                     "   and a.lmode != 0\n"
                     "   and c.session_id = a.sid\n"
                     "   and c.object_id = d.object_id\n"
                     "   and exists (select 'X'\n"
                     "                 from v$locked_object bb,\n"
                     "                      v$lock cc\n"
                     "                where bb.session_id = cc.sid\n"
                     "                  and cc.sid != a.sid\n"
                     "                  and cc.id1 = a.id1\n"
                     "                  and cc.id2 = a.id2\n"
                     "                  and bb.object_id = c.object_id)\n"
                     "   and a.id1 = e.id1\n"
                     "   and a.id2 = e.id2\n"
                     "   and e.sid = :f1<char[101]>\n"
                     "   and e.lmode != e.request\n"
                     "   and e.request != 0",
                     "List locks in a session");

//        'Number of waiters: '||num_waiters waiters,
//        'Seconds in Wait: '||in_wait_secs Seconds,
//        'SecondsSince Last Wait: '||time_since_last_wait_secs sincelw,
//        'Blocking Wait Chain:'||decode(blocker_chain_id,null, '',blocker_chain_id) blocker_chain,
//FROM   V$WAIT_CHAINS  a
//LEFT OUTER JOIN DBA_OBJECTS o ON(o.object_id = a.ROW_WAIT_OBJ#)
//AND    PRIOR a.instance     = a.blocker_instance
//AND    PRIOR a.osid         = a.blocker_osid
//AND    PRIOR a.pid          = a.blocker_pid
//AND    PRIOR a.sid          = a.blocker_sid
//AND    PRIOR a.sess_serial# = a.blocker_sess_serial#
//START WITH a.blocker_is_valid='FALSE'
//ORDER  BY a.chain_id , LEVEL

void toResultLock::startQuery(void)
{
    connect(Query, SIGNAL(dataAvailable(toEventQuery*)), this, SLOT(poll()));
    connect(Query, SIGNAL(done(toEventQuery*, unsigned long)), this, SLOT(queryDone()));
    Query->start();
} // startQuery

void toResultLock::query(const QString &sql, const toQueryParams &param)
{
    Q_UNUSED(sql);

    QString sid;

    if (!handled())
        return ;

    if (!setSqlAndParams(sql, param))
        return ;

    if (Query)
    {
        delete Query;
        Query = NULL;
    }
    clear();
    Checked.clear();

    try
    {
        LastItem = NULL;
        if (!param.isEmpty())
        {
            Query = new toEventQuery(this, connection(), toSQL::string(SQLLock, connection()), param, toEventQuery::READ_ALL);
        }
        else
        {
            Query = new toEventQuery(this, connection(), toSQL::string(SQLBlockingLock, connection()), toQueryParams(), toEventQuery::READ_ALL);
        }
        startQuery();
    }
    TOCATCH
} // query

void toResultLock::poll(void)
{
    try
    {
        if (Query)
        {
            while (Query->hasMore())
            {
                toTreeWidgetItem *item;
                // Check if item has to be added to top level...
                if (!LastItem)
                    item = new toResultViewItem(this, NULL);
                // ...or attached as a child record to some parent.
                else
                    item = new toResultViewItem(LastItem, NULL);
                toQColumnDescriptionList desc = Query->describe();
                for (int pos = 0; pos < desc.size(); pos++)
                    item->setText(pos, (QString)Query->readValue());
            }
        }
    }
    catch (const QString &exc)
    {
        delete Query;
        Query = NULL;
        Utils::toStatusMessage(exc);
    }
} // poll

// Which column is used as a marker for records processed for children. This
// column will get value "Yes" so that it is not processed on on subsequent pass.
#define MARK_COL 20

void toResultLock::queryDone(void)
{
    if (Query)
    {
        delete Query;
        Query = NULL;
    }

    LastItem = NULL;
    toTreeWidgetItem *next = NULL;
    for (toTreeWidgetItem *item = firstChild(); item; item = next)
    {
        int sid = item->text(0).toInt();
        if (item->text(MARK_COL).isEmpty())
        {
            item->setText(MARK_COL, QString::fromLatin1("Yes"));
            item->setOpen(true);
            if (!Checked[sid])
            {
                Checked[sid] = true;
                LastItem = item;
                toQueryParams par = toQueryParams() << LastItem->text(0);
                Query = new toEventQuery(this, connection(), toSQL::string(SQLLock, connection()), par, toEventQuery::READ_ALL);
                startQuery();
            }
            else
            {
                toTreeWidgetItem *cn = NULL;
                for (toTreeWidgetItem *ci = firstChild(); ci; ci = cn)
                {
                    if (ci != item && ci->text(0) == item->text(0))
                    {
                        if (ci->firstChild())
                        {
                            ci = ci->firstChild();
                            cn = new toResultViewItem(item, NULL);
                            for (int i = 0; i < columns(); i++)
                                cn->setText(i, ci->text(i));
                        }
                        break;
                    }
                    if (ci->firstChild())
                    {
                        cn = ci->firstChild();
                    }
                    else if (ci->nextSibling())
                        cn = ci->nextSibling();
                    else
                    {
                        cn = ci;
                        do
                        {
                            cn = cn->parent();
                        }
                        while (cn && !cn->nextSibling());
                        if (cn)
                            cn = cn->nextSibling();
                    }
                }
            }
            break;
        }
        if (item->firstChild())
        {
            next = item->firstChild();
        }
        else if (item->nextSibling())
            next = item->nextSibling();
        else
        {
            next = item;
            do
            {
                next = next->parent();
            }
            while (next && !next->nextSibling());
            if (next)
                next = next->nextSibling();
        }
    }
} // queryDone