/*
 * Fooyin
 * Copyright © 2024, Luke Taylor <luket@pm.me>
 *
 * Fooyin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin 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 Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "dirproxymodel.h"

#include <gui/guiconstants.h>
#include <gui/iconloader.h>
#include <utils/modelutils.h>
#include <utils/utils.h>

#include <QAbstractFileIconProvider>
#include <QApplication>
#include <QFileInfo>
#include <QFileSystemModel>
#include <QPalette>
#include <QPixmap>
#include <algorithm>

using namespace Qt::StringLiterals;

namespace Fooyin {
DirProxyModel::DirProxyModel(bool flat, QObject* parent)
    : QSortFilterProxyModel{parent}
    , m_flat{flat}
    , m_playingState{Player::PlayState::Stopped}
    , m_showIcons{true}
    , m_playingColour{QApplication::palette().highlight().color()}
{
    m_playingColour.setAlpha(90);
    setRecursiveFilteringEnabled(true);
}

void DirProxyModel::reset(const QModelIndex& root)
{
    if(!sourceModel()) {
        return;
    }

    beginResetModel();

    m_sourceRoot = root;
    m_nodes.clear();

    QDir path{root.data(QFileSystemModel::FilePathRole).toString()};
    m_rootPath = path.absolutePath();
    m_goUpPath = path.cdUp() ? path.absolutePath() : QString{};

    if(m_flat) {
        populate();
    }

    endResetModel();
}

void DirProxyModel::resetPalette()
{
    m_playingColour = QApplication::palette().highlight().color();
    m_playingColour.setAlpha(90);
    Utils::recursiveDataChanged(this, {}, {Qt::BackgroundRole});
}

void DirProxyModel::setSourceModel(QAbstractItemModel* model)
{
    if(model == sourceModel()) {
        return;
    }

    if(sourceModel()) {
        disconnect(sourceModel(), nullptr, this, nullptr);
    }

    if(auto* fileModel = qobject_cast<QFileSystemModel*>(model)) {
        m_iconProvider = fileModel->iconProvider();
    }

    // We only need to handle rowsRemoved as layoutChanged is emitted from the sourceModel
    // for all other changes, which resets this model
    QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &DirProxyModel::sourceRowsRemoved);

    QSortFilterProxyModel::setSourceModel(model);
}

Qt::ItemFlags DirProxyModel::flags(const QModelIndex& index) const
{
    if(!m_flat) {
        return QSortFilterProxyModel::flags(index);
    }

    if(!index.isValid()) {
        return Qt::NoItemFlags;
    }

    if(canGoUp() && index.row() <= 0) {
        return sourceModel()->flags(m_sourceRoot);
    }

    return sourceModel()->flags(mapToSource(index));
}

QVariant DirProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(role == Qt::TextAlignmentRole) {
        return Qt::AlignCenter;
    }

    return sourceModel()->headerData(section, orientation, role);
}

QVariant DirProxyModel::data(const QModelIndex& proxyIndex, int role) const
{
    if(!proxyIndex.isValid()) {
        return {};
    }

    if(!m_showIcons && role == Qt::DecorationRole) {
        return {};
    }

    if(role == Qt::ToolTipRole) {
        role = Qt::DisplayRole;
    }

    QString sourcePath;

    if(m_flat) {
        if(canGoUp() && proxyIndex.row() == 0 && proxyIndex.column() == 0) {
            if(role == Qt::DisplayRole) {
                return u"…"_s;
            }
            if(role == QFileSystemModel::FilePathRole) {
                return {};
            }
            if(role == Qt::TextAlignmentRole) {
                return Qt::AlignBottom;
            }
            if(m_iconProvider && m_showIcons && role == Qt::DecorationRole) {
                return m_iconProvider->icon(QAbstractFileIconProvider::IconType::Folder);
            }
        }
        sourcePath = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole).toString();
    }
    else {
        sourcePath = QSortFilterProxyModel::data(proxyIndex, QFileSystemModel::FilePathRole).toString();
    }

    const bool isPlayingTrack = m_playingState != Player::PlayState::Stopped && !m_playingTrackPath.isEmpty()
                             && sourcePath == m_playingTrackPath;

    if(role == IsPlaying) {
        return isPlayingTrack;
    }

    if(isPlayingTrack) {
        if(role == Qt::BackgroundRole) {
            return m_playingColour;
        }
        if(role == Qt::DecorationRole) {
            switch(m_playingState) {
                case(Player::PlayState::Playing):
                    return Gui::pixmapFromTheme(Constants::Icons::Play);
                case(Player::PlayState::Paused):
                    return Gui::pixmapFromTheme(Constants::Icons::Pause);
                case(Player::PlayState::Stopped):
                    break;
            }
        }
    }

    if(m_flat) {
        return sourceModel()->data(mapToSource(proxyIndex), role);
    }

    return QSortFilterProxyModel::data(proxyIndex, role);
}

QModelIndex DirProxyModel::parent(const QModelIndex& child) const
{
    if(m_flat) {
        return {};
    }

    return QSortFilterProxyModel::parent(child);
}

QModelIndex DirProxyModel::sibling(int row, int column, const QModelIndex& index) const
{
    if(!m_flat) {
        return QSortFilterProxyModel::sibling(row, column, index);
    }

    if(!index.isValid()) {
        return {};
    }

    if(row < 0 || std::cmp_greater_equal(row, m_nodes.size())) {
        return {};
    }

    return createIndex(row, column, m_nodes.at(row).get());
}

bool DirProxyModel::hasChildren(const QModelIndex& parent) const
{
    if(m_flat) {
        return !parent.isValid();
    }

    return QSortFilterProxyModel::hasChildren(parent);
}

QModelIndex DirProxyModel::index(int row, int column, const QModelIndex& parent) const
{
    if(!m_flat) {
        return QSortFilterProxyModel::index(row, column, parent);
    }

    if(!hasIndex(row, column, parent)) {
        return {};
    }

    if(canGoUp() && row == 0) {
        return createIndex(row, column, nullptr);
    }

    return createIndex(row, column, m_nodes.at(row).get());
}

int DirProxyModel::rowCount(const QModelIndex& index) const
{
    if(!m_flat) {
        return QSortFilterProxyModel::rowCount(index);
    }

    return index.isValid() ? 0 : nodeCount();
}

int DirProxyModel::columnCount(const QModelIndex& /*index*/) const
{
    return 1;
}

QModelIndex DirProxyModel::mapFromSource(const QModelIndex& index) const
{
    if(!m_flat) {
        return QSortFilterProxyModel::mapFromSource(index);
    }

    if(!index.isValid() || m_nodes.empty()) {
        return {};
    }

    const auto indexIt
        = std::ranges::find_if(m_nodes, [&index](const auto& node) { return node->sourceIndex == index; });
    if(indexIt != m_nodes.cend()) {
        const auto row = static_cast<int>(std::distance(m_nodes.begin(), indexIt));
        return createIndex(row, 0, indexIt->get());
    }

    return {};
}

QModelIndex DirProxyModel::mapToSource(const QModelIndex& index) const
{
    if(!m_flat) {
        return QSortFilterProxyModel::mapToSource(index);
    }

    if(!index.isValid() || m_nodes.empty()) {
        return {};
    }

    if(auto* node = static_cast<DirNode*>(index.internalPointer())) {
        return node->sourceIndex;
    }

    return {};
}

bool DirProxyModel::canGoUp() const
{
    return !m_goUpPath.isEmpty();
}

void DirProxyModel::setFlat(bool isFlat)
{
    m_flat = isFlat;

    if(auto* fileModel = qobject_cast<QFileSystemModel*>(sourceModel())) {
        reset(fileModel->index(fileModel->rootPath()));
    }
}

void DirProxyModel::setIconsEnabled(bool enabled)
{
    m_showIcons = enabled;
    Utils::recursiveDataChanged(this, {});
}

void DirProxyModel::setPlayState(Player::PlayState state)
{
    m_playingState = state;
    Utils::recursiveDataChanged(this, {}, {Qt::BackgroundRole, Qt::DecorationRole, IsPlaying});
}

void DirProxyModel::setPlayingPath(const QString& path)
{
    m_playingTrackPath = path;
    Utils::recursiveDataChanged(this, {}, {Qt::BackgroundRole, Qt::DecorationRole, IsPlaying});
}

void DirProxyModel::setSearchText(const QString& text)
{
    if(std::exchange(m_searchText, text) == text) {
        return;
    }

    if(m_flat) {
        reset(m_sourceRoot);
    }
    else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
        beginFilterChange();
        endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
        invalidateRowsFilter();
#endif
    }
}

bool DirProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
    if(m_flat || m_searchText.isEmpty()) {
        return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
    }

    return matchesSearch(sourceModel()->index(sourceRow, 0, sourceParent));
}

bool DirProxyModel::matchesSearch(const QModelIndex& sourceIndex) const
{
    if(m_searchText.isEmpty()) {
        return true;
    }

    const QString path = sourceIndex.data(QFileSystemModel::FilePathRole).toString();
    const QString name = path.isEmpty() ? sourceIndex.data(Qt::DisplayRole).toString() : QFileInfo{path}.fileName();
    return name.contains(m_searchText, Qt::CaseInsensitive);
}

void DirProxyModel::populate()
{
    m_nodes.clear();

    if(canGoUp()) {
        m_nodes.emplace_back(std::make_unique<DirNode>(QModelIndex{}));
    }

    const int rowCount = sourceModel()->rowCount(m_sourceRoot);

    if(rowCount <= 0) {
        return;
    }

    const int last = rowCount - 1;

    m_nodes.reserve(m_nodes.size() + rowCount);

    for(int row{0}; row <= last; ++row) {
        const QModelIndex sourceIndex = sourceModel()->index(row, 0, m_sourceRoot);
        if(matchesSearch(sourceIndex)) {
            m_nodes.emplace_back(std::make_unique<DirNode>(sourceIndex));
        }
    }
}

int DirProxyModel::nodeCount() const
{
    return static_cast<int>(m_nodes.size());
}

void DirProxyModel::sourceRowsRemoved(const QModelIndex& parent, int first, int last)
{
    if(!m_searchText.isEmpty()) {
        reset(m_sourceRoot);
        return;
    }

    const QString path = parent.data(QFileSystemModel::FilePathRole).toString();
    if(path != m_rootPath) {
        return;
    }

    const int firstRow = first + (canGoUp() ? 1 : 0);
    const int lastRow  = last + (canGoUp() ? 1 : 0);

    if(firstRow < 0 || std::cmp_greater_equal(firstRow, m_nodes.size())) {
        return;
    }

    if(lastRow < 0 || std::cmp_greater_equal(lastRow, m_nodes.size())) {
        return;
    }

    beginRemoveRows({}, firstRow, lastRow);
    m_nodes.erase(m_nodes.begin() + firstRow, std::next(m_nodes.begin() + lastRow));
    endRemoveRows();
}
} // namespace Fooyin
