/*
 *   SPDX-FileCopyrightText: 2022 Bart Ribbers <bribbers@disroot.org>
 *   SPDX-FileCopyrightText: 2022 Aditya Mehra <aix.m@outlook.com>
 *   SPDX-FileCopyrightText: 2026 Devin Lin <devin@kde.org>
 *
 *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */

#include "ceccontroller.h"
#include "../controllermanager.h"
#include "../device.h"
#include "cecworker.h"

#include <QDebug>
#include <QFile>

#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <QDBusMetaType>

#include <Solid/DeviceNotifier>

#include <libcec/cectypes.h>
#include <linux/input-event-codes.h>

using namespace CEC;

QDBusArgument &operator<<(QDBusArgument &arg, const cec_logical_address &address)
{
    arg.beginStructure();
    arg << static_cast<uchar>(address);
    arg.endStructure();
    return arg;
}

const QDBusArgument &operator>>(const QDBusArgument &arg, cec_logical_address &address)
{
    uchar value;
    arg.beginStructure();
    arg >> value;
    arg.endStructure();
    address = static_cast<cec_logical_address>(value);
    return arg;
}

CECController::CECController(QObject *parent)
    : QObject(parent)
{
    qDebug() << "CECController: Starting initialization";
    qDBusRegisterMetaType<cec_logical_address>();

    // Load key mappings from config
    KSharedConfigPtr config = KSharedConfig::openConfig();
    KConfigGroup group = config->group("CECRemote");

    auto map = [&group](const char *name, int cecKey, int evKey) {
        return std::make_pair<int, int>(group.readEntry(QString("Button") + name, cecKey), group.readEntry(QString("Key") + name, evKey));
    };

    m_keyMap = {
        map("Play", CEC_USER_CONTROL_CODE_PLAY, KEY_PLAY),
        map("Stop", CEC_USER_CONTROL_CODE_STOP, KEY_STOP),
        map("Pause", CEC_USER_CONTROL_CODE_PAUSE, KEY_PAUSE),
        map("Rewind", CEC_USER_CONTROL_CODE_REWIND, KEY_REWIND),
        map("Fastforward", CEC_USER_CONTROL_CODE_FAST_FORWARD, KEY_FASTFORWARD),
        map("Enter", CEC_USER_CONTROL_CODE_SELECT, KEY_ENTER),
        map("Up", CEC_USER_CONTROL_CODE_UP, KEY_UP),
        map("Down", CEC_USER_CONTROL_CODE_DOWN, KEY_DOWN),
        map("Left", CEC_USER_CONTROL_CODE_LEFT, KEY_LEFT),
        map("Right", CEC_USER_CONTROL_CODE_RIGHT, KEY_RIGHT),
        map("Number0", CEC_USER_CONTROL_CODE_NUMBER0, KEY_0),
        map("Number1", CEC_USER_CONTROL_CODE_NUMBER1, KEY_1),
        map("Number2", CEC_USER_CONTROL_CODE_NUMBER2, KEY_2),
        map("Number3", CEC_USER_CONTROL_CODE_NUMBER3, KEY_3),
        map("Number4", CEC_USER_CONTROL_CODE_NUMBER4, KEY_4),
        map("Number5", CEC_USER_CONTROL_CODE_NUMBER5, KEY_5),
        map("Number6", CEC_USER_CONTROL_CODE_NUMBER6, KEY_6),
        map("Number7", CEC_USER_CONTROL_CODE_NUMBER7, KEY_7),
        map("Number8", CEC_USER_CONTROL_CODE_NUMBER8, KEY_8),
        map("Number9", CEC_USER_CONTROL_CODE_NUMBER9, KEY_9),
        map("Blue", CEC_USER_CONTROL_CODE_F1_BLUE, KEY_BLUE),
        map("Red", CEC_USER_CONTROL_CODE_F2_RED, KEY_RED),
        map("Green", CEC_USER_CONTROL_CODE_F3_GREEN, KEY_GREEN),
        map("Yellow", CEC_USER_CONTROL_CODE_F4_YELLOW, KEY_YELLOW),
        map("ChannelUp", CEC_USER_CONTROL_CODE_CHANNEL_UP, KEY_CHANNELUP),
        map("ChannelDown", CEC_USER_CONTROL_CODE_CHANNEL_DOWN, KEY_CHANNELDOWN),
        map("Exit", CEC_USER_CONTROL_CODE_EXIT, KEY_ESC),
        map("Back", CEC_USER_CONTROL_CODE_AN_RETURN, KEY_ESC),
        map("Home", CEC_USER_CONTROL_CODE_ROOT_MENU, KEY_HOMEPAGE),
        map("Subtitle", CEC_USER_CONTROL_CODE_SUB_PICTURE, KEY_SUBTITLE),
        map("Info", CEC_USER_CONTROL_CODE_DISPLAY_INFORMATION, KEY_INFO),
    };

    m_homeActionKeys = {
        group.readEntry("ButtonSetupMenu", int(CEC_USER_CONTROL_CODE_SETUP_MENU)),
        group.readEntry("ButtonContentsMenu", int(CEC_USER_CONTROL_CODE_CONTENTS_MENU)),
        group.readEntry("ButtonFavoriteMenu", int(CEC_USER_CONTROL_CODE_FAVORITE_MENU)),
        group.readEntry("ButtonTopMenu", int(CEC_USER_CONTROL_CODE_TOP_MENU)),
        group.readEntry("ButtonDvdMenu", int(CEC_USER_CONTROL_CODE_DVD_MENU)),
    };

    // Hotplug debounce timer
    m_hotplugTimer.setSingleShot(true);
    m_hotplugTimer.setInterval(500);
    connect(&m_hotplugTimer, &QTimer::timeout, this, &CECController::onHotplugTimeout);

    // Next key timeout
    m_nextKeyTimer.setSingleShot(true);
    m_nextKeyTimer.setInterval(30000);
    connect(&m_nextKeyTimer, &QTimer::timeout, this, &CECController::onNextKeyTimeout);

    // Create worker in dedicated thread
    m_workerThread = new QThread(this);
    m_workerThread->setObjectName(QStringLiteral("CEC Worker"));
    m_worker = new CECWorker();
    m_worker->moveToThread(m_workerThread);

    connect(m_worker, &CECWorker::initialized, this, &CECController::onWorkerInitialized);
    connect(m_worker, &CECWorker::deviceDiscovered, this, &CECController::onDeviceDiscovered);
    connect(m_worker, &CECWorker::deviceOpened, this, &CECController::onDeviceOpened);
    connect(m_worker, &CECWorker::deviceOpenFailed, this, &CECController::onDeviceOpenFailed);
    connect(m_worker, &CECWorker::cecKeyPressed, this, &CECController::onCecKeyPressed);
    connect(m_worker, &CECWorker::cecStandbyReceived, this, &CECController::enterStandby);
    connect(m_worker, &CECWorker::cecSourceActivated, this, &CECController::sourceActivated);
    connect(m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater);

    m_workerThread->start();

    // Initialize asynchronously
    QString osdName = group.readEntry("OSDName", i18n("KDE Plasma"));
    qDebug() << "CECController: Using OSD name from config:" << osdName;
    QMetaObject::invokeMethod(m_worker, "initialize", Qt::QueuedConnection, Q_ARG(QString, osdName));

    // Listen for device hotplug
    connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, [this] {
        m_hotplugTimer.start();
    });
    connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &CECController::onDeviceRemoved);
}

CECController::~CECController()
{
    if (m_workerThread) {
        QMetaObject::invokeMethod(m_worker, "cleanup", Qt::BlockingQueuedConnection);
        m_workerThread->quit();
        m_workerThread->wait(5000);
    }
}

void CECController::onWorkerInitialized(bool success)
{
    qDebug() << "CECController: Worker initialization" << (success ? "succeeded" : "failed");
    m_initialized = success;
    if (success) {
        QMetaObject::invokeMethod(m_worker, "discoverDevices", Qt::QueuedConnection);
    }
}

void CECController::onDeviceDiscovered(const QString &comName)
{
    qDebug() << "CECController: Device discovered at" << comName;

    if (m_connectedDevices.contains(comName)) {
        qDebug() << "CECController: Device" << comName << "already in connected devices set, skipping";
        return;
    }

    if (ControllerManager::instance().isConnected(comName)) {
        qDebug() << "CECController: Device" << comName << "already connected via ControllerManager, skipping";
        m_connectedDevices.insert(comName);
        return;
    }
}

void CECController::onDeviceOpened(const QString &comName)
{
    qDebug() << "CECController: Adapter opened at" << comName;

    if (m_connectedDevices.contains(comName)) {
        qDebug() << "CECController: Adapter at" << comName << "already registered, skipping";
        return;
    }

    if (!m_device) {
        auto *device = new Device(DeviceCEC, QStringLiteral("CEC Controller"), QStringLiteral("cec"));

        QList<int> keyValues = m_keyMap.values();
        device->setUsedKeys(QSet<int>(keyValues.cbegin(), keyValues.cend()));

        ControllerManager::instance().newDevice(device);
        m_device = device;
        Q_EMIT controllerAdded(QStringLiteral("CEC Controller"));
    }

    m_connectedDevices.insert(comName);
    m_adapterCount++;
    qDebug() << "CECController: Successfully registered device" << comName << "- total adapters:" << m_adapterCount;
}

void CECController::onDeviceOpenFailed(const QString &comName, const QString &error)
{
    qWarning() << "CECController: Failed to open adapter at" << comName << "-" << error;
}

void CECController::onDeviceRemoved(const QString &udi)
{
    Q_UNUSED(udi);
    // The pointer we get doesn't have the path. Instead, we iterate over
    // the controllers we hold, and see if one vanished from the filesystem.
    QMutableSetIterator<QString> it(m_connectedDevices);
    bool removed = false;
    while (it.hasNext()) {
        const QString &comName = it.next();
        if (!QFile::exists(comName)) {
            qDebug() << "CECController: Adapter at" << comName << "removed";
            it.remove();
            m_adapterCount--;
            removed = true;
        }
    }
    if (removed) {
        // Tidy up on libcec's side
        QMetaObject::invokeMethod(m_worker, "closeAdapter", Qt::QueuedConnection);
    }
}

void CECController::onHotplugTimeout()
{
    if (!m_initialized) {
        return;
    }
    if (!m_connectedDevices.isEmpty()) {
        // We already have an adapter open. Repeated rediscovery while an
        // adapter is held causes libcec to re-Open() the same /dev/ttyACM*,
        // which briefly drops athena's CEC logical address. Some TVs
        // (Samsung Anynet+) stop forwarding remote keypresses to a device
        // that flaps like this and don't reroute until the device clearly
        // stabilises.
        qDebug() << "CECController: Hotplug timeout - skipping (adapter already open)";
        return;
    }
    qDebug() << "CECController: Hotplug timeout - triggering device discovery";
    QMetaObject::invokeMethod(m_worker, "discoverDevices", Qt::QueuedConnection);
}

bool CECController::sendStandby(int logicalAddress)
{
    if (!m_initialized || !m_worker) {
        qWarning() << "CECController: sendStandby called before worker initialised";
        return false;
    }
    bool result = false;
    QMetaObject::invokeMethod(m_worker,
                              "sendStandby",
                              Qt::BlockingQueuedConnection,
                              Q_RETURN_ARG(bool, result),
                              Q_ARG(int, logicalAddress));
    return result;
}

bool CECController::sendImageViewOn(int logicalAddress)
{
    if (!m_initialized || !m_worker) {
        qWarning() << "CECController: sendImageViewOn called before worker initialised";
        return false;
    }
    bool result = false;
    QMetaObject::invokeMethod(m_worker,
                              "sendImageViewOn",
                              Qt::BlockingQueuedConnection,
                              Q_RETURN_ARG(bool, result),
                              Q_ARG(int, logicalAddress));
    return result;
}

bool CECController::sendActiveSource()
{
    if (!m_initialized || !m_worker) {
        qWarning() << "CECController: sendActiveSource called before worker initialised";
        return false;
    }
    bool result = false;
    QMetaObject::invokeMethod(m_worker,
                              "sendActiveSource",
                              Qt::BlockingQueuedConnection,
                              Q_RETURN_ARG(bool, result));
    return result;
}

int CECController::queryDevicePowerStatus(int logicalAddress)
{
    if (!m_initialized || !m_worker) {
        qWarning() << "CECController: queryDevicePowerStatus called before worker initialised";
        return CEC_POWER_STATUS_UNKNOWN;
    }
    int result = CEC_POWER_STATUS_UNKNOWN;
    QMetaObject::invokeMethod(m_worker,
                              "queryDevicePowerStatus",
                              Qt::BlockingQueuedConnection,
                              Q_RETURN_ARG(int, result),
                              Q_ARG(int, logicalAddress));
    return result;
}

void CECController::requestNextKey()
{
    m_catchNextInput = true;
    m_nextKeyTimer.start();
}

void CECController::cancelNextKeyRequest()
{
    if (m_catchNextInput) {
        m_catchNextInput = false;
        m_nextKeyTimer.stop();
    }
}

void CECController::onNextKeyTimeout()
{
    if (m_catchNextInput) {
        m_catchNextInput = false;
        Q_EMIT nextKeyReceived(-1);
    }
}

void CECController::onCecKeyPressed(int keycode, int duration)
{
    qDebug() << "CECController: CEC key received - keycode:" << keycode << "duration:" << duration;

    if (m_catchNextInput) {
        qDebug() << "CECController: Catching next input mode active, returning keycode to requester";
        m_catchNextInput = false;
        m_nextKeyTimer.stop();
        Q_EMIT nextKeyReceived(keycode);
        return;
    }

    if (duration > 0 && m_lastHandledKeycode == keycode) {
        m_lastHandledKeycode = -1;
        return;
    }
    m_lastHandledKeycode = keycode;

    if (m_homeActionKeys.contains(keycode)) {
        qDebug() << "CECController: Menu key detected, emitting home action";
        ControllerManager::instance().emitHomeAction(m_device);
        return;
    }

    int nativeKey = m_keyMap.value(keycode, -1);
    if (nativeKey < 0) {
        qDebug() << "CECController: No mapping found for CEC keycode" << keycode;
        return;
    }

    qDebug() << "CECController: Mapped CEC keycode" << keycode << "to native key" << nativeKey;

    if (nativeKey == KEY_HOMEPAGE) {
        qDebug() << "CECController: Home key detected, emitting home action";
        ControllerManager::instance().emitHomeAction(m_device);
        return;
    }

    ControllerManager::instance().emitKey(m_device, nativeKey, 1);
    ControllerManager::instance().emitKey(m_device, nativeKey, 0);
}
