// Copyright (C) 2016 Vlad Seryakov <vseryakov@gmail.com>
// Copyright (C) 2016 Aaron McCarthy <mccarthy.aaron@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qgeoroutingmanagerenginemapbox.h"
#include "qgeoroutereplymapbox.h"
#include "qmapboxcommon.h"
#include <QtLocation/private/qgeorouteparserosrmv5_p.h>
#include <QtLocation/qgeoroutesegment.h>
#include <QtLocation/qgeomaneuver.h>

#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QUrlQuery>
#include <QtCore/QDebug>

QT_BEGIN_NAMESPACE

class QGeoRouteParserOsrmV5ExtensionMapbox: public QGeoRouteParserOsrmV5Extension
{
public:
    QGeoRouteParserOsrmV5ExtensionMapbox(const QString &accessToken, bool useMapboxTextInstructions);
    void updateQuery(QUrlQuery &query) const override;
    void updateSegment(QGeoRouteSegment &segment, const QJsonObject &step, const QJsonObject &maneuver) const override;

    QString m_accessToken;
    bool m_useMapboxTextInstructions = false;
};

QGeoRouteParserOsrmV5ExtensionMapbox::QGeoRouteParserOsrmV5ExtensionMapbox(const QString &accessToken, bool useMapboxTextInstructions)
    : QGeoRouteParserOsrmV5Extension(), m_accessToken(accessToken), m_useMapboxTextInstructions(useMapboxTextInstructions)
{

}

void QGeoRouteParserOsrmV5ExtensionMapbox::updateQuery(QUrlQuery &query) const
{
    if (!m_accessToken.isEmpty())
        query.addQueryItem(QLatin1String("access_token"), m_accessToken);

    query.addQueryItem(QLatin1String("annotations"), QLatin1String("duration,distance,speed,congestion"));

    query.addQueryItem(QLatin1String("voice_instructions"), QLatin1String("true"));
    query.addQueryItem(QLatin1String("banner_instructions"), QLatin1String("true"));
    query.addQueryItem(QLatin1String("roundabout_exits"), QLatin1String("true"));

    QLocale::MeasurementSystem unit = QLocale::system().measurementSystem();
    query.addQueryItem(QLatin1String("voice_units"), unit == QLocale::MetricSystem ? QLatin1String("metric") : QLatin1String("imperial"));
}

static QVariantMap parseMapboxVoiceInstruction(const QJsonObject &voiceInstruction)
{
    QVariantMap map;

    if (voiceInstruction.value(QLatin1String("distanceAlongGeometry")).isDouble())
        map.insert(QLatin1String("distance_along_geometry"), voiceInstruction.value(QLatin1String("distanceAlongGeometry")).toDouble());

    if (voiceInstruction.value(QLatin1String("announcement")).isString())
        map.insert(QLatin1String("announcement"), voiceInstruction.value(QLatin1String("announcement")).toString());

    if (voiceInstruction.value(QLatin1String("ssmlAnnouncement")).isString())
        map.insert(QLatin1String("ssml_announcement"), voiceInstruction.value(QLatin1String("ssmlAnnouncement")).toString());

    return map;
}

static QVariantList parseMapboxVoiceInstructions(const QJsonArray &voiceInstructions)
{
    QVariantList list;
    for (const QJsonValueConstRef voiceInstructionValue : voiceInstructions) {
        if (voiceInstructionValue.isObject())
            list << parseMapboxVoiceInstruction(voiceInstructionValue.toObject());
    }
    return list;
}

static QVariantMap parseMapboxBannerComponent(const QJsonObject &bannerComponent)
{
    QVariantMap map;

    if (bannerComponent.value(QLatin1String("type")).isString())
        map.insert(QLatin1String("type"), bannerComponent.value(QLatin1String("type")).toString());

    if (bannerComponent.value(QLatin1String("text")).isString())
        map.insert(QLatin1String("text"), bannerComponent.value(QLatin1String("text")).toString());

    if (bannerComponent.value(QLatin1String("abbr")).isString())
        map.insert(QLatin1String("abbr"), bannerComponent.value(QLatin1String("abbr")).toString());

    if (bannerComponent.value(QLatin1String("abbr_priority")).isDouble())
        map.insert(QLatin1String("abbr_priority"), bannerComponent.value(QLatin1String("abbr_priority")).toInt());

    return map;
}

static QVariantList parseMapboxBannerComponents(const QJsonArray &bannerComponents)
{
    QVariantList list;
    for (const QJsonValueConstRef bannerComponentValue : bannerComponents) {
        if (bannerComponentValue.isObject())
            list << parseMapboxBannerComponent(bannerComponentValue.toObject());
    }
    return list;
}

static QVariantMap parseMapboxBanner(const QJsonObject &banner)
{
    QVariantMap map;

    if (banner.value(QLatin1String("text")).isString())
        map.insert(QLatin1String("text"), banner.value(QLatin1String("text")).toString());

    if (banner.value(QLatin1String("components")).isArray())
        map.insert(QLatin1String("components"), parseMapboxBannerComponents(banner.value(QLatin1String("components")).toArray()));

    if (banner.value(QLatin1String("type")).isString())
        map.insert(QLatin1String("type"), banner.value(QLatin1String("type")).toString());

    if (banner.value(QLatin1String("modifier")).isString())
        map.insert(QLatin1String("modifier"), banner.value(QLatin1String("modifier")).toString());

    if (banner.value(QLatin1String("degrees")).isDouble())
        map.insert(QLatin1String("degrees"), banner.value(QLatin1String("degrees")).toDouble());

    if (banner.value(QLatin1String("driving_side")).isString())
        map.insert(QLatin1String("driving_side"), banner.value(QLatin1String("driving_side")).toString());

    return map;
}

static QVariantMap parseMapboxBannerInstruction(const QJsonObject &bannerInstruction)
{
    QVariantMap map;

    if (bannerInstruction.value(QLatin1String("distanceAlongGeometry")).isDouble())
        map.insert(QLatin1String("distance_along_geometry"), bannerInstruction.value(QLatin1String("distanceAlongGeometry")).toDouble());

    if (bannerInstruction.value(QLatin1String("primary")).isObject())
        map.insert(QLatin1String("primary"), parseMapboxBanner(bannerInstruction.value(QLatin1String("primary")).toObject()));

    if (bannerInstruction.value(QLatin1String("secondary")).isObject())
        map.insert(QLatin1String("secondary"), parseMapboxBanner(bannerInstruction.value(QLatin1String("secondary")).toObject()));

    if (bannerInstruction.value(QLatin1String("then")).isObject())
        map.insert(QLatin1String("then"), parseMapboxBanner(bannerInstruction.value(QLatin1String("then")).toObject()));

    return map;
}

static QVariantList parseMapboxBannerInstructions(const QJsonArray &bannerInstructions)
{
    QVariantList list;
    for (const QJsonValueConstRef bannerInstructionValue : bannerInstructions) {
        if (bannerInstructionValue.isObject())
            list << parseMapboxBannerInstruction(bannerInstructionValue.toObject());
    }
    return list;
}

void QGeoRouteParserOsrmV5ExtensionMapbox::updateSegment(QGeoRouteSegment &segment, const QJsonObject &step, const QJsonObject &maneuver) const
{
    QGeoManeuver m = segment.maneuver();
    QVariantMap extendedAttributes = m.extendedAttributes();
    if (m_useMapboxTextInstructions && maneuver.value(QLatin1String("instruction")).isString()) {
        QString maneuverInstructionText = maneuver.value(QLatin1String("instruction")).toString();
        if (!maneuverInstructionText.isEmpty())
            m.setInstructionText(maneuverInstructionText);
    }

    if (step.value(QLatin1String("voiceInstructions")).isArray())
        extendedAttributes.insert(QLatin1String("mapbox.voice_instructions"),
                                  parseMapboxVoiceInstructions(step.value(QLatin1String("voiceInstructions")).toArray()));
    if (step.value(QLatin1String("bannerInstructions")).isArray())
        extendedAttributes.insert(QLatin1String("mapbox.banner_instructions"),
                                  parseMapboxBannerInstructions(step.value(QLatin1String("bannerInstructions")).toArray()));

    m.setExtendedAttributes(extendedAttributes);
    segment.setManeuver(m);
}


QGeoRoutingManagerEngineMapbox::QGeoRoutingManagerEngineMapbox(const QVariantMap &parameters,
                                                         QGeoServiceProvider::Error *error,
                                                         QString *errorString)
    : QGeoRoutingManagerEngine(parameters),
      m_networkManager(new QNetworkAccessManager(this)),
      m_userAgent(mapboxDefaultUserAgent)
{
    if (parameters.contains(QStringLiteral("mapbox.useragent"))) {
        m_userAgent = parameters.value(QStringLiteral("mapbox.useragent")).toString().toLatin1();
    }

    if (parameters.contains(QStringLiteral("mapbox.access_token"))) {
        m_accessToken = parameters.value(QStringLiteral("mapbox.access_token")).toString();
    }

    bool use_mapbox_text_instructions = true;
    if (parameters.contains(QStringLiteral("mapbox.routing.use_mapbox_text_instructions"))) {
        use_mapbox_text_instructions = parameters.value(QStringLiteral("mapbox.routing.use_mapbox_text_instructions")).toBool();
    }

    QGeoRouteParserOsrmV5 *parser = new QGeoRouteParserOsrmV5(this);
    parser->setExtension(new QGeoRouteParserOsrmV5ExtensionMapbox(m_accessToken, use_mapbox_text_instructions));
    if (parameters.contains(QStringLiteral("mapbox.routing.traffic_side"))) {
        QString trafficSide = parameters.value(QStringLiteral("mapbox.routing.traffic_side")).toString();
        if (trafficSide == QStringLiteral("right"))
            parser->setTrafficSide(QGeoRouteParser::RightHandTraffic);
        else if (trafficSide == QStringLiteral("left"))
            parser->setTrafficSide(QGeoRouteParser::LeftHandTraffic);
    }
    m_routeParser = parser;

    *error = QGeoServiceProvider::NoError;
    errorString->clear();
}

QGeoRoutingManagerEngineMapbox::~QGeoRoutingManagerEngineMapbox()
{
}

QGeoRouteReply* QGeoRoutingManagerEngineMapbox::calculateRoute(const QGeoRouteRequest &request)
{
    QNetworkRequest networkRequest;
    networkRequest.setHeader(QNetworkRequest::UserAgentHeader, m_userAgent);

    QString url = mapboxDirectionsApiPath;

    QGeoRouteRequest::TravelModes travelModes = request.travelModes();
    if (travelModes.testFlag(QGeoRouteRequest::PedestrianTravel)) {
        url += QStringLiteral("walking/");
    } else if (travelModes.testFlag(QGeoRouteRequest::BicycleTravel)) {
        url += QStringLiteral("cycling/");
    } else if (travelModes.testFlag(QGeoRouteRequest::CarTravel)) {
        const QList<QGeoRouteRequest::FeatureType> &featureTypes = request.featureTypes();
        int trafficFeatureIdx = featureTypes.indexOf(QGeoRouteRequest::TrafficFeature);
        QGeoRouteRequest::FeatureWeight trafficWeight = request.featureWeight(QGeoRouteRequest::TrafficFeature);
        if (trafficFeatureIdx >= 0 &&
           (trafficWeight == QGeoRouteRequest::AvoidFeatureWeight || trafficWeight == QGeoRouteRequest::DisallowFeatureWeight)) {
            url += QStringLiteral("driving-traffic/");
        } else {
            url += QStringLiteral("driving/");
        }
    }

    networkRequest.setUrl(m_routeParser->requestUrl(request, url));

    QNetworkReply *reply = m_networkManager->get(networkRequest);

    QGeoRouteReplyMapbox *routeReply = new QGeoRouteReplyMapbox(reply, request, this);

    connect(routeReply, &QGeoRouteReplyMapbox::finished,
            this, &QGeoRoutingManagerEngineMapbox::replyFinished);
    connect(routeReply, &QGeoRouteReplyMapbox::errorOccurred,
            this, &QGeoRoutingManagerEngineMapbox::replyError);

    return routeReply;
}

const QGeoRouteParser *QGeoRoutingManagerEngineMapbox::routeParser() const
{
    return m_routeParser;
}

void QGeoRoutingManagerEngineMapbox::replyFinished()
{
    QGeoRouteReply *reply = qobject_cast<QGeoRouteReply *>(sender());
    if (reply)
        emit finished(reply);
}

void QGeoRoutingManagerEngineMapbox::replyError(QGeoRouteReply::Error errorCode,
                                             const QString &errorString)
{
    QGeoRouteReply *reply = qobject_cast<QGeoRouteReply *>(sender());
    if (reply)
        emit errorOccurred(reply, errorCode, errorString);
}

QT_END_NAMESPACE
