// Copyright (C) 2015-2016 Oleksandr Tymoshenko <gonzo@bluezbox.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qbsdkeyboard.h"

#include <QByteArray>
#include <QFile>
#include <QGuiApplication>
#include <QPoint>
#include <QSocketNotifier>
#include <QString>
#include <QStringList>

#include <QtCore/qglobal.h>
#include <qpa/qwindowsysteminterface.h>
#include <private/qcore_unix_p.h>
#include <private/qguiapplication_p.h>
#include <private/qinputdevicemanager_p_p.h>

#include <qdebug.h>
#include <cstdio>

#include <cerrno>
#include <fcntl.h>
#include <unistd.h>

#include <termios.h>
#include <sys/kbio.h>

// #define QT_BSD_KEYBOARD_DEBUG

#ifdef QT_BSD_KEYBOARD_DEBUG
#include <qdebug.h>
#endif

QT_BEGIN_NAMESPACE

enum {
    Bsd_KeyCodeMask     = 0x7f,
    Bsd_KeyPressedMask  = 0x80
};

#include "qbsdkeyboard_defaultmap.h"

QBsdKeyboardHandler::QBsdKeyboardHandler(const QString &key, const QString &specification)
{
    Q_UNUSED(key);

    setObjectName(QLatin1String("BSD Keyboard Handler"));

    QByteArray device;
    if (specification.startsWith("/dev/"))
        device = QFile::encodeName(specification);

    if (device.isEmpty()) {
        device = QByteArrayLiteral("STDIN");
        m_fd = fileno(stdin);
    }
    else {
        m_fd = QT_OPEN(device.constData(), O_RDONLY);
        if (!m_fd) {
            qErrnoWarning(errno, "open(%s) failed", device.constData());
            return;
        }
        m_shouldClose = true;
    }

    if (ioctl(m_fd, KDGKBMODE, &m_origKbdMode)) {
        qErrnoWarning(errno, "ioctl(%s, KDGKBMODE) failed", device.constData());
        revertTTYSettings();
        return;
    }

    if (ioctl(m_fd, KDSKBMODE, K_CODE) < 0) {
        qErrnoWarning(errno, "ioctl(%s, KDSKBMODE) failed", device.constData());
        revertTTYSettings();
        return;
    }

    termios kbdtty;
    if (tcgetattr(m_fd, &kbdtty) == 0) {

        m_kbdOrigTty.reset(new termios);
        *m_kbdOrigTty = kbdtty;

        kbdtty.c_iflag = IGNPAR | IGNBRK;
        kbdtty.c_oflag = 0;
        kbdtty.c_cflag = CREAD | CS8;
        kbdtty.c_lflag = 0;
        kbdtty.c_cc[VTIME] = 0;
        kbdtty.c_cc[VMIN] = 0;
        cfsetispeed(&kbdtty, 9600);
        cfsetospeed(&kbdtty, 9600);
        if (tcsetattr(m_fd, TCSANOW, &kbdtty) < 0) {
            qErrnoWarning(errno, "tcsetattr(%s) failed", device.constData());

            // TTY is still at old settings so we can
            // dispose of original termios data
            m_kbdOrigTty.reset();

            revertTTYSettings();
            return;
        }
    } else {
        qErrnoWarning(errno, "tcgetattr(%s) failed", device.constData());
        revertTTYSettings();
        return;
    }

    if (fcntl(m_fd, F_SETFL, O_NONBLOCK)) {
        qErrnoWarning(errno, "fcntl(%s, F_SETFL, O_NONBLOCK) failed", device.constData());
        revertTTYSettings();
        return;
    }

    resetKeymap();

    m_notifier.reset(new QSocketNotifier(m_fd, QSocketNotifier::Read, this));
    connect(m_notifier.data(), &QSocketNotifier::activated, this, &QBsdKeyboardHandler::readKeyboardData);
    QInputDeviceManagerPrivate::get(QGuiApplicationPrivate::inputDeviceManager())->setDeviceCount(
        QInputDeviceManager::DeviceTypeKeyboard, 1);
}

QBsdKeyboardHandler::~QBsdKeyboardHandler()
{
    revertTTYSettings();
}

void QBsdKeyboardHandler::revertTTYSettings()
{
    if (m_fd >= 0) {
        if (m_kbdOrigTty) {
            tcsetattr(m_fd, TCSANOW, m_kbdOrigTty.data());
            m_kbdOrigTty.reset();
        }

        if (m_origKbdMode != Bsd_NoKeyMode) {
            ioctl(m_fd, KDSKBMODE, m_origKbdMode);
            m_origKbdMode = Bsd_NoKeyMode;
        }

        if (m_shouldClose)
            close(m_fd);
        m_fd = -1;
    }
}

void QBsdKeyboardHandler::readKeyboardData()
{

    for (;;) {
        uint8_t buffer[32];
        int bytesRead = qt_safe_read(m_fd, buffer, sizeof(buffer));

        if (!bytesRead) {
            qWarning("Got EOF from the input device.");
            return;
        } else if (bytesRead < 0) {
            if (errno != EINTR && errno != EAGAIN)
                qWarning("Could not read from input device: %s", strerror(errno));
            return;
        }

        for (int i = 0; i < bytesRead; ++i) {
            const quint16 code = buffer[i] & Bsd_KeyCodeMask;
            const bool pressed = (buffer[i] & Bsd_KeyPressedMask) ? false : true;

            processKeycode(code, pressed, false);
        }
    }
}

void QBsdKeyboardHandler::processKeyEvent(int nativecode, int unicode, int qtcode,
                                          Qt::KeyboardModifiers modifiers, bool isPress,
                                          bool autoRepeat)
{
    const QString text = (unicode != 0xffff ) ? QString(unicode) : QString();
    const QEvent::Type eventType = isPress ? QEvent::KeyPress : QEvent::KeyRelease;

    QWindowSystemInterface::handleExtendedKeyEvent(0, eventType, qtcode, modifiers, nativecode, 0,
                                                   int(modifiers), text, autoRepeat);
}

void QBsdKeyboardHandler::processKeycode(quint16 keycode, bool pressed, bool autorepeat)
{
    const bool first_press = pressed && !autorepeat;

    const QBsdKeyboardMap::Mapping *map_plain = nullptr;
    const QBsdKeyboardMap::Mapping *map_withmod = nullptr;

    quint8 modifiers = m_modifiers;

    // get a specific and plain mapping for the keycode and the current modifiers
    for (const QBsdKeyboardMap::Mapping &m : m_keymap) {
        if (m.keycode == keycode) {
            if (m.modifiers == 0)
                map_plain = &m;

            quint8 testmods = m_modifiers;
            if (m_capsLock && (m.flags & QBsdKeyboardMap::IsLetter))
                testmods ^= QBsdKeyboardMap::ModShift;
            if (m.modifiers == testmods)
                map_withmod = &m;
        }
    }

    if (m_capsLock && map_withmod && (map_withmod->flags & QBsdKeyboardMap::IsLetter))
        modifiers ^= QBsdKeyboardMap::ModShift;

#ifdef QT_BSD_KEYBOARD_DEBUG
    qWarning("Processing key event: keycode=%3d, modifiers=%02x pressed=%d, autorepeat=%d", \
             keycode, modifiers, pressed ? 1 : 0, autorepeat ? 1 : 0);
#endif

    const QBsdKeyboardMap::Mapping *it = map_withmod ? map_withmod : map_plain;

    if (!it) {
#ifdef QT_BSD_KEYBOARD_DEBUG
        // we couldn't even find a plain mapping
        qWarning("Could not find a suitable mapping for keycode: %3d, modifiers: %02x", keycode, modifiers);
#endif
        return;
    }

    bool skip = false;
    quint16 unicode = it->unicode;
    quint32 qtcode = it->qtcode;

    if ((it->flags & QBsdKeyboardMap::IsModifier) && it->special) {
        // this is a modifier, i.e. Shift, Alt, ...
        if (pressed)
            m_modifiers |= quint8(it->special);
        else
            m_modifiers &= ~quint8(it->special);
    } else if (qtcode >= Qt::Key_CapsLock && qtcode <= Qt::Key_ScrollLock) {
        // (Caps|Num|Scroll)Lock
        if (first_press) {
            switch (qtcode) {
            case Qt::Key_CapsLock:
                m_capsLock = !m_capsLock;
                switchLed(LED_CAP, m_capsLock);
                break;
            case Qt::Key_NumLock:
                m_numLock = !m_numLock;
                switchLed(LED_NUM, m_numLock);
                break;
            case Qt::Key_ScrollLock:
                m_scrollLock = !m_scrollLock;
                switchLed(LED_SCR, m_scrollLock);
                break;
            default:
                break;
            }
        }
    }

    if (!skip) {
        // a normal key was pressed
        const int modmask = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier
            | Qt::MetaModifier | Qt::KeypadModifier;

        // we couldn't find a specific mapping for the current modifiers,
        // or that mapping didn't have special modifiers:
        // so just report the plain mapping with additional modifiers.
        if ((it == map_plain && it != map_withmod) ||
            (map_withmod && !(map_withmod->qtcode & modmask))) {
            qtcode |= QBsdKeyboardHandler::toQtModifiers(modifiers);
        }

#ifdef QT_BSD_KEYBOARD_DEBUG
        qWarning("Processing: uni=%04x, qt=%08x, qtmod=%08x", unicode, qtcode & ~modmask, (qtcode & modmask));
#endif
        //If NumLockOff and keypad key pressed remap event sent
        if (!m_numLock &&
             (qtcode & Qt::KeypadModifier)) {
            unicode = 0xffff;
            const int oldMask = (qtcode & modmask);
            switch (qtcode & ~modmask) {
            case Qt::Key_7: //7 --> Home
                qtcode = Qt::Key_Home;
                break;
            case Qt::Key_8: //8 --> Up
                qtcode = Qt::Key_Up;
                break;
            case Qt::Key_9: //9 --> PgUp
                qtcode = Qt::Key_PageUp;
                break;
            case Qt::Key_4: //4 --> Left
                qtcode = Qt::Key_Left;
                break;
            case Qt::Key_5: //5 --> Clear
                qtcode = Qt::Key_Clear;
                break;
            case Qt::Key_6: //6 --> right
                qtcode = Qt::Key_Right;
                break;
            case Qt::Key_1: //1 --> End
                qtcode = Qt::Key_End;
                break;
            case Qt::Key_2: //2 --> Down
                qtcode = Qt::Key_Down;
                break;
            case Qt::Key_3: //3 --> PgDn
                qtcode = Qt::Key_PageDown;
                break;
            case Qt::Key_0: //0 --> Ins
                qtcode = Qt::Key_Insert;
                break;
            case Qt::Key_Period: //. --> Del
                qtcode = Qt::Key_Delete;
                break;
            }
            qtcode |= oldMask;
        }

        // send the result to the server
        processKeyEvent(keycode, unicode, qtcode & ~modmask,
                        Qt::KeyboardModifiers(qtcode & modmask), pressed, autorepeat);
    }
}

void QBsdKeyboardHandler::switchLed(int led, bool state)
{
#ifdef QT_BSD_KEYBOARD_DEBUG
    qWarning() << "switchLed" << led << state;
#endif
    int leds = 0;
    if (ioctl(m_fd, KDGETLED, &leds) < 0) {
        qWarning("switchLed: Failed to query led states.");
        return;
    }

    if (state)
        leds |= led;
    else
        leds &= ~led;

    if (ioctl(m_fd, KDSETLED, leds) < 0)
        qWarning("switchLed: Failed to set led states.");
}

void QBsdKeyboardHandler::resetKeymap()
{
#ifdef QT_BSD_KEYBOARD_DEBUG
    qWarning() << "Unload current keymap and restore built-in";
#endif

    m_keymap.clear();

    const size_t mappingSize = sizeof(keymapDefault) / sizeof(keymapDefault[0]);
    m_keymap.resize(mappingSize);
    std::copy_n( &keymapDefault[0], mappingSize, m_keymap.begin() );

    // reset state, so we could switch keymaps at runtime
    m_modifiers = 0;
    m_capsLock = false;
    m_numLock = false;
    m_scrollLock = false;

    //Set locks according to keyboard leds
    int leds = 0;
    if (ioctl(m_fd, KDGETLED, &leds) < 0) {
        qWarning("Failed to query led states. Settings numlock & capslock off");
        switchLed(LED_NUM, false);
        switchLed(LED_CAP, false);
        switchLed(LED_SCR, false);
    } else {
        if ((leds & LED_CAP) > 0)
            m_capsLock = true;
        if ((leds & LED_NUM) > 0)
            m_numLock = true;
        if ((leds & LED_SCR) > 0)
            m_scrollLock = true;
#ifdef QT_BSD_KEYBOARD_DEBUG
        qWarning("numlock=%d , capslock=%d, scrolllock=%d",m_numLock, m_capsLock, m_scrollLock);
#endif
    }
}
