// Copyright (C) 2015 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qgeoroutingmanagerengine_nokia.h"
#include "qgeoroutereply_nokia.h"
#include "qgeonetworkaccessmanager.h"
#include "qgeouriprovider.h"
#include "uri_constants.h"

#include <QStringList>
#include <QUrl>
#include <QLocale>
#include <QtPositioning/QGeoRectangle>

QT_BEGIN_NAMESPACE

QGeoRoutingManagerEngineNokia::QGeoRoutingManagerEngineNokia(
        QGeoNetworkAccessManager *networkManager,
        const QVariantMap &parameters,
        QGeoServiceProvider::Error *error,
        QString *errorString)
        : QGeoRoutingManagerEngine(parameters)
        , m_networkManager(networkManager)
        , m_uriProvider(new QGeoUriProvider(this, parameters, QStringLiteral("here.routing.host"), ROUTING_HOST))

{
    Q_ASSERT(networkManager);
    m_networkManager->setParent(this);

    m_apiKey = parameters.value(QStringLiteral("here.apiKey")).toString();

    QGeoRouteRequest::FeatureTypes featureTypes;
    featureTypes |= QGeoRouteRequest::TollFeature;
    featureTypes |= QGeoRouteRequest::HighwayFeature;
    featureTypes |= QGeoRouteRequest::FerryFeature;
    featureTypes |= QGeoRouteRequest::TunnelFeature;
    featureTypes |= QGeoRouteRequest::DirtRoadFeature;
    featureTypes |= QGeoRouteRequest::ParksFeature;
    setSupportedFeatureTypes(featureTypes);

    QGeoRouteRequest::FeatureWeights featureWeights;
    featureWeights |= QGeoRouteRequest::DisallowFeatureWeight;
    featureWeights |= QGeoRouteRequest::AvoidFeatureWeight;
    featureWeights |= QGeoRouteRequest::PreferFeatureWeight;
    setSupportedFeatureWeights(featureWeights);

    QGeoRouteRequest::ManeuverDetails maneuverDetails;
    maneuverDetails |= QGeoRouteRequest::BasicManeuvers;
    setSupportedManeuverDetails(maneuverDetails);

    QGeoRouteRequest::RouteOptimizations optimizations;
    optimizations |= QGeoRouteRequest::ShortestRoute;
    optimizations |= QGeoRouteRequest::FastestRoute;
    setSupportedRouteOptimizations(optimizations);

    QGeoRouteRequest::TravelModes travelModes;
    travelModes |= QGeoRouteRequest::CarTravel;
    travelModes |= QGeoRouteRequest::PedestrianTravel;
    travelModes |= QGeoRouteRequest::PublicTransitTravel;
    travelModes |= QGeoRouteRequest::BicycleTravel;
    setSupportedTravelModes(travelModes);

    QGeoRouteRequest::SegmentDetails segmentDetails;
    segmentDetails |= QGeoRouteRequest::BasicSegmentData;
    setSupportedSegmentDetails(segmentDetails);

    if (error)
        *error = QGeoServiceProvider::NoError;

    if (errorString)
        *errorString = QString();
}

QGeoRoutingManagerEngineNokia::~QGeoRoutingManagerEngineNokia() {}

QGeoRouteReply *QGeoRoutingManagerEngineNokia::calculateRoute(const QGeoRouteRequest &request)
{
    const QStringList reqStrings = calculateRouteRequestString(request);

    if (reqStrings.isEmpty()) {
        QGeoRouteReply *reply = new QGeoRouteReply(QGeoRouteReply::UnsupportedOptionError, "The given route request options are not supported by this service provider.", this);
        emit errorOccurred(reply, reply->error(), reply->errorString());
        return reply;
    }

    QList<QNetworkReply*> replies;
    for (const QString &reqString : reqStrings)
        replies.append(m_networkManager->get(QNetworkRequest(QUrl(reqString))));

    QGeoRouteReplyNokia *reply = new QGeoRouteReplyNokia(request, replies, this);

    connect(reply, &QGeoRouteReplyNokia::finished,
            this, &QGeoRoutingManagerEngineNokia::routeFinished);
    connect(reply, &QGeoRouteReplyNokia::errorOccurred,
            this, &QGeoRoutingManagerEngineNokia::routeError);

    return reply;
}

QGeoRouteReply *QGeoRoutingManagerEngineNokia::updateRoute(const QGeoRoute &route, const QGeoCoordinate &position)
{
    const QStringList reqStrings = updateRouteRequestString(route, position);

    if (reqStrings.isEmpty()) {
        QGeoRouteReply *reply = new QGeoRouteReply(QGeoRouteReply::UnsupportedOptionError, "The given route request options are not supported by this service provider.", this);
        emit errorOccurred(reply, reply->error(), reply->errorString());
        return reply;
    }

    QList<QNetworkReply*> replies;
    for (const QString &reqString : reqStrings)
        replies.append(m_networkManager->get(QNetworkRequest(QUrl(reqString))));

    QGeoRouteRequest updateRequest(route.request());
    updateRequest.setTravelModes(route.travelMode());
    QGeoRouteReplyNokia *reply = new QGeoRouteReplyNokia(updateRequest, replies, this);

    connect(reply, &QGeoRouteReplyNokia::finished,
            this, &QGeoRoutingManagerEngineNokia::routeFinished);
    connect(reply, &QGeoRouteReplyNokia::errorOccurred,
            this, &QGeoRoutingManagerEngineNokia::routeError);

    return reply;
}

bool QGeoRoutingManagerEngineNokia::checkEngineSupport(const QGeoRouteRequest &request,
        QGeoRouteRequest::TravelModes travelModes) const
{
    const QList<QGeoRouteRequest::FeatureType> featureTypeList = request.featureTypes();
    QGeoRouteRequest::FeatureTypes featureTypeFlag = QGeoRouteRequest::NoFeature;
    QGeoRouteRequest::FeatureWeights featureWeightFlag = QGeoRouteRequest::NeutralFeatureWeight;

    for (const auto &featureType : featureTypeList) {
        featureTypeFlag |= featureType;
        featureWeightFlag |= request.featureWeight(featureType);
    }

    if ((featureTypeFlag & supportedFeatureTypes()) != featureTypeFlag)
        return false;

    if ((featureWeightFlag & supportedFeatureWeights()) != featureWeightFlag)
        return false;


    if ((request.maneuverDetail() & supportedManeuverDetails()) != request.maneuverDetail())
        return false;

    if ((request.segmentDetail() & supportedSegmentDetails()) != request.segmentDetail())
        return false;

    if ((request.routeOptimization() & supportedRouteOptimizations()) != request.routeOptimization())
        return false;

    if ((travelModes & supportedTravelModes()) != travelModes)
        return false;

    // Count the number of set bits (= number of travel modes) (popcount)
    int count = 0;

    for (unsigned bits = travelModes; bits; bits >>= 1)
        count += (bits & 1);

    // We only allow one travel mode at a time
    if (count != 1)
        return false;

    return true;
}

QStringList QGeoRoutingManagerEngineNokia::calculateRouteRequestString(const QGeoRouteRequest &request) const
{
    bool supported = checkEngineSupport(request, request.travelModes());

    if (!supported)
        return QStringList();
    QStringList requests;

    QString baseRequest = QStringLiteral("https://");
    baseRequest += m_uriProvider->getCurrentHost();
    baseRequest += QStringLiteral("/routing/7.2/calculateroute.xml");

    baseRequest += QStringLiteral("?alternatives=");
    baseRequest += QString::number(request.numberAlternativeRoutes());

    if (!m_apiKey.isEmpty()) {
        baseRequest += QStringLiteral("&apiKey=");
        baseRequest += m_apiKey;
    }

    const QList<QGeoCoordinate> waypoints = request.waypoints();
    int numWaypoints = waypoints.size();
    if (numWaypoints < 2)
        return QStringList();
    // Details: https://developer.here.com/documentation/routing/topics/resource-param-type-waypoint.html
    for (int i = 0;i < numWaypoints;++i) {
        const QGeoCoordinate &c = waypoints.at(i);
        baseRequest += QStringLiteral("&waypoint");
        baseRequest += QString::number(i);
        baseRequest += QStringLiteral("=geo!");
        baseRequest += trimDouble(c.latitude());
        baseRequest += ',';
        baseRequest += trimDouble(c.longitude());
        baseRequest += QStringLiteral(";;"); // ;<TransitRadius>;<UserLabel>
    }

    QGeoRouteRequest::RouteOptimizations optimization = request.routeOptimization();

    QStringList types;
    if (optimization.testFlag(QGeoRouteRequest::ShortestRoute))
        types.append("shortest");
    if (optimization.testFlag(QGeoRouteRequest::FastestRoute))
        types.append("fastest");

    for (const QString &optimization : types) {
        QString requestString = baseRequest;
        requestString += modesRequestString(request, request.travelModes(), optimization);
        requestString += routeRequestString(request);
        requests << requestString;
    }

    return requests;
}

QStringList QGeoRoutingManagerEngineNokia::updateRouteRequestString(const QGeoRoute &route, const QGeoCoordinate &position)
{
    if (!checkEngineSupport(route.request(), route.travelMode()))
        return QStringList();
    QStringList requests;

    QString baseRequest = "https://";
    baseRequest += m_uriProvider->getCurrentHost();
    baseRequest += "/routing/7.2/getroute.xml";

    baseRequest += "?routeid=";
    baseRequest += route.routeId();

    baseRequest += "&pos=";
    baseRequest += QString::number(position.latitude());
    baseRequest += ',';
    baseRequest += QString::number(position.longitude());

    QGeoRouteRequest::RouteOptimizations optimization = route.request().routeOptimization();

    QStringList types;
    if (optimization.testFlag(QGeoRouteRequest::ShortestRoute))
        types.append("shortest");
    if (optimization.testFlag(QGeoRouteRequest::FastestRoute))
        types.append("fastest");

    for (const QString &optimization : types) {
        QString requestString = baseRequest;
        requestString += modesRequestString(route.request(), route.travelMode(), optimization);
        requestString += routeRequestString(route.request());
        requests << requestString;
    }

    return requests;
}

QString QGeoRoutingManagerEngineNokia::modesRequestString(const QGeoRouteRequest &request,
        QGeoRouteRequest::TravelModes travelModes, const QString &optimization) const
{
    QString requestString;

    QStringList modes;
    if (travelModes.testFlag(QGeoRouteRequest::CarTravel))
        modes.append("car");
    if (travelModes.testFlag(QGeoRouteRequest::PedestrianTravel))
        modes.append("pedestrian");
    if (travelModes.testFlag(QGeoRouteRequest::PublicTransitTravel))
        modes.append("publicTransport");

    QStringList featureStrings;
    QList<QGeoRouteRequest::FeatureType> featureTypes = request.featureTypes();
    for (int i = 0; i < featureTypes.size(); ++i) {
        QGeoRouteRequest::FeatureWeight weight = request.featureWeight(featureTypes.at(i));

        if (weight == QGeoRouteRequest::NeutralFeatureWeight)
            continue;

        QString weightString = "";
        switch (weight) {
            case QGeoRouteRequest::PreferFeatureWeight:
                weightString = '1';
                break;
            case QGeoRouteRequest::AvoidFeatureWeight:
                weightString = "-1";
                break;
            case QGeoRouteRequest::DisallowFeatureWeight:
                weightString = "-3";
                break;
            case QGeoRouteRequest::NeutralFeatureWeight:
            case QGeoRouteRequest::RequireFeatureWeight:
                break;
        }

        if (weightString.isEmpty())
            continue;

        switch (featureTypes.at(i)) {
            case QGeoRouteRequest::TollFeature:
                featureStrings.append("tollroad:" + weightString);
                break;
            case QGeoRouteRequest::HighwayFeature:
                featureStrings.append("motorway:" + weightString);
                break;
            case QGeoRouteRequest::FerryFeature:
                featureStrings.append("boatFerry:" + weightString);
                featureStrings.append("railFerry:" + weightString);
                break;
            case QGeoRouteRequest::TunnelFeature:
                featureStrings.append("tunnel:" + weightString);
                break;
            case QGeoRouteRequest::DirtRoadFeature:
                featureStrings.append("dirtRoad:" + weightString);
                break;
            case QGeoRouteRequest::PublicTransitFeature:
            case QGeoRouteRequest::ParksFeature:
            case QGeoRouteRequest::MotorPoolLaneFeature:
            case QGeoRouteRequest::TrafficFeature:
            case QGeoRouteRequest::NoFeature:
                break;
        }
    }

    requestString += "&mode=";
    requestString += optimization + ';' + modes.join(',');
    if (featureStrings.count())
        requestString += ';' + featureStrings.join(',');
    return requestString;
}

QString QGeoRoutingManagerEngineNokia::routeRequestString(const QGeoRouteRequest &request) const
{
    QString requestString;

    for (const QGeoRectangle &area : request.excludeAreas()) {
        requestString += QLatin1String("&avoidareas=");
        requestString += trimDouble(area.topLeft().latitude());
        requestString += QLatin1String(",");
        requestString += trimDouble(area.topLeft().longitude());
        requestString += QLatin1String(";");
        requestString += trimDouble(area.bottomRight().latitude());
        requestString += QLatin1String(",");
        requestString += trimDouble(area.bottomRight().longitude());
    }

    QStringList legAttributes;
//    if (request.segmentDetail() & QGeoRouteRequest::BasicSegmentData)  // QTBUG-70501, this code expects to find links
    {
        requestString += "&linkattributes=sh,le"; //shape,length
        legAttributes.append("links");
    }

//    if (request.maneuverDetail() & QGeoRouteRequest::BasicManeuvers) // QTBUG-70501, this code expects to find maneuvers
    {
        legAttributes.append("maneuvers");
        //requestString += "&maneuverattributes=po,tt,le,di"; //position,traveltime,length,direction
        requestString += "&maneuverattributes=all";
        if (!(request.segmentDetail() & QGeoRouteRequest::NoSegmentData))
            requestString += ",li"; //link
    }

    // Handle QTBUG-70502, when API fixes it
    requestString += "&routeattributes=sm,sh,bb,lg"; //summary,shape,boundingBox,legs
    if (legAttributes.count() > 0) {
        requestString += "&legattributes=";
        requestString += legAttributes.join(",");
    }

    // Handle QTBUG-70503, when API fixes it
    requestString += "&departure=";
    requestString += QDateTime::currentDateTime().toUTC().toString("yyyy-MM-ddThh:mm:ssZ");

    requestString += "&instructionformat=text";

    // ToDo: make this request-able
    requestString += "&metricSystem=";
    if (QLocale::MetricSystem == measurementSystem())
        requestString  += "metric";
    else
        requestString += "imperial";

    const QLocale loc(locale());

    // ToDo: make this request-able
    if (QLocale::C != loc.language() && QLocale::AnyLanguage != loc.language()) {
        requestString += "&language=";
        requestString += loc.name();
        //If the first language isn't supported, english will be selected automatically
        if (QLocale::English != loc.language())
            requestString += ",en_US";
    }

    return requestString;
}

QString QGeoRoutingManagerEngineNokia::trimDouble(double degree, int decimalDigits)
{
    QString sDegree = QString::number(degree, 'g', decimalDigits);

    int index = sDegree.indexOf('.');

    if (index == -1)
        return sDegree;
    else
        return QString::number(degree, 'g', decimalDigits + index);
}

void QGeoRoutingManagerEngineNokia::routeFinished()
{
    QGeoRouteReply *reply = qobject_cast<QGeoRouteReply *>(sender());

    if (!reply)
        return;

    if (receivers(SIGNAL(finished(QGeoRouteReply*))) == 0) {
        reply->deleteLater();
        return;
    }

    emit finished(reply);
}

void QGeoRoutingManagerEngineNokia::routeError(QGeoRouteReply::Error error, const QString &errorString)
{
    QGeoRouteReply *reply = qobject_cast<QGeoRouteReply *>(sender());

    if (!reply)
        return;

    if (receivers(SIGNAL(errorOccurred(QGeoRouteReply*,QGeoRouteReply::Error,QString))) == 0) {
        reply->deleteLater();
        return;
    }

    emit errorOccurred(reply, error, errorString);
}

QT_END_NAMESPACE
