// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
//

#include "MonavConfigWidget.h"

#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "MonavMapsModel.h"
#include "MonavPlugin.h"

#include <QDomDocument>
#include <QFile>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QProcess>
#include <QPushButton>
#include <QRegExp>
#include <QShowEvent>
#include <QSignalMapper>
#include <QSortFilterProxyModel>

namespace Marble
{

class MonavStuffEntry
{
public:
    void setPayload(const QString &payload);

    QString payload() const;

    void setName(const QString &name);

    QString name() const;

    bool isValid() const;

    QString continent() const;

    QString state() const;

    QString region() const;

    QString transport() const;

private:
    QString m_payload;

    QString m_name;

    QString m_continent;

    QString m_state;

    QString m_region;

    QString m_transport;
};

class MonavConfigWidgetPrivate
{
public:
    MonavConfigWidget *m_parent = nullptr;

    MonavPlugin *m_plugin = nullptr;

    QNetworkAccessManager m_networkAccessManager;

    QNetworkReply *m_currentReply = nullptr;

    QProcess *m_unpackProcess;

    QSortFilterProxyModel *const m_filteredModel;

    MonavMapsModel *m_mapsModel = nullptr;

    bool m_initialized;

    QSignalMapper m_removeMapSignalMapper;

    QSignalMapper m_upgradeMapSignalMapper;

    QList<MonavStuffEntry> m_remoteMaps;

    QMap<QString, QString> m_remoteVersions;

    QString m_currentDownload;

    QFile m_currentFile;

    QString m_transport;

    MonavConfigWidgetPrivate(MonavConfigWidget *parent, MonavPlugin *plugin);

    void parseNewStuff(const QByteArray &data);

    bool updateContinents(QComboBox *comboBox);

    bool updateStates(const QString &continent, QComboBox *comboBox);

    bool updateRegions(const QString &continent, const QString &state, QComboBox *comboBox);

    MonavStuffEntry map(const QString &continent, const QString &state, const QString &region) const;

    void install();

    void installMap();

    void updateInstalledMapsView();

    void updateInstalledMapsViewButtons();

    void updateTransportPreference();

    static bool canExecute(const QString &executable);

    void setBusy(bool busy, const QString &message = QString()) const;

private:
    static bool fillComboBox(QStringList items, QComboBox *comboBox);
};

void MonavStuffEntry::setPayload(const QString &payload)
{
    m_payload = payload;
}

QString MonavStuffEntry::payload() const
{
    return m_payload;
}

void MonavStuffEntry::setName(const QString &name)
{
    m_name = name;
    QStringList parsed = name.split(QLatin1Char('/'));
    int size = parsed.size();
    m_continent = size > 0 ? parsed.at(0).trimmed() : QString();
    m_state = size > 1 ? parsed.at(1).trimmed() : QString();
    m_region = size > 2 ? parsed.at(2).trimmed() : QString();
    m_transport = QStringLiteral("Motorcar"); // No i18n

    if (size > 1) {
        QString last = parsed.last().trimmed();
        QRegExp regexp(QStringLiteral("([^(]+)\\(([^)]+)\\)"));
        if (regexp.indexIn(last) >= 0) {
            QStringList matches = regexp.capturedTexts();
            if (matches.size() == 3) {
                m_transport = matches.at(2).trimmed();
                if (size > 2) {
                    m_region = matches.at(1).trimmed();
                } else {
                    m_state = matches.at(1).trimmed();
                }
            }
        }
    }
}

QString MonavStuffEntry::name() const
{
    return m_name;
}

QString MonavStuffEntry::continent() const
{
    return m_continent;
}

QString MonavStuffEntry::state() const
{
    return m_state;
}

QString MonavStuffEntry::region() const
{
    return m_region;
}

QString MonavStuffEntry::transport() const
{
    return m_transport;
}

bool MonavStuffEntry::isValid() const
{
    return !m_continent.isEmpty() && !m_state.isEmpty() && m_payload.startsWith(QLatin1StringView("http://"));
}

MonavConfigWidgetPrivate::MonavConfigWidgetPrivate(MonavConfigWidget *parent, MonavPlugin *plugin)
    : m_parent(parent)
    , m_plugin(plugin)
    , m_networkAccessManager(nullptr)
    , m_currentReply(nullptr)
    , m_unpackProcess(nullptr)
    , m_filteredModel(new QSortFilterProxyModel(parent))
    , m_mapsModel(nullptr)
    , m_initialized(false)
{
    m_filteredModel->setFilterKeyColumn(1);
}

void MonavConfigWidgetPrivate::parseNewStuff(const QByteArray &data)
{
    QDomDocument xml;
    if (!xml.setContent(data)) {
        mDebug() << "Cannot parse xml file " << data;
        return;
    }

    QDomElement root = xml.documentElement();
    QDomNodeList items = root.elementsByTagName(QStringLiteral("stuff"));
    for (int i = 0; i < items.length(); ++i) {
        MonavStuffEntry item;
        QDomNode node = items.item(i);

        QDomNodeList names = node.toElement().elementsByTagName(QStringLiteral("name"));
        if (names.size() == 1) {
            item.setName(names.at(0).toElement().text());
        }

        QString releaseDate;
        QDomNodeList dates = node.toElement().elementsByTagName(QStringLiteral("releasedate"));
        if (dates.size() == 1) {
            releaseDate = dates.at(0).toElement().text();
        }

        QString filename;
        QDomNodeList payloads = node.toElement().elementsByTagName(QStringLiteral("payload"));
        if (payloads.size() == 1) {
            QString payload = payloads.at(0).toElement().text();
            filename = payload.mid(1 + payload.lastIndexOf(QLatin1Char('/')));
            item.setPayload(payload);
        }

        if (item.isValid()) {
            m_remoteMaps.push_back(item);
            if (!filename.isEmpty() && !releaseDate.isEmpty()) {
                m_remoteVersions[filename] = releaseDate;
            }
        }
    }

    m_mapsModel->setInstallableVersions(m_remoteVersions);
    updateInstalledMapsViewButtons();
}

bool MonavConfigWidgetPrivate::fillComboBox(QStringList items, QComboBox *comboBox)
{
    comboBox->clear();
    std::sort(items.begin(), items.end());
    comboBox->addItems(items);
    return !items.isEmpty();
}

bool MonavConfigWidgetPrivate::updateContinents(QComboBox *comboBox)
{
    QSet<QString> continents;
    for (const MonavStuffEntry &map : std::as_const(m_remoteMaps)) {
        Q_ASSERT(map.isValid());
        continents << map.continent();
    }

    return fillComboBox(continents.values(), comboBox);
}

bool MonavConfigWidgetPrivate::updateStates(const QString &continent, QComboBox *comboBox)
{
    QSet<QString> states;
    for (const MonavStuffEntry &map : std::as_const(m_remoteMaps)) {
        Q_ASSERT(map.isValid());
        if (map.continent() == continent) {
            states << map.state();
        }
    }

    return fillComboBox(states.values(), comboBox);
}

bool MonavConfigWidgetPrivate::updateRegions(const QString &continent, const QString &state, QComboBox *comboBox)
{
    comboBox->clear();
    QMap<QString, QString> regions;
    for (const MonavStuffEntry &map : std::as_const(m_remoteMaps)) {
        Q_ASSERT(map.isValid());
        if (map.continent() == continent && map.state() == state) {
            QString item = QStringLiteral("%1 - %2");
            if (map.region().isEmpty()) {
                item = item.arg(map.state());
                comboBox->addItem(item.arg(map.transport()), map.payload());
            } else {
                item = item.arg(map.region(), map.transport());
                regions.insert(item, map.payload());
            }
        }
    }

    QMapIterator<QString, QString> iter(regions);
    while (iter.hasNext()) {
        iter.next();
        comboBox->addItem(iter.key(), iter.value());
    }

    return true;
}

MonavStuffEntry MonavConfigWidgetPrivate::map(const QString &continent, const QString &state, const QString &region) const
{
    for (const MonavStuffEntry &entry : m_remoteMaps) {
        if (continent == entry.continent() && state == entry.state() && region == entry.region()) {
            return entry;
        }
    }

    return {};
}

MonavConfigWidget::MonavConfigWidget(MonavPlugin *plugin)
    : d(new MonavConfigWidgetPrivate(this, plugin))
{
    setupUi(this);
    m_statusLabel->setText(plugin->statusMessage());
    m_statusLabel->setHidden(m_statusLabel->text().isEmpty());
    d->setBusy(false);
    m_installedMapsListView->setModel(d->m_mapsModel);
    m_configureMapsListView->setModel(d->m_filteredModel);
    m_configureMapsListView->resizeColumnsToContents();

    updateComboBoxes();

    connect(m_continentComboBox, &QComboBox::currentIndexChanged, this, &MonavConfigWidget::updateStates);
    connect(m_transportTypeComboBox, &QComboBox::currentTextChanged, this, &MonavConfigWidget::updateTransportTypeFilter);
    connect(m_stateComboBox, &QComboBox::currentIndexChanged, this, &MonavConfigWidget::updateRegions);
    connect(m_installButton, &QAbstractButton::clicked, this, &MonavConfigWidget::downloadMap);
    connect(m_cancelButton, &QAbstractButton::clicked, this, &MonavConfigWidget::cancelOperation);
    connect(&d->m_removeMapSignalMapper, &QSignalMapper::mappedInt, this, &MonavConfigWidget::removeMap);
    connect(&d->m_upgradeMapSignalMapper, &QSignalMapper::mappedInt, this, &MonavConfigWidget::upgradeMap);
    connect(&d->m_networkAccessManager, &QNetworkAccessManager::finished, this, &MonavConfigWidget::retrieveMapList);
}

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

void MonavConfigWidget::loadSettings(const QHash<QString, QVariant> &settings)
{
    d->m_transport = settings[QStringLiteral("transport")].toString();
    d->updateTransportPreference();
}

void MonavConfigWidgetPrivate::updateTransportPreference()
{
    if (m_parent->m_transportTypeComboBox && m_mapsModel) {
        m_parent->m_transportTypeComboBox->blockSignals(true);
        m_parent->m_transportTypeComboBox->clear();
        QSet<QString> transportTypes;
        for (int i = 0; i < m_mapsModel->rowCount(); ++i) {
            QModelIndex index = m_mapsModel->index(i, 1);
            transportTypes << m_mapsModel->data(index).toString();
        }
        m_parent->m_transportTypeComboBox->addItems(transportTypes.values());
        m_parent->m_transportTypeComboBox->blockSignals(false);

        if (!m_transport.isEmpty() && m_parent->m_transportTypeComboBox) {
            for (int i = 1; i < m_parent->m_transportTypeComboBox->count(); ++i) {
                if (m_parent->m_transportTypeComboBox->itemText(i) == m_transport) {
                    m_parent->m_transportTypeComboBox->setCurrentIndex(i);
                    return;
                }
            }
        }
    }
}

QHash<QString, QVariant> MonavConfigWidget::settings() const
{
    QHash<QString, QVariant> settings;
    settings.insert(QStringLiteral("transport"), d->m_transport);
    return settings;
}

void MonavConfigWidget::retrieveMapList(QNetworkReply *reply)
{
    if (reply->isReadable() && d->m_currentDownload.isEmpty()) {
        // check if we are redirected
        QVariant const redirectionAttribute = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
        if (!redirectionAttribute.isNull()) {
            d->m_networkAccessManager.get(QNetworkRequest(redirectionAttribute.toUrl()));
        } else {
            disconnect(&d->m_networkAccessManager, &QNetworkAccessManager::finished, this, &MonavConfigWidget::retrieveMapList);
            d->parseNewStuff(reply->readAll());
            updateComboBoxes();
        }
    }
}

void MonavConfigWidget::retrieveData()
{
    if (d->m_currentReply && d->m_currentReply->isReadable() && !d->m_currentDownload.isEmpty()) {
        // check if we are redirected
        QVariant const redirectionAttribute = d->m_currentReply->attribute(QNetworkRequest::RedirectionTargetAttribute);
        if (!redirectionAttribute.isNull()) {
            d->m_currentReply = d->m_networkAccessManager.get(QNetworkRequest(redirectionAttribute.toUrl()));
            connect(d->m_currentReply, &QIODevice::readyRead, this, &MonavConfigWidget::retrieveData);
            connect(d->m_currentReply, &QIODevice::readChannelFinished, this, &MonavConfigWidget::retrieveData);
            connect(d->m_currentReply, &QNetworkReply::downloadProgress, this, &MonavConfigWidget::updateProgressBar);
        } else {
            d->m_currentFile.write(d->m_currentReply->readAll());
            if (d->m_currentReply->isFinished()) {
                d->m_currentReply->deleteLater();
                d->m_currentReply = nullptr;
                d->m_currentFile.close();
                d->installMap();
                d->m_currentDownload.clear();
            }
        }
    }
}

void MonavConfigWidget::updateComboBoxes()
{
    d->updateContinents(m_continentComboBox);
    updateStates();
    updateRegions();
}

void MonavConfigWidget::updateStates()
{
    bool const haveContinents = m_continentComboBox->currentIndex() >= 0;
    if (haveContinents) {
        QString const continent = m_continentComboBox->currentText();
        if (d->updateStates(continent, m_stateComboBox)) {
            updateRegions();
        }
    }
}

void MonavConfigWidget::updateRegions()
{
    bool haveRegions = false;
    if (m_continentComboBox->currentIndex() >= 0) {
        if (m_stateComboBox->currentIndex() >= 0) {
            QString const continent = m_continentComboBox->currentText();
            QString const state = m_stateComboBox->currentText();
            haveRegions = d->updateRegions(continent, state, m_regionComboBox);
        }
    }

    m_regionLabel->setVisible(haveRegions);
    m_regionComboBox->setVisible(haveRegions);
}

void MonavConfigWidget::downloadMap()
{
    if (d->m_currentDownload.isEmpty() && !d->m_currentFile.isOpen()) {
        d->m_currentDownload = m_regionComboBox->itemData(m_regionComboBox->currentIndex()).toString();
        d->install();
    }
}

void MonavConfigWidget::cancelOperation()
{
    if (!d->m_currentDownload.isEmpty() || d->m_currentFile.isOpen()) {
        d->m_currentReply->abort();
        d->m_currentReply->deleteLater();
        d->m_currentReply = nullptr;
        d->m_currentDownload.clear();
        d->setBusy(false);
        d->m_currentFile.close();
    }
}

void MonavConfigWidgetPrivate::install()
{
    if (!m_currentDownload.isEmpty()) {
        int const index = m_currentDownload.lastIndexOf(QLatin1Char('/'));
        const QString localFile = MarbleDirs::localPath() + QLatin1StringView("/maps") + m_currentDownload.mid(index);
        m_currentFile.setFileName(localFile);
        if (m_currentFile.open(QFile::WriteOnly)) {
            QFileInfo file(m_currentFile);
            const QString message = QObject::tr("Downloading %1").arg(file.fileName());
            setBusy(true, message);
            m_currentReply = m_networkAccessManager.get(QNetworkRequest(QUrl(m_currentDownload)));
            QObject::connect(m_currentReply, SIGNAL(readyRead()), m_parent, SLOT(retrieveData()));
            QObject::connect(m_currentReply, SIGNAL(readChannelFinished()), m_parent, SLOT(retrieveData()));
            QObject::connect(m_currentReply, SIGNAL(downloadProgress(qint64, qint64)), m_parent, SLOT(updateProgressBar(qint64, qint64)));
        } else {
            mDebug() << "Failed to write to " << localFile;
        }
    }
}

void MonavConfigWidgetPrivate::installMap()
{
    if (m_unpackProcess) {
        m_unpackProcess->close();
        delete m_unpackProcess;
        m_unpackProcess = nullptr;
        m_parent->m_installButton->setEnabled(true);
    } else if (m_currentFile.fileName().endsWith(QLatin1StringView("tar.gz")) && canExecute(QStringLiteral("tar"))) {
        QFileInfo file(m_currentFile);
        QString message = QObject::tr("Installing %1").arg(file.fileName());
        setBusy(true, message);
        m_parent->m_progressBar->setMaximum(0);
        if (file.exists() && file.isReadable()) {
            m_unpackProcess = new QProcess;
            QObject::connect(m_unpackProcess, SIGNAL(finished(int)), m_parent, SLOT(mapInstalled(int)));
            QStringList arguments = QStringList() << QStringLiteral("-x") << QStringLiteral("-z") << QStringLiteral("-f") << file.fileName();
            m_unpackProcess->setWorkingDirectory(file.dir().absolutePath());
            m_unpackProcess->start(QStringLiteral("tar"), arguments);
        }
    } else {
        if (!m_currentFile.fileName().endsWith(QLatin1StringView("tar.gz"))) {
            mDebug() << "Can only handle tar.gz files";
        } else {
            mDebug() << "Cannot extract archive: tar executable not found in PATH.";
        }
    }
}

void MonavConfigWidget::updateProgressBar(qint64 bytesReceived, qint64 bytesTotal)
{
    // Coarse MB resolution for the text to get it short,
    // finer Kb resolution for the progress values to see changes easily
    m_progressBar->setMaximum(bytesTotal / 1024);
    m_progressBar->setValue(bytesReceived / 1024);
    QString progress = tr("%1/%2 MB");
    m_progressBar->setFormat(progress.arg(bytesReceived / 1024 / 1024).arg(bytesTotal / 1024 / 1024));
}

bool MonavConfigWidgetPrivate::canExecute(const QString &executable)
{
    QString path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("PATH"), QStringLiteral("/usr/local/bin:/usr/bin:/bin"));
    for (const QString &dir : path.split(QLatin1Char(':'))) {
        QFileInfo application(QDir(dir), executable);
        if (application.exists()) {
            return true;
        }
    }

    return false;
}

void MonavConfigWidget::mapInstalled(int exitStatus)
{
    d->m_unpackProcess = nullptr;
    d->m_currentFile.remove();
    d->setBusy(false);

    if (exitStatus == 0) {
        d->m_plugin->reloadMaps();
        d->updateInstalledMapsView();
        monavTabWidget->setCurrentIndex(0);
    } else {
        mDebug() << "Error when unpacking archive, process exited with status code " << exitStatus;
    }
}

void MonavConfigWidget::showEvent(QShowEvent *event)
{
    // Lazy initialization
    RoutingRunnerPlugin::ConfigWidget::showEvent(event);
    if (!event->spontaneous() && !d->m_initialized) {
        d->m_initialized = true;
        d->updateInstalledMapsView();
        QUrl url = QUrl(QStringLiteral("http://files.kde.org/marble/newstuff/maps-monav.xml"));
        d->m_networkAccessManager.get(QNetworkRequest(url));
    }
}

void MonavConfigWidgetPrivate::updateInstalledMapsView()
{
    m_mapsModel = m_plugin->installedMapsModel();
    m_mapsModel->setInstallableVersions(m_remoteVersions);
    m_filteredModel->setSourceModel(m_mapsModel);
    m_parent->m_installedMapsListView->setModel(m_mapsModel);

    m_parent->m_configureMapsListView->setColumnHidden(1, true);
    m_parent->m_installedMapsListView->setColumnHidden(2, true);
    m_parent->m_configureMapsListView->setColumnHidden(3, true);
    m_parent->m_configureMapsListView->setColumnHidden(4, true);
    m_parent->m_installedMapsListView->setColumnHidden(5, true);

    m_parent->m_configureMapsListView->horizontalHeader()->setVisible(true);
    m_parent->m_installedMapsListView->horizontalHeader()->setVisible(true);
    m_parent->m_configureMapsListView->resizeColumnsToContents();
    m_parent->m_installedMapsListView->resizeColumnsToContents();

    updateTransportPreference();
    updateInstalledMapsViewButtons();
}

void MonavConfigWidgetPrivate::updateInstalledMapsViewButtons()
{
    m_removeMapSignalMapper.removeMappings(m_parent);
    m_upgradeMapSignalMapper.removeMappings(m_parent);
    for (int i = 0; i < m_mapsModel->rowCount(); ++i) {
        {
            auto button = new QPushButton(QIcon(QStringLiteral(":/system-software-update.png")), QString());
            button->setAutoFillBackground(true);
            QModelIndex index = m_mapsModel->index(i, 3);
            m_parent->m_installedMapsListView->setIndexWidget(index, button);
            m_upgradeMapSignalMapper.setMapping(button, index.row());
            QObject::connect(button, SIGNAL(clicked()), &m_upgradeMapSignalMapper, SLOT(map()));
            bool upgradable = m_mapsModel->data(index).toBool();
            QString canUpgradeText = QObject::tr("An update is available. Click to install it.");
            QString isLatestText = QObject::tr("No update available. You are running the latest version.");
            button->setToolTip(upgradable ? canUpgradeText : isLatestText);
            button->setEnabled(upgradable);
        }
        {
            auto button = new QPushButton(QIcon(QStringLiteral(":/edit-delete.png")), QString());
            button->setAutoFillBackground(true);
            QModelIndex index = m_mapsModel->index(i, 4);
            m_parent->m_installedMapsListView->setIndexWidget(index, button);
            m_removeMapSignalMapper.setMapping(button, index.row());
            QObject::connect(button, SIGNAL(clicked()), &m_removeMapSignalMapper, SLOT(map()));
            bool const writable = m_mapsModel->data(index).toBool();
            button->setEnabled(writable);
        }
    }
    m_parent->m_installedMapsListView->resizeColumnsToContents();
}

void MonavConfigWidget::updateTransportTypeFilter(const QString &filter)
{
    d->m_filteredModel->setFilterFixedString(filter);
    d->m_transport = filter;
    m_configureMapsListView->resizeColumnsToContents();
}

void MonavConfigWidget::removeMap(int index)
{
    QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel;
    const QString text = tr("Are you sure you want to delete this map from the system?");
    if (QMessageBox::question(this, tr("Remove Map"), text, buttons, QMessageBox::No) == QMessageBox::Yes) {
        d->m_mapsModel->deleteMapFiles(index);
        d->m_plugin->reloadMaps();
        d->updateInstalledMapsView();
    }
}

void MonavConfigWidget::upgradeMap(int index)
{
    QString payload = d->m_mapsModel->payload(index);
    if (!payload.isEmpty()) {
        for (const MonavStuffEntry &entry : std::as_const(d->m_remoteMaps)) {
            if (entry.payload().endsWith(QLatin1Char('/') + payload)) {
                d->m_currentDownload = entry.payload();
                d->install();
                return;
            }
        }
    }
}

void MonavConfigWidgetPrivate::setBusy(bool busy, const QString &message) const
{
    if (busy) {
        m_parent->m_stackedWidget->removeWidget(m_parent->m_settingsPage);
        m_parent->m_stackedWidget->addWidget(m_parent->m_progressPage);
    } else {
        m_parent->m_stackedWidget->removeWidget(m_parent->m_progressPage);
        m_parent->m_stackedWidget->addWidget(m_parent->m_settingsPage);
    }

    QString const defaultMessage = QObject::tr("Nothing to do.");
    m_parent->m_progressLabel->setText(message.isEmpty() ? defaultMessage : message);
}

}

#include "moc_MonavConfigWidget.cpp"
