// 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:filesystem-manipulation

#include "qgeofiletilecacheosm.h"
#include <QtLocation/private/qgeotilespec_p.h>
#include <QDir>
#include <QDirIterator>
#include <QPair>
#include <QDateTime>

QT_BEGIN_NAMESPACE

QGeoFileTileCacheOsm::QGeoFileTileCacheOsm(const QList<QGeoTileProviderOsm *> &providers,
                                           const QString &offlineDirectory,
                                           const QString &directory, QObject *parent)
    : QGeoFileTileCache(directory, parent),
      m_offlineDirectory(offlineDirectory),
      m_offlineData(false),
      m_providers(providers)
{
    m_highDpi.resize(providers.size());
    if (!offlineDirectory.isEmpty()) {
        m_offlineDirectory = QDir(offlineDirectory);
        if (m_offlineDirectory.exists())
            m_offlineData = true;
    }
    for (int i = 0; i < providers.size(); i++) {
        providers[i]->setParent(this);
        m_highDpi[i] = providers[i]->isHighDpi();
        connect(providers[i], &QGeoTileProviderOsm::resolutionFinished, this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
        connect(providers[i], &QGeoTileProviderOsm::resolutionError, this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
    }
}

QGeoFileTileCacheOsm::~QGeoFileTileCacheOsm()
{
}

QSharedPointer<QGeoTileTexture> QGeoFileTileCacheOsm::get(const QGeoTileSpec &spec)
{
    QSharedPointer<QGeoTileTexture> tt = getFromMemory(spec);
    if (tt)
        return tt;
    if ((tt = getFromOfflineStorage(spec)))
        return tt;
    return getFromDisk(spec);
}

void QGeoFileTileCacheOsm::onProviderResolutionFinished(const QGeoTileProviderOsm *provider)
{
    clearObsoleteTiles(provider);
    Q_UNUSED(provider);
    for (int i = 0; i < m_providers.size(); i++) {
        if (m_providers[i]->isHighDpi() != m_highDpi[i]) { // e.g., HiDpi was requested but only LoDpi is available
            int mapId = m_providers[i]->mapType().mapId();
            m_highDpi[i] = m_providers[i]->isHighDpi();

            // reload cache for mapId i
            dropTiles(mapId);
            loadTiles(mapId);

            // send signal to clear scene in all maps created through this provider that use the reloaded tiles
            emit mapDataUpdated(mapId);
        }
    }
}

// On resolution error the provider is removed.
// This happens ONLY if there is no enabled hardcoded fallback for the mapId.
// Hardcoded fallbacks also have a timestamp, that can get updated with Qt releases.
void QGeoFileTileCacheOsm::onProviderResolutionError(const QGeoTileProviderOsm *provider, QNetworkReply::NetworkError error)
{
    Q_UNUSED(error);
    clearObsoleteTiles(provider); // this still removes tiles who happen to be older than qgeotileproviderosm.cpp defaultTs
}

// init() is always called before the provider resolution starts
void QGeoFileTileCacheOsm::init()
{
    if (directory_.isEmpty())
        directory_ = baseLocationCacheDirectory();
    QDir::root().mkpath(directory_);

    // find max mapId
    int max = 0;
    for (auto p: std::as_const(m_providers))
        if (p->mapType().mapId() > max)
            max = p->mapType().mapId();
    // Create a mapId to maxTimestamp LUT..
    m_maxMapIdTimestamps.resize(max+1); // initializes to invalid QDateTime

    // .. by finding the newest file in each tileset (tileset = mapId).
    QDir dir(directory_);
    QStringList formats;
    formats << QLatin1String("*.*");
    const QStringList files = dir.entryList(formats, QDir::Files);

    for (const QString &tileFileName : files) {
        QGeoTileSpec spec = filenameToTileSpec(tileFileName);
        if (spec.zoom() == -1)
            continue;
        QFileInfo fi(dir.filePath(tileFileName));
        if (fi.lastModified() > m_maxMapIdTimestamps[spec.mapId()])
            m_maxMapIdTimestamps[spec.mapId()] = fi.lastModified();
    }

    // Base class ::init()
    QGeoFileTileCache::init();

    for (QGeoTileProviderOsm * p: std::as_const(m_providers))
        clearObsoleteTiles(p);
}

QSharedPointer<QGeoTileTexture> QGeoFileTileCacheOsm::getFromOfflineStorage(const QGeoTileSpec &spec)
{
    if (!m_offlineData)
        return QSharedPointer<QGeoTileTexture>();

    int providerId = spec.mapId() - 1;
    if (providerId < 0 || providerId >= m_providers.size())
        return QSharedPointer<QGeoTileTexture>();

    const QString fileName = tileSpecToFilename(spec, QStringLiteral("*"), providerId);
    QStringList validTiles = m_offlineDirectory.entryList({fileName});
    if (!validTiles.size())
        return QSharedPointer<QGeoTileTexture>();

    QFile file(m_offlineDirectory.absoluteFilePath(validTiles.first()));
    if (!file.open(QIODevice::ReadOnly))
        return QSharedPointer<QGeoTileTexture>();
    QByteArray bytes = file.readAll();
    file.close();

    QImage image;
    if (!image.loadFromData(bytes)) {
        handleError(spec, QLatin1String("Problem with tile image"));
        return QSharedPointer<QGeoTileTexture>();
    }

    addToMemoryCache(spec, bytes, QString());
    return addToTextureCache(spec, image);
}

void QGeoFileTileCacheOsm::dropTiles(int mapId)
{
    {
        const QList<QGeoTileSpec> keys = textureCache_.keys();
        for (const QGeoTileSpec &k : keys)
            if (k.mapId() == mapId)
                textureCache_.remove(k);
    }

    {
        const QList<QGeoTileSpec> keys = memoryCache_.keys();
        for (const QGeoTileSpec &k : keys)
            if (k.mapId() == mapId)
                memoryCache_.remove(k);
    }

    {
        const QList<QGeoTileSpec> keys =  diskCache_.keys();
        for (const QGeoTileSpec &k : keys)
            if (k.mapId() == mapId)
                diskCache_.remove(k);
    }
}

void QGeoFileTileCacheOsm::loadTiles(int mapId)
{
    QStringList formats;
    formats << QLatin1String("*.*");

    QDir dir(directory_);
    QStringList files = dir.entryList(formats, QDir::Files);

    for (int i = 0; i < files.size(); ++i) {
        QGeoTileSpec spec = filenameToTileSpec(files.at(i));
        if (spec.zoom() == -1 || spec.mapId() != mapId)
            continue;
        QString filename = dir.filePath(files.at(i));
        addToDiskCache(spec, filename);
    }
}

QString QGeoFileTileCacheOsm::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const
{
    int providerId = spec.mapId() - 1;
    if (providerId < 0 || providerId >= m_providers.size())
        return QString();

    QDir dir = QDir(directory);
    return dir.filePath(tileSpecToFilename(spec, format, providerId));
}

QString QGeoFileTileCacheOsm::tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, int providerId) const
{
    QString filename = spec.plugin();
    filename += QLatin1String("-");
    filename += (m_providers[providerId]->isHighDpi()) ? QLatin1Char('h') : QLatin1Char('l');
    filename += QLatin1String("-");
    filename += QString::number(spec.mapId());
    filename += QLatin1String("-");
    filename += QString::number(spec.zoom());
    filename += QLatin1String("-");
    filename += QString::number(spec.x());
    filename += QLatin1String("-");
    filename += QString::number(spec.y());

    //Append version if real version number to ensure backwards compatibility and eviction of old tiles
    if (spec.version() != -1) {
        filename += QLatin1String("-");
        filename += QString::number(spec.version());
    }

    filename += QLatin1String(".");
    filename += format;
    return filename;
}

QGeoTileSpec QGeoFileTileCacheOsm::filenameToTileSpec(const QString &filename) const
{
    QGeoTileSpec emptySpec;

    QStringList parts = filename.split('.');

    if (parts.length() != 2)
        return emptySpec;

    QString name = parts.at(0);
    QStringList fields = name.split('-');

    int length = fields.length();
    if (length != 6 && length != 7)
        return emptySpec;

    QList<int> numbers;

    bool ok = false;
    for (int i = 2; i < length; ++i) {
        ok = false;
        int value = fields.at(i).toInt(&ok);
        if (!ok)
            return emptySpec;
        numbers.append(value);
    }

    if (numbers.at(0) > m_providers.size())
        return emptySpec;

    bool highDpi = m_providers[numbers.at(0) - 1]->isHighDpi();
    if (highDpi && fields.at(1) != QLatin1Char('h'))
        return emptySpec;
    else if (!highDpi && fields.at(1) != QLatin1Char('l'))
        return emptySpec;

    //File name without version, append default
    if (numbers.length() < 5)
        numbers.append(-1);

    return QGeoTileSpec(fields.at(0),
                    numbers.at(0),
                    numbers.at(1),
                    numbers.at(2),
                    numbers.at(3),
                    numbers.at(4));
}

void QGeoFileTileCacheOsm::clearObsoleteTiles(const QGeoTileProviderOsm *p)
{
    // process initialized providers, and connect the others

        if (p->isResolved()) {
            if (m_maxMapIdTimestamps[p->mapType().mapId()].isValid() &&  // there are tiles in the cache
                p->timestamp() > m_maxMapIdTimestamps[p->mapType().mapId()]) { // and they are older than the provider
                qInfo() << "provider for " << p->mapType().name() << " timestamp: " << p->timestamp()
                        << " -- data last modified: " << m_maxMapIdTimestamps[p->mapType().mapId()] << ". Clearing.";
                clearMapId(p->mapType().mapId());
                m_maxMapIdTimestamps[p->mapType().mapId()] = p->timestamp(); // don't do it again.
            }
        } else {
            connect(p, &QGeoTileProviderOsm::resolutionFinished,
                    this, &QGeoFileTileCacheOsm::onProviderResolutionFinished);
#if 0 // If resolution fails, better not try to remove anything. Beside, on error, resolutionFinished is also emitted.
            connect(p, &QGeoTileProviderOsm::resolutionError,
                    this, &QGeoFileTileCacheOsm::onProviderResolutionError);
#endif
        }
}

QT_END_NAMESPACE
