/**
 * Copyright (C) 2013 Canonical, Ltd.
 * Copyright (C) 2025-2026 Andres Salomon <dilinger@queued.net>
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
 * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "ofonophonesimhelper.h"
#include "cellbroadcast-types.h"

#include <QDBusInterface>
#include <QDebug>
#include <QFileInfo>
#include <QTemporaryFile>

OfonoPhonesimHelper::OfonoPhonesimHelper(QObject *parent):
       QObject(parent),
       mModemName("/phonesim"),
       mSetting(OfonoModem::ManualSelect),
       mSimManager(0)
{
    mModem = new OfonoModem(mSetting, mModemName);
    QObject::connect(mModem, &OfonoModem::interfacesChanged, this,
                    &OfonoPhonesimHelper::onInterfacesChanged);

    // Power up the /phonesim modem; but don't put it online yet.
    mModem->setPowered(true);
}

void OfonoPhonesimHelper::onInterfacesChanged(const QStringList &interfaces)
{
    if (mSimManager || interfaces.size() < 10)
        return;

    if (interfaces.contains("org.ofono.SimManager")) {
        QObject::disconnect(mModem, &OfonoModem::interfacesChanged, this,
                &OfonoPhonesimHelper::onInterfacesChanged);

        // Now that /phonesim is populated with interfaces (and most
        // importantly, org.ofono.SimManager), we can create objects
        mMessageManager = new OfonoMessageManager(mSetting, mModemName);
        mVoiceCallManager = new OfonoVoiceCallManager(mSetting, mModemName);
        mCallVolume = new OfonoCallVolume(mSetting, mModemName);
        mNetworkRegistration = new OfonoNetworkRegistration(mSetting, mModemName);
        mCellBroadcast = new OfonoCellBroadcast(mSetting, mModemName);
        mSimManager = new OfonoSimManager(mSetting, mModemName);

        QObject::connect(mMessageManager, &OfonoMessageManager::messageAdded,
                this, &OfonoPhonesimHelper::MessageAdded);
        QObject::connect(mMessageManager,
                &OfonoMessageManager::useDeliveryReportsChanged, this,
                &OfonoPhonesimHelper::DeliveryReportsChanged);
        QObject::connect(mVoiceCallManager, &OfonoVoiceCallManager::callAdded,
                this, &OfonoPhonesimHelper::onCallAdded);
        QObject::connect(mVoiceCallManager, &OfonoVoiceCallManager::callRemoved,
                this, &OfonoPhonesimHelper::onCallRemoved);
        QObject::connect(mVoiceCallManager,
                &OfonoVoiceCallManager::swapCallsComplete, this,
                [&](const bool status) {
                    Q_EMIT SwapCallsComplete(status, mVoiceCallManager->errorName(),
                            mVoiceCallManager->errorMessage());
        });
        connect(mCallVolume, &OfonoCallVolume::mutedChanged,
                this, &OfonoPhonesimHelper::MutedChanged);
        connect(mCellBroadcast, &OfonoCellBroadcast::powerChanged,
                this, &OfonoPhonesimHelper::onCBPowerChanged);
    }
}

OfonoPhonesimHelper *OfonoPhonesimHelper::instance()
{
    static OfonoPhonesimHelper *self = new OfonoPhonesimHelper();
    return self;
}

bool OfonoPhonesimHelper::ExecJS(const QString &dir, const QString &script)
{
    QDBusMessage request;
    QVariantList arg;

    if (dir != mJsDir) {
        request = QDBusMessage::createMethodCall("org.ofono.phonesim", "/",
                "org.ofono.phonesim.Script", "SetPath");
        arg << QVariant(dir);
        request.setArguments(arg);
        QDBusConnection::sessionBus().call(request);
        arg.clear();

        mJsDir = dir;
    }

    request = QDBusMessage::createMethodCall("org.ofono.phonesim", "/",
            "org.ofono.phonesim.Script", "Run");
    arg << QVariant(script);
    request.setArguments(arg);
    QDBusConnection::sessionBus().call(request);

    const QDBusError &e = QDBusConnection::sessionBus().lastError();
    if (e.isValid()) {
        qCritical() << "running javascript failed: " << e.message() << "\n";
        return false;
    }

    return true;
}

bool OfonoPhonesimHelper::RunJavascript(const QString &js)
{
    QTemporaryFile file;
    bool success = false;

    qDebug() << "executing javascript on phonesim: " << js;

    if (file.open()) {
        QFileInfo info(file.fileName());
        if (file.write(js.toUtf8()) > 0) {
            file.flush();
            success = ExecJS(info.absolutePath(), info.fileName());
        }
    }

    return success;
}

int OfonoPhonesimHelper::findCall(const QString &call)
{
    for (int i = 0; i < mVoiceCalls.size(); i++) {
        if (mVoiceCalls[i]->path() == call)
            return i;
    }
    return -1;
}

void OfonoPhonesimHelper::onCallAdded(const QString &call, const QVariantMap &values)
{
    OfonoVoiceCall *c = new OfonoVoiceCall(call);

    mVoiceCalls.append(c);
    QObject::connect(c, &OfonoVoiceCall::stateChanged, this,
            &OfonoPhonesimHelper::VoiceCallChanged);

    Q_EMIT CallAdded(call, values);
}

void OfonoPhonesimHelper::onCallRemoved(const QString &call)
{
    int callNr = findCall(call);
    if (callNr < 0)
        return;

    mVoiceCalls[callNr]->deleteLater();
    mVoiceCalls.removeAt(callNr);
}

void OfonoPhonesimHelper::onCBPowerChanged(bool powered)
{
    if (powered)
        Q_EMIT CellBroadcastPoweredUp();
}

void OfonoPhonesimHelper::CellBroadcastPower(bool powered)
{
    mCellBroadcast->setPowered(powered);
}

bool OfonoPhonesimHelper::CellBroadcastCMASNotification(const QString &text, quint16 channel)
{
    QString s = QStringLiteral(R"(
            tabCBM.teContent.setPlainText("%1");
            tabCBM.groupBox.leChannel.text = "%2";
            tabCBM.pbSendCellBroadcast.click();
        )").arg(text)
        .arg(channel, 4, 16, QChar('0'));
    return RunJavascript(s);
}

bool OfonoPhonesimHelper::CellBroadcastETWSNotification(const QString &text, const QString &type, bool alert, bool popup)
{
    quint16 channel = 0;
    quint16 msgCode = (popup ? 1 << 8 : 0) | (alert ? 1 << 9 : 0);

    if (type == "Earthquake") {
        channel = CellBroadcast::ETWS_ALERT_EARTHQUAKE;
    } else if (type == "Tsunami") {
        channel = CellBroadcast::ETWS_ALERT_TSUNAMI;
    } else if (type == "Earthquake+Tsunami") {
        channel = CellBroadcast::ETWS_ALERT_EARTHQUAKE_AND_TSUNAMI;
    } else if (type == "Other") {
        channel = CellBroadcast::ETWS_ALERT_OTHER;
    } else if (type == "Fake") {
        channel = CellBroadcast::ETWS_ALERT_TEST; // discarded by ofono
    }

    QString s = QStringLiteral(R"(
            tabCBM.teContent.setPlainText("%1");
            tabCBM.groupBox.leChannel.text = "%2";
            tabCBM.groupBox.leMessageCode.text = "%3";
            tabCBM.pbSendCellBroadcast.click();
        )").arg(text)
        .arg(channel, 4, 16, QChar('0'))
        .arg(msgCode, 3, 16, QChar('0'));
    return RunJavascript(s);
}

bool OfonoPhonesimHelper::NetworkRegistrationSetStatus(const QString &status)
{
    QMap<QString, int> statuses{
        {"unregistered", 0},
        {"registered", 1},
        {"searching", 2},
        {"denied", 3},
        {"unknown", 4},
        {"roaming", 5}};
    QString s = QStringLiteral(R"(
            tabRegistration.gbNetworkRegistration.cbRegistrationStatus.currentIndex = %1;
            tabRegistration.gbNetworkRegistration.pbRegistration.click();
        )").arg(QString::number(statuses.value(status)));
    return RunJavascript(s);
}

bool OfonoPhonesimHelper::MessageManagerSendMessage(const QString &from, const QString &text)
{
    QString s = QStringLiteral(R"(
            tabSMS.gbMessage1.leMessageSender.text = "%1";
            tabSMS.gbMessage1.leSMSClass.text = "1";
            tabSMS.gbMessage1.teSMSText.setPlainText("%2");
            tabSMS.gbMessage1.pbSendSMSMessage.click();
        )").arg(from)
        .arg(text);
    return RunJavascript(s);
}

void OfonoPhonesimHelper::MessageManagerUseDeliveryReports(bool val)
{
    mMessageManager->setUseDeliveryReports(val);
}

bool OfonoPhonesimHelper::VoiceCallManagerIncomingCall(const QString &from)
{
    QString s = QStringLiteral(R"(
            tabCall.gbIncomingCall.leCaller.text = "%1";
            tabCall.gbIncomingCall.pbIncomingCall.click();
        )").arg(from);
    return RunJavascript(s);
}

#if 0
void OfonoMockController::VoiceCallManagerFailNextDtmf()
{
    // TODO?
}
#endif

void OfonoPhonesimHelper::VoiceCallManagerSwapCalls()
{
    mVoiceCallManager->swapCalls();
}

bool OfonoPhonesimHelper::VoiceCallSetAlerting(const QString &objPath)
{
    int callNr = findCall(objPath);
    if (callNr < 0) {
        qCritical() << "Error: " << objPath << " not found in call list\n";
        return false;
    }

    if (mVoiceCalls[callNr]->state() == "dialing") {
        QString s = QStringLiteral(R"(
                tabCall.twCallMgt.selectRow(%1);
                tabCall.pbAlerting.click();
            )").arg(QString::number(callNr));
        return RunJavascript(s);
    }
    return false;
}

bool OfonoPhonesimHelper::VoiceCallAnswer(const QString &objPath)
{
    int callNr = findCall(objPath);
    if (callNr < 0) {
        qCritical() << "Error: " << objPath << " not found in call list\n";
        return false;
    }

    const QString &state = mVoiceCalls[callNr]->state();

    if (state == "incoming" || state == "alerting") {
        QString s = QStringLiteral(R"(
                tabCall.twCallMgt.selectRow(%1);
                tabCall.pbActive.click();
            )").arg(QString::number(callNr));
        return RunJavascript(s);
    }
    return false;
}

bool OfonoPhonesimHelper::VoiceCallHangup(const QString &objPath)
{
    int callNr = findCall(objPath);
    if (callNr < 0) {
         qCritical() << "Error: " << objPath << " not found in call list\n";
        return false;
    }

    QString s = QStringLiteral(R"(
            tabCall.twCallMgt.selectRow(%1);
            tabCall.pbHangup.click();
        )").arg(QString::number(callNr));
    return RunJavascript(s);
}

void OfonoPhonesimHelper::ModemSetOnline(bool online)
{
    mModem->setOnline(online);
}

bool OfonoPhonesimHelper::ModemIsValid() const
{
    return mModem->isValid();
}

bool OfonoPhonesimHelper::IsPresent() const
{
    return mSimManager && mSimManager->present();
}

const QString OfonoPhonesimHelper::SimManagerGetPinRequired() const
{
    return mSimManager->pinRequired();
}

const QString OfonoPhonesimHelper::NetworkRegistrationStatus() const
{
    return mNetworkRegistration->status();
}

const QString OfonoPhonesimHelper::NetworkRegistrationGetName() const
{
    return mNetworkRegistration->name();
}
