// Copyright (C) 2016 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
// Qt-Security score:critical reason:network-protocol

#include "qgeorouteparserosrmv5_p.h"
#include "qgeoroute.h"
#include "qgeoroute_p.h"
#include "qgeorouteparser_p_p.h"
#include "qgeoroutesegment.h"
#include "qgeoroutesegment_p.h"
#include "qgeomaneuver.h"

#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QUrlQuery>
#include <QtPositioning/qgeopath.h>

#include <QtCore/private/qobject_p.h>
#include <QtPositioning/private/qlocationutils_p.h>

QT_BEGIN_NAMESPACE

static QList<QGeoCoordinate> decodePolyline(const QString &polylineString)
{
    QList<QGeoCoordinate> path;
    if (polylineString.isEmpty())
        return path;

    const QByteArray data = polylineString.toLatin1();

    bool parsingLatitude = true;

    int shift = 0;
    int value = 0;

    QGeoCoordinate coord(0, 0);

    for (qsizetype i = 0; i < data.length(); ++i) {
        unsigned char c = data.at(i) - 63;

        value |= (c & 0x1f) << shift;
        shift += 5;

        // another chunk
        if (c & 0x20)
            continue;

        int diff = (value & 1) ? ~(value >> 1) : (value >> 1);

        if (parsingLatitude) {
            coord.setLatitude(coord.latitude() + (double)diff/1e6);
        } else {
            coord.setLongitude(coord.longitude() + (double)diff/1e6);
            path.append(coord);
        }

        parsingLatitude = !parsingLatitude;

        value = 0;
        shift = 0;
    }

    return path;
}

static QString cardinalDirection4(QLocationUtils::CardinalDirection direction)
{
    switch (direction) {
        case QLocationUtils::CardinalN:
            //: Translations exist at https://github.com/Project-OSRM/osrm-text-instructions.
            //: Always used in "Head %1 [onto <street name>]"
            return QGeoRouteParserOsrmV5::tr("North");
        case QLocationUtils::CardinalE:
            return QGeoRouteParserOsrmV5::tr("East");
        case QLocationUtils::CardinalS:
            return QGeoRouteParserOsrmV5::tr("South");
        case QLocationUtils::CardinalW:
            return QGeoRouteParserOsrmV5::tr("West");
        default:
            return QString();
    }
}

static QString exitOrdinal(int exit)
{
    static QList<QString> ordinals;

    if (!ordinals.size()) {
        ordinals.append(QLatin1String(""));
        //: always used in " and take the %1 exit [onto <street name>]"
        ordinals.append(QGeoRouteParserOsrmV5::tr("first", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("second", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("third", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("fourth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("fifth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("sixth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("seventh", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("eighth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("ninth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("tenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("eleventh", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("twelfth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("thirteenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("fourteenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("fifteenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("sixteenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("seventeenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("eighteenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("nineteenth", "roundabout exit"));
        ordinals.append(QGeoRouteParserOsrmV5::tr("twentieth", "roundabout exit"));
    };

    if (exit < 1 || exit > ordinals.size())
        return QString();
    return ordinals[exit];
}

static QString exitDirection(int exit, const QString &wayName)
{
    /*: Always appended to one of the following strings:
        - "Enter the roundabout"
        - "Enter the rotary"
        - "Enter the rotary <rotaryname>"
    */
    static QString directionExit = QGeoRouteParserOsrmV5::tr(" and take the %1 exit");
    static QString directionExitOnto = QGeoRouteParserOsrmV5::tr(" and take the %1 exit onto %2");

    if (exit < 1 || exit > 20)
        return QString();
    if (wayName.isEmpty())
        return directionExit.arg(exitOrdinal(exit));
    else
        return directionExitOnto.arg(exitOrdinal(exit), wayName);
}

static QString instructionArrive(QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionForward:
            return QGeoRouteParserOsrmV5::tr("You have arrived at your destination, straight ahead");
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            return QGeoRouteParserOsrmV5::tr("You have arrived at your destination, on the left");
        case QGeoManeuver::DirectionUTurnRight:
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            return QGeoRouteParserOsrmV5::tr("You have arrived at your destination, on the right");
        default:
            return QGeoRouteParserOsrmV5::tr("You have arrived at your destination");
    }
}

static QString instructionContinue(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue straight");
            else
                return QGeoRouteParserOsrmV5::tr("Continue straight on %1").arg(wayName);
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue left");
            else
                return QGeoRouteParserOsrmV5::tr("Continue left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue slightly left");
            else
                return QGeoRouteParserOsrmV5::tr("Continue slightly left on %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue right");
            else
                return QGeoRouteParserOsrmV5::tr("Continue right onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue slightly right");
            else
                return QGeoRouteParserOsrmV5::tr("Continue slightly right on %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Make a U-turn");
            else
                return QGeoRouteParserOsrmV5::tr("Make a U-turn onto %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue");
            else
                return QGeoRouteParserOsrmV5::tr("Continue on %1").arg(wayName);
    }
}

static QString instructionDepart(const QJsonObject &maneuver, const QString &wayName)
{
    double bearing = maneuver.value(QLatin1String("bearing_after")).toDouble(-1.0);
    if (bearing >= 0.0) {
        if (wayName.isEmpty())
            //: %1 is "North", "South", "East" or "West"
            return QGeoRouteParserOsrmV5::tr("Head %1").arg(cardinalDirection4(QLocationUtils::azimuthToCardinalDirection4(bearing)));
        else
            return QGeoRouteParserOsrmV5::tr("Head %1 onto %2").arg(cardinalDirection4(QLocationUtils::azimuthToCardinalDirection4(bearing)), wayName);
    } else {
        if (wayName.isEmpty())
            return QGeoRouteParserOsrmV5::tr("Depart");
        else
            return QGeoRouteParserOsrmV5::tr("Depart onto %1").arg(wayName);
    }
}

static QString instructionEndOfRoad(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the end of the road, turn left");
            else
                return QGeoRouteParserOsrmV5::tr("At the end of the road, turn left onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the end of the road, turn right");
            else
                return QGeoRouteParserOsrmV5::tr("At the end of the road, turn right onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the end of the road, make a U-turn");
            else
                return QGeoRouteParserOsrmV5::tr("At the end of the road, make a U-turn onto %1").arg(wayName);
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the end of the road, continue straight");
            else
                return QGeoRouteParserOsrmV5::tr("At the end of the road, continue straight onto %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the end of the road, continue");
            else
                return QGeoRouteParserOsrmV5::tr("At the end of the road, continue onto %1").arg(wayName);
    }
}

static QString instructionFerry(const QString &wayName)
{
    QString instruction = QGeoRouteParserOsrmV5::tr("Take the ferry");
    if (!wayName.isEmpty())
        instruction += QLatin1String(" [") + wayName + QLatin1Char(']');

    return instruction;
}

static QString instructionFork(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionHardLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, take a sharp left");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, take a sharp left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, turn left");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, turn left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, keep left");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, keep left onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, take a sharp right");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, take a sharp right onto %1").arg(wayName);
        case QGeoManeuver::DirectionRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, turn right");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, turn right onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, keep right");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, keep right onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Make a U-turn");
            else
                return QGeoRouteParserOsrmV5::tr("Make a U-turn onto %1").arg(wayName);
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, continue straight ahead");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, continue straight ahead onto %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the fork, continue");
            else
                return QGeoRouteParserOsrmV5::tr("At the fork, continue onto %1").arg(wayName);
    }
}

static QString instructionMerge(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionHardLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge sharply left");
            else
                return QGeoRouteParserOsrmV5::tr("Merge sharply left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge left");
            else
                return QGeoRouteParserOsrmV5::tr("Merge left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge slightly left");
            else
                return QGeoRouteParserOsrmV5::tr("Merge slightly left on %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnRight:
        case QGeoManeuver::DirectionHardRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge sharply right");
            else
                return QGeoRouteParserOsrmV5::tr("Merge sharply right onto %1").arg(wayName);
        case QGeoManeuver::DirectionRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge right");
            else
                return QGeoRouteParserOsrmV5::tr("Merge right onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge slightly right");
            else
                return QGeoRouteParserOsrmV5::tr("Merge slightly right on %1").arg(wayName);
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge straight");
            else
                return QGeoRouteParserOsrmV5::tr("Merge straight on %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Merge");
            else
                return QGeoRouteParserOsrmV5::tr("Merge onto %1").arg(wayName);
    }
}

static QString instructionNewName(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionHardLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Take a sharp left");
            else
                return QGeoRouteParserOsrmV5::tr("Take a sharp left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn left");
            else
                return QGeoRouteParserOsrmV5::tr("Turn left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue slightly left");
            else
                return QGeoRouteParserOsrmV5::tr("Continue slightly left onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Take a sharp right");
            else
                return QGeoRouteParserOsrmV5::tr("Take a sharp right onto %1").arg(wayName);
        case QGeoManeuver::DirectionRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn right");
            else
                return QGeoRouteParserOsrmV5::tr("Turn right onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue slightly right");
            else
                return QGeoRouteParserOsrmV5::tr("Continue slightly right onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Make a U-turn");
            else
                return QGeoRouteParserOsrmV5::tr("Make a U-turn onto %1").arg(wayName);
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue straight");
            else
                return QGeoRouteParserOsrmV5::tr("Continue straight onto %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue");
            else
                return QGeoRouteParserOsrmV5::tr("Continue onto %1").arg(wayName);
    }
}

static QString instructionNotification(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue on the left");
            else
                return QGeoRouteParserOsrmV5::tr("Continue on the left on %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnRight:
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue on the right");
            else
                return QGeoRouteParserOsrmV5::tr("Continue on the right on %1").arg(wayName);
        case QGeoManeuver::DirectionForward:
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Continue");
            else
                return QGeoRouteParserOsrmV5::tr("Continue on %1").arg(wayName);
    }
}

static QString instructionOffRamp(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Take the ramp on the left");
            else
                return QGeoRouteParserOsrmV5::tr("Take the ramp on the left onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnRight:
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Take the ramp on the right");
            else
                return QGeoRouteParserOsrmV5::tr("Take the ramp on the right onto %1").arg(wayName);
        case QGeoManeuver::DirectionForward:
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Take the ramp");
            else
                return QGeoRouteParserOsrmV5::tr("Take the ramp onto %1").arg(wayName);
    }
}

static QString instructionOnRamp(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    return instructionOffRamp(wayName, direction);
}

static QString instructionPushingBike(const QString &wayName)
{
    if (wayName.isEmpty())
        return QGeoRouteParserOsrmV5::tr("Get off the bike and push");
    else
        return QGeoRouteParserOsrmV5::tr("Get off the bike and push onto %1").arg(wayName);
}

static QString instructionRotary(const QJsonObject &step, const QJsonObject &maneuver, const QString &wayName)
{
    QString instruction;
    QString rotaryName = step.value(QLatin1String("rotary_name")).toString();
    //QString modifier = maneuver.value(QLatin1String("modifier")).toString(); // Apparently not used for rotaries
    int exit = maneuver.value(QLatin1String("exit")).toInt(0);

    //: This string will be prepended to " and take the <nth> exit [onto <streetname>]
    instruction += QGeoRouteParserOsrmV5::tr("Enter the rotary");
    if (!rotaryName.isEmpty())
        instruction +=  QLatin1Char(' ') + rotaryName;
    instruction += exitDirection(exit, wayName);
    return instruction;
}

static QString instructionRoundabout(const QJsonObject &maneuver, const QString &wayName)
{
    QString instruction;
    //QString modifier = maneuver.value(QLatin1String("modifier")).toString(); // Apparently not used for rotaries
    int exit = maneuver.value(QLatin1String("exit")).toInt(0);

    //: This string will be prepended to " and take the <nth> exit [onto <streetname>]
    instruction += QGeoRouteParserOsrmV5::tr("Enter the roundabout");
    instruction += exitDirection(exit, wayName);
    return instruction;
}

static QString instructionRoundaboutTurn(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the roundabout, continue straight");
            else
                return QGeoRouteParserOsrmV5::tr("At the roundabout, continue straight on %1").arg(wayName);
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the roundabout, turn left");
            else
                return QGeoRouteParserOsrmV5::tr("At the roundabout, turn left onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the roundabout, turn right");
            else
                return QGeoRouteParserOsrmV5::tr("At the roundabout, turn right onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the roundabout, turn around");
            else
                return QGeoRouteParserOsrmV5::tr("At the roundabout, turn around onto %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("At the roundabout, continue");
            else
                return QGeoRouteParserOsrmV5::tr("At the roundabout, continue onto %1").arg(wayName);
    }
}

static QString instructionTrain(const QString &wayName)
{
    return wayName.isEmpty()
        ? QGeoRouteParserOsrmV5::tr("Take the train")
        : QGeoRouteParserOsrmV5::tr("Take the train [%1]").arg(wayName);
}

static QString instructionTurn(const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    switch (direction) {
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Go straight");
            else
                return QGeoRouteParserOsrmV5::tr("Go straight onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardLeft:
        case QGeoManeuver::DirectionLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn left");
            else
                return QGeoRouteParserOsrmV5::tr("Turn left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn slightly left");
            else
                return QGeoRouteParserOsrmV5::tr("Turn slightly left onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
        case QGeoManeuver::DirectionRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn right");
            else
                return QGeoRouteParserOsrmV5::tr("Turn right onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn slightly right");
            else
                return QGeoRouteParserOsrmV5::tr("Turn slightly right onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Make a U-turn");
            else
                return QGeoRouteParserOsrmV5::tr("Make a U-turn onto %1").arg(wayName);
        default:
            if (wayName.isEmpty())
                return QGeoRouteParserOsrmV5::tr("Turn");
            else
                return QGeoRouteParserOsrmV5::tr("Turn onto %1").arg(wayName);
    }
}

static QString instructionUseLane(const QJsonObject &maneuver, const QString &wayName, QGeoManeuver::InstructionDirection direction)
{
    QString laneTypes = maneuver.value(QLatin1String("laneTypes")).toString();
    QString laneInstruction;
    if (laneTypes == QLatin1String("xo") || laneTypes == QLatin1String("xoo") || laneTypes == QLatin1String("xxo"))
        //: "and <instruction direction> [onto <street name>] will be appended to this string. E.g., "Keep right and make a sharp left"
        laneInstruction = QLatin1String("Keep right");
    else if (laneTypes == QLatin1String("ox") || laneTypes == QLatin1String("oox") || laneTypes == QLatin1String("oxx"))
        laneInstruction = QLatin1String("Keep left");
    else if (laneTypes == QLatin1String("xox"))
        laneInstruction = QLatin1String("Use the middle lane");
    else if (laneTypes == QLatin1String("oxo"))
        laneInstruction = QLatin1String("Use the left or the right lane");

    if (laneInstruction.isEmpty()) {
        if (wayName.isEmpty())
            return QGeoRouteParserOsrmV5::tr("Continue straight");
        else
            return QGeoRouteParserOsrmV5::tr("Continue straight onto %1").arg(wayName);
    }

    switch (direction) {
        case QGeoManeuver::DirectionForward:
            if (wayName.isEmpty())
                //: This string will be prepended with lane instructions. E.g., "Use the left or the right lane and continue straight"
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and continue straight");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and continue straight onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardLeft:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a sharp left");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a sharp left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLeft:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and turn left");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and turn left onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightLeft:
        case QGeoManeuver::DirectionBearLeft:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a slight left");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a slight left onto %1").arg(wayName);
        case QGeoManeuver::DirectionHardRight:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a sharp right");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a sharp right onto %1").arg(wayName);
        case QGeoManeuver::DirectionRight:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and turn right");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and turn right onto %1").arg(wayName);
        case QGeoManeuver::DirectionLightRight:
        case QGeoManeuver::DirectionBearRight:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a slight right");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a slight right onto %1").arg(wayName);
        case QGeoManeuver::DirectionUTurnLeft:
        case QGeoManeuver::DirectionUTurnRight:
            if (wayName.isEmpty())
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a U-turn");
            else
                return laneInstruction + QGeoRouteParserOsrmV5::tr(" and make a U-turn onto %1").arg(wayName);
        default:
            return laneInstruction;
    }
}

static QString instructionText(const QJsonObject &step, const QJsonObject &maneuver, QGeoManeuver::InstructionDirection direction) {
    QString modifier;
    if (maneuver.value(QLatin1String("modifier")).isString())
        modifier = maneuver.value(QLatin1String("modifier")).toString();
    QString maneuverType;
    if (maneuver.value(QLatin1String("type")).isString())
        maneuverType = maneuver.value(QLatin1String("type")).toString();
    QString wayName = QLatin1String("unknown street");
    if (step.value(QLatin1String("name")).isString())
        wayName = step.value(QLatin1String("name")).toString();


    if (maneuverType == QLatin1String("arrive"))
        return instructionArrive(direction);
    else if (maneuverType == QLatin1String("continue"))
        return instructionContinue(wayName, direction);
    else if (maneuverType == QLatin1String("depart"))
        return instructionDepart(maneuver, wayName);
    else if (maneuverType == QLatin1String("end of road"))
        return instructionEndOfRoad(wayName, direction);
    else if (maneuverType == QLatin1String("ferry"))
        return instructionFerry(wayName);
    else if (maneuverType == QLatin1String("fork"))
        return instructionFork(wayName, direction);
    else if (maneuverType == QLatin1String("merge"))
        return instructionMerge(wayName, direction);
    else if (maneuverType == QLatin1String("new name"))
        return instructionNewName(wayName, direction);
    else if (maneuverType == QLatin1String("notification"))
        return instructionNotification(wayName, direction);
    else if (maneuverType == QLatin1String("off ramp"))
        return instructionOffRamp(wayName, direction);
    else if (maneuverType == QLatin1String("on ramp"))
        return instructionOnRamp(wayName, direction);
    else if (maneuverType == QLatin1String("pushing bike"))
        return instructionPushingBike(wayName);
    else if (maneuverType == QLatin1String("rotary"))
        return instructionRotary(step, maneuver, wayName);
    else if (maneuverType == QLatin1String("roundabout"))
        return instructionRoundabout(maneuver, wayName);
    else if (maneuverType == QLatin1String("roundabout turn"))
        return instructionRoundaboutTurn(wayName, direction);
    else if (maneuverType == QLatin1String("train"))
        return instructionTrain(wayName);
    else if (maneuverType == QLatin1String("turn"))
        return instructionTurn(wayName, direction);
    else if (maneuverType == QLatin1String("use lane"))
        return instructionUseLane(maneuver, wayName, direction);
    else
        return maneuverType + QLatin1String(" to/onto ") + wayName;
}

static QGeoManeuver::InstructionDirection instructionDirection(const QJsonObject &maneuver, QGeoRouteParser::TrafficSide trafficSide)
{
    QString modifier;
    if (maneuver.value(QLatin1String("modifier")).isString())
        modifier = maneuver.value(QLatin1String("modifier")).toString();

    if (modifier.isEmpty())
        return QGeoManeuver::NoDirection;
    else if (modifier == QLatin1String("straight"))
        return QGeoManeuver::DirectionForward;
    else if (modifier == QLatin1String("right"))
        return QGeoManeuver::DirectionRight;
    else if (modifier == QLatin1String("sharp right"))
        return QGeoManeuver::DirectionHardRight;
    else if (modifier == QLatin1String("slight right"))
        return QGeoManeuver::DirectionLightRight;
    else if (modifier == QLatin1String("uturn")) {
        switch (trafficSide) {
        case QGeoRouteParser::RightHandTraffic:
            return QGeoManeuver::DirectionUTurnLeft;
        case QGeoRouteParser::LeftHandTraffic:
            return QGeoManeuver::DirectionUTurnRight;
        }
        return QGeoManeuver::DirectionUTurnLeft;
    } else if (modifier == QLatin1String("left"))
        return QGeoManeuver::DirectionLeft;
    else if (modifier == QLatin1String("sharp left"))
        return QGeoManeuver::DirectionHardLeft;
    else if (modifier == QLatin1String("slight left"))
        return QGeoManeuver::DirectionLightLeft;
    else
        return QGeoManeuver::NoDirection;
}

class QGeoRouteParserOsrmV5Private :  public QGeoRouteParserPrivate
{
    Q_DECLARE_PUBLIC(QGeoRouteParserOsrmV5)
public:
    QGeoRouteParserOsrmV5Private();
    virtual ~QGeoRouteParserOsrmV5Private();

    QGeoRouteSegment parseStep(const QJsonObject &step, int legIndex, int stepIndex) const;

    // QGeoRouteParserPrivate

    QGeoRouteReply::Error parseReply(QList<QGeoRoute> &routes, QString &errorString, const QByteArray &reply) const override;
    QUrl requestUrl(const QGeoRouteRequest &request, const QString &prefix) const override;

    QVariantMap m_vendorParams;
    const QGeoRouteParserOsrmV5Extension *m_extension = nullptr;
};

QGeoRouteParserOsrmV5Private::QGeoRouteParserOsrmV5Private()
    : QGeoRouteParserPrivate()
{
}

QGeoRouteParserOsrmV5Private::~QGeoRouteParserOsrmV5Private()
{
    delete m_extension;
}

QGeoRouteSegment QGeoRouteParserOsrmV5Private::parseStep(const QJsonObject &step, int legIndex, int stepIndex) const {
    // OSRM Instructions documentation: https://github.com/Project-OSRM/osrm-text-instructions
    // This goes on top of OSRM: https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md
    // Mapbox however, includes this in the reply, under "instruction".
    QGeoRouteSegment segment;
    if (!step.value(QLatin1String("maneuver")).isObject())
        return segment;
    QJsonObject maneuver = step.value(QLatin1String("maneuver")).toObject();
    if (!step.value(QLatin1String("duration")).isDouble())
        return segment;
    if (!step.value(QLatin1String("distance")).isDouble())
        return segment;
    if (!step.value(QLatin1String("intersections")).isArray())
        return segment;
    if (!maneuver.value(QLatin1String("location")).isArray())
        return segment;

    double time = step.value(QLatin1String("duration")).toDouble();
    double distance = step.value(QLatin1String("distance")).toDouble();

    QJsonArray position = maneuver.value(QLatin1String("location")).toArray();
    if (position.isEmpty())
        return segment;
    double latitude = position[1].toDouble();
    double longitude = position[0].toDouble();
    QGeoCoordinate coord(latitude, longitude);

    QString geometry = step.value(QLatin1String("geometry")).toString();
    QList<QGeoCoordinate> path = decodePolyline(geometry);

    QGeoManeuver::InstructionDirection maneuverInstructionDirection = instructionDirection(maneuver, trafficSide);

    QString maneuverInstructionText = instructionText(step, maneuver, maneuverInstructionDirection);

    QGeoManeuver geoManeuver;
    geoManeuver.setDirection(maneuverInstructionDirection);
    geoManeuver.setDistanceToNextInstruction(distance);
    geoManeuver.setTimeToNextInstruction(time);
    geoManeuver.setInstructionText(maneuverInstructionText);
    geoManeuver.setPosition(coord);
    geoManeuver.setWaypoint(coord);

    QVariantMap extraAttributes;
    static const QStringList extras {
        QLatin1String("bearing_before"),
        QLatin1String("bearing_after"),
        QLatin1String("instruction"),
        QLatin1String("type"),
        QLatin1String("modifier") };
    for (const QString &e: extras) {
        if (maneuver.find(e) != maneuver.end())
            extraAttributes.insert(e, maneuver.value(e).toVariant());
    }
    // These should be removed as soon as route leg support is introduced.
    // Ref: http://project-osrm.org/docs/v5.15.2/api/#routeleg-object
    extraAttributes.insert(QLatin1String("leg_index"), legIndex);
    extraAttributes.insert(QLatin1String("step_index"), stepIndex);

    geoManeuver.setExtendedAttributes(extraAttributes);

    segment.setDistance(distance);
    segment.setPath(path);
    segment.setTravelTime(time);
    segment.setManeuver(geoManeuver);
    if (m_extension)
        m_extension->updateSegment(segment, step, maneuver);
    return segment;
}

QGeoRouteReply::Error QGeoRouteParserOsrmV5Private::parseReply(QList<QGeoRoute> &routes, QString &errorString, const QByteArray &reply) const
{
    // OSRM v5 specs: https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md
    // Mapbox Directions API spec: https://www.mapbox.com/api-documentation/#directions
    QJsonDocument document = QJsonDocument::fromJson(reply);
    if (document.isObject()) {
        QJsonObject object = document.object();

        QString status = object.value(QLatin1String("code")).toString();
        if (status != QLatin1String("Ok")) {
            errorString = status;
            return QGeoRouteReply::UnknownError;
        }
        if (!object.value(QLatin1String("routes")).isArray()) {
            errorString = QLatin1String("No routes found");
            return QGeoRouteReply::ParseError;
        }

        const QJsonArray osrmRoutes = object.value(QLatin1String("routes")).toArray();
        for (const QJsonValueConstRef r : osrmRoutes) {
            if (!r.isObject())
                continue;
            QJsonObject routeObject = r.toObject();
            if (!routeObject.value(QLatin1String("legs")).isArray())
                continue;
            if (!routeObject.value(QLatin1String("duration")).isDouble())
                continue;
            if (!routeObject.value(QLatin1String("distance")).isDouble())
                continue;

            double distance = routeObject.value(QLatin1String("distance")).toDouble();
            double travelTime = routeObject.value(QLatin1String("duration")).toDouble();
            bool error = false;
            QList<QGeoRouteSegment> segments;

            const QJsonArray legs = routeObject.value(QLatin1String("legs")).toArray();
            QList<QGeoRoute> routeLegs;
            QGeoRoute route;
            for (int legIndex = 0; legIndex < legs.size(); ++legIndex) {
                const QJsonValue &l = legs.at(legIndex);
                QGeoRoute routeLeg;
                QList<QGeoRouteSegment> legSegments;
                if (!l.isObject()) { // invalid leg record
                    error = true;
                    break;
                }
                const QJsonObject leg = l.toObject();
                if (!leg.value(QLatin1String("steps")).isArray()) { // Invalid steps field
                    error = true;
                    break;
                }
                const double legDistance = leg.value(QLatin1String("distance")).toDouble();
                const double legTravelTime = leg.value(QLatin1String("duration")).toDouble();
                const QJsonArray steps = leg.value(QLatin1String("steps")).toArray();
                QGeoRouteSegment segment;
                for (int stepIndex = 0; stepIndex < steps.size(); ++stepIndex) {
                    const QJsonValue &s = steps.at(stepIndex);
                    if (!s.isObject()) {
                        error = true;
                        break;
                    }
                    segment = parseStep(s.toObject(), legIndex, stepIndex);
                    if (segment.isValid()) {
                        // setNextRouteSegment done below for all segments in the route.
                        legSegments.append(segment);
                    } else {
                        error = true;
                        break;
                    }
                }
                if (error)
                    break;

                QGeoRouteSegmentPrivate *segmentPrivate = QGeoRouteSegmentPrivate::get(segment);
                segmentPrivate->setLegLastSegment(true);
                QList<QGeoCoordinate> path;
                for (const QGeoRouteSegment &s: std::as_const(legSegments))
                    path.append(s.path());
                routeLeg.setLegIndex(legIndex);
                routeLeg.setOverallRoute(route); // QGeoRoute::d_ptr is explicitlySharedDataPointer. Modifiers below won't detach it.
                routeLeg.setDistance(legDistance);
                routeLeg.setTravelTime(legTravelTime);
                if (!path.isEmpty()) {
                    routeLeg.setPath(path);
                    routeLeg.setFirstRouteSegment(legSegments.first());
                }
                routeLegs << routeLeg;

                segments.append(legSegments);
            }

            if (!error) {
                QList<QGeoCoordinate> path;
                for (const QGeoRouteSegment &s : segments)
                    path.append(s.path());

                for (qsizetype i = segments.size() - 1; i > 0; --i)
                    segments[i-1].setNextRouteSegment(segments[i]);

                route.setDistance(distance);
                route.setTravelTime(travelTime);
                if (!path.isEmpty()) {
                    route.setPath(path);
                    route.setBounds(QGeoPath(path).boundingGeoRectangle());
                    route.setFirstRouteSegment(segments.first());
                }
                route.setRouteLegs(routeLegs);
                //r.setTravelMode(QGeoRouteRequest::CarTravel); // The only one supported by OSRM demo service, but other OSRM servers might do cycle or pedestrian too
                routes.append(route);
            }
        }

        // setError(QGeoRouteReply::NoError, status);  // can't do this, or NoError is emitted and does damages
        return QGeoRouteReply::NoError;
    } else {
        errorString = QLatin1String("Couldn't parse json.");
        return QGeoRouteReply::ParseError;
    }
}

QUrl QGeoRouteParserOsrmV5Private::requestUrl(const QGeoRouteRequest &request, const QString &prefix) const
{
    QString routingUrl = prefix;
    int notFirst = 0;
    const QList<QGeoCoordinate> waypoints = request.waypoints();
    for (qsizetype i = 0; i < waypoints.size(); i++) {
        const QGeoCoordinate &c = waypoints.at(i);
        if (notFirst)
            routingUrl.append(QLatin1Char(';'));
        routingUrl.append(QString::number(c.longitude(), 'f', 7)).append(QLatin1Char(',')).append(QString::number(c.latitude(), 'f', 7));
        ++notFirst;
    }

    QUrl url(routingUrl);
    QUrlQuery query;
    query.addQueryItem(QLatin1String("overview"), QLatin1String("full"));
    query.addQueryItem(QLatin1String("steps"), QLatin1String("true"));
    query.addQueryItem(QLatin1String("geometries"), QLatin1String("polyline6"));
    query.addQueryItem(QLatin1String("alternatives"), QLatin1String("true"));
    if (m_extension)
        m_extension->updateQuery(query);
    url.setQuery(query);
    return url;
}

QGeoRouteParserOsrmV5::QGeoRouteParserOsrmV5(QObject *parent)
    : QGeoRouteParser(*new QGeoRouteParserOsrmV5Private(), parent)
{
}

QGeoRouteParserOsrmV5::~QGeoRouteParserOsrmV5()
{
}

void QGeoRouteParserOsrmV5::setExtension(const QGeoRouteParserOsrmV5Extension *extension)
{
    Q_D(QGeoRouteParserOsrmV5);
    if (extension)
        d->m_extension = extension;
}

QT_END_NAMESPACE
