// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QLoggingCategory>
#include <QTime>
#include <QGuiApplication>

#include "handleatspievents.h"
#include "vkbhidetimer.h"
#include "atspi/atspi.h"

namespace  {
const QString KAtspiBusLauncher = "at-spi-bus-launcher";
const QString KAtspiRegistryd = "at-spi2-registryd";
const int KProsessIsRunning = 0;
}

Q_LOGGING_CATEGORY(lcHandleAtspiEvents, "qt.virtualkeyboard.tests.manual.x11vkbwrapper.handleatspievents")

/**
 * @brief focusEventFromInput Called when a widget is focused.
 * @param event
 * @param user_data
 */
 void focusEventFromInput(AtspiEvent *event, void *user_data)
{
    qCDebug(lcHandleAtspiEvents) << Q_FUNC_INFO;
    auto *handleATSPIEvents = static_cast<HandleATSPIEvents *>(user_data);
    handleATSPIEvents->gotFocusEventFromInput(event);
}

/**
 * @brief HandleATSPIEvents::HandleATSPIEvents
 * @param parent
 */
HandleATSPIEvents::HandleATSPIEvents(QObject *parent)
    : QObject(parent),
    m_keyboardVisible(false),
    m_focuses(0)
{

}

/**
 * @brief HandleATSPIEvents::~HandleATSPIEvents
 */
HandleATSPIEvents::~HandleATSPIEvents()
{
    qCDebug(lcHandleAtspiEvents) << Q_FUNC_INFO;

    m_focuses.clear();

    if (!atspi_event_listener_deregister_from_callback(focusEventFromInput, static_cast<void*>(this), "object:state-changed:focused", nullptr)) {
            qWarning() << "Error occurred: Problem deregistering focus listener";
     }
}

/**
 * @brief HandleATSPIEvents::init
 * @return false if at-spi is not running or callback regitering fail
 */

bool HandleATSPIEvents::init()
{
    qCDebug(lcHandleAtspiEvents) << Q_FUNC_INFO;
    /** Check that At-spi is running */
    if (KProsessIsRunning != system(QString("pidof -x %1 > /dev/null").arg(KAtspiBusLauncher).toLatin1().data()) ||
            KProsessIsRunning != system(QString("pidof -x %1 > /dev/null").arg(KAtspiRegistryd).toLatin1().data())) {
        qWarning() << "One or both of the At-Spi related processes are not running.";
        return false;
    }

    GError *error = nullptr;
    /** Registered the spi events to monitor focus and show on editable widgets. */
    if (!atspi_event_listener_register_from_callback(focusEventFromInput,
                                                     static_cast<void*>(this),
                                                     nullptr,
                                                     "object:state-changed:focused",
                                                     &error)){
        qWarning() << Q_FUNC_INFO << "Error occurred: ATSPI listener register failed. Error message:" << error->message;
        return false;
    }

    QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::visibleChanged, [this] {
        this->setKeyboardVisible(QGuiApplication::inputMethod()->isVisible());
    });

    return true;
}

/**
 * @brief handleATSPIEvents::setKeyboardVisible
 * @param visible
 */
void HandleATSPIEvents::setKeyboardVisible(const bool visible)
{
    if (m_keyboardVisible != visible) {
        m_keyboardVisible = visible;
        qCDebug(lcHandleAtspiEvents) << "SET VKB visible " << visible;
        if (m_keyboardVisible && !QGuiApplication::inputMethod()->isVisible()) {
            QGuiApplication::inputMethod()->show();
        } else {
            QGuiApplication::inputMethod()->hide();
        }
    }
}

/**
 * @brief handleATSPIEvents::storeFocusElement
 * @param role
 * @param focus
 */
void HandleATSPIEvents::storeFocusElement(const qint8 role)
{
    m_focuses.append(role);
    qCDebug(lcHandleAtspiEvents) << "*****INSERTED FOCUS ELEMENT*****" << role << "TOTAL:"  << m_focuses.size();
}

/**
 * @brief handleATSPIEvents::isThereFocus
 * AT-SPI focus in/out events are received in random order and for some
 * objects AT-SPI doesn't send any focus OUT event at all.
 * This function keeps track if there's an accepted type of object in focus and
 * knows to release/ignore the objects that do not receive focus OUT event.
 * @param role
 */
bool HandleATSPIEvents::isThereFocus(const qint8 role)
{
    qCDebug(lcHandleAtspiEvents) << " FOCUS ELEMENT to EXAMINE: " << role;
    qint8 roleValue = ATSPI_ROLE_INVALID;
    for (auto iter = m_focuses.begin() ; iter != m_focuses.end() ; iter++) {
        roleValue = *iter;
        if (roleValue == role ||
            roleValue == ATSPI_ROLE_DOCUMENT_WEB ||
            roleValue == ATSPI_ROLE_ENTRY ||
            roleValue == ATSPI_ROLE_LINK) {
            qCDebug(lcHandleAtspiEvents) << "*****REMOVING FOCUS ELEMENT*****: " << *iter;
            m_focuses.erase(iter--);
        }
    }
    m_focuses.squeeze();
    return !m_focuses.isEmpty();
}

/**
 * @brief handleATSPIEvents::gotFocusEventFromInput
 * @param event
 */
void HandleATSPIEvents::gotFocusEventFromInput(const AtspiEvent *event)
{
    qCDebug(lcHandleAtspiEvents) << Q_FUNC_INFO << event->type << event->detail1 << event->detail2 << QTime::currentTime().toString();

    GError *error = nullptr;
    AtspiStateSet *state_set = atspi_accessible_get_state_set(event->source);
    AtspiRole role = atspi_accessible_get_role(event->source, &error);

    if (error) {
        qCDebug(lcHandleAtspiEvents) << Q_FUNC_INFO << "Event error message:" << error->message;
    }
    qCDebug(lcHandleAtspiEvents) << "ATSPI focus event received. Object role=" << role;

    if ((((role == ATSPI_ROLE_TERMINAL) || (role == ATSPI_ROLE_PANEL)  ||
          (role == ATSPI_ROLE_TABLE_CELL) ||
          (((role == ATSPI_ROLE_TEXT) ||
            (role == ATSPI_ROLE_PASSWORD_TEXT) ||
            (role == ATSPI_ROLE_SECTION) ||
            (role == ATSPI_ROLE_PARAGRAPH) ||
            (role = ATSPI_ROLE_ENTRY)) &&
           state_set &&
           atspi_state_set_contains(state_set, ATSPI_STATE_EDITABLE))))) {

        if (event->detail1) {
            qCDebug(lcHandleAtspiEvents) << "ACCEPTED FOCUS IN";
            VkbHideTimer::getInstance()->startTimer();
            this->storeFocusElement(role);
            if (!m_keyboardVisible) {
                setKeyboardVisible(true);
            }
        } else if (m_keyboardVisible && !isThereFocus(role)) {
            setKeyboardVisible(false);
        }

    } else {
        qCDebug(lcHandleAtspiEvents) << " ELSE: SET VKB visible FALSE";
        setKeyboardVisible(false);
        m_focuses.clear();
        m_focuses.squeeze();
    }
}

