// SPDX-FileCopyrightText: 2006-2007 Torsten Rahn <tackat@kde.org>
// SPDX-FileCopyrightText: 2007-2008 Inge Wallin <ingwa@kde.org>
// SPDX-FileCopyrightText: 2011 Niko Sams <niko.sams@gmail.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "TileCreator.h"

#include <cmath>

#include <QApplication>
#include <QDir>
#include <QImage>
#include <QList>
#include <QPainter>
#include <QRect>
#include <QSize>

#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "MarbleGlobal.h"
#include "TileLoaderHelper.h"

namespace Marble
{

class TileCreatorPrivate
{
public:
    TileCreatorPrivate(TileCreatorSource *source, const QString &dem, const QString &targetDir = QString())
        : m_dem(dem)
        , m_targetDir(targetDir)
        , m_cancelled(false)
        , m_tileFormat(QStringLiteral("jpg"))
        , m_resume(false)
        , m_verify(false)
        , m_source(source)
    {
        if (m_dem == QLatin1StringView("true")) {
            m_tileQuality = 70;
        } else {
            m_tileQuality = 85;
        }
    }

    ~TileCreatorPrivate()
    {
        delete m_source;
    }

public:
    QString m_dem;
    QString m_targetDir;
    bool m_cancelled;
    QString m_tileFormat;
    int m_tileQuality;
    bool m_resume;
    bool m_verify;

    TileCreatorSource *m_source;
};

class TileCreatorSourceImage : public TileCreatorSource
{
public:
    explicit TileCreatorSourceImage(const QString &sourcePath)
        : m_sourceImage(QImage(sourcePath))
        , m_cachedRowNum(-1)
    {
    }

    QSize fullImageSize() const override
    {
        if (m_sourceImage.size().width() > 21600 || m_sourceImage.height() > 10800) {
            qDebug("Install map too large!");
            return {};
        }
        return m_sourceImage.size();
    }

    QImage tile(int n, int m, int maxTileLevel) override
    {
        int mmax = TileLoaderHelper::levelToColumn(defaultLevelZeroColumns, maxTileLevel);
        int nmax = TileLoaderHelper::levelToRow(defaultLevelZeroRows, maxTileLevel);

        int imageHeight = m_sourceImage.height();
        int imageWidth = m_sourceImage.width();

        // If the image size of the image source does not match the expected
        // geometry we need to smooth-scale the image in advance to match
        // the required size
        bool needsScaling = (imageWidth != 2 * nmax * (int)(c_defaultTileSize) || imageHeight != nmax * (int)(c_defaultTileSize));

        if (needsScaling)
            mDebug() << "Image Size doesn't match 2*n*TILEWIDTH x n*TILEHEIGHT geometry. Scaling ...";

        int stdImageWidth = 2 * nmax * c_defaultTileSize;
        if (stdImageWidth == 0)
            stdImageWidth = 2 * c_defaultTileSize;

        int stdImageHeight = nmax * c_defaultTileSize;
        if (stdImageWidth != imageWidth) {
            mDebug()
                << QStringLiteral("TileCreator::createTiles() The size of the final image will measure  %1 x %2 pixels").arg(stdImageWidth).arg(stdImageHeight);
        }

        QImage row;

        if (m_cachedRowNum == n) {
            row = m_rowCache;

        } else {
            QRect sourceRowRect(0, (int)((qreal)(n * imageHeight) / (qreal)(nmax)), imageWidth, (int)((qreal)(imageHeight) / (qreal)(nmax)));

            row = m_sourceImage.copy(sourceRowRect);

            if (needsScaling) {
                // Pick the current row and smooth scale it
                // to make it match the expected size
                QSize destSize(stdImageWidth, c_defaultTileSize);
                row = row.scaled(destSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
            }

            m_cachedRowNum = n;
            m_rowCache = row;
        }

        if (row.isNull()) {
            mDebug() << "Read-Error! Null QImage!";
            return {};
        }

        QImage tile = row.copy(m * stdImageWidth / mmax, 0, c_defaultTileSize, c_defaultTileSize);

        return tile;
    }

private:
    QImage m_sourceImage;

    QImage m_rowCache;
    int m_cachedRowNum;
};

TileCreator::TileCreator(const QString &sourceDir, const QString &installMap, const QString &dem, const QString &targetDir)
    : QThread(nullptr)
    , d(new TileCreatorPrivate(nullptr, dem, targetDir))

{
    mDebug() << "Prefix: " << sourceDir << "installmap:" << installMap;

    QString sourcePath;

    // If the sourceDir starts with a '/' assume an absolute path.
    // Otherwise assume a relative marble data path
    if (QDir::isAbsolutePath(sourceDir)) {
        sourcePath = sourceDir + QLatin1Char('/') + installMap;
        mDebug() << "Trying absolute path*:" << sourcePath;
    } else {
        sourcePath = MarbleDirs::path(QLatin1StringView("maps/") + sourceDir + QLatin1Char('/') + installMap);
        mDebug() << "Trying relative path*:" << QLatin1StringView("maps/") + sourceDir + QLatin1Char('/') + installMap;
    }

    mDebug() << "Creating tiles from*: " << sourcePath;

    d->m_source = new TileCreatorSourceImage(sourcePath);

    if (d->m_targetDir.isNull())
        d->m_targetDir = MarbleDirs::cachePath() + QLatin1StringView("/maps/") + sourcePath.section(QLatin1Char('/'), -3, -2) + QLatin1Char('/');

    setTerminationEnabled(true);
}

TileCreator::TileCreator(TileCreatorSource *source, const QString &dem, const QString &targetDir)
    : QThread(nullptr)
    , d(new TileCreatorPrivate(source, dem, targetDir))
{
    setTerminationEnabled(true);
}

TileCreator::~TileCreator()
{
    delete d;
}

void TileCreator::cancelTileCreation()
{
    d->m_cancelled = true;
}

void TileCreator::run()
{
    if (d->m_resume && d->m_tileFormat == QLatin1StringView("jpg") && d->m_tileQuality != 100) {
        qWarning() << "Resuming jpegs is only supported with tileQuality 100";
        return;
    }

    if (!d->m_targetDir.endsWith(QLatin1Char('/')))
        d->m_targetDir += QLatin1Char('/');

    mDebug() << "Installing tiles to: " << d->m_targetDir;

    QList<QRgb> grayScalePalette;
    for (int cnt = 0; cnt <= 255; ++cnt) {
        grayScalePalette.insert(cnt, qRgb(cnt, cnt, cnt));
    }

    QSize fullImageSize = d->m_source->fullImageSize();
    int imageWidth = fullImageSize.width();
    int imageHeight = fullImageSize.height();

    mDebug() << QStringLiteral("TileCreator::createTiles() image dimensions %1 x %2").arg(imageWidth).arg(imageHeight);

    if (imageWidth < 1 || imageHeight < 1) {
        qDebug("Invalid imagemap!");
        return;
    }

    // Calculating Maximum Tile Level
    float approxMaxTileLevel = std::log(imageWidth / (2.0 * c_defaultTileSize)) / std::log(2.0);

    int maxTileLevel = 0;
    if (approxMaxTileLevel == int(approxMaxTileLevel))
        maxTileLevel = static_cast<int>(approxMaxTileLevel);
    else
        maxTileLevel = static_cast<int>(approxMaxTileLevel + 1);

    if (maxTileLevel < 0) {
        mDebug() << QStringLiteral("TileCreator::createTiles(): Invalid Maximum Tile Level: %1").arg(maxTileLevel);
    }
    mDebug() << "Maximum Tile Level: " << maxTileLevel;

    if (!QDir(d->m_targetDir).exists())
        (QDir::root()).mkpath(d->m_targetDir);

    // Counting total amount of tiles to be generated for the progressbar
    // to prevent compiler warnings this var should
    // match the type of maxTileLevel
    int tileLevel = 0;
    int totalTileCount = 0;

    while (tileLevel <= maxTileLevel) {
        totalTileCount += (TileLoaderHelper::levelToRow(defaultLevelZeroRows, tileLevel) * TileLoaderHelper::levelToColumn(defaultLevelZeroColumns, tileLevel));
        tileLevel++;
    }

    mDebug() << totalTileCount << " tiles to be created in total.";

    int mmax = TileLoaderHelper::levelToColumn(defaultLevelZeroColumns, maxTileLevel);
    int nmax = TileLoaderHelper::levelToRow(defaultLevelZeroRows, maxTileLevel);

    // Loading each row at highest spatial resolution and cropping tiles
    int percentCompleted = 0;
    int createdTilesCount = 0;
    QString tileName;

    // Creating directory structure for the highest level
    QString dirName(d->m_targetDir + QStringLiteral("%1").arg(maxTileLevel));
    if (!QDir(dirName).exists())
        (QDir::root()).mkpath(dirName);

    for (int n = 0; n < nmax; ++n) {
        QString dirName(d->m_targetDir + QStringLiteral("%1/%2").arg(maxTileLevel).arg(n, tileDigits, 10, QLatin1Char('0')));
        if (!QDir(dirName).exists())
            (QDir::root()).mkpath(dirName);
    }

    for (int n = 0; n < nmax; ++n) {
        for (int m = 0; m < mmax; ++m) {
            mDebug() << "** tile" << m << "x" << n;

            if (d->m_cancelled)
                return;

            tileName = d->m_targetDir
                + QStringLiteral("%1/%2/%2_%3.%4")
                      .arg(maxTileLevel)
                      .arg(n, tileDigits, 10, QLatin1Char('0'))
                      .arg(m, tileDigits, 10, QLatin1Char('0'))
                      .arg(d->m_tileFormat);

            if (QFile::exists(tileName) && d->m_resume) {
                // mDebug() << tileName << "exists already";

            } else {
                QImage tile = d->m_source->tile(n, m, maxTileLevel);

                if (tile.isNull()) {
                    mDebug() << "Read-Error! Null QImage!";
                    return;
                }

                if (d->m_dem == QLatin1StringView("true")) {
                    tile = tile.convertToFormat(QImage::Format_Indexed8, grayScalePalette, Qt::ThresholdDither);
                }

                bool ok = tile.save(tileName, d->m_tileFormat.toLatin1().data(), d->m_tileFormat == QLatin1StringView("jpg") ? 100 : d->m_tileQuality);
                if (!ok)
                    mDebug() << "Error while writing Tile: " << tileName;

                mDebug() << tileName << "size" << QFile(tileName).size();

                if (d->m_verify) {
                    QImage writtenTile(tileName);
                    Q_ASSERT(writtenTile.size() == tile.size());
                    for (int i = 0; i < writtenTile.size().width(); ++i) {
                        for (int j = 0; j < writtenTile.size().height(); ++j) {
                            if (writtenTile.pixel(i, j) != tile.pixel(i, j)) {
                                unsigned int pixel = tile.pixel(i, j);
                                unsigned int writtenPixel = writtenTile.pixel(i, j);
                                qWarning() << "***** pixel" << i << j << "is off by" << (pixel - writtenPixel) << "pixel" << pixel << "writtenPixel"
                                           << writtenPixel;
                                QByteArray baPixel((char *)&pixel, sizeof(unsigned int));
                                qWarning() << "pixel" << baPixel.size() << "0x" << baPixel.toHex();
                                QByteArray baWrittenPixel((char *)&writtenPixel, sizeof(unsigned int));
                                qWarning() << "writtenPixel" << baWrittenPixel.size() << "0x" << baWrittenPixel.toHex();
                                Q_ASSERT(false);
                            }
                        }
                    }
                }
            }

            percentCompleted = (int)(90 * (qreal)(createdTilesCount) / (qreal)(totalTileCount));
            createdTilesCount++;

            mDebug() << "percentCompleted" << percentCompleted;
            Q_EMIT progress(percentCompleted);
        }
    }

    mDebug() << "tileLevel: " << maxTileLevel << " successfully created.";

    tileLevel = maxTileLevel;

    // Now that we have the tiles at the highest resolution lets build
    // them together four by four.

    while (tileLevel > 0) {
        tileLevel--;

        int nmaxit = TileLoaderHelper::levelToRow(defaultLevelZeroRows, tileLevel);

        for (int n = 0; n < nmaxit; ++n) {
            QString dirName(d->m_targetDir + QStringLiteral("%1/%2").arg(tileLevel).arg(n, tileDigits, 10, QLatin1Char('0')));

            // mDebug() << "dirName: " << dirName;
            if (!QDir(dirName).exists())
                (QDir::root()).mkpath(dirName);

            int mmaxit = TileLoaderHelper::levelToColumn(defaultLevelZeroColumns, tileLevel);
            for (int m = 0; m < mmaxit; ++m) {
                if (d->m_cancelled)
                    return;

                QString newTileName = d->m_targetDir
                    + QStringLiteral("%1/%2/%2_%3.%4")
                          .arg(tileLevel)
                          .arg(n, tileDigits, 10, QLatin1Char('0'))
                          .arg(m, tileDigits, 10, QLatin1Char('0'))
                          .arg(d->m_tileFormat);

                if (QFile::exists(newTileName) && d->m_resume) {
                    // mDebug() << newTileName << "exists already";
                } else {
                    tileName = d->m_targetDir
                        + QStringLiteral("%1/%2/%2_%3.%4")
                              .arg(tileLevel + 1)
                              .arg(2 * n, tileDigits, 10, QLatin1Char('0'))
                              .arg(2 * m, tileDigits, 10, QLatin1Char('0'))
                              .arg(d->m_tileFormat);
                    QImage img_topleft(tileName);

                    tileName = d->m_targetDir
                        + QStringLiteral("%1/%2/%2_%3.%4")
                              .arg(tileLevel + 1)
                              .arg(2 * n, tileDigits, 10, QLatin1Char('0'))
                              .arg(2 * m + 1, tileDigits, 10, QLatin1Char('0'))
                              .arg(d->m_tileFormat);
                    QImage img_topright(tileName);

                    tileName = d->m_targetDir
                        + QStringLiteral("%1/%2/%2_%3.%4")
                              .arg(tileLevel + 1)
                              .arg(2 * n + 1, tileDigits, 10, QLatin1Char('0'))
                              .arg(2 * m, tileDigits, 10, QLatin1Char('0'))
                              .arg(d->m_tileFormat);
                    QImage img_bottomleft(tileName);

                    tileName = d->m_targetDir
                        + QStringLiteral("%1/%2/%2_%3.%4")
                              .arg(tileLevel + 1)
                              .arg(2 * n + 1, tileDigits, 10, QLatin1Char('0'))
                              .arg(2 * m + 1, tileDigits, 10, QLatin1Char('0'))
                              .arg(d->m_tileFormat);
                    QImage img_bottomright(tileName);

                    QSize const expectedSize(c_defaultTileSize, c_defaultTileSize);
                    if (img_topleft.size() != expectedSize || img_topright.size() != expectedSize || img_bottomleft.size() != expectedSize
                        || img_bottomright.size() != expectedSize) {
                        mDebug() << "Tile write failure. Missing write permissions?";
                        Q_EMIT progress(100);
                        return;
                    }
                    QImage tile = img_topleft;

                    if (d->m_dem == QLatin1StringView("true")) {
                        tile.setColorTable(grayScalePalette);
                        uchar *destLine;

                        for (uint y = 0; y < c_defaultTileSize / 2; ++y) {
                            destLine = tile.scanLine(y);
                            const uchar *srcLine = img_topleft.scanLine(2 * y);
                            for (uint x = 0; x < c_defaultTileSize / 2; ++x)
                                destLine[x] = srcLine[2 * x];
                        }
                        for (uint y = 0; y < c_defaultTileSize / 2; ++y) {
                            destLine = tile.scanLine(y);
                            const uchar *srcLine = img_topright.scanLine(2 * y);
                            for (uint x = c_defaultTileSize / 2; x < c_defaultTileSize; ++x)
                                destLine[x] = srcLine[2 * (x - c_defaultTileSize / 2)];
                        }
                        for (uint y = c_defaultTileSize / 2; y < c_defaultTileSize; ++y) {
                            destLine = tile.scanLine(y);
                            const uchar *srcLine = img_bottomleft.scanLine(2 * (y - c_defaultTileSize / 2));
                            for (uint x = 0; x < c_defaultTileSize / 2; ++x)
                                destLine[x] = srcLine[2 * x];
                        }
                        for (uint y = c_defaultTileSize / 2; y < c_defaultTileSize; ++y) {
                            destLine = tile.scanLine(y);
                            const uchar *srcLine = img_bottomright.scanLine(2 * (y - c_defaultTileSize / 2));
                            for (uint x = c_defaultTileSize / 2; x < c_defaultTileSize; ++x)
                                destLine[x] = srcLine[2 * (x - c_defaultTileSize / 2)];
                        }
                    } else {
                        // tile.depth() != 8

                        img_topleft = img_topleft.convertToFormat(QImage::Format_ARGB32);
                        img_topright = img_topright.convertToFormat(QImage::Format_ARGB32);
                        img_bottomleft = img_bottomleft.convertToFormat(QImage::Format_ARGB32);
                        img_bottomright = img_bottomright.convertToFormat(QImage::Format_ARGB32);
                        tile = img_topleft;

                        QRgb *destLine;

                        for (uint y = 0; y < c_defaultTileSize / 2; ++y) {
                            destLine = (QRgb *)tile.scanLine(y);
                            const QRgb *srcLine = (QRgb *)img_topleft.scanLine(2 * y);
                            for (uint x = 0; x < c_defaultTileSize / 2; ++x)
                                destLine[x] = srcLine[2 * x];
                        }
                        for (uint y = 0; y < c_defaultTileSize / 2; ++y) {
                            destLine = (QRgb *)tile.scanLine(y);
                            const QRgb *srcLine = (QRgb *)img_topright.scanLine(2 * y);
                            for (uint x = c_defaultTileSize / 2; x < c_defaultTileSize; ++x)
                                destLine[x] = srcLine[2 * (x - c_defaultTileSize / 2)];
                        }
                        for (uint y = c_defaultTileSize / 2; y < c_defaultTileSize; ++y) {
                            destLine = (QRgb *)tile.scanLine(y);
                            const QRgb *srcLine = (QRgb *)img_bottomleft.scanLine(2 * (y - c_defaultTileSize / 2));
                            for (uint x = 0; x < c_defaultTileSize / 2; ++x)
                                destLine[x] = srcLine[2 * x];
                        }
                        for (uint y = c_defaultTileSize / 2; y < c_defaultTileSize; ++y) {
                            destLine = (QRgb *)tile.scanLine(y);
                            const QRgb *srcLine = (QRgb *)img_bottomright.scanLine(2 * (y - c_defaultTileSize / 2));
                            for (uint x = c_defaultTileSize / 2; x < c_defaultTileSize; ++x)
                                destLine[x] = srcLine[2 * (x - c_defaultTileSize / 2)];
                        }
                    }

                    mDebug() << newTileName;

                    // Saving at 100% JPEG quality to have a high-quality
                    // version to create the remaining needed tiles from.
                    bool ok = tile.save(newTileName, d->m_tileFormat.toLatin1().data(), d->m_tileFormat == QLatin1StringView("jpg") ? 100 : d->m_tileQuality);
                    if (!ok)
                        mDebug() << "Error while writing Tile: " << newTileName;
                }

                percentCompleted = (int)(90 * (qreal)(createdTilesCount) / (qreal)(totalTileCount));
                createdTilesCount++;

                Q_EMIT progress(percentCompleted);
                mDebug() << "percentCompleted" << percentCompleted;
            }
        }
        mDebug() << "tileLevel: " << tileLevel << " successfully created.";
    }
    mDebug() << "Tile creation completed.";

    if (d->m_tileFormat == QLatin1StringView("jpg") && d->m_tileQuality != 100) {
        // Applying correct lower JPEG compression now that we created all tiles
        int savedTilesCount = 0;

        tileLevel = 0;
        while (tileLevel <= maxTileLevel) {
            int nmaxit = TileLoaderHelper::levelToRow(defaultLevelZeroRows, tileLevel);
            for (int n = 0; n < nmaxit; ++n) {
                int mmaxit = TileLoaderHelper::levelToColumn(defaultLevelZeroColumns, tileLevel);
                for (int m = 0; m < mmaxit; ++m) {
                    if (d->m_cancelled)
                        return;

                    savedTilesCount++;

                    tileName = d->m_targetDir
                        + QStringLiteral("%1/%2/%2_%3.%4")
                              .arg(tileLevel)
                              .arg(n, tileDigits, 10, QLatin1Char('0'))
                              .arg(m, tileDigits, 10, QLatin1Char('0'))
                              .arg(d->m_tileFormat);
                    QImage tile(tileName);

                    bool ok;

                    ok = tile.save(tileName, d->m_tileFormat.toLatin1().data(), d->m_tileQuality);

                    if (!ok)
                        mDebug() << "Error while writing Tile: " << tileName;
                    // Don't exceed 99% as this would cancel the thread unexpectedly
                    percentCompleted = 90 + (int)(9 * (qreal)(savedTilesCount) / (qreal)(totalTileCount));
                    Q_EMIT progress(percentCompleted);
                    mDebug() << "percentCompleted" << percentCompleted;
                    // mDebug() << "Saving Tile #" << savedTilesCount
                    //          << " of " << totalTileCount
                    //          << " Percent: " << percentCompleted;
                }
            }
            tileLevel++;
        }
    }

    percentCompleted = 100;
    Q_EMIT progress(percentCompleted);

    mDebug() << "percentCompleted: " << percentCompleted;
}

void TileCreator::setTileFormat(const QString &format)
{
    d->m_tileFormat = format;
}

QString TileCreator::tileFormat() const
{
    return d->m_tileFormat;
}

void TileCreator::setTileQuality(int quality)
{
    d->m_tileQuality = quality;
}

int TileCreator::tileQuality() const
{
    return d->m_tileQuality;
}

void TileCreator::setResume(bool resume)
{
    d->m_resume = resume;
}

bool TileCreator::resume() const
{
    return d->m_resume;
}

void TileCreator::setVerifyExactResult(bool verify)
{
    d->m_verify = verify;
}

bool TileCreator::verifyExactResult() const
{
    return d->m_verify;
}

}

#include "moc_TileCreator.cpp"
