/***************************************************************************
 *   SPDX-License-Identifier: GPL-2.0-or-later
 *                                                                         *
 *   SPDX-FileCopyrightText: 2024 Thomas Fischer <fischer@unix-ag.uni-kl.de>
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program 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 this program; if not, see <https://www.gnu.org/licenses/>. *
 ***************************************************************************/

#include "onlinesearchgooglebooks.h"

#include <QFile>
#include <QQueue>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <QRegularExpression>

#ifdef HAVE_KF
#include <KLocalizedString>
#endif // HAVE_KF

#include <FileImporterBibTeX>
#include "isbn.h"
#include "internalnetworkaccessmanager.h"
#include "logging_networking.h"

class OnlineSearchGoogleBooks::Private
{
public:
    QMap<QString, QSharedPointer<Entry>> queuedEntries;

    Private(OnlineSearchGoogleBooks *parent)
    {
        Q_UNUSED(parent)
    }

    QUrl buildQueryUrl(const QMap<QueryKey, QString> &query, int numResults) {
        Q_UNUSED(numResults)

        const QString isbn {
            ISBN::locate(query[QueryKey::FreeText])
        };
        if (!isbn.isEmpty()) {
            return QUrl::fromUserInput(QString(QStringLiteral("https://www.googleapis.com/books/v1/volumes?q=isbn:%1")).arg(isbn));
        }
        else
            return QUrl();
    }


    QVector<QSharedPointer<Entry>> parseJSON(const QByteArray &jsonData, bool *ok) {
        QVector<QSharedPointer<Entry>> result;

        // Source code generated by Python script 'onlinesearch-parser-generator.py'
        // using information from configuration file 'onlinesearchgooglebooks-parser.in.cpp'
#include "onlinesearch/onlinesearchgooglebooks-parser.generated.cpp"

        return result;
    }
};


OnlineSearchGoogleBooks::OnlineSearchGoogleBooks(QObject *parent)
        : OnlineSearchAbstract(parent), d(new Private(this))
{
    // TODO
}

OnlineSearchGoogleBooks::~OnlineSearchGoogleBooks()
{
    delete d;
}

void OnlineSearchGoogleBooks::startSearch(const QMap<QueryKey, QString> &query, int numResults)
{
    m_hasBeenCanceled = false;
    Q_EMIT progress(curStep = 0, numSteps = 2);

    const QUrl url = d->buildQueryUrl(query, numResults);
    if (url.isValid()) {
        QNetworkRequest request(url);
        QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request);
        InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply);
        connect(reply, &QNetworkReply::finished, this, &OnlineSearchGoogleBooks::downloadDone);

        refreshBusyProperty();
    } else
        delayedStoppedSearch(resultNoError);
}

QString OnlineSearchGoogleBooks::label() const
{
    return i18n("Google Books");
}

QUrl OnlineSearchGoogleBooks::homepage() const
{
    return QUrl(QStringLiteral("https://developers.google.com/books"));
}

#ifdef BUILD_TESTING
QVector<QSharedPointer<Entry>> OnlineSearchGoogleBooks::parseGoogleBooks(const QByteArray &jsonData, bool *ok)
{
    return d->parseJSON(jsonData, ok);
}
#endif // BUILD_TESTING

void OnlineSearchGoogleBooks::downloadDone()
{
    Q_EMIT progress(++curStep, numSteps);
    QNetworkReply *reply = static_cast<QNetworkReply *>(sender());

    QUrl redirUrl;
    if (handleErrors(reply, redirUrl)) {
        if (redirUrl.isValid()) {
            /// redirection to another url
            ++numSteps;

            QNetworkRequest request(redirUrl);
            QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request);
            InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply);
            connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleBooks::downloadDone);
        } else {  /// ensure proper treatment of UTF-8 characters
            const QByteArray jsonCode {reply->readAll()};
            bool ok = false;
            const QVector<QSharedPointer<Entry>> entries = d->parseJSON(jsonCode, &ok);

            if (ok) {
                if (entries.isEmpty())
                    stopSearch(resultNoError);
                else {
                    // Data from Google Books can be retrieved via two methods:
                    //  - an JSON-based API as called earlier and evaluated in  parseJSON(..)
                    //  - a BibTeX code retrieval URL as will be done next
                    // Both methods may return different results. It has been observed that
                    // the JSON method lacked a publisher included in the BibTeX code, but the
                    // BibTeX code had only initials in the authors' names whereas the JSON
                    // data had full author names.
                    // Both data source will eventually merged in downloadBibTeXDone()
                    d->queuedEntries.clear();
                    numSteps += entries.size() - 1; //< -1 because we started with numSteps=2 in anticipation of this step here
                    for (const auto &entry : entries) {
                        const QString googleId{PlainTextValue::text(entry->value(QStringLiteral("x-google-id")))};
                        if (!googleId.isEmpty())
                            d->queuedEntries.insert(googleId, entry);
                    }
                    if (!d->queuedEntries.isEmpty()) {
                        auto it = d->queuedEntries.constBegin();
                        const QUrl url{QUrl::fromUserInput(QString(QStringLiteral("https://books.google.com/books/download/?id=%1&hl=en&output=bibtex")).arg(it.key()))};
                        QNetworkRequest request(url);
                        QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request);
                        reply->setProperty("googleid", QVariant::fromValue<QString>(it.key()));
                        InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply);
                        connect(reply, &QNetworkReply::finished, this, &OnlineSearchGoogleBooks::downloadBibTeXDone);
                    } else
                        stopSearch(resultNoError);
                }
            } else {
                qCWarning(LOG_KBIBTEX_NETWORKING) << "Failed to parse JSON data from" << InternalNetworkAccessManager::removeApiKey(reply->url()).toDisplayString();
                stopSearch(resultUnspecifiedError);
            }
        }
    }

    refreshBusyProperty();
}

void OnlineSearchGoogleBooks::downloadBibTeXDone()
{
    Q_EMIT progress(++curStep, numSteps);
    QNetworkReply *reply = static_cast<QNetworkReply *>(sender());

    QUrl redirUrl;
    if (handleErrors(reply, redirUrl)) {
        if (redirUrl.isValid()) {
            /// redirection to another url
            ++numSteps;

            QNetworkRequest request(redirUrl);
            QNetworkReply *newReply = InternalNetworkAccessManager::instance().get(request);
            newReply->setProperty("googleid", reply->property("googleid"));
            InternalNetworkAccessManager::instance().setNetworkReplyTimeout(newReply);
            connect(newReply, &QNetworkReply::finished, this, &OnlineSearchGoogleBooks::downloadBibTeXDone);
        } else {
            const QString bibTeXcode = QString::fromUtf8(reply->readAll().constData());
            const QString googleId{reply->property("googleid").toString()};
            QSharedPointer<Entry> entryFromAPI = d->queuedEntries[googleId];
            FileImporterBibTeX importer(this);
            File *bibtexFile = importer.fromString(bibTeXcode);
            if (bibtexFile != nullptr) {
                if (!bibtexFile->isEmpty()) {
                    QSharedPointer<Entry> entryFromBibTeX = bibtexFile->first().dynamicCast<Entry>();
                    if (!entryFromBibTeX.isNull()) {
                        const auto efbk {entryFromBibTeX->keys()};
                        for (const QString &field : efbk) {
                            if (!entryFromAPI->contains(field))
                                entryFromAPI->insert(field, entryFromBibTeX->value(field));
                        }
                    }
                }
                delete bibtexFile;
            }
            Q_EMIT foundEntry(entryFromAPI);
            d->queuedEntries.remove(googleId);

            if (!d->queuedEntries.isEmpty()) {
                auto it = d->queuedEntries.constBegin();
                const QUrl url{QUrl::fromUserInput(QString(QStringLiteral("https://books.google.com/books/download/?id=%1&hl=en&output=bibtex")).arg(it.key()))};
                QNetworkRequest request(url);
                QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request);
                reply->setProperty("googleid", QVariant::fromValue<QString>(it.key()));
                InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply);
                connect(reply, &QNetworkReply::finished, this, &OnlineSearchGoogleBooks::downloadBibTeXDone);
            } else
                stopSearch(resultNoError);
        }
    }

    refreshBusyProperty();
}

void OnlineSearchGoogleBooks::sanitizeEntry(QSharedPointer<Entry> entry)
{
    OnlineSearchAbstract::sanitizeEntry(entry);

    // Sometimes, the title served from Google is like "Book about Everything, 3rd edition"
    // Idea: extract edition number, remove edition text from title, add edition field to entry
    static const QRegularExpression edition{QStringLiteral("[;:,.]?\\s*\\b(?<number>[1-9]\\d*)(st|nd|rd|th)?\\s+edition\\b"), QRegularExpression::CaseInsensitiveOption};
    const QString title = PlainTextValue::text(entry->value(Entry::ftTitle));
    const QRegularExpressionMatch match = edition.match(title);
    if (match.hasMatch())
    {
        const QString titleWithoutEdition {title.left(match.capturedStart()) + title.mid(match.capturedEnd())};
        entry->insert(Entry::ftTitle, Value() << QSharedPointer<PlainText>(new PlainText(titleWithoutEdition)));
        entry->insert(Entry::ftEdition, Value() << QSharedPointer<PlainText>(new PlainText(match.captured(QStringLiteral("number")))));
    }
}
