///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021, 2023 Jon Beniston, M7RCE <jon@beniston.com>               //
//                                                                               //
// This program is free software; you can redistribute it and/or modify          //
// it under the terms of the GNU General Public License as published by          //
// the Free Software Foundation as version 3 of the License, or                  //
// (at your option) any later version.                                           //
//                                                                               //
// This program is distributed in the hope that it will be useful,               //
// but WITHOUT ANY WARRANTY; without even the implied warranty of                //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                  //
// GNU General Public License V3 for more details.                               //
//                                                                               //
// You should have received a copy of the GNU General Public License             //
// along with this program. If not, see <http://www.gnu.org/licenses/>.          //
///////////////////////////////////////////////////////////////////////////////////

#ifndef INCLUDE_MAPTILESERVER_H_
#define INCLUDE_MAPTILESERVER_H_

#include <QTcpServer>
#include <QTcpSocket>
#include <QRegularExpression>
#include <QDebug>
#include <QtNetwork>
#include <QImage>
#include <QPainter>
#include <QHash>
#include <QMutex>
#include <QMutexLocker>
#include <QNetworkDiskCache>

class MapTileServer : public QTcpServer
{
    Q_OBJECT
private:
    QString m_thunderforestAPIKey;
    QString m_maptilerAPIKey;
    QNetworkAccessManager m_manager;
    QMutex m_mutex;

    struct TileJob {
        QTcpSocket* m_socket;
        QList<QString> m_urls;
        QHash<QString, QImage> m_images;
        QString m_format;
    };
    QList<TileJob *> m_tileJobs;
    QHash<QNetworkReply *, TileJob *> m_replies;

    QNetworkDiskCache *m_cache;

    QString m_radarPath;
    QString m_satellitePath;
    QString m_nasaGlobalImageryPath;
    QString m_nasaGlobalImageryFormat;
    bool m_displayRain;
    bool m_displayClouds;
    bool m_displaySeaMarks;
    bool m_displayRailways;
    bool m_displayNASAGlobalImagery;

public:
    // port - port to listen on / is listening on. Use 0 for any free port.
    MapTileServer(quint16 &port, QObject* parent = 0) :
        QTcpServer(parent),
        m_thunderforestAPIKey(""),
        m_maptilerAPIKey(""),
        m_radarPath(""),
        m_satellitePath(""),
        m_nasaGlobalImageryPath(""),
        m_nasaGlobalImageryFormat(""),
        m_displayRain(false),
        m_displayClouds(false),
        m_displaySeaMarks(false),
        m_displayRailways(false),
        m_displayNASAGlobalImagery(false)
    {
        connect(&m_manager, &QNetworkAccessManager::finished, this, &MapTileServer::downloadFinished);
        listen(QHostAddress::Any, port);
        port = serverPort();

        QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
        QDir writeableDir(locations[0]);
        if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("maptiles"))) {
            qDebug() << "Failed to create cache/maptiles";
        }

        m_cache = new QNetworkDiskCache();
        m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("maptiles"));
        m_cache->setMaximumCacheSize(1000000000);
        m_manager.setCache(m_cache);
    }

    ~MapTileServer()
    {
       disconnect(&m_manager, &QNetworkAccessManager::finished, this, &MapTileServer::downloadFinished);
       delete m_cache;
    }

    void setThunderforestAPIKey(const QString& thunderforestAPIKey)
    {
        m_thunderforestAPIKey = thunderforestAPIKey;
    }

    void setMaptilerAPIKey(const QString& maptilerAPIKey)
    {
        m_maptilerAPIKey = maptilerAPIKey;
    }

    void setRadarPath(const QString& radarPath)
    {
        m_radarPath = radarPath;
    }

    void setSatellitePath(const QString& satellitePath)
    {
        m_satellitePath = satellitePath;
    }

    void setNASAGlobalImageryPath(const QString& nasaGlobalImageryPath)
    {
        m_nasaGlobalImageryPath = nasaGlobalImageryPath;
    }

    void setNASAGlobalImageryFormat(const QString& nasaGlobalImageryFormat)
    {
        m_nasaGlobalImageryFormat = nasaGlobalImageryFormat;
    }

    void setDisplaySeaMarks(bool displaySeaMarks)
    {
        m_displaySeaMarks = displaySeaMarks;
    }

    void setDisplayRailways(bool displayRailways)
    {
        m_displayRailways = displayRailways;
    }

    void setDisplayRain(bool displayRain)
    {
        m_displayRain = displayRain;
    }

    void setDisplayClouds(bool displayClouds)
    {
        m_displayClouds = displayClouds;
    }

    void setDisplayNASAGlobalImagery(bool displayNASAGlobalImagery)
    {
        m_displayNASAGlobalImagery = displayNASAGlobalImagery;
    }

    void incomingConnection(qintptr socket) override
    {
        QTcpSocket* s = new QTcpSocket(this);
        connect(s, SIGNAL(readyRead()), this, SLOT(readClient()));
        connect(s, SIGNAL(disconnected()), this, SLOT(discardClient()));
        s->setSocketDescriptor(socket);
        //addPendingConnection(socket);
    }

    bool isHttpRedirect(QNetworkReply *reply)
    {
        int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        // 304 is file not changed, but maybe we did
        return (status >= 301 && status <= 308);
    }

    QNetworkReply *download(const QUrl &url)
    {
        QNetworkRequest request(url);
        request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
        request.setRawHeader("User-Agent", "SDRangel"); // Required by a.tile.openstreetmap.org

        // Don't cache rainviwer data as it's dynamic
        if (!url.toString().contains("tilecache.rainviewer")) {
            request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
        }

        QNetworkReply *reply = m_manager.get(request);
#ifndef QT_NO_OPENSSL
        connect(reply, &QNetworkReply::sslErrors, this, &MapTileServer::sslErrors);
#endif
        //qDebug() << "MapTileServer: Downloading from " << url;
        return reply;
    }

    QImage combine(const TileJob *job)
    {
        // Don't use job->m_images[job->m_urls[0]].size() as not always valid (E.g. map tiler can return http 204 - no content)
        // Do we need to support 512x512?
        QImage image(QSize(256, 256), QImage::Format_ARGB32_Premultiplied);
        image.fill(qPremultiply(QColor(0, 0, 0, 0).rgba()));
        QPainter painter(&image);

        for (int i = 0; i < job->m_images.size(); i++) {
            const QImage &img = job->m_images[job->m_urls[i]];
            //qDebug() << "Image format " << i << " is " << img.format() << img.size();
        }

        for (int i = 0; i < job->m_images.size(); i++) {
            const QImage &img = job->m_images[job->m_urls[i]];
            //img.save(QString("in%1.png").arg(i), "PNG");
            if (img.format() != QImage::Format_Invalid) {
                painter.drawImage(image.rect(), img);
            }
        }

        return image;
    }

    void replyImage(QTcpSocket* socket, const QImage& image, const QString& format)
    {
        QByteArray ba;
        QBuffer buffer(&ba);
        buffer.open(QIODevice::WriteOnly);
        image.save(&buffer, qPrintable(format));

        //qDebug() << "socket: " << socket << "thread:" << QThread::currentThread();
        socket->write("HTTP/1.0 200 Ok\r\n"
            "Content-Type: image/png\r\n"
            "\r\n");
        socket->write(buffer.buffer());
        socket->close();

        if (socket->state() == QTcpSocket::UnconnectedState) {
            delete socket;
        }
    }

    void replyError(QTcpSocket* socket)
    {
        QTextStream os(socket);
        os.setAutoDetectUnicode(true);
        os << "HTTP/1.0 404 Not Found\r\n"
            "Content-Type: text/html\r\n"
            "\r\n"
            "<html>Not found</html>\r\n";
        socket->close();

        if (socket->state() == QTcpSocket::UnconnectedState) {
            delete socket;
        }
    }

private slots:

    void readClient()
    {
        QMutexLocker locker(&m_mutex);

        QTcpSocket* socket = (QTcpSocket*)sender();
        if (socket->canReadLine())
        {
            QString line = socket->readLine();
            //qDebug() << "HTTP Request: " << line;
            QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*"));
            if (tokens[0] == "GET")
            {
                QString xml = "";

                // Create multiple requests for each image
                // https://wiki.openstreetmap.org/wiki/Raster_tile_providers
                // rain radar: https://tilecache.rainviewer.com/v2/radar/{timestamp=1705359600}/{size=256}/z/x/y/{color=0}/{options=1_1}.png

                // "GET /street/1/2/3.png HTTP/1.1\r\n"
                const QRegularExpression re("\\/([A-Za-z0-9\\-_]+)\\/([0-9]+)\\/([0-9]+)\\/([0-9]+).(png|jpg)");
                QRegularExpressionMatch match = re.match(tokens[1]);
                if (match.hasMatch())
                {
                    QString map, x, y, z, format;

                    map = match.captured(1);
                    z = match.captured(2);
                    x = match.captured(3);
                    y = match.captured(4);
                    format = match.captured(5);

                    TileJob *job = new TileJob;
                    //qDebug() << "Created job" << job << "socket:" << socket << "thread:" << QThread::currentThread() ;
                    job->m_socket = socket;
                    if (format == "png") {
                        job->m_format = "PNG";
                    } else {
                        job->m_format = "JPG";
                    }

                    // This should match code in OSMTemplateServer::readClient
                    QString baseMapURL;
                    if (map == "street") {
                        baseMapURL = QString("https://tile.openstreetmap.org/%3/%1/%2.png").arg(x).arg(y).arg(z);
                    } else if (map == "satellite") {
                        baseMapURL = QString("https://api.maptiler.com/tiles/satellite-v2/%3/%1/%2.jpg?key=%4").arg(x).arg(y).arg(z).arg(m_maptilerAPIKey);
                    } else if ((map == "dark_nolabels") || (map == "light_nolabels")) {
                        baseMapURL = QString("http://1.basemaps.cartocdn.com/%4/%3/%1/%2.png").arg(x).arg(y).arg(z).arg(map);
                    } else {
                        baseMapURL = QString("http://a.tile.thunderforest.com/%4/%3/%1/%2.png?apikey=%5").arg(x).arg(y).arg(z).arg(map).arg(m_thunderforestAPIKey);
                    }

                    job->m_urls.append(baseMapURL);
                    if (m_displaySeaMarks) {
                        job->m_urls.append(QString("https://tiles.openseamap.org/seamark/%3/%1/%2.png").arg(x).arg(y).arg(z));
                    }
                    if (m_displayRailways) {
                        job->m_urls.append(QString("https://a.tiles.openrailwaymap.org/standard/%3/%1/%2.png").arg(x).arg(y).arg(z));
                    }
                    if (m_displayNASAGlobalImagery && !m_nasaGlobalImageryPath.isEmpty()) {
                        job->m_urls.append(QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/%4/%3/%2/%1.%5").arg(x).arg(y).arg(z).arg(m_nasaGlobalImageryPath).arg(m_nasaGlobalImageryFormat)); // x,y reversed compared to others
                    }
                    if (m_displayClouds && !m_satellitePath.isEmpty()) {
                        job->m_urls.append(QString("https://tilecache.rainviewer.com%4/256/%3/%1/%2/0/0_0.png").arg(x).arg(y).arg(z).arg(m_satellitePath));
                    }
                    if (m_displayRain && !m_radarPath.isEmpty()) {
                        job->m_urls.append(QString("https://tilecache.rainviewer.com%4/256/%3/%1/%2/4/1_1.png").arg(x).arg(y).arg(z).arg(m_radarPath));
                    }
                    m_tileJobs.append(job);
                    for (const auto& url : job->m_urls)
                    {
                        QNetworkReply *reply = download(QUrl(url));
                        m_replies.insert(reply, job);
                    }
                }
                else
                {
                    replyError(socket);
                }
            }
        }
    }

    void discardClient()
    {
        QTcpSocket* socket = (QTcpSocket*)sender();
        //qDebug() << "discardClient socket:" << socket;
        socket->deleteLater();
        for (auto job : m_tileJobs) {
            if (job->m_socket == socket) {
                //qDebug() << "Socket closed on active job. job: " << job << "socket" << socket;
                job->m_socket = nullptr;
            }
        }
    }

    void downloadFinished(QNetworkReply *reply)
    {
        QMutexLocker locker(&m_mutex);
        //QString url = reply->url().toEncoded().constData();
        QString url = reply->request().url().toEncoded().constData(); // reply->url() may differ if redirection occurred, so use requested

        if (!isHttpRedirect(reply))
        {
            QByteArray data = reply->readAll();
            QImage image;
            if (!reply->error())
            {
                if (!image.loadFromData(data))
                {
                    qDebug() << "MapTileServer::downloadFinished: Failed to load image: " << url;
                }
            }
            else
            {
                qDebug() << "MapTileServer::downloadFinished: Error: " << reply->error() << "for" << url;
            }

            bool found = false;
            TileJob *job = m_replies[reply];
            if (!m_tileJobs.contains(job)) {
                qDebug() << "job has been deleted!";
            }
            for (const auto& jobURL : job->m_urls)
            {
                if (jobURL == url)
                {
                    job->m_images.insert(url, image);
                    if (job->m_urls.size() == job->m_images.size())
                    {
                        // All images available
                        QImage combinedImage = combine(job);
                        if (job->m_socket)
                        {
                            replyImage(job->m_socket, combinedImage, job->m_format);
                            job->m_socket = nullptr;
                            m_tileJobs.removeAll(job);
                            delete job;
                            //qDebug() << "Delete job" << job;
                        }
                        else
                        {
                            qDebug() << "Socket was null. URL: " << url << "job:" << job;
                        }
                    }
                    found = true;
                    break;
                }
            }
            if (!found) {
                qDebug() << "MapTileServer::downloadFinished: Failed to match URL: " << url;
            }
        }
        else
        {
            qDebug() << "MapTileServer::downloadFinished: Redirect";
        }
        reply->deleteLater();
        m_replies.remove(reply);
    }

#ifndef QT_NO_OPENSSL
    void sslErrors(const QList<QSslError> &sslErrors)
    {
        for (const QSslError &error : sslErrors)
        {
            qCritical() << "MapTileServer: SSL error" << (int)error.error() << ": " << error.errorString();
    #ifdef ANDROID
            // On Android 6 (but not on 12), we always seem to get: "The issuer certificate of a locally looked up certificate could not be found"
            // which causes downloads to fail, so ignore
            if (error.error() == QSslError::UnableToGetLocalIssuerCertificate)
            {
                QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
                QList<QSslError> errorsThatCanBeIgnored;
                errorsThatCanBeIgnored << QSslError(QSslError::UnableToGetLocalIssuerCertificate, error.certificate());
                reply->ignoreSslErrors(errorsThatCanBeIgnored);
            }
    #endif
        }
    }
#endif

};

#endif
