// 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 "qgeotileproviderosm.h"

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

QT_BEGIN_NAMESPACE

static const int maxValidZoom = 30;
static const QDateTime defaultTs = QDateTime::fromString(QStringLiteral("2016-06-01T00:00:00"), Qt::ISODate);

static void setSSL(QGeoMapType &mapType, bool isHTTPS)
{
    QVariantMap metadata = mapType.metadata();
    metadata["isHTTPS"] = isHTTPS;

    mapType = QGeoMapType(mapType.style(), mapType.name(), mapType.description(), mapType.mobile(),
                          mapType.night(), mapType.mapId(), mapType.pluginName(), mapType.cameraCapabilities(),
                          metadata);
}

QGeoTileProviderOsm::QGeoTileProviderOsm(QNetworkAccessManager *nm, const QGeoMapType &mapType,
                                         const QList<TileProvider *> &providers,
                                         const QGeoCameraCapabilities &cameraCapabilities)
    : m_nm(nm),
      m_provider(nullptr),
      m_mapType(mapType),
      m_status(Idle),
      m_cameraCapabilities(cameraCapabilities)
{
    for (int i = 0; i < providers.size(); ++i) {
        TileProvider *p = providers[i];
        if (!m_provider)
            m_providerId = i;
        addProvider(p);
    }

    if (!m_provider || m_provider->isValid())
        m_status = Resolved;

    if (m_provider && m_provider->isValid())
        setSSL(m_mapType, m_provider->isHTTPS());

    connect(this, &QGeoTileProviderOsm::resolutionFinished, this, &QGeoTileProviderOsm::updateCameraCapabilities);
}

QGeoTileProviderOsm::~QGeoTileProviderOsm()
{
}

QUrl QGeoTileProviderOsm::tileAddress(int x, int y, int z) const
{
    if (m_status != Resolved || !m_provider)
        return QUrl();
    return m_provider->tileAddress(x, y, z);
}

QString QGeoTileProviderOsm::mapCopyRight() const
{
    if (m_status != Resolved || !m_provider)
        return QString();
    return m_provider->mapCopyRight();
}

QString QGeoTileProviderOsm::dataCopyRight() const
{
    if (m_status != Resolved || !m_provider)
        return QString();
    return m_provider->dataCopyRight();
}

QString QGeoTileProviderOsm::styleCopyRight() const
{
    if (m_status != Resolved || !m_provider)
        return QString();
    return m_provider->styleCopyRight();
}

QString QGeoTileProviderOsm::format() const
{
    if (m_status != Resolved || !m_provider)
        return QString();
    return m_provider->format();
}

int QGeoTileProviderOsm::minimumZoomLevel() const
{
    if (m_status != Resolved || !m_provider)
        return 0;
    return m_provider->minimumZoomLevel();
}

int QGeoTileProviderOsm::maximumZoomLevel() const
{
    if (m_status != Resolved || !m_provider)
        return 20;
    return m_provider->maximumZoomLevel();
}

bool QGeoTileProviderOsm::isHighDpi() const
{
    if (!m_provider)
        return false;
    return m_provider->isHighDpi();
}

QDateTime QGeoTileProviderOsm::timestamp() const
{
    if (!m_provider)
        return QDateTime();
    return m_provider->timestamp();
}

QGeoCameraCapabilities QGeoTileProviderOsm::cameraCapabilities() const
{
    return m_cameraCapabilities;
}

const QGeoMapType &QGeoTileProviderOsm::mapType() const
{
    return m_mapType;
}

bool QGeoTileProviderOsm::isValid() const
{
    if (m_status != Resolved || !m_provider)
        return false;
    return m_provider->isValid();
}

bool QGeoTileProviderOsm::isResolved() const
{
    return (m_status == Resolved);
}

void QGeoTileProviderOsm::resolveProvider()
{
    if (m_status == Resolved || m_status == Resolving)
        return;

    m_status = Resolving;
    // Provider can't be null while on Idle status.
    connect(m_provider, &TileProvider::resolutionFinished, this, &QGeoTileProviderOsm::onResolutionFinished);
    connect(m_provider, &TileProvider::resolutionError, this, &QGeoTileProviderOsm::onResolutionError);
    m_provider->resolveProvider();
}

void QGeoTileProviderOsm::disableRedirection()
{
    if (m_provider && m_provider->isValid())
        return;
    bool found = false;
    for (TileProvider *p: m_providerList) {
        if (p->isValid() && !found) {
            m_provider = p;
            m_providerId = m_providerList.indexOf(p);
            found = true;
        }
        p->disconnect(this);
    }
    m_status = Resolved;
}

void QGeoTileProviderOsm::onResolutionFinished(TileProvider *provider)
{
    Q_UNUSED(provider);
    // provider and m_provider are the same, at this point. m_status is Resolving.
    m_status = Resolved;
    emit resolutionFinished(this);
}

void QGeoTileProviderOsm::onResolutionError(TileProvider *provider)
{
    Q_UNUSED(provider);
    // provider and m_provider are the same at this point. m_status is Resolving.
    if (!m_provider || m_provider->isInvalid()) {
        m_provider = nullptr;
        m_status = Resolved;
        if (m_providerId >= m_providerList.size() -1) { // no hope left
            emit resolutionError(this);
            return;
        }
        // Advance the pointer in the provider list, and possibly start resolution on the next in the list.
        for (int i = m_providerId + 1; i < m_providerList.size(); ++i) {
            m_providerId = i;
            TileProvider *p = m_providerList[m_providerId];
            if (!p->isInvalid()) {
                m_provider = p;
                if (!p->isValid()) {
                    m_status = Idle;
#if 0  // leaving triggering the retry to the tile fetcher, instead of constantly spinning it in here.
                    m_status = Resolving;
                    p->resolveProvider();
#endif
                    emit resolutionRequired();
                } else {
                    emit resolutionFinished(this);
                }
                break;
            }
        }
        if (!m_provider)
            emit resolutionError(this);
    } else if (m_provider->isValid()) {
        m_status = Resolved;
        emit resolutionFinished(this);
    } else { // still not resolved. But network error is recoverable.
        m_status = Idle;
#if 0   // leaving triggering the retry to the tile fetcher
        m_provider->resolveProvider();
#endif
    }
}

void QGeoTileProviderOsm::updateCameraCapabilities()
{
    // Set proper min/max ZoomLevel coming from the json, if available.
    m_cameraCapabilities.setMinimumZoomLevel(minimumZoomLevel());
    m_cameraCapabilities.setMaximumZoomLevel(maximumZoomLevel());

    m_mapType = QGeoMapType(m_mapType.style(), m_mapType.name(), m_mapType.description(), m_mapType.mobile(),
                            m_mapType.night(), m_mapType.mapId(), m_mapType.pluginName(), m_cameraCapabilities,
                            m_mapType.metadata());

    if (m_provider && m_provider->isValid())
        setSSL(m_mapType, m_provider->isHTTPS());
}

void QGeoTileProviderOsm::addProvider(TileProvider *provider)
{
    if (!provider)
        return;
    std::unique_ptr<TileProvider> p(provider);
    if (provider->status() == TileProvider::Invalid)
        return; // if the provider is already resolved and invalid, no point in adding it.

    provider = p.release();
    provider->setNetworkManager(m_nm);
    provider->setParent(this);
    m_providerList.append(provider);
    if (!m_provider)
        m_provider = provider;
}


/*
    Class TileProvder
*/

static void sort2(int &a, int &b)
{
    if (a > b) {
        int temp=a;
        a=b;
        b=temp;
    }
}

TileProvider::TileProvider() : m_status(Invalid), m_nm(nullptr), m_timestamp(defaultTs), m_highDpi(false)
{

}

TileProvider::TileProvider(const QUrl &urlRedirector, bool highDpi)
:   m_status(Idle), m_urlRedirector(urlRedirector), m_nm(nullptr), m_timestamp(defaultTs), m_highDpi(highDpi)
{
    if (!m_urlRedirector.isValid())
        m_status = Invalid;
}

TileProvider::TileProvider(const QString &urlTemplate,
                           const QString &format,
                           const QString &copyRightMap,
                           const QString &copyRightData,
                           bool highDpi,
                           int minimumZoomLevel,
                           int maximumZoomLevel)
:   m_status(Invalid), m_nm(nullptr), m_urlTemplate(urlTemplate),
    m_format(format), m_copyRightMap(copyRightMap), m_copyRightData(copyRightData),
    m_minimumZoomLevel(minimumZoomLevel), m_maximumZoomLevel(maximumZoomLevel), m_timestamp(defaultTs), m_highDpi(highDpi)
{
    setupProvider();
}

TileProvider::~TileProvider()
{
}

void TileProvider::resolveProvider()
{
    if (!m_nm)
        return;

    switch (m_status) {
    case Resolving:
    case Invalid:
    case Valid:
        return;
    case Idle:
        m_status = Resolving;
        break;
    }

    QNetworkRequest request;
    request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("QGeoTileFetcherOsm"));
    request.setUrl(m_urlRedirector);
    request.setAttribute(QNetworkRequest::BackgroundRequestAttribute, true);
    request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferNetwork);
    QNetworkReply *reply = m_nm->get(request);
    connect(reply, &QNetworkReply::finished,
            this, &TileProvider::onNetworkReplyFinished);
    connect(reply, &QNetworkReply::errorOccurred,
            this, &TileProvider::onNetworkReplyError);
}

void TileProvider::handleError(QNetworkReply::NetworkError error)
{
    switch (error) {
    case QNetworkReply::ConnectionRefusedError:
    case QNetworkReply::TooManyRedirectsError:
    case QNetworkReply::InsecureRedirectError:
    case QNetworkReply::ContentAccessDenied:
    case QNetworkReply::ContentOperationNotPermittedError:
    case QNetworkReply::ContentNotFoundError:
    case QNetworkReply::AuthenticationRequiredError:
    case QNetworkReply::ContentGoneError:
    case QNetworkReply::OperationNotImplementedError:
    case QNetworkReply::ServiceUnavailableError:
        // Errors we don't expect to recover from in the near future, which
        // prevent accessing the redirection info but not the actual providers.
        m_status = Invalid;
        break;
    default:
        //qWarning() << "QGeoTileProviderOsm network error:" << error;
        break;
    }
}

void TileProvider::onNetworkReplyFinished()
{
    QNetworkReply *reply = static_cast<QNetworkReply *>(sender());
    reply->deleteLater();

    switch (m_status) {
    case Resolving:
        m_status = Idle;
        break;
    case Idle: // should not happen
    case Invalid: // should not happen
        break;
    case Valid: // should not happen
        emit resolutionFinished(this);
        return;
    }

    QObject errorEmitter;
    QMetaObject::Connection errorEmitterConnection =
        connect(&errorEmitter, &QObject::destroyed, this, [this](){ this->resolutionError(this); });

    if (reply->error() != QNetworkReply::NoError) {
        handleError(reply->error());
        return;
    }
    m_status = Invalid;

    /*
     * The content of a provider information file must be in JSON format, containing
     * (as of Qt 5.6.2) the following fields:
     *
     * {
     *     "Enabled" : bool,  (optional)
     *     "UrlTemplate" : "<url template>", (mandatory)
     *     "ImageFormat" : "<image format>", (mandatory)
     *     "MapCopyRight" : "<copyright>", (mandatory)
     *     "DataCopyRight" : "<copyright>", (mandatory)
     *     "StyleCopyRight" : "<copyright>", (optional)
     *     "MinimumZoomLevel" : <minimumZoomLevel>, (optional)
     *     "MaximumZoomLevel" : <maximumZoomLevel>,  (optional)
     *     "Timestamp" : <timestamp>, (optional)
     * }
     *
     * Enabled is optional, and allows to temporarily disable a tile provider if it becomes
     * unavailable, without making the osm plugin fire requests to it. Default is true.
     *
     * MinimumZoomLevel and MaximumZoomLevel are also optional, and allow to prevent invalid tile
     * requests to the providers, if they do not support the specific ZL. Default is 0 and 20,
     * respectively.
     *
     * UrlTemplate is required, and is the tile url template, with %x, %y and %z as
     * placeholders for the actual parameters.
     * Example:
     * http://localhost:8080/maps/%z/%x/%y.png
     *
     * ImageFormat is required, and is the format of the tile.
     * Examples:
     * "png", "jpg"
     *
     * MapCopyRight is required and is the string that will be displayed in the "Map (c)" part
     * of the on-screen copyright notice. Can be an empty string.
     * Example:
     * "<a href='http://www.mapquest.com/'>MapQuest</a>"
     *
     * DataCopyRight is required and is the string that will be displayed in the "Data (c)" part
     * of the on-screen copyright notice. Can be an empty string.
     * Example:
     * "<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors"
     *
     * StyleCopyRight is optional and is the string that will be displayed in the optional "Style (c)" part
     * of the on-screen copyright notice.
     *
     * Timestamp is optional, and if set will cause QtLocation to clear the content of the cache older
     * than this timestamp. The purpose is to prevent mixing tiles from different providers in the cache
     * upon provider change. The value must be a string in ISO 8601 format (see Qt::ISODate)
     */

    QJsonParseError error;
    QJsonDocument d = QJsonDocument::fromJson(reply->readAll(), &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "QGeoTileProviderOsm: Error parsing redirection data: "<<error.errorString() << "at "<<m_urlRedirector;
        return;
    }
    if (!d.isObject()) {
        qWarning() << "QGeoTileProviderOsm: Invalid redirection data" << "at "<<m_urlRedirector;
        return;
    }
    const QJsonObject json = d.object();
    const QJsonValue urlTemplate = json.value(QLatin1String("UrlTemplate"));
    const QJsonValue imageFormat = json.value(QLatin1String("ImageFormat"));
    const QJsonValue copyRightMap = json.value(QLatin1String("MapCopyRight"));
    const QJsonValue copyRightData = json.value(QLatin1String("DataCopyRight"));
    if ( urlTemplate == QJsonValue::Undefined
         || imageFormat == QJsonValue::Undefined
         || copyRightMap  == QJsonValue::Undefined
         || copyRightData  == QJsonValue::Undefined
         || !urlTemplate.isString()
         || !imageFormat.isString()
         || !copyRightMap.isString()
         || !copyRightData.isString()) {
        qWarning() << "QGeoTileProviderOsm: Incomplete redirection data" << "at "<<m_urlRedirector;
        return;
    }

    m_urlTemplate = urlTemplate.toString();
    m_format = imageFormat.toString();
    m_copyRightMap = copyRightMap.toString();
    m_copyRightData = copyRightData.toString();

    const QJsonValue enabled = json.value(QLatin1String("Enabled"));
    if (enabled.isBool() && ! enabled.toBool()) {
        qWarning() << "QGeoTileProviderOsm: Tileserver disabled" << "at "<<m_urlRedirector;
        return;
    }

    const QJsonValue copyRightStyle = json.value(QLatin1String("StyleCopyRight"));
    if (copyRightStyle != QJsonValue::Undefined && copyRightStyle.isString())
        m_copyRightStyle = copyRightStyle.toString();

    m_minimumZoomLevel = 0;
    m_maximumZoomLevel = 20;
    const QJsonValue minZoom = json.value(QLatin1String("MinimumZoomLevel"));
    if (minZoom.isDouble())
        m_minimumZoomLevel = qBound(0, int(minZoom.toDouble()), maxValidZoom);
    const QJsonValue maxZoom = json.value(QLatin1String("MaximumZoomLevel"));
    if (maxZoom.isDouble())
        m_maximumZoomLevel = qBound(0, int(maxZoom.toDouble()), maxValidZoom);

    const QJsonValue ts = json.value(QLatin1String("Timestamp"));
    if (ts.isString())
        m_timestamp = QDateTime::fromString(ts.toString(), Qt::ISODate);

    setupProvider();
    if (isValid()) {
        QObject::disconnect(errorEmitterConnection);
        emit resolutionFinished(this);
    }
}

void TileProvider::onNetworkReplyError(QNetworkReply::NetworkError error)
{
    if (m_status == Resolving)
        m_status = Idle;

    handleError(error);
    static_cast<QNetworkReply *>(sender())->deleteLater();
    emit resolutionError(this);
}

void TileProvider::setupProvider()
{
    if (m_urlTemplate.isEmpty())
        return;

    if (m_format.isEmpty())
        return;

    if (m_minimumZoomLevel < 0 || m_minimumZoomLevel > 30)
        return;

    if (m_maximumZoomLevel < 0 || m_maximumZoomLevel > 30 ||  m_maximumZoomLevel < m_minimumZoomLevel)
        return;

    // Currently supporting only %x, %y and &z
    int offset[3];
    offset[0] = m_urlTemplate.indexOf(QLatin1String("%x"));
    if (offset[0] < 0)
        return;

    offset[1] = m_urlTemplate.indexOf(QLatin1String("%y"));
    if (offset[1] < 0)
        return;

    offset[2] = m_urlTemplate.indexOf(QLatin1String("%z"));
    if (offset[2] < 0)
        return;

    int sortedOffsets[3];
    std::copy(offset, offset + 3, sortedOffsets);
    sort2(sortedOffsets[0] ,sortedOffsets[1]);
    sort2(sortedOffsets[1] ,sortedOffsets[2]);
    sort2(sortedOffsets[0] ,sortedOffsets[1]);

    int min = sortedOffsets[0];
    int max = sortedOffsets[2];
    int mid = sortedOffsets[1];

    // Initing LUT
    for (int i=0; i<3; i++) {
        if (offset[0] == sortedOffsets[i])
            paramsLUT[i] = 0;
        else if (offset[1] == sortedOffsets[i])
            paramsLUT[i] = 1;
        else
            paramsLUT[i] = 2;
    }

    m_urlPrefix = m_urlTemplate.mid(0 , min);
    m_urlSuffix = m_urlTemplate.mid(max + 2, m_urlTemplate.size() - max - 2);

    paramsSep[0] = m_urlTemplate.mid(min + 2, mid - min - 2);
    paramsSep[1] = m_urlTemplate.mid(mid + 2, max - mid - 2);
    m_status = Valid;
}

bool TileProvider::isValid() const
{
    return m_status == Valid;
}

bool TileProvider::isInvalid() const
{
    return m_status == Invalid;
}

bool TileProvider::isResolved() const
{
    return (m_status == Valid || m_status == Invalid);
}

QString TileProvider::mapCopyRight() const
{
    return m_copyRightMap;
}

QString TileProvider::dataCopyRight() const
{
    return m_copyRightData;
}

QString TileProvider::styleCopyRight() const
{
    return m_copyRightStyle;
}

QString TileProvider::format() const
{
    return m_format;
}

int TileProvider::minimumZoomLevel() const
{
    return m_minimumZoomLevel;
}

int TileProvider::maximumZoomLevel() const
{
    return m_maximumZoomLevel;
}

const QDateTime &TileProvider::timestamp() const
{
    return m_timestamp;
}

bool TileProvider::isHighDpi() const
{
    return m_highDpi;
}

bool TileProvider::isHTTPS() const
{
    return m_urlTemplate.startsWith(QStringLiteral("https"));
}

void TileProvider::setStyleCopyRight(const QString &copyright)
{
    m_copyRightStyle = copyright;
}

void TileProvider::setTimestamp(const QDateTime &timestamp)
{
    m_timestamp = timestamp;
}

QUrl TileProvider::tileAddress(int x, int y, int z) const
{
    if (z < m_minimumZoomLevel || z > m_maximumZoomLevel)
        return QUrl();
    int params[3] = { x, y, z};
    QString url;
    url += m_urlPrefix;
    url += QString::number(params[paramsLUT[0]]);
    url += paramsSep[0];
    url += QString::number(params[paramsLUT[1]]);
    url += paramsSep[1];
    url += QString::number(params[paramsLUT[2]]);
    url += m_urlSuffix;
    return QUrl(url);
}

void TileProvider::setNetworkManager(QNetworkAccessManager *nm)
{
    m_nm = nm;
}

TileProvider::Status TileProvider::status() const
{
    return m_status;
}


QT_END_NAMESPACE
