/*
 * 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 "decodermodel.h"

#include <utils/helpers.h>

#include <QIODevice>
#include <QMimeData>

using namespace Qt::StringLiterals;

constexpr auto LoaderItems = "application/x-fooyin-loaderitems";

namespace Fooyin {
namespace {
QString supportedExtensionsTooltip(const QStringList& extensions, const QString& title)
{
    static constexpr auto MaxLineLength = 120;

    QStringList lines;
    QString currentLine;

    for(const QString& extension : extensions) {
        const QString segment = currentLine.isEmpty() ? extension : u", %1"_s.arg(extension);

        if(!currentLine.isEmpty() && currentLine.size() + segment.size() > MaxLineLength) {
            lines.emplace_back(currentLine);
            currentLine = extension;
            continue;
        }

        currentLine += segment;
    }

    if(!currentLine.isEmpty()) {
        lines.emplace_back(std::move(currentLine));
    }

    return lines.isEmpty() ? title : u"%1:\n%2"_s.arg(title, lines.join(u"\n"_s));
}
} // namespace

DecoderModel::DecoderModel(QObject* parent)
    : QAbstractListModel{parent}
{ }

void DecoderModel::setup(const LoaderVariant& loaders, SettingsHandlerMap settingsHandlers)
{
    beginResetModel();
    m_loaders          = loaders;
    m_settingsHandlers = std::move(settingsHandlers);
    endResetModel();
}

DecoderModel::LoaderVariant DecoderModel::loaders() const
{
    return m_loaders;
}

bool DecoderModel::showSettings(const QModelIndex& index, QWidget* parent)
{
    if(!checkIndex(index, CheckIndexOption::IndexIsValid)) {
        return false;
    }

    const auto showSettingsForLoader = [&](const auto& loaders) {
        if(index.row() < 0 || std::cmp_greater_equal(index.row(), loaders.size())) {
            return false;
        }

        const auto& loader    = loaders.at(index.row());
        const auto settingsIt = m_settingsHandlers.find(loader.name);
        if(settingsIt == m_settingsHandlers.cend()) {
            return false;
        }

        settingsIt->second(parent);
        return true;
    };

    return std::visit(showSettingsForLoader, m_loaders);
}

Qt::ItemFlags DecoderModel::flags(const QModelIndex& index) const
{
    auto flags = QAbstractListModel::flags(index);
    if(index.isValid()) {
        flags |= Qt::ItemIsDragEnabled | Qt::ItemIsUserCheckable;
    }
    else {
        flags |= Qt::ItemIsDropEnabled;
    }
    return flags;
}

int DecoderModel::rowCount(const QModelIndex& /*parent*/) const
{
    return std::visit([](const auto& loaders) { return static_cast<int>(loaders.size()); }, m_loaders);
}

QVariant DecoderModel::data(const QModelIndex& index, int role) const
{
    if(!checkIndex(index, CheckIndexOption::IndexIsValid)) {
        return {};
    }

    auto getDataForRole = [&](const auto& loaders) -> QVariant {
        if(index.row() < 0 || index.row() >= static_cast<int>(loaders.size())) {
            return {};
        }

        const auto& loader = loaders.at(index.row());

        switch(role) {
            case Qt::DisplayRole:
                return loader.name;
            case Qt::ToolTipRole:
                return supportedExtensionsTooltip(loader.extensions, tr("Supported extensions"));
            case Qt::CheckStateRole:
                return loader.enabled ? Qt::Checked : Qt::Unchecked;
            case HasSettings:
                return m_settingsHandlers.contains(loader.name);
            default:
                return {};
        }
    };

    return std::visit(getDataForRole, m_loaders);
}

bool DecoderModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
    if(!checkIndex(index, CheckIndexOption::IndexIsValid)) {
        return false;
    }

    auto setDataForRole = [&](auto& loaders) -> bool {
        if(index.row() < 0 || index.row() >= static_cast<int>(loaders.size())) {
            return false;
        }

        auto& loader = loaders.at(index.row());

        if(role == Qt::CheckStateRole) {
            const bool isChecked = (value.value<Qt::CheckState>() == Qt::Checked);
            if(std::exchange(loader.enabled, isChecked) != isChecked) {
                Q_EMIT dataChanged(index, index, {role});
                return true;
            }
        }

        return false;
    };

    return std::visit(setDataForRole, m_loaders);
}

QStringList DecoderModel::mimeTypes() const
{
    return {QString::fromLatin1(LoaderItems)};
}

QMimeData* DecoderModel::mimeData(const QModelIndexList& indexes) const
{
    QStringList selected;
    for(const QModelIndex& index : indexes) {
        selected.emplace_back(index.data(Qt::DisplayRole).toString());
    }

    QByteArray data;
    QDataStream stream{&data, QIODevice::WriteOnly};
    stream << selected;

    auto* mimeData = new QMimeData();
    mimeData->setData(QString::fromLatin1(LoaderItems), data);
    return mimeData;
}

bool DecoderModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column,
                                   const QModelIndex& parent) const
{
    if(action == Qt::MoveAction && data->hasFormat(QString::fromLatin1(LoaderItems))) {
        return true;
    }

    return QAbstractItemModel::canDropMimeData(data, action, row, column, parent);
}

bool DecoderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column,
                                const QModelIndex& parent)
{
    if(!canDropMimeData(data, action, row, column, parent)) {
        return false;
    }

    if(row < 0) {
        row = std::visit([](const auto& loaders) { return static_cast<int>(loaders.size()); }, m_loaders);
    }

    if(data->hasFormat(QString::fromLatin1(LoaderItems))) {
        QByteArray decoderData = data->data(QString::fromLatin1(LoaderItems));
        QStringList selected;
        QDataStream stream(&decoderData, QIODevice::ReadOnly);
        stream >> selected;

        beginResetModel();

        auto handleDrop = [&selected, &row](auto& loaders) {
            for(const QString& loaderName : std::as_const(selected)) {
                auto loaderIt = std::find_if(loaders.begin(), loaders.end(),
                                             [&](const auto& loader) { return loader.name == loaderName; });

                if(loaderIt != loaders.end()) {
                    const int currentIndex = static_cast<int>(std::distance(loaders.begin(), loaderIt));
                    const int newIndex     = row > currentIndex ? row - 1 : row;
                    Utils::move(loaders, currentIndex, newIndex);
                    ++row;
                }
            }

            for(int i{0}; auto& loader : loaders) {
                loader.index = i++;
            }
        };

        std::visit(handleDrop, m_loaders);

        endResetModel();
    }

    return true;
}

Qt::DropActions DecoderModel::supportedDropActions() const
{
    return Qt::MoveAction;
}

Qt::DropActions DecoderModel::supportedDragActions() const
{
    return Qt::MoveAction;
}
} // namespace Fooyin
