/*
 * SPDX-FileCopyrightText: 2020~2020 CSSlayer <wengxt@gmail.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 */
#include "addonmodel.h"
#include "config.h"
#include "model.h"
#include <QAbstractListModel>
#include <QCollator>
#include <QDir>
#include <QFileInfo>
#include <QHash>
#include <QLatin1String>
#include <QMap>
#include <QObject>
#include <QProcess>
#include <QString>
#include <QStringList>
#include <QVariant>
#include <Qt>
#include <fcitx-utils/i18n.h>
#include <fcitx-utils/standardpaths.h>
#include <fcitx/addoninfo.h>
#include <functional>

namespace fcitx::kcm {
namespace {

QString categoryName(int category) {
    if (category >= 5 || category < 0) {
        return QString();
    }

    const char *str[] = {N_("Input Method"), N_("Frontend"), N_("Loader"),
                         N_("Module"), N_("UI")};

    return _(str[category]);
}

} // namespace

AddonModel::AddonModel(QObject *parent) : CategorizedItemModel(parent) {}

QVariant AddonModel::dataForCategory(const QModelIndex &index, int role) const {
    switch (role) {

    case Qt::DisplayRole:
        return categoryName(addonEntryList_[index.row()].first);

    case CategoryRole:
        return addonEntryList_[index.row()].first;

    case RowTypeRole:
        return CategoryType;

    default:
        return QVariant();
    }
}

QVariant AddonModel::dataForItem(const QModelIndex &index, int role) const {
    const auto &addonList = addonEntryList_[index.parent().row()].second;
    const auto &addon = addonList[index.row()];

    switch (role) {
    case Qt::DisplayRole:
        return addon.name();

    case CommentRole:
        return addon.comment();

    case ConfigurableRole:
        return addon.configurable();

    case AddonNameRole:
        return addon.uniqueName();

    case CategoryRole:
        return addon.category();

    case Qt::CheckStateRole:
        if (disabledList_.contains(addon.uniqueName())) {
            return false;
        } else if (enabledList_.contains(addon.uniqueName())) {
            return true;
        }
        return addon.enabled();

    case RowTypeRole:
        return AddonType;
    default:
        break;
    }
    return {};
}

bool AddonModel::setData(const QModelIndex &index, const QVariant &value,
                         int role) {
    if (!index.isValid() || !index.parent().isValid() ||
        index.parent().row() >= addonEntryList_.size() ||
        index.parent().column() > 0 || index.column() > 0) {
        return false;
    }

    const auto &addonList = addonEntryList_[index.parent().row()].second;

    if (index.row() >= addonList.size()) {
        return false;
    }

    bool ret = false;

    const auto &item = addonList[index.row()];
    if (role == Qt::CheckStateRole) {
        auto oldData = data(index, role).toBool();
        auto enabled = value.toBool();
        if (item.enabled() == enabled) {
            enabledList_.remove(item.uniqueName());
            disabledList_.remove(item.uniqueName());
        } else if (enabled) {
            enabledList_.insert(item.uniqueName());
            disabledList_.remove(item.uniqueName());
        } else {
            enabledList_.remove(item.uniqueName());
            disabledList_.insert(item.uniqueName());
        }
        auto newData = data(index, role).toBool();
        ret = oldData != newData;

        if (ret) {
            Q_EMIT dataChanged(index, index);
            Q_EMIT changed(item.uniqueName(), newData);
        }
    }

    return ret;
}
QModelIndex AddonModel::findAddon(const QString &addon) const {
    for (int i = 0; i < addonEntryList_.size(); i++) {
        for (int j = 0; j < addonEntryList_[i].second.size(); j++) {
            const auto &addonList = addonEntryList_[i].second;
            if (addonList[j].uniqueName() == addon) {
                return index(j, 0, index(i, 0));
            }
        }
    }
    return QModelIndex();
}

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

bool FlatAddonModel::setData(const QModelIndex &index, const QVariant &value,
                             int role) {
    if (!index.isValid() || index.row() >= addonEntryList_.size() ||
        index.column() > 0) {
        return false;
    }

    bool ret = false;

    if (role == Qt::CheckStateRole) {
        auto oldData = data(index, role).toBool();
        auto &item = addonEntryList_[index.row()];
        auto enabled = value.toBool();
        if (item.enabled() == enabled) {
            enabledList_.remove(item.uniqueName());
            disabledList_.remove(item.uniqueName());
        } else if (enabled) {
            enabledList_.insert(item.uniqueName());
            disabledList_.remove(item.uniqueName());
        } else {
            enabledList_.remove(item.uniqueName());
            disabledList_.insert(item.uniqueName());
        }
        auto newData = data(index, role).toBool();
        ret = oldData != newData;
    }

    if (ret) {
        Q_EMIT dataChanged(index, index);
        Q_EMIT changed();
    }

    return ret;
}

QVariant FlatAddonModel::data(const QModelIndex &index, int role) const {
    if (!index.isValid() || index.row() >= addonEntryList_.size()) {
        return {};
    }

    const auto &addon = addonEntryList_.at(index.row());

    switch (role) {
    case Qt::DisplayRole:
        return addon.name();

    case CommentRole:
        return addon.comment();

    case ConfigurableRole:
        return addon.configurable();

    case AddonNameRole:
        return addon.uniqueName();

    case CategoryRole:
        return addon.category();

    case CategoryNameRole:
        return categoryName(addon.category());

    case DependenciesRole:
        return reverseDependencies_.value(addon.uniqueName());
        ;

    case OptDependenciesRole:
        return reverseOptionalDependencies_.value(addon.uniqueName());

    case Qt::CheckStateRole:
        if (disabledList_.contains(addon.uniqueName())) {
            return false;
        } else if (enabledList_.contains(addon.uniqueName())) {
            return true;
        }
        return addon.enabled();

    case RowTypeRole:
        return AddonType;

    default:
        break;
    }
    return {};
}

int FlatAddonModel::rowCount(const QModelIndex &parent) const {
    if (parent.isValid()) {
        return 0;
    }

    return addonEntryList_.count();
}

QHash<int, QByteArray> FlatAddonModel::roleNames() const {
    return {{Qt::DisplayRole, "name"},
            {CommentRole, "comment"},
            {ConfigurableRole, "configurable"},
            {AddonNameRole, "uniqueName"},
            {CategoryRole, "category"},
            {CategoryNameRole, "categoryName"},
            {Qt::CheckStateRole, "enabled"},
            {DependenciesRole, "dependencies"},
            {OptDependenciesRole, "optionalDependencies"}};
}

void FlatAddonModel::setAddons(const fcitx::FcitxQtAddonInfoV2List &list) {
    beginResetModel();
    addonEntryList_ = list;
    nameToAddonMap_.clear();
    reverseDependencies_.clear();
    reverseOptionalDependencies_.clear();
    for (const auto &addon : list) {
        nameToAddonMap_[addon.uniqueName()] = addon;
    }
    for (const auto &addon : list) {
        for (const auto &dep : addon.dependencies()) {
            if (!nameToAddonMap_.contains(dep)) {
                continue;
            }
            reverseDependencies_[dep].append(addon.uniqueName());
        }
        for (const auto &dep : addon.optionalDependencies()) {
            if (!nameToAddonMap_.contains(dep)) {
                continue;
            }
            reverseOptionalDependencies_[dep].append(addon.uniqueName());
        }
    }
    enabledList_.clear();
    disabledList_.clear();
    endResetModel();
}

void FlatAddonModel::enable(const QString &addon) {
    for (int i = 0; i < addonEntryList_.size(); i++) {
        if (addonEntryList_[i].uniqueName() == addon) {
            setData(index(i, 0), true, Qt::CheckStateRole);
            return;
        }
    }
}

bool AddonProxyModel::filterAcceptsRow(int sourceRow,
                                       const QModelIndex &sourceParent) const {
    Q_UNUSED(sourceParent)
    const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);

    if (index.data(RowTypeRole) == CategoryType) {
        return filterCategory(index);
    }

    return filterAddon(index);
}

bool AddonProxyModel::filterCategory(const QModelIndex &index) const {
    int childCount = index.model()->rowCount(index);
    if (childCount == 0) {
        return false;
    }

    for (int i = 0; i < childCount; ++i) {
        if (filterAddon(index.model()->index(i, 0, index))) {
            return true;
        }
    }
    return false;
}

bool AddonProxyModel::filterAddon(const QModelIndex &index) const {
    auto name = index.data(Qt::DisplayRole).toString();
    auto uniqueName = index.data(AddonNameRole).toString();
    auto comment = index.data(CommentRole).toString();

    if (!filterText_.isEmpty()) {
        return name.contains(filterText_, Qt::CaseInsensitive) ||
               uniqueName.contains(filterText_, Qt::CaseInsensitive) ||
               comment.contains(filterText_, Qt::CaseInsensitive);
    }

    return true;
}

bool AddonProxyModel::lessThan(const QModelIndex &left,
                               const QModelIndex &right) const {

    int lhs = left.data(CategoryRole).toInt();
    int rhs = right.data(CategoryRole).toInt();
    // Reorder the addon category.
    // UI and module are more common, because input method config is accessible
    // in the main page.
    static const QMap<int, int> category = {
        {static_cast<int>(AddonCategory::UI), 0},
        {static_cast<int>(AddonCategory::Module), 1},
        {static_cast<int>(AddonCategory::InputMethod), 2},
        {static_cast<int>(AddonCategory::Frontend), 3},
        {static_cast<int>(AddonCategory::Loader), 4},
    };

    int lvalue = category.value(lhs, category.size());
    int rvalue = category.value(rhs, category.size());
    int result = lvalue - rvalue;

    if (result < 0) {
        return true;
    }
    if (result > 0) {
        return false;
    }

    QString l = left.data(Qt::DisplayRole).toString();
    QString r = right.data(Qt::DisplayRole).toString();
    return QString::localeAwareCompare(l, r) < 0;
}

void AddonProxyModel::setFilterText(const QString &text) {
    if (filterText_ != text) {
        filterText_ = text;
        invalidate();
    }
}

void launchExternalConfig(const QString &uri, WId wid) {
    if (uri.isEmpty()) {
        return;
    }
    QFileInfo pathToWrapper(FCITX5_QT_GUI_WRAPPER);
    QDir dirToWrapper = pathToWrapper.dir();
    if (uri.startsWith("fcitx://config/addon/")) {
        QLatin1String qt5Wrapper("fcitx5-qt5-gui-wrapper");
        QLatin1String qt6Wrapper("fcitx5-qt6-gui-wrapper");

        QString qt5WrapperPath = dirToWrapper.filePath(qt5Wrapper);
        QString qt6WrapperPath = dirToWrapper.filePath(qt6Wrapper);

        for (QString &wrapperPath :
             {std::ref(qt6WrapperPath), std::ref(qt5WrapperPath)}) {
            if (!QFileInfo(wrapperPath).isExecutable()) {
                wrapperPath = QString::fromStdString(
                    StandardPaths::fcitxPath("libexecdir") /
                    QFileInfo(wrapperPath).fileName().toStdString());
            }
        }

        QString wrapperToUse;
        for (const QString &wrapperPath :
             {std::cref(qt6WrapperPath), std::cref(qt5WrapperPath)}) {
            QStringList args;
            args << QLatin1String("--test");
            args << uri;
            int exit_status = QProcess::execute(wrapperPath, args);
            if (exit_status == 0) {
                wrapperToUse = wrapperPath;
                break;
            }
        }

        if (wrapperToUse.isEmpty()) {
            return;
        }

        QStringList args;
        if (wid) {
            args << "-w";
            args << QString::number(wid);
        }
        args << uri;
        QProcess::startDetached(wrapperToUse, args);
    } else {
        // Assume this is a program path.
        QStringList args = QProcess::splitCommand(uri);
        if (args.isEmpty()) {
            return;
        }
        QString program = args.takeFirst();
        QProcess::startDetached(program, args);
    }
}

} // namespace fcitx::kcm
