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

#include "MonavPlugin.h"

#include "MonavConfigWidget.h"
#include "MonavMap.h"
#include "MonavMapsModel.h"
#include "MonavRunner.h"
#include "signals.h"

#include "GeoDataData.h"
#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "routing/RouteRequest.h"

#include <QDirIterator>
#include <QLocalSocket>
#include <QProcess>
#include <QTextStream>
#include <QThread>

namespace Marble
{

/** A helper class to have a portable sleep call */
class MonavWaiter : private QThread
{
public:
    static void msleep(unsigned long milliSeconds)
    {
        QThread::msleep(milliSeconds);
    }

private:
    MonavWaiter() = delete;
    Q_DISABLE_COPY(MonavWaiter)
};

class MonavPluginPrivate
{
public:
    QDir m_mapDir;

    QList<MonavMap> m_maps;

    bool m_ownsServer;

    QString m_monavDaemonProcess;

    MonavPlugin::MonavRoutingDaemonVersion m_monavVersion;

    MonavPluginPrivate();

    ~MonavPluginPrivate();

    bool startDaemon();

    void stopDaemon();

    static bool isDaemonRunning();

    static bool isDaemonInstalled();

    void loadMaps();

    void initialize();

    static bool areaLessThan(const MonavMap &first, const MonavMap &second);

private:
    void loadMap(const QString &path);

    bool m_initialized;
};

MonavPluginPrivate::MonavPluginPrivate()
    : m_ownsServer(false)
    , m_monavDaemonProcess(QStringLiteral("monav-daemon"))
    , m_monavVersion(MonavPlugin::Monav_0_3)
    , m_initialized(false)
{
    // nothing to do
}

MonavPluginPrivate::~MonavPluginPrivate()
{
    stopDaemon();
}

bool MonavPluginPrivate::isDaemonRunning()
{
    QLocalSocket socket;
    socket.connectToServer(QStringLiteral("MoNavD"));
    return socket.waitForConnected();
}

bool MonavPluginPrivate::isDaemonInstalled()
{
    QString path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("PATH"), QStringLiteral("/usr/local/bin:/usr/bin:/bin"));
    auto const applications = QStringList() << QStringLiteral("monav-daemon") << QStringLiteral("MoNavD");
    for (const QString &application : applications) {
        for (const QString &dir : path.split(QLatin1Char(':'))) {
            QFileInfo executable(QDir(dir), application);
            if (executable.exists()) {
                return true;
            }
        }
    }

    return false;
}

bool MonavPluginPrivate::startDaemon()
{
    if (!isDaemonRunning()) {
        if (QProcess::startDetached(m_monavDaemonProcess, QStringList())) {
            m_ownsServer = true;
        } else {
            if (QProcess::startDetached(QStringLiteral("MoNavD"), QStringList())) {
                m_ownsServer = true;
                m_monavDaemonProcess = QStringLiteral("MoNavD");
                m_monavVersion = MonavPlugin::Monav_0_2;
            } else {
                return false;
            }
        }

        // Give monav-daemon up to one second to set up its server
        // Without that, the first route request would fail
        for (int i = 0; i < 10; ++i) {
            if (isDaemonRunning()) {
                break;
            }
            MonavWaiter::msleep(100);
        }

        return true;
    }

    return true;
}

void MonavPluginPrivate::stopDaemon()
{
    if (m_ownsServer) {
        m_ownsServer = false;
        QProcess::startDetached(m_monavDaemonProcess, QStringList() << QStringLiteral("-t"));
    }
}

void MonavPluginPrivate::loadMaps()
{
    if (m_maps.isEmpty()) {
        QStringList const baseDirs = QStringList() << MarbleDirs::systemPath() << MarbleDirs::localPath();
        for (const QString &baseDir : baseDirs) {
            const QString base = baseDir + QLatin1StringView("/maps/earth/monav/");
            loadMap(base);
            QDir::Filters filters = QDir::AllDirs | QDir::Readable | QDir::NoDotAndDotDot;
            QDirIterator::IteratorFlags flags = QDirIterator::Subdirectories | QDirIterator::FollowSymlinks;
            QDirIterator iter(base, filters, flags);
            while (iter.hasNext()) {
                iter.next();
                loadMap(iter.filePath());
            }
        }
        // Prefer maps where bounding boxes are known
        std::sort(m_maps.begin(), m_maps.end(), MonavMap::areaLessThan);
    }
}

void MonavPluginPrivate::loadMap(const QString &path)
{
    QDir mapDir(path);
    QFileInfo pluginsFile(mapDir, QStringLiteral("plugins.ini"));
    QFileInfo moduleFile(mapDir, QStringLiteral("Module.ini"));
    if (pluginsFile.exists() && !moduleFile.exists()) {
        qDebug() << "Migrating" << mapDir.dirName() << "from monav-0.2";
        QFile file(moduleFile.absoluteFilePath());
        file.open(QIODevice::WriteOnly);
        QTextStream stream(&file);
        stream << "[General]\nconfigVersion=2\n";
        stream << "router=Contraction Hierarchies\ngpsLookup=GPS Grid\n";
        stream << "routerFileFormatVersion=1\ngpsLookupFileFormatVersion=1\n";
        stream.flush();
        file.close();
        moduleFile.refresh();
    }

    if (moduleFile.exists()) {
        MonavMap map;
        map.setDirectory(mapDir);
        m_maps.append(map);
    }
}

void MonavPluginPrivate::initialize()
{
    if (!m_initialized) {
        m_initialized = true;
        loadMaps();
    }
}

MonavPlugin::MonavPlugin(QObject *parent)
    : RoutingRunnerPlugin(parent)
    , d(new MonavPluginPrivate)
{
    setSupportedCelestialBodies(QStringList(QStringLiteral("earth")));
    setCanWorkOffline(true);

    if (d->isDaemonInstalled()) {
        d->initialize();
        if (d->m_maps.isEmpty()) {
            setStatusMessage(tr("No offline maps installed yet."));
        }
    } else {
        setStatusMessage(tr("The monav routing daemon does not seem to be installed on your system."));
    }

    connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(stopDaemon()));
}

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

QString MonavPlugin::name() const
{
    return tr("Monav Routing");
}

QString MonavPlugin::guiString() const
{
    return tr("Monav");
}

QString MonavPlugin::nameId() const
{
    return QStringLiteral("monav");
}

QString MonavPlugin::version() const
{
    return QStringLiteral("1.0");
}

QString MonavPlugin::description() const
{
    return tr("Offline routing using the monav daemon");
}

QString MonavPlugin::copyrightYears() const
{
    return QStringLiteral("2010");
}

QList<PluginAuthor> MonavPlugin::pluginAuthors() const
{
    return QList<PluginAuthor>() << PluginAuthor(QStringLiteral("Dennis Nienhüser"), QStringLiteral("nienhueser@kde.org"));
}

RoutingRunner *MonavPlugin::newRunner() const
{
    d->initialize();
    if (!d->startDaemon()) {
        mDebug() << "Failed to start the monav routing daemon";
    }

    return new MonavRunner(this);
}

QString MonavPlugin::mapDirectoryForRequest(const RouteRequest *request) const
{
    d->initialize();

    QHash<QString, QVariant> settings = request->routingProfile().pluginSettings()[nameId()];
    const QString transport = settings[QStringLiteral("transport")].toString();

    for (int j = 0; j < d->m_maps.size(); ++j) {
        bool valid = true;
        if (transport.isEmpty() || transport == d->m_maps[j].transport()) {
            for (int i = 0; i < request->size(); ++i) {
                GeoDataCoordinates via = request->at(i);
                if (!d->m_maps[j].containsPoint(via)) {
                    valid = false;
                    break;
                }
            }
        } else {
            valid = false;
        }

        if (valid) {
            if (j) {
                // Subsequent route requests will likely be in the same country
                qSwap(d->m_maps[0], d->m_maps[j]);
            }
            // mDebug() << "Using " << d->m_maps.first().m_directory.dirName() << " as monav map";
            return d->m_maps.first().directory().absolutePath();
        }
    }

    return {};
}

QStringList MonavPlugin::mapDirectoriesForRequest(const RouteRequest *request) const
{
    QStringList result;
    d->initialize();
    QHash<QString, QVariant> settings = request->routingProfile().pluginSettings()[nameId()];
    const QString transport = settings[QStringLiteral("transport")].toString();

    for (int j = 0; j < d->m_maps.size(); ++j) {
        bool valid = true;
        if (transport.isEmpty() || transport == d->m_maps[j].transport()) {
            for (int i = 0; i < request->size(); ++i) {
                GeoDataCoordinates via = request->at(i);
                if (!d->m_maps[j].containsPoint(via)) {
                    valid = false;
                    break;
                }
            }
        } else {
            valid = false;
        }

        if (valid) {
            result << d->m_maps[j].directory().absolutePath();
        }
    }

    return result;
}

RoutingRunnerPlugin::ConfigWidget *MonavPlugin::configWidget()
{
    return new MonavConfigWidget(this);
}

MonavMapsModel *MonavPlugin::installedMapsModel()
{
    d->initialize();
    return new MonavMapsModel(d->m_maps);
}

void MonavPlugin::reloadMaps()
{
    d->m_maps.clear();
    d->loadMaps();
}

bool MonavPlugin::canWork() const
{
    d->initialize();
    return !d->m_maps.isEmpty();
}

bool MonavPlugin::supportsTemplate(RoutingProfilesModel::ProfileTemplate profileTemplate) const
{
    // Since we support multiple maps, pretty much anything can be installed, but ecological is
    // not supported by monav
    return profileTemplate != RoutingProfilesModel::CarEcologicalTemplate;
}

QHash<QString, QVariant> MonavPlugin::templateSettings(RoutingProfilesModel::ProfileTemplate profileTemplate) const
{
    QHash<QString, QVariant> result;
    switch (profileTemplate) {
    case RoutingProfilesModel::CarFastestTemplate:
        result[QStringLiteral("transport")] = QStringLiteral("Motorcar");
        break;
    case RoutingProfilesModel::CarShortestTemplate:
        result[QStringLiteral("transport")] = QStringLiteral("Motorcar");
        break;
    case RoutingProfilesModel::CarEcologicalTemplate:
        break;
    case RoutingProfilesModel::BicycleTemplate:
        result[QStringLiteral("transport")] = QStringLiteral("Bicycle");
        break;
    case RoutingProfilesModel::PedestrianTemplate:
        result[QStringLiteral("transport")] = QStringLiteral("Pedestrian");
        break;
    case RoutingProfilesModel::LastTemplate:
        Q_ASSERT(false);
        break;
    }
    return result;
}

MonavPlugin::MonavRoutingDaemonVersion MonavPlugin::monavVersion() const
{
    return d->m_monavVersion;
}

}

#include "moc_MonavPlugin.cpp"
