/*
    SPDX-FileCopyrightText: 2005-2007 Joris Guisson <joris.guisson@gmail.com>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include <QStringList>
#include <QTextStream>
#include <QUrlQuery>

#include <KIO/CopyJob>
#include <KLocalizedString>

#include "opensearchdownloadjob.h"
#include "searchenginelist.h"
#include <interfaces/functions.h>
#include <util/error.h>
#include <util/fileops.h>
#include <util/log.h>

using namespace bt;

namespace kt
{
SearchEngineList::SearchEngineList(ProxyHelper *proxy, const QString &data_dir)
    : m_proxy(proxy)
    , data_dir(data_dir)
{
    // default_opensearch_urls << QUrl(QStringLiteral("https://torrentproject.com"));
    default_urls << QUrl(QStringLiteral("http://btdig.com")) << QUrl(QStringLiteral("http://btdb.in")) << QUrl(QStringLiteral("https://torrentproject.se"))
                 << QUrl(QStringLiteral("http://duckduckgo.com"));
}

SearchEngineList::~SearchEngineList()
{
    qDeleteAll(engines);
}

void SearchEngineList::loadEngines()
{
    if (!bt::Exists(data_dir)) {
        if (bt::Exists(kt::DataDir() + QStringLiteral("search_engines"))) {
            try {
                if (!bt::Exists(data_dir))
                    bt::MakeDir(data_dir);
            } catch (...) {
                return;
            }

            convertSearchEnginesFile();
        } else {
            Out(SYS_SRC | LOG_DEBUG) << "Setting up default engines" << endl;
            addDefaults();
        }
    } else {
        QStringList subdirs = QDir(data_dir).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
        for (const QString &sd : qAsConst(subdirs)) {
            // Load only if there is an opensearch.xml file and not a removed file
            if (bt::Exists(data_dir + sd + QStringLiteral("/opensearch.xml")) && !bt::Exists(data_dir + sd + QStringLiteral("/removed"))) {
                Out(SYS_SRC | LOG_DEBUG) << "Loading " << sd << endl;
                SearchEngine *se = new SearchEngine(data_dir + sd + QLatin1Char('/'));
                if (!se->load(data_dir + sd + QStringLiteral("/opensearch.xml")))
                    delete se;
                else
                    engines.append(se);
            }
        }

        // check if new engines have been added
        loadDefault(false);
    }
}

void SearchEngineList::convertSearchEnginesFile()
{
    QFile fptr(kt::DataDir() + QStringLiteral("search_engines"));
    if (!fptr.open(QIODevice::ReadOnly)) {
        addDefaults();
        return;
    }

    QTextStream in(&fptr);

    while (!in.atEnd()) {
        QString line = in.readLine();

        if (line.startsWith(QLatin1Char('#')) || line.startsWith(QLatin1Char(' ')) || line.isEmpty())
            continue;

        QStringList tokens = line.split(QLatin1Char(' '));
        QString name = tokens[0];
        name = name.replace(QLatin1String("%20"), QLatin1String(" "));
        QUrlQuery url = QUrlQuery(QUrl(tokens[1]));

        for (Uint32 i = 2; i < (Uint32)tokens.count(); ++i)
            url.addQueryItem(tokens[i].section(QLatin1Char('='), 0, 0), tokens[i].section(QLatin1Char('='), 1, 1));

        try {
            QString dir = data_dir + name;
            if (!dir.endsWith(QLatin1Char('/')))
                dir += QLatin1Char('/');

            bt::MakeDir(dir);
            addEngine(dir, url.toString().replace(QLatin1String("FOOBAR"), QLatin1String("{searchTerms}")));
        } catch (bt::Error &err) {
            Out(SYS_SRC | LOG_NOTICE) << err.toString() << endl;
        }
    }
}

QUrl SearchEngineList::search(bt::Uint32 engine, const QString &terms)
{
    QUrl u;
    if (engine < (Uint32)engines.count())
        u = engines[engine]->search(terms);

    Out(SYS_SRC | LOG_NOTICE) << "Searching " << u.toDisplayString() << endl;
    return u;
}

QString SearchEngineList::getEngineName(bt::Uint32 engine) const
{
    if (engine >= (Uint32)engines.count())
        return QString();
    else
        return engines[engine]->engineName();
}

void SearchEngineList::openSearchDownloadJobFinished(KJob *j)
{
    OpenSearchDownloadJob *osdj = (OpenSearchDownloadJob *)j;
    if (!osdj->error()) {
        SearchEngine *se = new SearchEngine(osdj->directory());
        if (!se->load(osdj->directory() + QStringLiteral("opensearch.xml"))) {
            delete se;
            bt::Delete(osdj->directory(), true);
        } else {
            engines.append(se);
            insertRow(engines.count() - 1);
        }
    } else {
        bt::Delete(osdj->directory(), true);
    }
}

void SearchEngineList::addEngine(OpenSearchDownloadJob *j)
{
    openSearchDownloadJobFinished(j);
}

void SearchEngineList::addEngine(const QString &dir, const QString &url)
{
    QFile fptr(dir + QStringLiteral("opensearch.xml"));
    if (!fptr.open(QIODevice::WriteOnly))
        throw bt::Error(i18n("Cannot open %1: %2", dir + QStringLiteral("opensearch.xml"), fptr.errorString()));

    QUrl kurl(url);
    QTextStream out(&fptr);
    QString xml_template = QStringLiteral(
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
        "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
        "<ShortName>%1</ShortName>\n"
        "<Url type=\"text/html\" template=\"%2\" />\n"
        "<Image>%3/favicon.ico</Image>\n"
        "</OpenSearchDescription>\n");

    QString base = kurl.scheme() + QStringLiteral("://") + kurl.host();
    if (kurl.port() > 0)
        base += QString::fromLatin1(":%1").arg(kurl.port());

    QString tmp = url;
    tmp = tmp.replace(QLatin1Char('&'), QLatin1String("&amp;"));
    out << xml_template.arg(kurl.host()).arg(tmp).arg(base) << Qt::endl;

    SearchEngine *se = new SearchEngine(dir);

    if (!se->load(dir + QStringLiteral("opensearch.xml"))) {
        delete se;
        throw bt::Error(i18n("Failed to parse %1", dir + QStringLiteral("opensearch.xml")));
    }

    engines.append(se);
    insertRow(engines.count() - 1);
}

void SearchEngineList::removeEngines(const QModelIndexList &sel)
{
    QList<SearchEngine *> to_remove;
    for (const QModelIndex &idx : sel) {
        if (idx.isValid() && idx.row() >= 0 && idx.row() < engines.count())
            to_remove.append(engines.at(idx.row()));
    }

    beginResetModel();
    for (SearchEngine *se : qAsConst(to_remove)) {
        bt::Touch(se->engineDir() + QStringLiteral("removed"));
        engines.removeAll(se);
        delete se;
    }
    endResetModel();
}

void SearchEngineList::removeAllEngines()
{
    beginResetModel();
    removeRows(0, engines.count(), QModelIndex());
    engines.clear();
    endResetModel();
}

void SearchEngineList::addDefaults()
{
    // data dir does not exist yet so create it and add the default list
    try {
        if (!bt::Exists(data_dir))
            bt::MakeDir(data_dir);
    } catch (...) {
        return;
    }

    beginResetModel();
    for (const QUrl &u : qAsConst(default_opensearch_urls)) {
        Out(SYS_SRC | LOG_DEBUG) << "Setting up default engine " << u.toDisplayString() << endl;
        QString dir = data_dir + u.host() + QLatin1Char('/');
        if (!bt::Exists(dir)) {
            OpenSearchDownloadJob *j = new OpenSearchDownloadJob(u, dir, m_proxy);
            connect(j, &OpenSearchDownloadJob::result, this, &SearchEngineList::openSearchDownloadJobFinished);
            j->start();
        } else {
            loadEngine(dir, dir, true);
        }
    }

    // also add the engines which don't have an opensearch description
    loadDefault(true);
    endResetModel();
}

void SearchEngineList::loadEngine(const QString &global_dir, const QString &user_dir, bool load_removed)
{
    if (!bt::Exists(user_dir)) {
        // create directory to store icons
        bt::MakeDir(user_dir);
    }

    if (bt::Exists(user_dir + QStringLiteral("removed"))) {
        // if the removed file is there don't load, if we are not allowed
        if (!load_removed)
            return;
        else
            bt::Delete(user_dir + QStringLiteral("removed"));
    }

    if (!alreadyLoaded(user_dir)) {
        SearchEngine *se = new SearchEngine(user_dir);
        if (!se->load(global_dir + QStringLiteral("opensearch.xml")))
            delete se;
        else
            engines.append(se);
    }
}

void SearchEngineList::loadDefault(bool removed_to)
{
    QStringList dir_list =
        QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("ktorrent/opensearch"), QStandardPaths::LocateDirectory);
    if (dir_list.isEmpty())
        dir_list = QStandardPaths::locateAll(QStandardPaths::DataLocation, QStringLiteral("ktorrent/opensearch"), QStandardPaths::LocateDirectory);
    if (dir_list.isEmpty())
        dir_list = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("ktorrent/opensearch"), QStandardPaths::LocateDirectory);

    for (const QString &dir : qAsConst(dir_list)) {
        const QStringList subdirs = QDir(dir).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
        for (const QString &sd : subdirs) {
            loadEngine(QDir::cleanPath(dir) + QLatin1Char('/') + sd + QLatin1Char('/'), data_dir + sd + QLatin1Char('/'), removed_to);
        }
    }
}

bool SearchEngineList::alreadyLoaded(const QString &user_dir)
{
    for (const SearchEngine *se : qAsConst(engines)) {
        if (se->engineDir() == user_dir)
            return true;
    }

    return false;
}

int SearchEngineList::rowCount(const QModelIndex &parent) const
{
    if (!parent.isValid())
        return engines.count();
    else
        return 0;
}

QVariant SearchEngineList::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    SearchEngine *se = engines.at(index.row());
    if (!se)
        return QVariant();

    if (role == Qt::DisplayRole) {
        return se->engineName();
    } else if (role == Qt::DecorationRole) {
        return se->engineIcon();
    } else if (role == Qt::ToolTipRole) {
        return i18n("URL: <b>%1</b>", se->engineUrl());
    }

    return QVariant();
}

bool SearchEngineList::insertRows(int row, int count, const QModelIndex &parent)
{
    Q_UNUSED(parent);
    beginInsertRows(QModelIndex(), row, row + count - 1);
    endInsertRows();
    return true;
}

bool SearchEngineList::removeRows(int row, int count, const QModelIndex &parent)
{
    Q_UNUSED(parent);
    beginRemoveRows(QModelIndex(), row, row + count - 1);
    for (int i = 0; i < count; i++) {
        SearchEngine *se = engines.takeAt(row);
        bt::Touch(se->engineDir() + QLatin1String("removed"));
        delete se;
    }
    endRemoveRows();
    return true;
}
}
