/*
 * Copyright (C) 2023, KylinSoft Co., Ltd.
 *
 * 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, either 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Authors: wangyan <wangyan@kylinos.cn>
 *
 */

#include "devicehelper.h"

#include <QDBusInterface>
#include <KF5/KScreen/kscreen/types.h>
#include <KF5/KScreen/kscreen/getconfigoperation.h>
#include <KF5/KScreen/kscreen/output.h>
#include <KF5/KScreen/kscreen/config.h>

#include <QProcess>
#include <X11/extensions/XInput2.h>
#include <X11/Xatom.h>

static const char* KWIN_DBUS_SERVICE = "org.kde.KWin";
static const char* KWIN_DBUS_INPUT_DEVICE_PATH = "/org/kde/KWin/InputDevice";
static const char* KWIN_DBUS_DEVICE_INTERFACE = "org.kde.KWin.InputDevice";
static const char* KWIN_DBUS_DEVICE_MANAGER_INTERFACE = "org.kde.KWin.InputDeviceManager";

namespace {

QList<Device> getInputDevicesX11()
{
    //LCOV_EXCL_START
    QList<Device> devices;
    Display* dpy = XOpenDisplay(nullptr);
    int deviceNum = 0;
    XIDeviceInfo* devs = XIQueryDevice(dpy, XIAllDevices, &deviceNum);

    for (int i = 0; i < deviceNum; i++) {
        XIDeviceInfo* dev = &devs[i];
        Device device(dev->name, DeviceType::InputDevice, dev->deviceid);
        devices.append(std::move(device));
    }

    XIFreeDeviceInfo(devs);
    XCloseDisplay(dpy);

    return devices;
    //LCOV_EXCL_STOP

}

QString getInputNameX11(int id)
{
    //LCOV_EXCL_START
    QString inputName;
    QList<Device> devices = getInputDevicesX11();

    for (const Device& device : devices) {
        if(device.deviceId != id) {
            continue;
        }

        inputName = device.name;
    }

    if (inputName.isEmpty()) {
        qWarning() << "The input device name is empty: id = " << id;
    }

    return inputName;
    //LCOV_EXCL_STOP
}

QString getInputProductIdX11(int id)
{
    //LCOV_EXCL_START
    Display* dpy = XOpenDisplay(nullptr);
    if (dpy == nullptr) {
        qWarning() << __FILE__ << __FUNCTION__ << "Open display failed";
        return QString();
    }

    Atom atom = XInternAtom(dpy, "Device Product ID", True);
    Atom actType;
    int actFormat;
    unsigned long nitems, bytesAfter;
    unsigned char *data;
    QString productId = QString();

    int rc = XIGetProperty(dpy, id, atom, 0, 1000, False,
                  XA_INTEGER, &actType, &actFormat,
                  &nitems, &bytesAfter, &data);

    if (rc == Success) {
        QString vId = QString::number(((uint32_t*) data)[0]);
        QString pId = QString::number(((uint32_t*) data)[1]);

        productId = vId + " " + pId;

    } else {
        qWarning() << __FILE__ << __FUNCTION__ << "XIGetProperty failed";
    }

    XFree(data);
    XCloseDisplay(dpy);

    return productId;
    //LCOV_EXCL_STOP
}

void mapToOutputX11(int inputDeviceId, const QString& outputName)
{
    //LCOV_EXCL_START
    QProcess process;
    QStringList arguments = {"--map-to-output", QString::number(inputDeviceId), outputName};
    process.setProgram("xinput");
    process.setArguments(arguments);
    process.start();
    process.waitForFinished();

    QByteArray error = process.readAllStandardError();
    QByteArray message = process.readAllStandardOutput();
    if (!error.isEmpty()) {
        qWarning() << "xinput map-to-output"<< QString::number(inputDeviceId) << outputName
                   << "error:" << error << process.errorString();
    }
    else if (!message.isEmpty()) {
        qWarning() << "xinput map-to-output"<< QString::number(inputDeviceId) << outputName
                   << "error:" << message;
    }
    else {
        qDebug() << "xinput map-to-output"<< inputDeviceId << outputName;
    }
    //LCOV_EXCL_STOP
}

#if 0
void mapToOutputWayland(int inputDeviceId, const QString& outputName)
{
    WaylandScreenMap* screenMap = new WaylandScreenMap();
    QObject::connect(screenMap, &WaylandScreenMap::initFinish, [screenMap, inputDeviceId, outputName]() {
        screenMap->mapToOutput(inputDeviceId, outputName);
        screenMap->disconnect();
        screenMap->deleteLater();
    });

    QObject::connect(screenMap, &WaylandScreenMap::initError, [screenMap]() {
        qWarning() << "Connect to wayland server error;";
        screenMap->disconnect();
        screenMap->deleteLater();
    });
}
#endif

void mapToOutputWayland(int inputDeviceId, const QString& outputName)
{
    QDBusInterface interface(KWIN_DBUS_SERVICE,
                             QString(KWIN_DBUS_INPUT_DEVICE_PATH )+ "/event" + QString::number(inputDeviceId),
                             KWIN_DBUS_DEVICE_INTERFACE,
                             QDBusConnection::sessionBus());

    if (!interface.isValid()) {
        qWarning() << "Interface is invalid: service :" << QString(KWIN_DBUS_SERVICE)
                   << "path : " << QString(KWIN_DBUS_INPUT_DEVICE_PATH )+ "/event" + QString::number(inputDeviceId)
                   << "interface: " << QString(KWIN_DBUS_DEVICE_INTERFACE);
        return;
    }
    interface.setProperty("outputName", QVariant(outputName));
}

bool isTouchDevice(XIDeviceInfo *dev)
{
    //LCOV_EXCL_START
    if(dev == nullptr) {
        return false;
    }
    if(dev->use != XISlavePointer){
        return false;
    }
    if(!dev->enabled) {
        return false;
    }

    bool isTouchDevice = false;
    for (int j = 0; j < dev->num_classes; j++) {
        if (dev->classes[j]->type == XITouchClass) {
            isTouchDevice = true;
            break;
        }
    }
    return isTouchDevice;
    //LCOV_EXCL_STOP
}

QList<Device> getX11TouchDevices()
{
    //LCOV_EXCL_START
    QList<Device> devices;
    Display* dpy = XOpenDisplay(nullptr);
    int deviceNum = 0;
    XIDeviceInfo* devs = XIQueryDevice(dpy, XIAllDevices, &deviceNum);

    for (int i = 0; i < deviceNum; i++) {
        XIDeviceInfo* dev = &devs[i];
        if (!isTouchDevice(dev)) {
            continue;
        }
        Device device(dev->name, DeviceType::InputDevice, dev->deviceid);
        devices.append(std::move(device));
        qDebug() << device.name;
    }

    XIFreeDeviceInfo(devs);
    XCloseDisplay(dpy);

    return devices;
    //LCOV_EXCL_STOP
}

KScreen::ConfigPtr getConfigPtr()
{
    KScreen::GetConfigOperation *op = new KScreen::GetConfigOperation;
    if (!op->exec()) {
        qWarning() << op->errorString();
        return KScreen::ConfigPtr();
    }

    return op->config();
}

QString getInputNameWayland(int id)
{
    QDBusInterface interface(KWIN_DBUS_SERVICE,
                             QString(KWIN_DBUS_INPUT_DEVICE_PATH )+ "/event" + QString::number(id),
                             KWIN_DBUS_DEVICE_INTERFACE);

    if (!interface.isValid()) {
        qWarning() << "Interface is invalid: service :" << QString(KWIN_DBUS_SERVICE)
                   << "path : " << QString(KWIN_DBUS_INPUT_DEVICE_PATH )+ "/event" + QString::number(id)
                   << "interface: " << QString(KWIN_DBUS_DEVICE_INTERFACE);
        return QString();
    }
    return interface.property("name").toString();
}

QString getInputProductIdWayland(int id)
{
    QDBusInterface interface(KWIN_DBUS_SERVICE,
                             QString(KWIN_DBUS_INPUT_DEVICE_PATH) + "/event" + QString::number(id),
                             KWIN_DBUS_DEVICE_INTERFACE);

    if (!interface.isValid()) {
        qWarning() << "Interface is invalid: service :" << QString(KWIN_DBUS_SERVICE)
                   << "path : " << QString(KWIN_DBUS_INPUT_DEVICE_PATH )+ "/event" + QString::number(id)
                   << "interface: " << QString(KWIN_DBUS_DEVICE_INTERFACE);
        return QString();
    }

    QVariant PID = interface.property("product");
    QVariant VID = interface.property("vendor");
    QString productId = PID.toString() + " " + VID.toString();

    return productId;
}

bool isWaylandTouchDevice(int id)
{
    QDBusInterface interface(KWIN_DBUS_SERVICE,
                             QString(KWIN_DBUS_INPUT_DEVICE_PATH) + "/event" + QString::number(id),
                             KWIN_DBUS_DEVICE_INTERFACE);

    if (!interface.isValid()) {
        qWarning() << "Interface is invalid: service :" << QString(KWIN_DBUS_SERVICE)
                   << "path : " << QString(KWIN_DBUS_INPUT_DEVICE_PATH )+ "/event" + QString::number(id)
                   << "interface: " << QString(KWIN_DBUS_DEVICE_INTERFACE);
        return false;
    }

    QVariant isTouchDevice = interface.property("touch");
    QVariant isTabletToolDevice = interface.property("tabletTool");
    if (isTouchDevice.toBool() == true
        || isTabletToolDevice.toBool() == true) {
        return true;
    }

    return false;
}

QList<Device> getWaylandTouchDevices()
{
    QList<Device> devices;
    QDBusInterface interface(KWIN_DBUS_SERVICE,
                             KWIN_DBUS_INPUT_DEVICE_PATH,
                             KWIN_DBUS_DEVICE_MANAGER_INTERFACE);

    if (!interface.isValid()) {
        qWarning() << "Interface is invalid: service :" << QString(KWIN_DBUS_SERVICE)
                   << "path : " << QString(KWIN_DBUS_INPUT_DEVICE_PATH )
                   << "interface: " << QString(KWIN_DBUS_DEVICE_MANAGER_INTERFACE);

        return QList<Device>();
    }

    QVariant deviceEvents = interface.property("devicesSysNames");
    QStringList deviceList = deviceEvents.toStringList();

    for (int i = 0; i < deviceList.size(); i++) {
        QString id = deviceList.at(i);
        id.remove("event");
        int deviceId = id.toInt();

        if(!isWaylandTouchDevice(deviceId)) {
            continue;
        }

        QString deviceName = getInputNameWayland(deviceId);
        Device device(deviceName, DeviceType::InputDevice, deviceId);
        devices.append(std::move(device));

    }

    return devices;
}

} //namespace

QList<Device> DeviceHelper::getTouchDevices()
{
    if (env::isWayland()) {
        return getWaylandTouchDevices();
    }
    return getX11TouchDevices();
}

QString DeviceHelper::getInputName(int id)
{
    if (env::isWayland()) {
        return getInputNameWayland(id);
    }
    return getInputNameX11(id);
}

QString DeviceHelper::getInputProductId(int id)
{
    if (env::isWayland()) {
        return getInputProductIdWayland(id);
    }
    return getInputProductIdX11(id);
}

/*该参数在映射中没有用，以后将弃用；为了兼容使用该参数的用户，此处提供了一个接口返回默认值*/
QString DeviceHelper::getInputSerial(int id)
{
    Q_UNUSED(id);
    return "kydefault";
}

QList<Device> DeviceHelper::getOutputs()
{
    QList<Device> devices;

    const KScreen::ConfigPtr config = getConfigPtr();
    for (auto &output: config->outputs()) {
        qDebug() << output.data()->name();
        if (!output.data()->isConnected()) {
            continue;
        }

        QString outputName;
        if (env::isWayland()) {
            outputName = output.data()->outputName();
        } else {
            outputName = output.data()->name();
        }

        Device device(outputName, DeviceType::OutputDevice, output.data()->id());
        devices.append(std::move(device));

    }

    return devices;
}

bool DeviceHelper::hasTouchScreen()
{
    if (env::isWayland()) {
        return !getWaylandTouchDevices().isEmpty();
    }
    return !getX11TouchDevices().isEmpty();
}

void DeviceHelper::mapToOutput(int inputDeviceId, const QString& outputName)
{
    if (env::isWayland()) {
        mapToOutputWayland(inputDeviceId, outputName);
        return;
    }
    mapToOutputX11(inputDeviceId, outputName);
}

bool DeviceHelper::isPrimaryOutput(int id)
{
    const KScreen::ConfigPtr config = getConfigPtr();
    KScreen::OutputPtr output = config->output(id);
    return output->isPrimary();
}


