/*
 * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG
 *  Copyright (C) 2020-2026 Rosalie Wanders <rosalie@mailbox.org>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 3.
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
#include "NetplaySessionBrowserDialog.hpp"
#include "NetplaySessionPasswordDialog.hpp"
#include "Utilities/QtMessageBox.hpp"
#include "NetplayCommon.hpp"

#include <QRegularExpressionValidator>
#include <QRegularExpression>
#include <QNetworkDatagram>
#include <QJsonDocument>
#include <QInputDialog>
#include <QPushButton>
#include <QFileDialog>
#include <QJsonObject>
#include <QJsonArray>
#include <QUrlQuery>
#include <QFile>

#include <RMG-Core/Settings.hpp>
#include <RMG-Core/Rom.hpp>

using namespace UserInterface::Dialog;
using namespace Utilities;

//
// Exported Functions
//

NetplaySessionBrowserDialog::NetplaySessionBrowserDialog(QWidget *parent, QWebSocket* webSocket, QMap<QString, CoreRomSettings> modelData) : QDialog(parent)
{
    this->setupUi(this);

    // prepare web socket
    this->webSocket = webSocket;
    connect(this->webSocket, &QWebSocket::connected, this, &NetplaySessionBrowserDialog::on_webSocket_connected);
    connect(this->webSocket, &QWebSocket::disconnected, this, &NetplaySessionBrowserDialog::on_webSocket_disconnected);
    connect(this->webSocket, &QWebSocket::textMessageReceived, this, &NetplaySessionBrowserDialog::on_webSocket_textMessageReceived);
    connect(this->webSocket, &QWebSocket::pong, this, &NetplaySessionBrowserDialog::on_webSocket_pong);

    // copy rom data for later
    this->romData = modelData;

    // prepare broadcast
    broadcastSocket.bind(QHostAddress(QHostAddress::AnyIPv4), 0);
    connect(&this->broadcastSocket, &QUdpSocket::readyRead, this, &NetplaySessionBrowserDialog::on_broadcastSocket_readyRead);
    QByteArray multirequest;
    multirequest.append(1);
    broadcastSocket.writeDatagram(multirequest, QHostAddress::Broadcast, 45000);

    // change ok button name
    QPushButton* joinButton = this->buttonBox->button(QDialogButtonBox::Ok);
    joinButton->setText("Join");
    joinButton->setEnabled(false);

    // change restore defaults button name
    QPushButton* refreshButton = this->buttonBox->button(QDialogButtonBox::RestoreDefaults);
    refreshButton->setText("Refresh");
    refreshButton->setIcon(QIcon::fromTheme("refresh-line"));
    refreshButton->setEnabled(false);

    // set validator for nickname
    QRegularExpression re(NETPLAYCOMMON_NICKNAME_REGEX);
    this->nickNameLineEdit->setValidator(new QRegularExpressionValidator(re, this));
    this->nickNameLineEdit->setText(QString::fromStdString(CoreSettingsGetStringValue(SettingsID::Netplay_Nickname)));

    // configure dispatcher network access manager
    this->dispatcherNetworkAccessManager = new QNetworkAccessManager(this);
    this->dispatcherNetworkAccessManager->setTransferTimeout(15000);
    connect(this->dispatcherNetworkAccessManager, &QNetworkAccessManager::finished, this, &NetplaySessionBrowserDialog::on_dispatcherRetrieveServers_Finished);

    // request server list
    QString serverUrl = QString::fromStdString(CoreSettingsGetStringValue(SettingsID::Netplay_ServerJsonUrl));
    if (!serverUrl.isEmpty())
    {
        QFile qFile(serverUrl);
        if (qFile.exists())
        {
            if (qFile.open(QIODevice::ReadOnly))
            {
                NetplayCommon::AddServers(this->serverComboBox, 
                                          QJsonDocument::fromJson(qFile.readAll()));   
            }
            else
            {
                QtMessageBox::Error(this, "Server Error", "Failed to open server list json: " + qFile.errorString());
            }
        }
        else if (QUrl(serverUrl).isValid())
        {
            QNetworkAccessManager* networkAccessManager = new QNetworkAccessManager(this);
            connect(networkAccessManager, &QNetworkAccessManager::finished, this, &NetplaySessionBrowserDialog::on_jsonServerListDownload_Finished);
            networkAccessManager->setTransferTimeout(15000);
            networkAccessManager->get(QNetworkRequest(QUrl(serverUrl)));
        }
    }

    QString dispatcherUrl = QString::fromStdString(CoreSettingsGetStringValue(SettingsID::Netplay_DispatcherUrl));
    if (!dispatcherUrl.isEmpty() && QUrl(dispatcherUrl).isValid())
    {
        this->dispatcherUrl = dispatcherUrl;

        QNetworkAccessManager* networkAccessManager = new QNetworkAccessManager(this);
        connect(networkAccessManager, &QNetworkAccessManager::finished, this, &NetplaySessionBrowserDialog::on_dispatcherRegionListDownload_Finished);
        networkAccessManager->setTransferTimeout(15000);

        networkAccessManager->get(NetplayCommon::GetNetworkRequest(this->dispatcherUrl + "/getRegions"));
    }

    this->validateJoinButton();
}

NetplaySessionBrowserDialog::~NetplaySessionBrowserDialog(void)
{
    QString nickname = this->nickNameLineEdit->text();
    if (!nickname.isEmpty())
    {
        CoreSettingsSetValue(SettingsID::Netplay_Nickname, nickname.toStdString());
    }

    QString server = this->serverComboBox->currentText();
    if (!server.isEmpty())
    {
        CoreSettingsSetValue(SettingsID::Netplay_SelectedServer, server.toStdString());
    }
}

QJsonObject NetplaySessionBrowserDialog::GetSessionJson(void)
{
    return this->sessionJson;
}

QString NetplaySessionBrowserDialog::GetSessionFile(void)
{
    return this->sessionFile;
}

QString NetplaySessionBrowserDialog::showROMDialog(QString name, QString md5)
{
    QString title = "Open " + name;
    QString file = QFileDialog::getOpenFileName(this, title, "", "N64 ROMs (*.n64 *.z64 *.v64 *.zip *.7z)");
    CoreRomSettings romSettings;

    if (!file.isEmpty())
    {
        if (!CoreOpenRom(file.toStdU32String()) ||
            !CoreGetCurrentRomSettings(romSettings))
        {
            CoreCloseRom();
            return "";
        }

        CoreCloseRom();

        if (md5.toStdString() != romSettings.MD5)
        {
            QString details = "Expected MD5: " + md5 + "\n";
            details        += "Received MD5: " + QString::fromStdString(romSettings.MD5);
            QtMessageBox::Error(this, "Incorrect ROM Selected", details);
            return "";
        }
    }

    return file;
}

bool NetplaySessionBrowserDialog::validate(void)
{
    if (this->nickNameLineEdit->text().isEmpty() ||
        this->nickNameLineEdit->text().contains(' ') ||
        this->nickNameLineEdit->text().size() > 128)
    {
        return false;
    }

    if (!this->sessionBrowserWidget->IsCurrentSessionValid() ||
        this->serverComboBox->count() == 0)
    {
        return false;
    }

    return true;
}

void NetplaySessionBrowserDialog::validateJoinButton(void)
{
    QPushButton* joinButton = this->buttonBox->button(QDialogButtonBox::Ok);
    joinButton->setEnabled(this->validate());
}

void NetplaySessionBrowserDialog::toggleUI(bool enable, bool enableJoinButton, bool toggleOtherUI)
{
    QPushButton* joinButton = this->buttonBox->button(QDialogButtonBox::Ok);
    joinButton->setEnabled(enableJoinButton);

    QPushButton* refreshButton = this->buttonBox->button(QDialogButtonBox::RestoreDefaults);
    refreshButton->setEnabled(enable);

    if (toggleOtherUI)
    {
        this->serverComboBox->setEnabled(enable);
        this->nickNameLineEdit->setReadOnly(!enable);
    }
}

void NetplaySessionBrowserDialog::refreshSessions(void)
{
    // disable join and refresh button while refreshing
    this->toggleUI(false, false, false);

    if (NetplayCommon::IsServerDispatcher(this->serverComboBox))
    {
        this->on_serverComboBox_currentIndexChanged(this->serverComboBox->currentIndex());
    }
    else
    {
        this->on_webSocket_connected();
    }
}

void NetplaySessionBrowserDialog::joinSession(void)
{
    QJsonObject json;
    QJsonObject session;
    session.insert("port", sessionData.Port);
    session.insert("password", this->sessionPassword);
    session.insert("MD5", this->sessionData.MD5);

    json.insert("type", "request_join_room");
    json.insert("player_name", this->nickNameLineEdit->text());
    json.insert("room", session);
    NetplayCommon::AddCommonJson(json);

    this->webSocket->sendTextMessage(QJsonDocument(json).toJson());
}

void NetplaySessionBrowserDialog::resetDispatcherState(void)
{
    if (this->dispatcherTimerId != -1)
    {
        this->killTimer(this->dispatcherTimerId);
        this->dispatcherTimerId = -1;
    }

    if (this->dispatcherTimeoutTimerId != -1)
    {
        this->killTimer(this->dispatcherTimeoutTimerId);
        this->dispatcherTimeoutTimerId = -1;
    }

    if (this->dispatcherNetworkReply != nullptr)
    {
        this->dispatcherNetworkReply->abort();
        this->dispatcherNetworkReply = nullptr;
    }

    this->dispatcherMoveThroughList  = false;
    this->dispatcherJoinSession      = false;
    this->dispatcherAddressListIndex = false;
}

void NetplaySessionBrowserDialog::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == this->pingTimerId)
    {
        if (this->webSocket->isValid())
        {
            this->webSocket->ping();
        }
    }
    else if (event->timerId() == this->dispatcherTimerId)
    {
        if (this->dispatcherMoveThroughList)
        {
            this->dispatcherAddressListIndex++;

            if (this->dispatcherAddressListIndex < this->dispatcherAddressList.size())
            {
                QString address = this->dispatcherAddressList.at(this->dispatcherAddressListIndex);
                this->webSocket->open(QUrl(address));

                this->dispatcherMoveThroughList = false;

                // start socket timeout
                if (this->dispatcherTimeoutTimerId != -1)
                {
                    this->killTimer(this->dispatcherTimeoutTimerId);
                    this->dispatcherTimeoutTimerId = -1;
                }

                this->dispatcherTimeoutTimerId = this->startTimer(15000);
            }
            else
            {
                this->sessionBrowserWidget->RefreshDone();
                this->resetDispatcherState();
            }
        }
    }
    else if (event->timerId() == this->dispatcherTimeoutTimerId)
    {
        this->webSocket->close();
        this->dispatcherMoveThroughList = true;

        this->killTimer(this->dispatcherTimeoutTimerId);
        this->dispatcherTimeoutTimerId = -1;

    }
}

void NetplaySessionBrowserDialog::on_webSocket_connected(void)
{
    if (!this->webSocket->isValid())
    {
        QtMessageBox::Error(this, "Server Error", "Connection Failed");
        return;
    }

    bool dispatcher = NetplayCommon::IsServerDispatcher(this->serverComboBox);

    if (dispatcher && this->dispatcherJoinSession)
    {
        this->joinSession();
        this->dispatcherJoinSession = false;
        return;
    }

    // clear sessions when we're not using the dispatcher server
    if (!dispatcher)
    {
        this->sessionBrowserWidget->StartRefresh();
    }

    // request session list from server
    QJsonObject json;
    json.insert("type", "request_get_rooms");
    NetplayCommon::AddCommonJson(json);

    this->webSocket->sendTextMessage(QJsonDocument(json).toJson());
}

void NetplaySessionBrowserDialog::on_webSocket_textMessageReceived(QString message)
{
    QJsonDocument jsonDocument = QJsonDocument::fromJson(message.toUtf8());
    QJsonObject json = jsonDocument.object();

    QString type = json.value("type").toString();

    bool dispatcher = NetplayCommon::IsServerDispatcher(this->serverComboBox);

    if (type == "reply_get_rooms")
    {
        if (json.value("accept").toInt() == 0)
        {
            QJsonArray  sessions = json.value("rooms").toArray();
            QJsonObject session;
            for (int i = 0; i < sessions.size(); i++)
            {
                session = sessions.at(i).toObject();

                this->sessionBrowserWidget->AddSessionData(session.value("room_name").toString(), 
                                                            session.value("game_name").toString(), 
                                                            session.value("MD5").toString(), 
                                                            session.value("protected").toBool(),
                                                            session.value("port").toInt(),
                                                            session.value("features").toObject().value("cpu_emulator").toString(),
                                                            session.value("features").toObject().value("rsp_plugin").toString(),
                                                            session.value("features").toObject().value("gfx_plugin").toString(),
                                                            this->webSocket->requestUrl());
            }

            // we're done refreshing the sessions
            if (!dispatcher)
            {
                this->sessionBrowserWidget->RefreshDone();
            }
        }
        else
        {
            this->sessionBrowserWidget->Reset();
            QtMessageBox::Error(this, "Server Error", json.value("message").toString());
        }
    }
    else if (type == "reply_join_room")
    {
        if (json.value("accept").toInt() == 0)
        {
            this->sessionJson = json;
            QDialog::accept();
        }
        else
        {
            QtMessageBox::Error(this, "Server Error", json.value("message").toString());
            this->toggleUI(true, this->validate());
        }
    }

    if (dispatcher)
    {
        this->dispatcherMoveThroughList = true;
    }
}

void NetplaySessionBrowserDialog::on_webSocket_pong(quint64 elapsedTime, const QByteArray&)
{
    if (!NetplayCommon::IsServerDispatcher(this->serverComboBox))
    {
        this->pingLineEdit->setText(QString::number(elapsedTime) + " ms");
    }
}

void NetplaySessionBrowserDialog::on_webSocket_disconnected()
{
    if (!NetplayCommon::IsServerDispatcher(this->serverComboBox))
    {
        this->sessionBrowserWidget->Reset();
    }
}

void NetplaySessionBrowserDialog::on_broadcastSocket_readyRead(void)
{
    while (this->broadcastSocket.hasPendingDatagrams())
    {
        QNetworkDatagram datagram = this->broadcastSocket.receiveDatagram();
        QByteArray incomingData = datagram.data();

        NetplayCommon::AddServers(this->serverComboBox, QJsonDocument::fromJson(incomingData));
    }

    NetplayCommon::RestoreSelectedServer(this->serverComboBox);
}

void NetplaySessionBrowserDialog::on_jsonServerListDownload_Finished(QNetworkReply* reply)
{
    if (reply->error())
    {
        this->sessionBrowserWidget->Reset();
        QtMessageBox::Error(this, "Server Error", "Failed to retrieve server list json: " + reply->errorString());
        reply->deleteLater();
        return;
    }

    NetplayCommon::AddServers(this->serverComboBox, 
                              QJsonDocument::fromJson(reply->readAll()));

    reply->deleteLater();
}

void NetplaySessionBrowserDialog::on_dispatcherRegionListDownload_Finished(QNetworkReply* reply)
{
    if (reply->error())
    {
        this->sessionBrowserWidget->Reset();
        QtMessageBox::Error(this, "Server Error", "Failed to retrieve region list: " + reply->errorString());
        reply->deleteLater();
        return;
    }

    NetplayCommon::AddServers(this->serverComboBox, 
                              QJsonDocument::fromJson(reply->readAll()), true);

    reply->deleteLater();
}

void NetplaySessionBrowserDialog::on_dispatcherRetrieveServers_Finished(QNetworkReply* reply)
{
    // don't show an error when the request has been aborted
    if (reply->error() == QNetworkReply::OperationCanceledError)
    {
        reply->deleteLater();
        return;
    }

    if (reply->error())
    {
        this->sessionBrowserWidget->Reset();
        QtMessageBox::Error(this, "Server Error", "Failed to retrieve server list: " + reply->errorString());
        reply->deleteLater();
        return;
    }

    QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll());
    QJsonObject jsonObject = jsonDocument.object();
    QStringList jsonKeys = jsonObject.keys();

    this->sessionBrowserWidget->StartRefresh();
    this->resetDispatcherState();

    // we only care about the addresses
    this->dispatcherAddressList.clear();
    for (int i = 0; i < jsonKeys.size(); i++)
    {
        QString address = jsonObject[jsonKeys.at(i)].toString();
        this->dispatcherAddressList.append(address);
    }

    this->dispatcherAddressListIndex = -1;
    this->dispatcherMoveThroughList = true;
    this->dispatcherTimerId = this->startTimer(250);

    reply->deleteLater();
}

void NetplaySessionBrowserDialog::on_serverComboBox_currentIndexChanged(int index)
{
    if (index == -1)
    {
        return;
    }

    // disable join and refresh button while refreshing
    this->toggleUI(false, false, false);

    bool dispatcher = NetplayCommon::IsServerDispatcher(this->serverComboBox, index);

    if (this->pingTimerId != -1)
    {
        this->killTimer(this->pingTimerId);
        this->pingTimerId = -1;
    }

    this->resetDispatcherState();
    this->pingLineEdit->setText(dispatcher ? "N/A" : "Calculating...");
    this->sessionBrowserWidget->StartRefresh();

    if (dispatcher)
    {
        QString region = NetplayCommon::GetServerData(this->serverComboBox, index);

        QUrl url(this->dispatcherUrl + "/getServers");

        QUrlQuery urlQuery;
        urlQuery.addQueryItem("region", region);
        url.setQuery(urlQuery);

        QNetworkRequest networkRequest = NetplayCommon::GetNetworkRequest(url);

        // sadly we have to force HTTP/1 here due to a Qt bug,
        // we abort the connection which sadly in Qt6 causes an error and eventually segfault,
        // so to workaround this, force HTTP/1 until it's fixed in Qt6
        //
        // thank you MapLibre devs for figuring out this issue:
        // https://github.com/maplibre/maplibre-native/issues/3644
        networkRequest.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);

        this->dispatcherNetworkReply = this->dispatcherNetworkAccessManager->get(networkRequest);
    }
    else
    {
        this->pingTimerId = this->startTimer(2000);

        QString address = NetplayCommon::GetServerData(this->serverComboBox, index);
        this->webSocket->open(QUrl(address));
    }
}

void NetplaySessionBrowserDialog::on_sessionBrowserWidget_OnSessionChanged(bool valid)
{
    this->validateJoinButton();
}

void NetplaySessionBrowserDialog::on_sessionBrowserWidget_OnRefreshDone(void)
{
    QPushButton* refreshButton = this->buttonBox->button(QDialogButtonBox::RestoreDefaults);
    refreshButton->setEnabled(true);
    this->validateJoinButton();
}

void NetplaySessionBrowserDialog::on_nickNameLineEdit_textChanged()
{
    this->validateJoinButton();
}

void NetplaySessionBrowserDialog::on_buttonBox_clicked(QAbstractButton* button)
{
    // refresh session list when refresh button has been pressed
    if (button == this->buttonBox->button(QDialogButtonBox::RestoreDefaults))
    {
        this->refreshSessions();
    }
}

void NetplaySessionBrowserDialog::accept()
{
    if (!NetplayCommon::IsServerDispatcher(this->serverComboBox) &&
        !this->webSocket->isValid())
    {
        QtMessageBox::Error(this, "Server Error", "Connection Failed");
        return;
    }

    // retrieve session data
    NetplaySessionData sessionData;
    if (!this->sessionBrowserWidget->GetCurrentSession(sessionData))
    {
        return;
    }

    // disable join button while we're processing the request
    this->toggleUI(false, false);

    // request password when needed
    if (sessionData.PasswordProtected)
    {
        NetplaySessionPasswordDialog dialog(this);
        dialog.exec();
        this->sessionPassword = dialog.GetPassword();

        // do nothing if password is empty
        if (this->sessionPassword.isEmpty())
        {
            this->toggleUI(true, true);
            return;
        }
    }

    // ensure features match to what we currently have
    QList<QString> pluginNames = NetplayCommon::GetPluginNames(sessionData.MD5);
    QString details;
    if (sessionData.RspPlugin != pluginNames[0])
    {
        details  = "Expected RSP Plugin: " + sessionData.RspPlugin + "\n";
        details += "Current  RSP Plugin: " + pluginNames[0];
        QtMessageBox::Error(this, "RSP Plugin Mismatch", details);
        this->toggleUI(true, true);
        return;
    }
    if (sessionData.GfxPlugin != pluginNames[1])
    {
        details  = "Expected GFX Plugin: " + sessionData.GfxPlugin + "\n";
        details += "Current  GFX Plugin: " + pluginNames[1];
        QtMessageBox::Error(this, "GFX Plugin Mismatch", details);
        this->toggleUI(true, true);
        return;
    }


    // attempt to find ROM from the ROM browser data
    std::string md5 = sessionData.MD5.toStdString();
    for (auto it = this->romData.begin(); it != this->romData.end(); it++)
    {
        if (it.value().MD5 == md5)
        {
            this->sessionFile = it.key();
            break;
        }
    }

    // show ROM dialog when we haven't found the ROM
    if (this->sessionFile.isEmpty())
    {
        this->sessionFile = this->showROMDialog(sessionData.GameName, sessionData.MD5);
        if (this->sessionFile.isEmpty())
        {
            this->toggleUI(true, true);
            return;
        }
    }

    this->sessionData = sessionData;

    if (NetplayCommon::IsServerDispatcher(this->serverComboBox))
    {
        this->dispatcherJoinSession = true;
        this->webSocket->open(sessionData.Address);
    }
    else
    {
        this->joinSession();
    }
}
