// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <functional>
#include <cctype>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qurl.h>
#include <QtNetwork/qtcpserver.h>
#include <QTcpSocket>

class WebServer : public QTcpServer
{
public:
    class HttpRequest {
        friend class WebServer;
        friend class TlsWebServer;

        quint16 port = 0;
        enum class State {
            ReadingMethod,
            ReadingUrl,
            ReadingStatus,
            ReadingHeader,
            ReadingBody,
            AllDone
        } state = State::ReadingMethod;
        QByteArray fragment;
        int bytesLeft = 0;

        bool readMethod(QTcpSocket *socket);
        bool readUrl(QTcpSocket *socket);
        bool readStatus(QTcpSocket *socket);
        bool readHeaders(QTcpSocket *socket);
        bool readBody(QTcpSocket *socket);

    public:
        enum class Method {
            Unknown,
            Head,
            Get,
            Put,
            Post,
            Delete,
        } method = Method::Unknown;
        QUrl url;
        QPair<quint8, quint8> version;
        QMap<QByteArray, QByteArray> headers;
        QByteArray body;
    };

    typedef std::function<void(const HttpRequest &request, QTcpSocket *socket)> Handler;

    WebServer(Handler handler, QObject *parent = nullptr);

    QUrl url(const QString &path);

private:
    Handler handler;

    QMap<QTcpSocket *, HttpRequest> clients;
};

WebServer::WebServer(Handler h, QObject *parent) :
    QTcpServer(parent),
    handler(h)
{
    connect(this, &QTcpServer::newConnection, this, [this]() {
        auto socket = nextPendingConnection();
        connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
        connect(socket, &QTcpSocket::readyRead, this, [this, socket]() {
            if (!clients.contains(socket))
                clients[socket].port = serverPort();

            auto *request = &clients[socket];
            auto ok = true;

            while (socket->bytesAvailable()) {
                if (Q_LIKELY(request->state == HttpRequest::State::ReadingMethod))
                    if (Q_UNLIKELY(!(ok = request->readMethod(socket))))
                        qWarning("Invalid Method");

                if (Q_LIKELY(ok && request->state == HttpRequest::State::ReadingUrl))
                    if (Q_UNLIKELY(!(ok = request->readUrl(socket))))
                        qWarning("Invalid URL");

                if (Q_LIKELY(ok && request->state == HttpRequest::State::ReadingStatus))
                    if (Q_UNLIKELY(!(ok = request->readStatus(socket))))
                        qWarning("Invalid Status");

                if (Q_LIKELY(ok && request->state == HttpRequest::State::ReadingHeader))
                    if (Q_UNLIKELY(!(ok = request->readHeaders(socket))))
                        qWarning("Invalid Header");

                if (Q_LIKELY(ok && request->state == HttpRequest::State::ReadingBody))
                    if (Q_UNLIKELY(!(ok = request->readBody(socket))))
                        qWarning("Invalid Body");
            }
            if (Q_UNLIKELY(!ok)) {
                socket->disconnectFromHost();
                clients.remove(socket);
            } else if (Q_LIKELY(request->state == HttpRequest::State::AllDone)) {
                Q_ASSERT(handler);
                if (request->headers.contains("Host")) {
                    const auto parts = request->headers["Host"].split(':');
                    request->url.setHost(parts.at(0));
                    if (parts.size() == 2)
                        request->url.setPort(parts.at(1).toUInt());
                }
                handler(*request, socket);
                socket->disconnectFromHost();
                clients.remove(socket);
            }
        });
    });

    const auto ok = listen(QHostAddress::LocalHost);
    Q_ASSERT(ok);
}

QUrl WebServer::url(const QString &path)
{
    const QString format("http://127.0.0.1:%1%2");
    return QUrl(format.arg(serverPort()).arg(path.startsWith('/') ? path : "/" + path));
}

bool WebServer::HttpRequest::readMethod(QTcpSocket *socket)
{
    bool finished = false;
    while (socket->bytesAvailable() && !finished) {
        const auto c = socket->read(1).at(0);
        if (std::isspace(c))
            finished = true;
        else if (std::isupper(c) && fragment.size() < 8)
            fragment += c;
        else
            return false;
    }
    if (finished) {
        if (fragment == "HEAD")
            method = Method::Head;
        else if (fragment == "GET")
            method = Method::Get;
        else if (fragment == "PUT")
            method = Method::Put;
        else if (fragment == "POST")
            method = Method::Post;
        else if (fragment == "DELETE")
            method = Method::Delete;
        else
            qWarning("Invalid operation %s", fragment.data());

        state = State::ReadingUrl;
        fragment.clear();

        return method != Method::Unknown;
    }
    return true;
}

bool WebServer::HttpRequest::readUrl(QTcpSocket *socket)
{
    bool finished = false;
    while (socket->bytesAvailable() && !finished) {
        const auto c = socket->read(1).at(0);
        if (std::isspace(c))
            finished = true;
        else
            fragment += c;
    }
    if (finished) {
        if (!fragment.startsWith("/")) {
            qWarning("Invalid URL path %s", fragment.constData());
            return false;
        }
        url.setUrl(QStringLiteral("http://127.0.0.1:") + QString::number(port) +
                   QString::fromUtf8(fragment));
        state = State::ReadingStatus;
        if (!url.isValid()) {
            qWarning("Invalid URL %s", fragment.constData());
            return false;
        }
        fragment.clear();
    }
    return true;
}

bool WebServer::HttpRequest::readStatus(QTcpSocket *socket)
{
    bool finished = false;
    while (socket->bytesAvailable() && !finished) {
        fragment += socket->read(1);
        if (fragment.endsWith("\r\n")) {
            finished = true;
            fragment.resize(fragment.size() - 2);
        }
    }
    if (finished) {
        if (!std::isdigit(fragment.at(fragment.size() - 3)) ||
                fragment.at(fragment.size() - 2) != '.' ||
                !std::isdigit(fragment.at(fragment.size() - 1))) {
            qWarning("Invalid version");
            return false;
        }
        version = qMakePair(fragment.at(fragment.size() - 3) - '0',
                            fragment.at(fragment.size() - 1) - '0');
        state = State::ReadingHeader;
        fragment.clear();
    }
    return true;
}

bool WebServer::HttpRequest::readHeaders(QTcpSocket *socket)
{
    while (socket->bytesAvailable()) {
        fragment += socket->read(1);
        if (fragment.endsWith("\r\n")) {
            if (fragment == "\r\n") {
                state = State::ReadingBody;
                fragment.clear();
                return true;
            } else {
                fragment.chop(2);
                const int index = fragment.indexOf(':');
                if (index == -1)
                    return false;

                const QByteArray key = fragment.mid(0, index).trimmed();
                const QByteArray value = fragment.mid(index + 1).trimmed();
                headers.insert(key, value);
                fragment.clear();
            }
        }
    }
    return true;
}

bool WebServer::HttpRequest::readBody(QTcpSocket *socket)
{
    if (headers.contains("Content-Length")) {
        bool conversionResult;
        bytesLeft = headers["Content-Length"].toInt(&conversionResult);
        if (Q_UNLIKELY(!conversionResult))
            return false;
        fragment.resize(bytesLeft);
    }
    while (bytesLeft) {
        int got = socket->read(&fragment.data()[fragment.size() - bytesLeft], bytesLeft);
        if (got < 0)
            return false; // error
        bytesLeft -= got;
        if (bytesLeft)
            qApp->processEvents();
    }
    fragment.swap(body);
    state = State::AllDone;
    return true;
}

#endif // WEBSERVER_H
