/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2023-05-15
 * Description : geolocation engine based on Marble.
 *               (c) 2007-2022 Marble Team
 *               https://invent.kde.org/education/marble/-/raw/master/data/credits_authors.html
 *
 * SPDX-FileCopyrightText: 2023-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * ============================================================ */

#include "GeoSceneTileDataset.h"

// Qt includes

#include <QImage>
#include <QUrl>

// Local includes

#include "GeoSceneTypes.h"
#include "GeoSceneEquirectTileProjection.h"
#include "GeoSceneMercatorTileProjection.h"
#include "DownloadPolicy.h"
#include "MarbleDirs.h"
#include "ServerLayout.h"
#include "TileId.h"

#include "digikam_debug.h"

namespace Marble
{

GeoSceneTileDataset::GeoSceneTileDataset(const QString& name)
    : GeoSceneAbstractDataset(name),
      m_sourceDir(),
      m_installMap(),
      m_storageLayoutMode(Marble),
      m_serverLayout(new MarbleServerLayout(this)),
      m_levelZeroColumns(defaultLevelZeroColumns),
      m_levelZeroRows(defaultLevelZeroRows),
      m_minimumTileLevel(0),
      m_maximumTileLevel(-1),
      m_tileProjection(new GeoSceneEquirectTileProjection()),
      m_blending(),
      m_downloadUrls(),
      m_nextUrl(m_downloadUrls.constEnd())
{
    m_tileProjection->setLevelZeroColumns(m_levelZeroColumns);
    m_tileProjection->setLevelZeroRows(m_levelZeroRows);
}

GeoSceneTileDataset::~GeoSceneTileDataset()
{
    qDeleteAll(m_downloadPolicies);
    delete m_serverLayout;
    delete m_tileProjection;
}

const char* GeoSceneTileDataset::nodeType() const
{
    return GeoSceneTypes::GeoSceneTileDatasetType;
}

QString GeoSceneTileDataset::sourceDir() const
{
    return m_sourceDir;
}

void GeoSceneTileDataset::setSourceDir(const QString& sourceDir)
{
    m_sourceDir = sourceDir;
}

QString GeoSceneTileDataset::installMap() const
{
    return m_installMap;
}

void GeoSceneTileDataset::setInstallMap(const QString& installMap)
{
    m_installMap = installMap;
}

GeoSceneTileDataset::StorageLayout GeoSceneTileDataset::storageLayout() const
{
    return m_storageLayoutMode;
}

void GeoSceneTileDataset::setStorageLayout(const StorageLayout layout)
{
    m_storageLayoutMode = layout;
}

void GeoSceneTileDataset::setServerLayout(const ServerLayout* layout)
{
    delete m_serverLayout;
    m_serverLayout = layout;
}

const ServerLayout* GeoSceneTileDataset::serverLayout() const
{
    return m_serverLayout;
}

int GeoSceneTileDataset::levelZeroColumns() const
{
    return m_levelZeroColumns;
}

void GeoSceneTileDataset::setLevelZeroColumns(const int columns)
{
    m_levelZeroColumns = columns;
    m_tileProjection->setLevelZeroColumns(m_levelZeroColumns);
}

int GeoSceneTileDataset::levelZeroRows() const
{
    return m_levelZeroRows;
}

void GeoSceneTileDataset::setLevelZeroRows(const int rows)
{
    m_levelZeroRows = rows;
    m_tileProjection->setLevelZeroRows(m_levelZeroRows);
}

int GeoSceneTileDataset::maximumTileLevel() const
{
    return m_maximumTileLevel;
}

void GeoSceneTileDataset::setMaximumTileLevel(const int maximumTileLevel)
{
    m_maximumTileLevel = maximumTileLevel;
}

int GeoSceneTileDataset::minimumTileLevel() const
{
    return m_minimumTileLevel;
}

void GeoSceneTileDataset::setMinimumTileLevel(int level)
{
    m_minimumTileLevel = level;
}

void GeoSceneTileDataset::setTileLevels(const QString& tileLevels)
{
    if (tileLevels.isEmpty())
    {
        m_tileLevels.clear();
        return;
    }

    const QStringList values = tileLevels.split(QLatin1Char(','));

    for (const QString& value : values)
    {
        bool canParse(false);
        int const tileLevel = value.trimmed().toInt(&canParse);

        if (canParse && tileLevel >= 0 && tileLevel < 100)
        {
            m_tileLevels << tileLevel;
        }

        else
        {
            qCDebug(DIGIKAM_MARBLE_LOG) << "Cannot parse tile level part " << value << " in " << tileLevels << ", ignoring it.";
        }
    }

    if (!m_tileLevels.isEmpty())
    {
        std::sort(m_tileLevels.begin(), m_tileLevels.end());
        m_minimumTileLevel = m_tileLevels.first();
        m_maximumTileLevel = m_tileLevels.last();
    }
}

QVector<int> GeoSceneTileDataset::tileLevels() const
{
    return m_tileLevels;
}

QVector<QUrl> GeoSceneTileDataset::downloadUrls() const
{
    return m_downloadUrls;
}

const QSize GeoSceneTileDataset::tileSize() const
{
    if (m_tileSize.isEmpty())
    {
        const TileId id(0, 0, 0, 0);
        QString const fileName = relativeTileFileName(id);
        QFileInfo const dirInfo(fileName);
        QString const path = dirInfo.isAbsolute() ? fileName : MarbleDirs::path(fileName);

        QImage testTile(path);

        if (testTile.isNull())
        {
            qCDebug(DIGIKAM_MARBLE_LOG) << "Tile size is missing in dgml and no base tile found in " << themeStr();
            qCDebug(DIGIKAM_MARBLE_LOG) << "Using default tile size " << c_defaultTileSize;
            m_tileSize = QSize(c_defaultTileSize, c_defaultTileSize);
        }

        else
        {
            m_tileSize = testTile.size();
        }

        if (m_tileSize.isEmpty())
        {
            qCDebug(DIGIKAM_MARBLE_LOG) << "Tile width or height cannot be 0. Falling back to default tile size.";
            m_tileSize = QSize(c_defaultTileSize, c_defaultTileSize);
        }
    }

    Q_ASSERT(!m_tileSize.isEmpty());
    return m_tileSize;
}

GeoDataLatLonBox GeoSceneTileDataset::latLonBox() const
{
    return m_latLonBox;
}

void GeoSceneTileDataset::setLatLonBox(const GeoDataLatLonBox& box)
{
    m_latLonBox = box;
}

void GeoSceneTileDataset::setTileSize(const QSize& tileSize)
{
    if (tileSize.isEmpty())
    {
        qCDebug(DIGIKAM_MARBLE_LOG) << "Ignoring invalid tile size " << tileSize;
    }

    else
    {
        m_tileSize = tileSize;
    }
}

void GeoSceneTileDataset::setTileProjection(GeoSceneAbstractTileProjection::Type projectionType)
{
    if (m_tileProjection->type() == projectionType)
    {
        return;
    }

    delete m_tileProjection;

    if (projectionType == GeoSceneAbstractTileProjection::Mercator)
    {
        m_tileProjection = new GeoSceneMercatorTileProjection();
    }

    else
    {
        m_tileProjection = new GeoSceneEquirectTileProjection();
    }

    m_tileProjection->setLevelZeroColumns(m_levelZeroColumns);
    m_tileProjection->setLevelZeroRows(m_levelZeroRows);
}

const GeoSceneAbstractTileProjection* GeoSceneTileDataset::tileProjection() const
{
    return m_tileProjection;
}

GeoSceneAbstractTileProjection::Type GeoSceneTileDataset::tileProjectionType() const
{
    return m_tileProjection->type();
}

// Even though this method changes the internal state, it may be const
// because the compiler is forced to invoke this method for different TileIds.
QUrl GeoSceneTileDataset::downloadUrl(const TileId& id) const
{
    // default download url
    if (m_downloadUrls.empty())
    {
        QUrl const defaultUrl = QUrl(QLatin1String("https://maps.kde.org/") + m_serverLayout->sourceDir());
        qCDebug(DIGIKAM_MARBLE_LOG) << "No download URL specified for tiles stored in "
                                    << m_sourceDir << ", falling back to " << defaultUrl.toString();
        return m_serverLayout->downloadUrl(defaultUrl, id);
    }

    else if (m_downloadUrls.size() == 1)
    {
        return m_serverLayout->downloadUrl(*m_nextUrl, id);
    }

    else
    {
        if (m_nextUrl == m_downloadUrls.constEnd())
        {
            m_nextUrl = m_downloadUrls.constBegin();
        }

        const QUrl url = m_serverLayout->downloadUrl(*m_nextUrl, id);
        ++m_nextUrl;
        return url;
    }
}

void GeoSceneTileDataset::addDownloadUrl(const QUrl& url)
{
    m_downloadUrls.append(url);
    // FIXME: this could be done only once
    m_nextUrl = m_downloadUrls.constBegin();
}

QString GeoSceneTileDataset::relativeTileFileName(const TileId& id) const
{
    const QString suffix = fileFormat().toLower();

    QString relFileName;

    switch (m_storageLayoutMode)
    {
        case GeoSceneTileDataset::Marble:
            relFileName = QStringLiteral("%1/%2/%3/%3_%4.%5")
                          .arg(themeStr())
                          .arg(id.zoomLevel())
                          .arg(id.y(), tileDigits, 10, QLatin1Char('0'))
                          .arg(id.x(), tileDigits, 10, QLatin1Char('0'))
                          .arg(suffix);
            break;

        case GeoSceneTileDataset::OpenStreetMap:
            relFileName = QStringLiteral("%1/%2/%3/%4.%5")
                          .arg(themeStr())
                          .arg(id.zoomLevel())
                          .arg(id.x())
                          .arg(id.y())
                          .arg(suffix);
            break;

        case GeoSceneTileDataset::TileMapService:
            relFileName = QStringLiteral("%1/%2/%3/%4.%5")
                          .arg(themeStr())
                          .arg(id.zoomLevel())
                          .arg(id.x())
                          .arg((1 << id.zoomLevel()) - id.y() - 1)    //Y coord in TMS runs from bottom to top
                          .arg(suffix);
            break;
    }

    return relFileName;
}

QString GeoSceneTileDataset::themeStr() const
{
    QFileInfo const dirInfo(sourceDir());
    return dirInfo.isAbsolute() ? sourceDir() : QLatin1String("maps/") + sourceDir();
}

QList<const DownloadPolicy*> GeoSceneTileDataset::downloadPolicies() const
{
    return m_downloadPolicies;
}

void GeoSceneTileDataset::addDownloadPolicy(const DownloadUsage usage, const int maximumConnections)
{
    DownloadPolicy* const policy = new DownloadPolicy(DownloadPolicyKey(hostNames(), usage));
    policy->setMaximumConnections(maximumConnections);
    m_downloadPolicies.append(policy);
    qCDebug(DIGIKAM_MARBLE_LOG) << "added download policy" << hostNames() << usage << maximumConnections;
}

QStringList GeoSceneTileDataset::hostNames() const
{
    QStringList result;
    result.reserve(m_downloadUrls.size());

    QVector<QUrl>::const_iterator pos = m_downloadUrls.constBegin();
    QVector<QUrl>::const_iterator const end = m_downloadUrls.constEnd();

    for (; pos != end; ++pos)
    {
        result.append((*pos).host());
    }

    return result;
}

} // namespace Marble
