/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * Copyright (C) 2023 Tianjin KYLIN Information Technology 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */
#include <QDBusPendingCall>
#include "touch-calibrate.h"
extern "C"{
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/extensions/XInput2.h>
#include <X11/extensions/Xrandr.h>
#include <xorg/xserver-properties.h>
#include <gudev/gudev.h>
#include "clib-syslog.h"
}
#include <QDBusInterface>
#include "usd_base_class.h"

//尺寸检测
bool TouchCalibrate::checkMatch(double output_width,  double output_height,
                double input_width, double input_height)
{
    double w_diff, h_diff;
    w_diff = ABS(1 - (output_width / input_width));
    h_diff = ABS(1 - (output_height / input_height));

    SYS_LOG(LOG_DEBUG,"w_diff--------%f,h_diff----------%f", w_diff, h_diff);
    if (w_diff < MAX_SIZE_MATCH_DIFF && h_diff < MAX_SIZE_MATCH_DIFF) {
        return true;
    }
    return false;
}

QVariantList TouchCalibrate::getDeviceProductId(int id)
{
    QVariantList valueList;
    Atom           real_type;
    int            real_format;
    unsigned long  nitems;
    unsigned long  bytes_after;
    unsigned char* data = nullptr;
    unsigned char* dataPtr = nullptr;

    Atom prop = XInternAtom(QX11Info::display(), XI_PROP_PRODUCT_ID, False);

    if (Success == XIGetProperty(QX11Info::display(), id, prop, 0,
                                1000, False, AnyPropertyType,  &real_type,
                                &real_format, &nitems, &bytes_after, &data)) {
        dataPtr = data;
        for (int i = 0; i < nitems; ++i) {
            if (real_type == XA_INTEGER && real_format == 32) {
                valueList << *(reinterpret_cast<int32_t*>(dataPtr));
            }
            dataPtr += real_format / 8;
        }
        XFree(data);
    }
    return valueList;
}

//获取设备节点
QString TouchCalibrate::getDeviceNode(int id)
{
    QString node;
    Atom  prop;
    Atom act_type;
    int  act_format;
    unsigned long nitems, bytes_after;
    unsigned char *data;
    prop = XInternAtom(m_display, XI_PROP_DEVICE_NODE, False);
    if (!prop) {
        return node;
    }

    if (XIGetProperty(m_display, id, prop, 0, 1000, False,
                      AnyPropertyType, &act_type, &act_format, &nitems, &bytes_after, &data) == Success) {
         node = QString::fromLatin1(reinterpret_cast<char*>(data));
         XFree(data);
    }
    return node;
}
//获取触摸设备物理尺寸
void TouchCalibrate::getTouchSize(const char* node, int& width, int& height)
{
    const char *udevSubsystems[] = {"input", NULL};
    GUdevClient *udevClient = g_udev_client_new(udevSubsystems);
    if (!udevClient) {
        SYS_LOG(LOG_DEBUG, " Failed to new udev client.");
        return;
    }
    GUdevDevice *udevDevice = g_udev_client_query_by_device_file(udevClient, node);

    if (g_udev_device_has_property(udevDevice, "ID_INPUT_WIDTH_MM")) {
        width = g_udev_device_get_property_as_uint64(udevDevice, "ID_INPUT_WIDTH_MM");
    }
    if (g_udev_device_has_property(udevDevice, "ID_INPUT_HEIGHT_MM")) {
        height = g_udev_device_get_property_as_uint64(udevDevice, "ID_INPUT_HEIGHT_MM");
    }
    g_clear_object(&udevClient);
}

TouchCalibrate::TouchCalibrate(const QString& path, QObject *parent)
    : QObject(parent)
    , m_display(XOpenDisplay(NULL))
    , m_touchConfigPath(path)
{

}

TouchCalibrate::~TouchCalibrate()
{
    if (m_display) {
        XCloseDisplay(m_display);
    }
    m_screenInfoMap.clear();
    m_touchScreenList.clear();
    m_tabletList.clear();
    m_touchConfigList.clear();
}

bool TouchCalibrate::initDisplay()
{

}

//获取屏幕列表
void TouchCalibrate::getScreenList()
{
    int event_base, error_base, major, minor;
    Window  root;
    XRRScreenResources *res;

    if (!XRRQueryExtension(m_display, &event_base, &error_base) ||
        !XRRQueryVersion(m_display, &major, &minor)) {
        SYS_LOG(LOG_ERR, "RandR extension missing.");
        return;
    }
    root = DefaultRootWindow(m_display);
    if (major >= 1 && minor >= 5) {
        res = XRRGetScreenResources(m_display, root);
        if (!res) {
            SYS_LOG(LOG_ERR, "get screen resources failed");
            return;
        }

        for (int o = 0; o < res->noutput; ++o) {
            XRROutputInfo *output = XRRGetOutputInfo(m_display, res, res->outputs[o]);
            if (!output){
                SYS_LOG(LOG_ERR, "could not get output.");
                continue;
            }
            if (output->connection == RR_Connected) {
                ScreenInfoPtr screen(new ScreenInfo);
                screen->name = QString::fromLatin1(output->name);
                screen->width = output->mm_width;
                screen->height = output->mm_height;
                m_screenInfoMap.insert(screen->name, screen);
                SYS_LOG(LOG_DEBUG, "%s  width : %d height : %d",
                        screen->name.toLatin1().data(), screen->width, screen->height);
            }
            XRRFreeOutputInfo(output);
        }
        XRRFreeScreenResources(res);
    }
}

void TouchCalibrate::addTouchDevice(const XDeviceInfo& device, TouchDeviceList& touchList)
{
    const QString& node = getDeviceNode(device.id);
    const QVariantList& product = getDeviceProductId(device.id);
    if (!node.isEmpty()) {
        TouchDevicePtr dev = TouchDevicePtr(new TouchDevice);
        dev->id = device.id;
        dev->name = QString::fromLatin1(device.name);
        dev->node = node;
        getTouchSize(node.toLatin1().data(), dev->width, dev->height);
        if (product.count() > 1) {
            dev->product.isValid = true;
            dev->product.vid = product[0].toInt();
            dev->product.pid = product[1].toInt();
        }
        touchList.append(dev);
        SYS_LOG(LOG_DEBUG, "%s id : %d node: %s width : %d height : %d",
                dev->name.toLatin1().data(), dev->id, dev->node.toLatin1().data(), dev->width, dev->height);
    }
}

// 获取触摸设备列表
void TouchCalibrate::getTouchDeviceList()
{
    int ndevices;
    XDeviceInfo* deviceList = XListInputDevices(m_display ,&ndevices);
    for (int i = 0 ; i < ndevices ; ++i) {
        XDeviceInfo device = deviceList[i];
        if (device.type == XInternAtom(m_display, XI_TOUCHSCREEN, False)) {
            addTouchDevice(device, m_touchScreenList);
        } else if (device.type == XInternAtom(m_display, XI_TABLET, False) ||
                   device.type == XInternAtom(m_display, "STYLUS", False)) {
            addTouchDevice(device, m_tabletList);
        }
    }
    XFreeDeviceList(deviceList);
}

//获取触摸配置
void TouchCalibrate::getTouchConfigure()
{
    QFileInfo file(m_touchConfigPath);
    if(file.exists()) {
        QSettings *configSettings = new QSettings(m_touchConfigPath, QSettings::IniFormat);
        int mapNum = configSettings->value("/COUNT/num").toInt();
        if (mapNum < 1) {
            return;
        }
        for (int i = 0; i < mapNum ;++i) {
            QString mapName = QString("/MAP%1/%2");
            QString touchName = configSettings->value(mapName.arg(i+1).arg("name")).toString();
            if(touchName.isEmpty()) {
                continue;
            }

            QString scrname = configSettings->value(mapName.arg(i+1).arg("scrname")).toString();
            if(scrname.isEmpty()) {
                continue;
            }

            // serial 不判空，暂不关注，无法通过序列号匹配屏幕
            QString serial = configSettings->value(mapName.arg(i+1).arg("serial")).toString();

            TouchConfigPtr touchConfig(new TouchConfig);
            touchConfig->sTouchName = touchName;
            touchConfig->sMonitorName = scrname;
            touchConfig->sTouchSerial = serial;

            //读取配置中productId,如果无此配置则无效，不进行product比较
            const QStringList& productList = configSettings->value(mapName.arg(i+1).arg("productId")).toString().split(" ");
            if (productList.count() > 1) {
                touchConfig->product.isValid = true;
                touchConfig->product.vid = productList[0].toInt();
                touchConfig->product.pid = productList[1].toInt();
            }
            m_touchConfigList.append(touchConfig);
        }
        configSettings->deleteLater();
    }
}

//进行映射
void TouchCalibrate::calibrate()
{
    if (UsdBaseClass::isWlcom()) {
        calibrateWayland();
    } else {
        calibrateX();
    }
}

void TouchCalibrate::calibrateX()
{
    if (!m_display) {
        SYS_LOG(LOG_DEBUG, "Failed to get x display");
        return;
    }
    //获取设备
    getScreenList();
    getTouchDeviceList();
    getTouchConfigure();
    calibrateTouchScreen();
    calibrateTablet();
}

void TouchCalibrate::calibrateWayland()
{
    getTouchConfigure();
    calibrateWithDbus();
}

void TouchCalibrate::calibrateWithDbus()
{
    QDBusInterface wlcomInputInterface(C_WLCOM_SERVICE,
                                               C_WLCOM_INPUT_PATH,
                                               C_WLCOM_INPUT_INTERFACE,
                                               QDBusConnection::sessionBus());

    Q_FOREACH (const TouchConfigPtr& touchConfig, m_touchConfigList) {
        QList<QVariant> args;
        QString deviceName = QString("2:%1:%2:%3").arg(touchConfig->product.pid).
                arg(touchConfig->product.vid).
                arg(touchConfig->sTouchName.replace(" ","_"));
        args << deviceName;
        args << touchConfig->sMonitorName;
        wlcomInputInterface.asyncCallWithArgumentList("MapToOutput", args);
    }
}

void TouchCalibrate::autoMaticMapping(TouchDeviceList &touchList, ScreenInfoMap &screenMap, bool isTouchScreen)
{
    //如果配置文件无法覆盖所有的屏幕组合，继续通过比较触摸设备和屏幕物理尺寸进行映射
    TouchDeviceList::iterator it_tc = touchList.begin();
    while (it_tc != touchList.end()) {
        if ((*it_tc)->isMapped) {
            ++it_tc;
            continue;
        }
        ScreenInfoMap::iterator it_sc = screenMap.begin();
        while (it_sc != screenMap.end()) {

            //屏幕映射后，不再进行映射,触摸笔无需判断，多个可映射在同一屏幕
            if ((it_sc.value()->isMapped && isTouchScreen) || (*it_tc)->isMapped) {
                ++it_sc;
                continue;
            }

            if (checkMatch(it_sc.value()->width, it_sc.value()->height, (*it_tc)->width, (*it_tc)->height)) {
                calibrateDevice((*it_tc)->id, it_sc.value()->name);
                (*it_tc)->isMapped = true;
                it_sc.value()->isMapped = true;
            }
            ++it_sc;
        }
        ++it_tc;
    }
    //通过配置文件，尺寸比较后，仍然无法映射所有触摸设备与屏幕，则对剩下的设备进行随机映射
    it_tc = touchList.begin();
    while (it_tc != touchList.end()) {
        if ((*it_tc)->isMapped) {
            ++it_tc;
            continue;
        }
        ScreenInfoMap::iterator it_sc = screenMap.begin();
        while (it_sc != screenMap.end()) {
            if (it_sc.value()->isMapped || (*it_tc)->isMapped) {
                ++it_sc;
                continue;
            }
            //此处不再比较触摸设备与屏幕大小，为映射过的设备和屏幕进行随机映射
            calibrateDevice((*it_tc)->id, it_sc.value()->name);
            ++it_sc;
        }
        ++it_tc;
    }
}

//touchscreen
void TouchCalibrate::calibrateTouchScreen()
{
    //首先配置文件映射
    Q_FOREACH (const TouchConfigPtr& touchConfig, m_touchConfigList) {
        Q_FOREACH (const TouchDevicePtr& touch , m_touchScreenList) {
            if (touch->name == touchConfig->sTouchName) {
                if (touchConfig->product.isValid) { // 检查配置里product id 是否有效，无效则不进行判断
                    if (touchConfig->product != touch->product) {
                        continue;
                    }
                }
                const ScreenInfoPtr& screen = m_screenInfoMap.value(touchConfig->sMonitorName);
                if (screen.data()) {
                    calibrateDevice(touch->id, screen->name);
                    touch->isMapped = true;
                    screen->isMapped = true;
                }
            }
        }
    }
    autoMaticMapping(m_touchScreenList, m_screenInfoMap);
}

//tablet
void TouchCalibrate::calibrateTablet()
{
    //触摸笔映射，重置屏幕映射
    ScreenInfoMap::iterator it_sc = m_screenInfoMap.begin();
    while (it_sc != m_screenInfoMap.end()) {
        if (it_sc.value()->isMapped) {
            it_sc.value()->isMapped = false;
        }
        ++it_sc;
    }
    autoMaticMapping(m_tabletList, m_screenInfoMap, false);
}

void TouchCalibrate::calibrateDevice(int id, const QString& output)
{
    QStringList arguments;
    arguments << "--map-to-output" << QString::number(id) << output;
    QProcess process;
    process.setProgram("xinput");
    process.setArguments(arguments);
    if (!process.startDetached()) {
        SYS_LOG(LOG_DEBUG, "xinput map to output failed");
    }
    SYS_LOG(LOG_DEBUG, "xinput touch device map to output [%d : %s]", id, output.toLatin1().data());
}
