/*
    This file is part of the KDE Control Center Module for Joysticks

    SPDX-FileCopyrightText: 2003, 2012 Martin Koller <kollix@aon.at>
    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "joywidget.h"
#include "caldialog.h"
#include "joydevice.h"
#include "poswidget.h"

#include <QApplication>
#include <QCheckBox>
#include <QFontMetrics>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QPushButton>
#include <QStyle>
#include <QTableWidget>
#include <QTimer>
#include <QVBoxLayout>

#include <KComboBox>
#include <KLocalizedString>
#include <KMessageBox>
#include <KMessageWidget>
#include <KUrlCompletion>

#include <stdio.h>

class TableWidget : public QTableWidget
{
public:
    TableWidget(int row, int col)
        : QTableWidget(row, col)
    {
    }

    QSize sizeHint() const override
    {
        return QSize(150, 100); // return a smaller size than the Qt default(256, 192)
    }
};

JoyWidget::JoyWidget(QWidget *parent)
    : QWidget(parent)
    , idle(nullptr)
    , joydev(nullptr)
{
    QVBoxLayout *mainVbox = new QVBoxLayout(this);
    mainVbox->setContentsMargins(0, 0, 0, 0);

    // create area to show an icon + message if no joystick was detected
    {
        messageBox = new KMessageWidget(this);
        messageBox->setMessageType(KMessageWidget::Information);
        messageBox->setCloseButtonVisible(false);
        messageBox->hide();
        messageBox->setWordWrap(true);

        mainVbox->addWidget(messageBox);
    }

    QHBoxLayout *devHbox = new QHBoxLayout;
    devHbox->addWidget(new QLabel(i18n("Device:")));
    devHbox->addWidget(device = new KComboBox(true));

    device->setInsertPolicy(QComboBox::NoInsert);
    KUrlCompletion *kc = new KUrlCompletion(KUrlCompletion::FileCompletion);
    device->setCompletionObject(kc);
    device->setAutoDeleteCompletionObject(true);
    connect(device, SIGNAL(activated(QString)), this, SLOT(deviceChanged(QString)));
    connect(device, SIGNAL(returnPressed(QString)), this, SLOT(deviceChanged(QString)));
    devHbox->setStretchFactor(device, 3);

    QHBoxLayout *hbox = new QHBoxLayout;

    mainVbox->addLayout(devHbox);
    mainVbox->addLayout(hbox);

    QVBoxLayout *vboxLeft = new QVBoxLayout;
    vboxLeft->addWidget(new QLabel(i18nc("Cue for deflection of the stick", "Position:")));
    vboxLeft->addWidget(xyPos = new PosWidget);

    vboxLeft->addWidget(trace = new QCheckBox(i18n("Show trace")));
    connect(trace, &QAbstractButton::toggled, this, &JoyWidget::traceChanged);

    QVBoxLayout *vboxMid = new QVBoxLayout;

    QVBoxLayout *vboxRight = new QVBoxLayout;

    // calculate the column width we need
    QFontMetrics fm(font());
    int colWidth = qMax(fm.horizontalAdvance(i18n("PRESSED")), fm.horizontalAdvance(QStringLiteral("-32767"))) + 10; // -32767 largest string

    vboxMid->addWidget(new QLabel(i18n("Buttons:")));
    buttonTbl = new TableWidget(0, 1);
    buttonTbl->setSelectionMode(QAbstractItemView::NoSelection);
    buttonTbl->setEditTriggers(QAbstractItemView::NoEditTriggers);
    buttonTbl->setHorizontalHeaderLabels(QStringList(i18n("State")));
    buttonTbl->setSortingEnabled(false);
    buttonTbl->horizontalHeader()->setSectionsClickable(false);
    buttonTbl->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    buttonTbl->horizontalHeader()->resizeSection(0, colWidth);
    buttonTbl->verticalHeader()->setSectionsClickable(false);
    vboxMid->addWidget(buttonTbl);

    vboxRight->addWidget(new QLabel(i18n("Axes:")));
    axesTbl = new TableWidget(0, 1);
    axesTbl->setSelectionMode(QAbstractItemView::NoSelection);
    axesTbl->setEditTriggers(QAbstractItemView::NoEditTriggers);
    axesTbl->setHorizontalHeaderLabels(QStringList(i18n("Value")));
    axesTbl->setSortingEnabled(false);
    axesTbl->horizontalHeader()->setSectionsClickable(false);
    axesTbl->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    axesTbl->horizontalHeader()->resizeSection(0, colWidth);
    axesTbl->verticalHeader()->setSectionsClickable(false);
    vboxRight->addWidget(axesTbl);

    hbox->addLayout(vboxLeft);
    hbox->addLayout(vboxMid);
    hbox->addLayout(vboxRight);

    // calibrate button
    calibrate = new QPushButton(i18n("Calibrate"));
    connect(calibrate, &QAbstractButton::clicked, this, &JoyWidget::calibrateDevice);
    calibrate->setEnabled(false);

    vboxLeft->addStretch();
    vboxLeft->addWidget(calibrate);

    // set up a timer for idle processing of joystick events
    idle = new QTimer(this);
    connect(idle, &QTimer::timeout, this, &JoyWidget::checkDevice);

    // check which devicefiles we have
    init();
}

JoyWidget::~JoyWidget()
{
    delete joydev;
}

void JoyWidget::init()
{
    // check which devicefiles we have
    int i;
    bool first = true;
    char dev[30];

    device->clear();
    buttonTbl->setRowCount(0);
    axesTbl->setRowCount(0);

    for (i = 0; i < 5; i++) // check the first 5 devices
    {
        sprintf(dev, "/dev/js%d", i); // first look in /dev
        JoyDevice *joy = new JoyDevice(dev);

        if (joy->open() != JoyDevice::SUCCESS) {
            delete joy;
            sprintf(dev, "/dev/input/js%d", i); // then look in /dev/input
            joy = new JoyDevice(dev);

            if (joy->open() != JoyDevice::SUCCESS) {
                delete joy;
                continue; // try next number
            }
        }

        // we found one

        device->addItem(QStringLiteral("%1 (%2)").arg(joy->text()).arg(joy->device()));

        // display values for first device
        if (first) {
            showDeviceProps(joy); // this sets the joy object into this->joydev
            first = false;
        } else
            delete joy;
    }

    /* KDE 4: Remove this check(and i18n) when all KCM wrappers properly test modules */
    if (device->count() == 0) {
        messageBox->show();
        messageBox->setText(QStringLiteral("<qt>%1</qt>")
                                .arg(i18n("No joystick device automatically found on this computer.<br />"
                                          "Checks were done in /dev/js[0-4] and /dev/input/js[0-4]<br />"
                                          "If you know that there is one attached, please enter the correct device file.")));
    }
}

void JoyWidget::traceChanged(bool state)
{
    xyPos->showTrace(state);
}

void JoyWidget::restoreCurrDev()
{
    if (!joydev) // no device open
    {
        device->setEditText(QString());
        calibrate->setEnabled(false);
    } else {
        // try to find the current open device in the combobox list
        int index = device->findText(joydev->device(), Qt::MatchContains);

        if (index == -1) // the current open device is one the user entered (not in the list)
            device->setEditText(joydev->device());
        else
            device->setEditText(device->itemText(index));
    }
}

void JoyWidget::deviceChanged(const QString &dev)
{
    // find "/dev" in given string
    int start, stop;
    QString devName;

    if ((start = dev.indexOf(QLatin1String("/dev"))) == -1) {
        KMessageBox::error(this,
                           i18n("The given device name is invalid (does not contain /dev).\n"
                                "Please select a device from the list or\n"
                                "enter a device file, like /dev/js0."),
                           i18n("Unknown Device"));

        restoreCurrDev();
        return;
    }

    if ((stop = dev.indexOf(QLatin1Char(')'), start)) != -1) // seems to be text selected from our list
        devName = dev.mid(start, stop - start);
    else
        devName = dev.mid(start);

    if (joydev && (devName == joydev->device()))
        return; // user selected the current device; ignore it

    JoyDevice *joy = new JoyDevice(devName);
    JoyDevice::ErrorCode ret = joy->open();

    if (ret != JoyDevice::SUCCESS) {
        KMessageBox::error(this, joy->errText(ret), i18n("Device Error"));

        delete joy;
        restoreCurrDev();
        return;
    }

    showDeviceProps(joy);
}

void JoyWidget::showDeviceProps(JoyDevice *joy)
{
    joydev = joy;

    buttonTbl->setRowCount(joydev->numButtons());

    axesTbl->setRowCount(joydev->numAxes());
    if (joydev->numAxes() >= 2) {
        axesTbl->setVerticalHeaderItem(0, new QTableWidgetItem(i18n("1(x)")));
        axesTbl->setVerticalHeaderItem(1, new QTableWidgetItem(i18n("2(y)")));
    }

    calibrate->setEnabled(true);
    idle->start(0);

    // make both tables use the same space for header; this looks nicer
    // TODO: Don't know how to do this in Qt4; the following does no longer work
    // Probably by setting a sizeHint for every single header item ?
    /*
    buttonTbl->verticalHeader()->setFixedWidth(qMax(buttonTbl->verticalHeader()->width(),
                                                      axesTbl->verticalHeader()->width()));
    axesTbl->verticalHeader()->setFixedWidth(buttonTbl->verticalHeader()->width());
    */
}

void JoyWidget::checkDevice()
{
    if (!joydev)
        return; // no open device yet

    JoyDevice::EventType type;
    int number, value;

    // sleep-wait for the first event
    if (!joydev->getEvent(type, number, value, true))
        return;

    // process all pending events
    do {
        if (type == JoyDevice::BUTTON) {
            if (!buttonTbl->item(number, 0))
                buttonTbl->setItem(number, 0, new QTableWidgetItem());

            if (value == 0) // button release
                buttonTbl->item(number, 0)->setText(QStringLiteral("-"));
            else
                buttonTbl->item(number, 0)->setText(i18n("PRESSED"));
        }

        if (type == JoyDevice::AXIS) {
            if (number == 0) // x-axis
                xyPos->changeX(value);

            if (number == 1) // y-axis
                xyPos->changeY(value);

            if (!axesTbl->item(number, 0))
                axesTbl->setItem(number, 0, new QTableWidgetItem());

            axesTbl->item(number, 0)->setText(QStringLiteral("%1").arg(int(value)));
        }
    } while (joydev->getEvent(type, number, value, false)); // return immediately when no events are left
}

void JoyWidget::calibrateDevice()
{
    if (!joydev)
        return; // just to be save

    JoyDevice::ErrorCode ret = joydev->initCalibration();

    if (ret != JoyDevice::SUCCESS) {
        KMessageBox::error(this, joydev->errText(ret), i18n("Communication Error"));
        return;
    }

    if (KMessageBox::messageBox(this,
                                KMessageBox::Information,
                                i18n("<qt>Calibration is about to check the precision.<br /><br />"
                                     "<b>Please move all axes to their center position and then "
                                     "do not touch the joystick anymore.</b><br /><br />"
                                     "Click OK to start the calibration.</qt>"),
                                i18n("Calibration"),
                                KStandardGuiItem::ok(),
                                KStandardGuiItem::cancel())
        != KMessageBox::Ok)
        return;

    idle->stop(); // stop the joystick event getting; this must be done inside the calibrate dialog

    CalDialog dlg(this, joydev);
    dlg.calibrate();

    // user canceled somewhere during calibration, therefore the device is in a bad state
    if (dlg.result() == QDialog::Rejected)
        joydev->restoreCorr();

    idle->start(0); // continue with event getting
}

void JoyWidget::resetCalibration()
{
    if (!joydev)
        return; // just to be save

    JoyDevice::ErrorCode ret = joydev->restoreCorr();

    if (ret != JoyDevice::SUCCESS) {
        KMessageBox::error(this, joydev->errText(ret), i18n("Communication Error"));
    } else {
        KMessageBox::information(this, i18n("Restored all calibration values for joystick device %1.", joydev->device()), i18n("Calibration Success"));
    }
}
