/*
    This file is part of Kiten, a KDE Japanese Reference Tool...
    SPDX-FileCopyrightText: 2011 Daniel E. Moctezuma <democtezuma@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "dictionaryupdatemanager.h"

#include "kiten.h"
#include "kitenconfig.h"
#include "kitenmacros.h"

#include <KActionCollection>
#include <KCompressionDevice>
#include <KIO/StoredTransferJob>
#include <KLocalizedString>
#include <KMessageBox>
#include <QAction>
#include <QDir>
#include <QUrl>

#include <QStandardPaths>
#include <QStringDecoder>
#include <QTemporaryFile>
#include <QTextStream>

// URL to the information file.
#define INFO_URL QStringLiteral("http://ftp.edrdg.org/pub/Nihongo/edicthdr.txt")
// URL to the EDICT dictionary.
#define EDICT_URL QStringLiteral("http://ftp.edrdg.org/pub/Nihongo/edict.gz")
// URL to the KANJIDIC dictionary.
#define KANJIDIC_URL QStringLiteral("http://ftp.edrdg.org/pub/Nihongo/kanjidic.gz")

using namespace Qt::StringLiterals;

DictionaryUpdateManager::DictionaryUpdateManager(Kiten *parent)
    : QObject(parent)
    , _parent(parent)
    , _config(parent->getConfig())
    , _succeeded(QStringList())
    , _failed(QStringList())
    , _counter(0)
{
    _actionUpdate = _parent->actionCollection()->add<QAction>(QStringLiteral("update_dictionaries"));
    _actionUpdate->setText(i18n("Check for dictionary &updates"));
    _parent->actionCollection()->setDefaultShortcut(_actionUpdate, Qt::CTRL | Qt::Key_U);

    connect(_actionUpdate, &QAction::triggered, this, &DictionaryUpdateManager::checkForUpdates);
}

void DictionaryUpdateManager::checkForUpdates()
{
    qDebug() << "Checking for EDICT & KANJIDIC updates.";
    // Download the information file we need to check
    // whether or not an update to our dictionaries is necessary.
    KIO::StoredTransferJob *job = KIO::storedGet(QUrl(INFO_URL));
    connect(job, &KIO::StoredTransferJob::result, this, &DictionaryUpdateManager::checkInfoFile);
}

void DictionaryUpdateManager::checkIfUpdateFinished()
{
    // Emit the updateFinished signal once we finished
    // to update our installed dictionaries.
    if (_counter == _config->dictionary_list().size()) {
        // Make sure to reset this variable to 0.
        _counter = 0;
        Q_EMIT updateFinished();
    }
}

void DictionaryUpdateManager::checkInfoFile(KJob *job)
{
    KIO::StoredTransferJob *storedJob = static_cast<KIO::StoredTransferJob *>(job);
    QByteArray data(storedJob->data());
    // Check if we have valid data to work with.
    if (data.isNull() || data.isEmpty()) {
        KMessageBox::error(nullptr, i18n("Update canceled.\nCould not read file."));
        job->deleteLater();
        return;
    }

    // We don't need to store this information file in our Hard Disk Drive,
    // a temporary file is enough.
    QTemporaryFile tempFile;
    if (!tempFile.open()) {
        KMessageBox::error(nullptr, i18n("Update canceled.\nCould not open file."));
        qDebug() << "Could not open tempFile.";
        tempFile.deleteLater();
        job->deleteLater();
        return;
    }
    tempFile.write(data);
    tempFile.close();

    // Get the latest creation date for the EDICT dictionary on the server.
    QDate webFileDate = getFileDate(tempFile);
    if (!webFileDate.isValid()) {
        // The program should not get to this point.
        // Maybe the format/content of (one or both) the files changed?
        // or maybe one or both of the files could not be opened in the
        // getFileDate function that would return an invalid empty date.
        KMessageBox::error(nullptr, i18n("Update canceled.\nThe update information file has an invalid date."));
        tempFile.deleteLater();
        job->deleteLater();
        return;
    }

    // At this point we have a valid creation date from the server.
    // Now we can check our dictionaries.

    // Connect to this signal to know when the update process is finished.
    connect(this, &DictionaryUpdateManager::updateFinished, this, &DictionaryUpdateManager::showUpdateResults);

    // This variable help us to know if we need to download any file.
    bool updates = false;
    // Iterate on each of our installed dictionaries.
    for (const QString &dict : _config->dictionary_list()) {
        QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kiten/") + dict.toLower());
        QFile file(filePath);
        qDebug() << "Local dictionary path:" << file.fileName();

        // Get the creation date for this dictionary.
        QDate localFileDate = getFileDate(file);

        if (!localFileDate.isValid()) {
            // Add it to our 'failed to udate' list.
            _failed.append(dict.toUpper());
            qDebug() << "Failed (invalid date):" << dict.toUpper();
            continue;
        } else if (localFileDate == webFileDate) {
            // Add it to our 'up to date' list.
            _succeeded.append(dict.toUpper());
            qDebug() << "Success (up to date):" << dict.toUpper();
            continue;
        } else {
            // Indicate we need to download something.
            updates = true;

            if (dict.toLower() == EDICT) {
                downloadDictionary(EDICT_URL);
            } else {
                downloadDictionary(KANJIDIC_URL);
            }
        }
    }

    // Let Kiten know we finished and don't need to download anything else.
    if (!updates) {
        Q_EMIT updateFinished();
    }

    tempFile.deleteLater();
    job->deleteLater();
}

void DictionaryUpdateManager::downloadDictionary(const QString &url)
{
    qDebug() << "Download started!";
    // Download dictionary.
    KIO::StoredTransferJob *dictionaryJob = KIO::storedGet(QUrl(url));
    connect(dictionaryJob, &KIO::StoredTransferJob::result, this, &DictionaryUpdateManager::installDictionary);
}

QDate DictionaryUpdateManager::getFileDate(QFile &file)
{
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug() << "Could not open " << file.fileName();
        return QDate();
    }

    QStringDecoder decoder("EUC-JP");
    const QString decoded = decoder(file.readAll());

    QTextStream fileStream(decoded.toUtf8());

    // The first line of the file is in the following form:
    // 　？？？ /
    // EDICT, EDICT_SUB(P), EDICT2 Japanese-English Electronic Dictionary Files/
    // Copyright Electronic Dictionary Research & Development Group - year/
    // Created: year-month-day/
    // ###### entries

    // NOTE: the above last line only appears in the web status file for EDICT.
    // That file has only 2 lines, one is the '?' symbols with the name of the
    // dictionary, copyright and creation date, the other one is the number
    // of entries in the EDICT dictionary.
    // (see INFO_URL macro).

    // We take the 4th section which is the last one
    // separated by the '/' character and the
    // 10 rightmost characters for that section.
    QString dateSection = fileStream.readLine().section('/'_L1, -1, -1, QString::SectionSkipEmpty).right(10);

    // dateSection has the following value: year-month-day
    // Finally we take the numbers separated by the '-' character.
    int year = dateSection.section('-'_L1, 0, 0).toInt();
    int month = dateSection.section('-'_L1, 1, 1).toInt();
    int day = dateSection.section('-'_L1, 2, 2).toInt();

    file.close();

    qDebug() << "Date found:" << dateSection << "(" << file.fileName() << ")";

    return QDate(year, month, day);
}

void DictionaryUpdateManager::installDictionary(KJob *job)
{
    // Increase the number of dictionaries we try to install.
    // This way we can know when we finished with our installed dictionaries.
    _counter++;

    KIO::StoredTransferJob *storedJob = static_cast<KIO::StoredTransferJob *>(job);
    QByteArray data(storedJob->data());
    QString url(storedJob->url().toDisplayString());

    // What we actually downloaded was a GZIP file that we need to extract.
    // As there is no need to keep this file, we make it a temporary file.
    QTemporaryFile compressedFile;
    if (!compressedFile.open()) {
        qDebug() << "Could not create the downloaded .gz file.";
        _failed.append(url.contains(EDICT) ? EDICT : KANJIDIC);
        job->deleteLater();
        checkIfUpdateFinished();
        return;
    }
    // Create the GZIP file from the downloaded data.
    compressedFile.write(data);
    compressedFile.close();

    qDebug() << "Dictionary download finished!";
    qDebug() << "Extracting dictionary...";

    // Extract the GZIP file.
    // QIODevice *device = KFilterDev::deviceForFile( compressedFile.fileName(), "application/x-gzip" );
    KCompressionDevice *device = new KCompressionDevice(compressedFile.fileName(), KCompressionDevice::GZip);
    if (!device->open(QIODevice::ReadOnly)) {
        qDebug() << "Could not extract the dictionary file.";
        _failed.append(url.contains(EDICT) ? EDICT : KANJIDIC);
        delete device;
        job->deleteLater();
        checkIfUpdateFinished();
        return;
    }

    // Check the first line's first character of the extracted file.
    // EDICT starts with a '　', KANJIDIC starts with a '#'.
    QString fileName = device->readLine().startsWith('#') ? QStringLiteral("kanjidic") : QStringLiteral("edict");
    // Reset the position where we are going to start reading the content.
    device->reset();
    // Thanks to the above lines we can get the path to the correct file to be updated.
    QString dictPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kiten/");
    QDir dir(dictPath);
    dir.mkpath(dictPath);

    QFile dictionary(dictPath + fileName);
    if (!dictionary.open(QIODevice::WriteOnly)) {
        qDebug() << "Could not create the new dictionary file.";
        _failed.append(fileName.toUpper());
        device->close();
        delete device;
        job->deleteLater();
        checkIfUpdateFinished();
        return;
    }

    // Write the new dictionary file to disk.
    dictionary.write(device->readAll());
    dictionary.close();
    device->close();

    delete device;
    job->deleteLater();

    // Check if we finished updating.
    checkIfUpdateFinished();
    qDebug() << "Successfully installed at:" << dictionary.fileName();
}

void DictionaryUpdateManager::showUpdateResults()
{
    // Avoid multiple calls to this slot.
    disconnect(this, &DictionaryUpdateManager::updateFinished, this, &DictionaryUpdateManager::showUpdateResults);

    if (!_succeeded.isEmpty() && _failed.isEmpty()) {
        KMessageBox::information(nullptr, i18n("You already have the latest updates."));
    } else if (_succeeded.isEmpty() && _failed.isEmpty()) {
        KMessageBox::information(nullptr, i18n("Successfully updated your dictionaries."));
    } else if (!_succeeded.isEmpty() && !_failed.isEmpty()) {
        KMessageBox::information(nullptr, i18n("Successfully updated:\n%1\n\nFailed to update:\n%2", _succeeded.join("\n"_L1), _failed.join("\n"_L1)));
    } else if (_succeeded.isEmpty() && !_failed.isEmpty()) {
        KMessageBox::error(nullptr, i18n("Failed to update:\n%1", _failed.join("\n"_L1)));
    }

    // Avoid repetitions in our lists.
    _succeeded.clear();
    _failed.clear();
}

#include "moc_dictionaryupdatemanager.cpp"
