// 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
// Qt-Security score:critical reason:network-protocol

#include "qplacemanagerengineosm.h"
#include "qplacesearchreplyosm.h"
#include "qplacecategoriesreplyosm.h"

#include <QtCore/QElapsedTimer>
#include <QtCore/QLocale>
#include <QtCore/QRegularExpression>
#include <QtCore/QUrlQuery>
#include <QtCore/QXmlStreamReader>

#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>

#include <QtPositioning/QGeoCircle>

#include <QtLocation/QPlaceCategory>
#include <QtLocation/QPlaceSearchRequest>
#include <QtLocation/private/unsupportedreplies_p.h>

namespace
{
QString SpecialPhrasesBaseUrl = QStringLiteral("http://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/");

QString nameForTagKey(const QString &tagKey)
{
    if (tagKey == QLatin1String("aeroway"))
        return QPlaceManagerEngineOsm::tr("Aeroway");
    else if (tagKey == QLatin1String("amenity"))
        return QPlaceManagerEngineOsm::tr("Amenity");
    else if (tagKey == QLatin1String("building"))
        return QPlaceManagerEngineOsm::tr("Building");
    else if (tagKey == QLatin1String("highway"))
        return QPlaceManagerEngineOsm::tr("Highway");
    else if (tagKey == QLatin1String("historic"))
        return QPlaceManagerEngineOsm::tr("Historic");
    else if (tagKey == QLatin1String("landuse"))
        return QPlaceManagerEngineOsm::tr("Land use");
    else if (tagKey == QLatin1String("leisure"))
        return QPlaceManagerEngineOsm::tr("Leisure");
    else if (tagKey == QLatin1String("man_made"))
        return QPlaceManagerEngineOsm::tr("Man made");
    else if (tagKey == QLatin1String("natural"))
        return QPlaceManagerEngineOsm::tr("Natural");
    else if (tagKey == QLatin1String("place"))
        return QPlaceManagerEngineOsm::tr("Place");
    else if (tagKey == QLatin1String("railway"))
        return QPlaceManagerEngineOsm::tr("Railway");
    else if (tagKey == QLatin1String("shop"))
        return QPlaceManagerEngineOsm::tr("Shop");
    else if (tagKey == QLatin1String("tourism"))
        return QPlaceManagerEngineOsm::tr("Tourism");
    else if (tagKey == QLatin1String("waterway"))
        return QPlaceManagerEngineOsm::tr("Waterway");
    else
        return tagKey;
}

}

QPlaceManagerEngineOsm::QPlaceManagerEngineOsm(const QVariantMap &parameters,
                                               QGeoServiceProvider::Error *error,
                                               QString *errorString)
:   QPlaceManagerEngine(parameters), m_networkManager(new QNetworkAccessManager(this)),
    m_categoriesReply(0)
{
    if (parameters.contains(QStringLiteral("osm.useragent")))
        m_userAgent = parameters.value(QStringLiteral("osm.useragent")).toString().toLatin1();
    else
        m_userAgent = "Qt Location based application";

    if (parameters.contains(QStringLiteral("osm.places.host")))
        m_urlPrefix = parameters.value(QStringLiteral("osm.places.host")).toString();
    else
        m_urlPrefix = QStringLiteral("http://nominatim.openstreetmap.org/search");


    if (parameters.contains(QStringLiteral("osm.places.debug_query")))
        m_debugQuery = parameters.value(QStringLiteral("osm.places.debug_query")).toBool();

    if (parameters.contains(QStringLiteral("osm.places.page_size"))
            && parameters.value(QStringLiteral("osm.places.page_size")).canConvert<int>())
        m_pageSize = parameters.value(QStringLiteral("osm.places.page_size")).toInt();

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

QPlaceManagerEngineOsm::~QPlaceManagerEngineOsm()
{
}

QPlaceSearchReply *QPlaceManagerEngineOsm::search(const QPlaceSearchRequest &request)
{
    bool unsupported = false;

    // Only public visibility supported
    unsupported |= request.visibilityScope() != QLocation::UnspecifiedVisibility &&
                   request.visibilityScope() != QLocation::PublicVisibility;
    unsupported |= request.searchTerm().isEmpty() && request.categories().isEmpty();

    if (unsupported)
        return QPlaceManagerEngine::search(request);

    QUrlQuery queryItems;

    queryItems.addQueryItem(QStringLiteral("format"), QStringLiteral("jsonv2"));

    //queryItems.addQueryItem(QStringLiteral("accept-language"), QStringLiteral("en"));

    QGeoRectangle boundingBox = request.searchArea().boundingGeoRectangle();

    if (!boundingBox.isEmpty()) {
        queryItems.addQueryItem(QStringLiteral("bounded"), QStringLiteral("1"));
        QString coordinates;
        coordinates = QString::number(boundingBox.topLeft().longitude()) + QLatin1Char(',') +
                      QString::number(boundingBox.topLeft().latitude()) + QLatin1Char(',') +
                      QString::number(boundingBox.bottomRight().longitude()) + QLatin1Char(',') +
                      QString::number(boundingBox.bottomRight().latitude());
        queryItems.addQueryItem(QStringLiteral("viewbox"), coordinates);
    }

    QStringList queryParts;
    if (!request.searchTerm().isEmpty())
        queryParts.append(request.searchTerm());

    const auto categoriesList = request.categories();
    for (const QPlaceCategory &category : categoriesList) {
        QString id = category.categoryId();
        queryParts.append(QLatin1Char('[') + id + QLatin1Char(']'));
    }

    queryItems.addQueryItem(QStringLiteral("q"), queryParts.join(QLatin1Char('+')));

    QVariantMap parameters = request.searchContext().toMap();

    QStringList placeIds = parameters.value(QStringLiteral("ExcludePlaceIds")).toStringList();
    if (!placeIds.isEmpty())
        queryItems.addQueryItem(QStringLiteral("exclude_place_ids"), placeIds.join(QLatin1Char(',')));

    queryItems.addQueryItem(QStringLiteral("addressdetails"), QStringLiteral("1"));
    queryItems.addQueryItem(QStringLiteral("limit"), (request.limit() > 0) ? QString::number(request.limit())
                                                                           : QString::number(m_pageSize));

    QUrl requestUrl(m_urlPrefix);
    requestUrl.setQuery(queryItems);

    QNetworkRequest rq(requestUrl);
    rq.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
    QNetworkReply *networkReply = m_networkManager->get(rq);

    QPlaceSearchReplyOsm *reply = new QPlaceSearchReplyOsm(request, networkReply, this);
    connect(reply, &QPlaceSearchReplyOsm::finished,
            this, &QPlaceManagerEngineOsm::replyFinished);
    connect(reply, &QPlaceSearchReplyOsm::errorOccurred,
            this, &QPlaceManagerEngineOsm::replyError);

    if (m_debugQuery)
        reply->requestUrl = requestUrl.url(QUrl::None);

    return reply;
}

QPlaceReply *QPlaceManagerEngineOsm::initializeCategories()
{
    // Only fetch categories once
    if (m_categories.isEmpty() && !m_categoriesReply) {
        m_categoryLocales = m_locales;
        m_categoryLocales.append(QLocale(QLocale::English));
        fetchNextCategoryLocale();
    }

    QPlaceCategoriesReplyOsm *reply = new QPlaceCategoriesReplyOsm(this);
    connect(reply, &QPlaceCategoriesReplyOsm::finished,
            this, &QPlaceManagerEngineOsm::replyFinished);
    connect(reply, &QPlaceCategoriesReplyOsm::errorOccurred,
            this, &QPlaceManagerEngineOsm::replyError);

    // TODO delayed finished() emission
    if (!m_categories.isEmpty())
        reply->emitFinished();

    m_pendingCategoriesReply.append(reply);
    return reply;
}

QString QPlaceManagerEngineOsm::parentCategoryId(const QString &categoryId) const
{
    Q_UNUSED(categoryId);

    // Only a two category levels
    return QString();
}

QStringList QPlaceManagerEngineOsm::childCategoryIds(const QString &categoryId) const
{
    return m_subcategories.value(categoryId);
}

QPlaceCategory QPlaceManagerEngineOsm::category(const QString &categoryId) const
{
    return m_categories.value(categoryId);
}

QList<QPlaceCategory> QPlaceManagerEngineOsm::childCategories(const QString &parentId) const
{
    QList<QPlaceCategory> categories;
    const QStringList subcategoriesList = m_subcategories.value(parentId);
    for (const QString &id : subcategoriesList)
        categories.append(m_categories.value(id));
    return categories;
}

QList<QLocale> QPlaceManagerEngineOsm::locales() const
{
    return m_locales;
}

void QPlaceManagerEngineOsm::setLocales(const QList<QLocale> &locales)
{
    m_locales = locales;
}

void QPlaceManagerEngineOsm::categoryReplyFinished()
{
    QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
    reply->deleteLater();

    QXmlStreamReader parser(reply);
    while (!parser.atEnd() && parser.readNextStartElement()) {
        if (parser.name() == QLatin1String("mediawiki"))
            continue;
        if (parser.name() == QLatin1String("page"))
            continue;
        if (parser.name() == QLatin1String("revision"))
            continue;
        if (parser.name() == QLatin1String("text")) {
            // parse
            QString page = parser.readElementText();
            QRegularExpression regex(QStringLiteral("\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([^|]+) \\|\\| ([\\-YN])"));
            QRegularExpressionMatchIterator i = regex.globalMatch(page);
            while (i.hasNext()) {
                QRegularExpressionMatch match = i.next();
                QString name = match.capturedView(1).toString();
                QString tagKey = match.capturedView(2).toString();
                QString tagValue = match.capturedView(3).toString();
                QString op = match.capturedView(4).toString();
                QString plural = match.capturedView(5).toString();

                // Only interested in any operator plural forms
                if (op != QLatin1String("-") || plural != QLatin1String("Y"))
                    continue;

                if (!m_categories.contains(tagKey)) {
                    QPlaceCategory category;
                    category.setCategoryId(tagKey);
                    category.setName(nameForTagKey(tagKey));
                    m_categories.insert(category.categoryId(), category);
                    m_subcategories[QString()].append(tagKey);
                    emit categoryAdded(category, QString());
                }

                QPlaceCategory category;
                category.setCategoryId(tagKey + QLatin1Char('=') + tagValue);
                category.setName(name);

                if (!m_categories.contains(category.categoryId())) {
                    m_categories.insert(category.categoryId(), category);
                    m_subcategories[tagKey].append(category.categoryId());
                    emit categoryAdded(category, tagKey);
                }
            }
        }

        parser.skipCurrentElement();
    }

    if (m_categories.isEmpty() && !m_categoryLocales.isEmpty()) {
        fetchNextCategoryLocale();
        return;
    } else {
        m_categoryLocales.clear();
    }

    for (QPlaceCategoriesReplyOsm *reply : std::as_const(m_pendingCategoriesReply))
        reply->emitFinished();
    m_pendingCategoriesReply.clear();
}

void QPlaceManagerEngineOsm::categoryReplyError()
{
    for (QPlaceCategoriesReplyOsm *reply : std::as_const(m_pendingCategoriesReply))
        reply->setError(QPlaceReply::CommunicationError, tr("Network request error"));
}

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

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

void QPlaceManagerEngineOsm::fetchNextCategoryLocale()
{
    if (m_categoryLocales.isEmpty()) {
        qWarning("No locales specified to fetch categories for");
        return;
    }

    QLocale locale = m_categoryLocales.takeFirst();

    // FIXME: Categories should be cached.
    QUrl requestUrl = QUrl(SpecialPhrasesBaseUrl + locale.name().left(2).toUpper());

    m_categoriesReply = m_networkManager->get(QNetworkRequest(requestUrl));
    connect(m_categoriesReply, &QNetworkReply::finished,
            this, &QPlaceManagerEngineOsm::categoryReplyFinished);
    connect(m_categoriesReply, &QNetworkReply::errorOccurred,
            this, &QPlaceManagerEngineOsm::categoryReplyError);
}
