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

#include "RoutingManager.h"

#include "AlternativeRoutesModel.h"
#include "GeoDataData.h"
#include "GeoDataDocument.h"
#include "GeoDataExtendedData.h"
#include "GeoDataFolder.h"
#include "GeoDataParser.h"
#include "GeoDataPlacemark.h"
#include "GeoDataTreeModel.h"
#include "GeoWriter.h"
#include "MarbleColors.h"
#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "MarbleModel.h"
#include "PluginManager.h"
#include "PositionProviderPlugin.h"
#include "PositionTracking.h"
#include "Route.h"
#include "RouteRequest.h"
#include "RoutingModel.h"
#include "RoutingProfilesModel.h"
#include "RoutingRunnerManager.h"
#include "RoutingRunnerPlugin.h"
#include <KmlElementDictionary.h>

#include <QCheckBox>
#include <QFile>
#include <QMessageBox>
#include <QMutexLocker>

namespace Marble
{

class RoutingManagerPrivate
{
public:
    RoutingManager *const q;

    RouteRequest m_routeRequest;

    RoutingModel m_routingModel;

    RoutingProfilesModel m_profilesModel;

    RoutingManager::State m_state;

    const PluginManager *const m_pluginManager;

    GeoDataTreeModel *const m_treeModel;

    PositionTracking *const m_positionTracking;

    AlternativeRoutesModel m_alternativeRoutesModel;

    RoutingRunnerManager m_runnerManager;

    bool m_haveRoute;

    bool m_guidanceModeEnabled;

    QMutex m_fileMutex;

    bool m_shutdownPositionTracking;

    bool m_guidanceModeWarning;

    QString m_lastOpenPath;

    QString m_lastSavePath;

    QColor m_routeColorStandard;

    QColor m_routeColorHighlighted;

    QColor m_routeColorAlternative;

    RoutingManagerPrivate(MarbleModel *marbleModel, RoutingManager *manager);

    static GeoDataFolder *createFolderFromRequest(const RouteRequest &request);

    static QString stateFile(const QString &name = QStringLiteral("route.kml"));

    void saveRoute(const QString &filename);

    void loadRoute(const QString &filename);

    void addRoute(GeoDataDocument *route);

    void routingFinished();

    void setCurrentRoute(const GeoDataDocument *route);

    void recalculateRoute(bool deviated);

    static void importPlacemark(RouteSegment &outline, QList<RouteSegment> &segments, const GeoDataPlacemark *placemark);
};

RoutingManagerPrivate::RoutingManagerPrivate(MarbleModel *model, RoutingManager *manager)
    : q(manager)
    , m_routeRequest(manager)
    , m_routingModel(&m_routeRequest, model->positionTracking(), manager)
    , m_profilesModel(model->pluginManager())
    , m_state(RoutingManager::Retrieved)
    , m_pluginManager(model->pluginManager())
    , m_treeModel(model->treeModel())
    , m_positionTracking(model->positionTracking())
    , m_alternativeRoutesModel(manager)
    , m_runnerManager(model, manager)
    , m_haveRoute(false)
    , m_guidanceModeEnabled(false)
    , m_shutdownPositionTracking(false)
    , m_guidanceModeWarning(true)
    , m_routeColorStandard(Oxygen::skyBlue4)
    , m_routeColorHighlighted(Oxygen::skyBlue1)
    , m_routeColorAlternative(Oxygen::aluminumGray4)
{
    m_routeColorStandard.setAlpha(200);
    m_routeColorHighlighted.setAlpha(200);
    m_routeColorAlternative.setAlpha(200);
}

GeoDataFolder *RoutingManagerPrivate::createFolderFromRequest(const RouteRequest &request)
{
    auto result = new GeoDataFolder;

    result->setName(QStringLiteral("Route Request"));

    for (int i = 0; i < request.size(); ++i) {
        auto placemark = new GeoDataPlacemark(request[i]);
        result->append(placemark);
    }

    return result;
}

QString RoutingManagerPrivate::stateFile(const QString &name)
{
    QString const subdir = QStringLiteral("routing");
    QDir dir(MarbleDirs::localPath());
    if (!dir.exists(subdir)) {
        if (!dir.mkdir(subdir)) {
            mDebug() << "Unable to create dir " << dir.absoluteFilePath(subdir);
            return dir.absolutePath();
        }
    }

    if (!dir.cd(subdir)) {
        mDebug() << "Cannot change into " << dir.absoluteFilePath(subdir);
    }

    return dir.absoluteFilePath(name);
}

void RoutingManagerPrivate::saveRoute(const QString &filename)
{
    GeoWriter writer;
    writer.setDocumentType(QString::fromLatin1(kml::kmlTag_nameSpaceOgc22));

    QMutexLocker locker(&m_fileMutex);
    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
        mDebug() << "Cannot write to " << file.fileName();
        return;
    }

    GeoDataDocument container;
    container.setName(QStringLiteral("Route"));
    GeoDataFolder *request = createFolderFromRequest(m_routeRequest);
    if (request) {
        container.append(request);
    }

    const GeoDataDocument *route = m_alternativeRoutesModel.currentRoute();
    if (route) {
        container.append(new GeoDataDocument(*route));
    }

    if (!writer.write(&file, &container)) {
        mDebug() << "Can not write route state to " << file.fileName();
    }
    file.close();
}

void RoutingManagerPrivate::loadRoute(const QString &filename)
{
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly)) {
        mDebug() << "Can not read route from " << file.fileName();
        return;
    }

    GeoDataParser parser(GeoData_KML);
    if (!parser.read(&file)) {
        mDebug() << "Could not parse file: " << parser.errorString();
        return;
    }

    GeoDocument *doc = parser.releaseDocument();
    file.close();
    bool loaded = false;

    auto container = dynamic_cast<GeoDataDocument *>(doc);
    if (container && !container->isEmpty()) {
        auto viaPoints = dynamic_cast<GeoDataFolder *>(&container->first());
        if (viaPoints) {
            loaded = true;
            QList<GeoDataPlacemark *> placemarks = viaPoints->placemarkList();
            for (int i = 0; i < placemarks.size(); ++i) {
                if (i < m_routeRequest.size()) {
                    m_routeRequest[i] = *placemarks[i];
                } else {
                    m_routeRequest.append(*placemarks[i]);
                }
            }

            // clear unneeded via points
            const int viaPoints_needed = placemarks.size();
            for (int i = m_routeRequest.size(); i > viaPoints_needed; --i) {
                m_routeRequest.remove(viaPoints_needed);
            }
        } else {
            mDebug() << "Expected a GeoDataDocument with at least one child, didn't get one though";
        }
    }

    if (container && container->size() == 2) {
        auto route = dynamic_cast<GeoDataDocument *>(&container->last());
        if (route) {
            loaded = true;
            m_alternativeRoutesModel.clear();
            m_alternativeRoutesModel.addRoute(new GeoDataDocument(*route), AlternativeRoutesModel::Instant);
            m_alternativeRoutesModel.setCurrentRoute(0);
            m_state = RoutingManager::Retrieved;
            Q_EMIT q->stateChanged(m_state);
            Q_EMIT q->routeRetrieved(route);
        } else {
            mDebug() << "Expected a GeoDataDocument child, didn't get one though";
        }
    }

    if (loaded) {
        delete doc; // == container
    } else {
        mDebug() << "File " << filename << " is not a valid Marble route .kml file";
        if (container) {
            m_treeModel->addDocument(container);
        }
    }
}

RoutingManager::RoutingManager(MarbleModel *marbleModel, QObject *parent)
    : QObject(parent)
    , d(new RoutingManagerPrivate(marbleModel, this))
{
    connect(&d->m_runnerManager, SIGNAL(routeRetrieved(GeoDataDocument *)), this, SLOT(addRoute(GeoDataDocument *)));
    connect(&d->m_runnerManager, SIGNAL(routingFinished()), this, SLOT(routingFinished()));
    connect(&d->m_alternativeRoutesModel, SIGNAL(currentRouteChanged(const GeoDataDocument *)), this, SLOT(setCurrentRoute(const GeoDataDocument *)));
    connect(&d->m_routingModel, SIGNAL(deviatedFromRoute(bool)), this, SLOT(recalculateRoute(bool)));
}

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

RoutingProfilesModel *RoutingManager::profilesModel()
{
    return &d->m_profilesModel;
}

RoutingModel *RoutingManager::routingModel()
{
    return &d->m_routingModel;
}

const RoutingModel *RoutingManager::routingModel() const
{
    return &d->m_routingModel;
}

RouteRequest *RoutingManager::routeRequest()
{
    return &d->m_routeRequest;
}

RoutingManager::State RoutingManager::state() const
{
    return d->m_state;
}

void RoutingManager::retrieveRoute()
{
    d->m_haveRoute = false;

    int realSize = 0;
    for (int i = 0; i < d->m_routeRequest.size(); ++i) {
        // Sort out dummy targets
        if (d->m_routeRequest.at(i).isValid()) {
            ++realSize;
        }
    }

    d->m_alternativeRoutesModel.newRequest(&d->m_routeRequest);
    if (realSize > 1) {
        d->m_state = RoutingManager::Downloading;
        d->m_runnerManager.retrieveRoute(&d->m_routeRequest);
    } else {
        d->m_routingModel.clear();
        d->m_state = RoutingManager::Retrieved;
    }
    Q_EMIT stateChanged(d->m_state);
}

void RoutingManagerPrivate::addRoute(GeoDataDocument *route)
{
    if (route) {
        m_alternativeRoutesModel.addRoute(route);
    }

    if (!m_haveRoute) {
        m_haveRoute = route != nullptr;
    }

    Q_EMIT q->routeRetrieved(route);
}

void RoutingManagerPrivate::routingFinished()
{
    m_state = RoutingManager::Retrieved;
    Q_EMIT q->stateChanged(m_state);
}

void RoutingManagerPrivate::setCurrentRoute(const GeoDataDocument *document)
{
    QList<RouteSegment> segments;
    RouteSegment outline;

    if (document != nullptr) {
        const auto folders = document->folderList();
        for (const auto folder : folders) {
            for (const auto placemark : folder->placemarkList()) {
                importPlacemark(outline, segments, placemark);
            }
        }

        for (const auto placemark : document->placemarkList()) {
            importPlacemark(outline, segments, placemark);
        }
    }

    if (segments.isEmpty()) {
        segments << outline;
    }

    // Map via points onto segments
    if (m_routeRequest.size() > 1 && segments.size() > 1) {
        int index = 0;
        for (int j = 0; j < m_routeRequest.size(); ++j) {
            QPair<int, qreal> minimum(-1, -1.0);
            int viaIndex = -1;
            for (int i = index; i < segments.size(); ++i) {
                const RouteSegment &segment = segments[i];
                GeoDataCoordinates closest;
                const qreal distance = segment.distanceTo(m_routeRequest.at(j), closest, closest);
                if (minimum.first < 0 || distance < minimum.second) {
                    minimum.first = i;
                    minimum.second = distance;
                    viaIndex = j;
                }
            }

            if (minimum.first >= 0) {
                index = minimum.first;
                Maneuver viaPoint = segments[minimum.first].maneuver();
                viaPoint.setWaypoint(m_routeRequest.at(viaIndex), viaIndex);
                segments[minimum.first].setManeuver(viaPoint);
            }
        }
    }

    Route route;

    if (!segments.isEmpty()) {
        for (const RouteSegment &segment : std::as_const(segments)) {
            route.addRouteSegment(segment);
        }
    }

    m_routingModel.setRoute(route);
}

void RoutingManagerPrivate::importPlacemark(RouteSegment &outline, QList<RouteSegment> &segments, const GeoDataPlacemark *placemark)
{
    const GeoDataGeometry *geometry = placemark->geometry();
    const auto lineString = dynamic_cast<const GeoDataLineString *>(geometry);
    QStringList blacklist = QStringList() << QString() << QStringLiteral("Route") << QStringLiteral("Tessellated");
    RouteSegment segment;
    bool isOutline = true;
    if (!blacklist.contains(placemark->name())) {
        if (lineString) {
            Maneuver maneuver;
            maneuver.setInstructionText(placemark->name());
            maneuver.setPosition(lineString->at(0));

            if (placemark->extendedData().contains(QStringLiteral("turnType"))) {
                QVariant turnType = placemark->extendedData().value(QStringLiteral("turnType")).value();
                // The enum value is converted to/from an int in the QVariant
                // because only a limited set of data types can be serialized with QVariant's
                // toString() method (which is used to serialize <ExtendedData>/<Data> values)
                maneuver.setDirection(Maneuver::Direction(turnType.toInt()));
            }

            if (placemark->extendedData().contains(QStringLiteral("roadName"))) {
                QVariant roadName = placemark->extendedData().value(QStringLiteral("roadName")).value();
                maneuver.setRoadName(roadName.toString());
            }

            segment.setManeuver(maneuver);
            isOutline = false;
        }
    }

    if (lineString) {
        segment.setPath(*lineString);

        if (isOutline) {
            outline = segment;
        } else {
            segments.push_back(segment);
        }
    }
}

AlternativeRoutesModel *RoutingManager::alternativeRoutesModel()
{
    return &d->m_alternativeRoutesModel;
}

void RoutingManager::writeSettings() const
{
    d->saveRoute(d->stateFile());
}

void RoutingManager::saveRoute(const QString &filename) const
{
    d->saveRoute(filename);
}

void RoutingManager::loadRoute(const QString &filename)
{
    d->loadRoute(filename);
}

RoutingProfile RoutingManager::defaultProfile(RoutingProfile::TransportType transportType) const
{
    RoutingProfile profile;
    RoutingProfilesModel::ProfileTemplate tpl = RoutingProfilesModel::CarFastestTemplate;
    switch (transportType) {
    case RoutingProfile::Motorcar:
        tpl = RoutingProfilesModel::CarFastestTemplate;
        profile.setName(QStringLiteral("Motorcar"));
        profile.setTransportType(RoutingProfile::Motorcar);
        break;
    case RoutingProfile::Bicycle:
        tpl = RoutingProfilesModel::BicycleTemplate;
        profile.setName(QStringLiteral("Bicycle"));
        profile.setTransportType(RoutingProfile::Bicycle);
        break;
    case RoutingProfile::Pedestrian:
        tpl = RoutingProfilesModel::PedestrianTemplate;
        profile.setName(QStringLiteral("Pedestrian"));
        profile.setTransportType(RoutingProfile::Pedestrian);
        break;
    }

    for (RoutingRunnerPlugin *plugin : d->m_pluginManager->routingRunnerPlugins()) {
        if (plugin->supportsTemplate(tpl)) {
            profile.pluginSettings()[plugin->nameId()] = plugin->templateSettings(tpl);
        }
    }

    return profile;
}

void RoutingManager::readSettings()
{
    d->loadRoute(d->stateFile());
}

void RoutingManager::setGuidanceModeEnabled(bool enabled)
{
    if (d->m_guidanceModeEnabled == enabled) {
        return;
    }

    d->m_guidanceModeEnabled = enabled;

    if (enabled) {
        d->saveRoute(d->stateFile(QStringLiteral("guidance.kml")));

        if (d->m_guidanceModeWarning) {
            QString text = QLatin1StringView("<p>") + tr("Caution: Driving instructions may be incomplete or wrong.") + QLatin1Char(' ')
                + tr("Road construction, weather and other unforeseen variables can result in the suggested route not to be the most expedient or safest route "
                     "to your destination.")
                + QLatin1Char(' ') + tr("Please use common sense while navigating.") + QLatin1StringView("</p>") + QLatin1StringView("<p>")
                + tr("The Marble development team wishes you a pleasant and safe journey.") + QLatin1StringView("</p>");
            QPointer<QMessageBox> messageBox = new QMessageBox(QMessageBox::Information, tr("Guidance Mode"), text, QMessageBox::Ok);
            auto showAgain = new QCheckBox(tr("Show again"));
            showAgain->setChecked(true);
            showAgain->blockSignals(true); // otherwise it'd close the dialog
            messageBox->addButton(showAgain, QMessageBox::ActionRole);
            const bool smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
            messageBox->resize(380, smallScreen ? 400 : 240);
            messageBox->exec();
            if (!messageBox.isNull()) {
                d->m_guidanceModeWarning = showAgain->isChecked();
            }
            delete messageBox;
        }
    } else {
        d->loadRoute(d->stateFile(QStringLiteral("guidance.kml")));
    }

    PositionProviderPlugin *positionProvider = d->m_positionTracking->positionProviderPlugin();
    if (!positionProvider && enabled) {
        const QList<const PositionProviderPlugin *> plugins = d->m_pluginManager->positionProviderPlugins();
        if (!plugins.isEmpty()) {
            positionProvider = plugins.first()->newInstance();
        }
        d->m_positionTracking->setPositionProviderPlugin(positionProvider);
        d->m_shutdownPositionTracking = true;
    } else if (positionProvider && !enabled && d->m_shutdownPositionTracking) {
        d->m_shutdownPositionTracking = false;
        d->m_positionTracking->setPositionProviderPlugin(nullptr);
    }

    Q_EMIT guidanceModeEnabledChanged(d->m_guidanceModeEnabled);
}

void RoutingManagerPrivate::recalculateRoute(bool deviated)
{
    if (m_guidanceModeEnabled && deviated) {
        for (int i = m_routeRequest.size() - 3; i >= 0; --i) {
            if (m_routeRequest.visited(i)) {
                m_routeRequest.remove(i);
            }
        }

        if (m_routeRequest.size() == 2 && m_routeRequest.visited(0) && !m_routeRequest.visited(1)) {
            m_routeRequest.setPosition(0, m_positionTracking->currentLocation(), QObject::tr("Current Location"));
            q->retrieveRoute();
        } else if (m_routeRequest.size() != 0 && !m_routeRequest.visited(m_routeRequest.size() - 1)) {
            m_routeRequest.insert(0, m_positionTracking->currentLocation(), QObject::tr("Current Location"));
            q->retrieveRoute();
        }
    }
}

void RoutingManager::reverseRoute()
{
    d->m_routeRequest.reverse();
    retrieveRoute();
}

void RoutingManager::clearRoute()
{
    d->m_routeRequest.clear();
    retrieveRoute();
}

void RoutingManager::setShowGuidanceModeStartupWarning(bool show)
{
    d->m_guidanceModeWarning = show;
}

bool RoutingManager::showGuidanceModeStartupWarning() const
{
    return d->m_guidanceModeWarning;
}

void RoutingManager::setLastOpenPath(const QString &path)
{
    d->m_lastOpenPath = path;
}

QString RoutingManager::lastOpenPath() const
{
    return d->m_lastOpenPath;
}

void RoutingManager::setLastSavePath(const QString &path)
{
    d->m_lastSavePath = path;
}

QString RoutingManager::lastSavePath() const
{
    return d->m_lastSavePath;
}

void RoutingManager::setRouteColorStandard(const QColor &color)
{
    d->m_routeColorStandard = color;
}

QColor RoutingManager::routeColorStandard() const
{
    return d->m_routeColorStandard;
}

void RoutingManager::setRouteColorHighlighted(const QColor &color)
{
    d->m_routeColorHighlighted = color;
}

QColor RoutingManager::routeColorHighlighted() const
{
    return d->m_routeColorHighlighted;
}

void RoutingManager::setRouteColorAlternative(const QColor &color)
{
    d->m_routeColorAlternative = color;
}

QColor RoutingManager::routeColorAlternative() const
{
    return d->m_routeColorAlternative;
}

bool RoutingManager::guidanceModeEnabled() const
{
    return d->m_guidanceModeEnabled;
}

} // namespace Marble

#include "moc_RoutingManager.cpp"
