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

#ifndef QTESTACCESSIBLE_H
#define QTESTACCESSIBLE_H

#if 0
// inform syncqt
#pragma qt_no_master_include
#endif

#include <QtCore/qglobal.h>

#define QVERIFY_EVENT(event) \
    QVERIFY(QTestAccessibility::verifyEvent(event))

#include <QtCore/qlist.h>
#include <QtCore/qdebug.h>
#include <QtGui/qaccessible.h>
#include <QtGui/qguiapplication.h>
#include <QtTest/qttestglobal.h>
#include <QtTest/qtestsystem.h>

#if QT_CONFIG(accessibility)

QT_BEGIN_NAMESPACE

class QObject;

inline bool QTestAccessibility_cmpEvent(const QAccessibleEvent &l, const QAccessibleEvent &r)
{
    if (l.type() != r.type()) {
//        qDebug() << "QAccessibleEvent with wrong type: " << qAccessibleEventString(l.type()) << " and " << qAccessibleEventString(r.type());
        return false;
    }
    if (l.object() != r.object() ||
            l.child() != r.child()) {
//        qDebug() << "QAccessibleEvent for wrong object: " << l.object() << " and " << r.object() << " child: " << l.child() << " and " << r.child();
        return false;
    }

    if (l.type() == QAccessible::StateChanged) {
        return static_cast<const QAccessibleStateChangeEvent*>(&l)->changedStates()
                == static_cast<const QAccessibleStateChangeEvent*>(&r)->changedStates();
    } else if (l.type() == QAccessible::TextCaretMoved) {
        return static_cast<const QAccessibleTextCursorEvent*>(&l)->cursorPosition()
                == static_cast<const QAccessibleTextCursorEvent*>(&r)->cursorPosition();
    } else if (l.type() == QAccessible::TextSelectionChanged) {
        const QAccessibleTextSelectionEvent *le = static_cast<const QAccessibleTextSelectionEvent*>(&l);
        const QAccessibleTextSelectionEvent *re = static_cast<const QAccessibleTextSelectionEvent*>(&r);
        return  le->cursorPosition() == re->cursorPosition() &&
                le->selectionStart() == re->selectionStart() &&
                le->selectionEnd() == re->selectionEnd();
    } else if (l.type() == QAccessible::TextInserted) {
        const QAccessibleTextInsertEvent *le = static_cast<const QAccessibleTextInsertEvent*>(&l);
        const QAccessibleTextInsertEvent *re = static_cast<const QAccessibleTextInsertEvent*>(&r);
        return  le->cursorPosition() == re->cursorPosition() &&
                le->changePosition() == re->changePosition() &&
                le->textInserted() == re->textInserted();
    } else if (l.type() == QAccessible::TextRemoved) {
        const QAccessibleTextRemoveEvent *le = static_cast<const QAccessibleTextRemoveEvent*>(&l);
        const QAccessibleTextRemoveEvent *re = static_cast<const QAccessibleTextRemoveEvent*>(&r);
        return  le->cursorPosition() == re->cursorPosition() &&
                le->changePosition() == re->changePosition() &&
                le->textRemoved() == re->textRemoved();
    } else if (l.type() == QAccessible::TextUpdated) {
        const QAccessibleTextUpdateEvent *le = static_cast<const QAccessibleTextUpdateEvent*>(&l);
        const QAccessibleTextUpdateEvent *re = static_cast<const QAccessibleTextUpdateEvent*>(&r);
        return  le->cursorPosition() == re->cursorPosition() &&
                le->changePosition() == re->changePosition() &&
                le->textInserted() == re->textInserted() &&
                le->textRemoved() == re->textRemoved();
    } else if (l.type() == QAccessible::ValueChanged) {
        const QAccessibleValueChangeEvent *le = static_cast<const QAccessibleValueChangeEvent*>(&l);
        const QAccessibleValueChangeEvent *re = static_cast<const QAccessibleValueChangeEvent*>(&r);
        return le->value() == re->value();
    }
    return true;
}

class QTestAccessibility
{
    Q_DISABLE_COPY_MOVE(QTestAccessibility)
public:
    // Use pointers since we subclass QAccessibleEvent
    using EventList = QList<QAccessibleEvent*>;

    static void initialize()
    {
        if (!instance()) {
            instance() = new QTestAccessibility;
            qAddPostRoutine(cleanup);
        }
    }

    static void cleanup()
    {
        delete instance();
        instance() = nullptr;
    }
    static void clearEvents() { eventList().clear(); }
    static EventList events() { return eventList(); }
    static bool verifyEvent(QAccessibleEvent *ev)
    {
        for (int i = 0; eventList().isEmpty() && i < 5; ++i)
            QTest::qWait(50);
        if (eventList().isEmpty()) {
            qWarning("Timeout waiting for accessibility event.");
            return false;
        }

        for (qsizetype i = 0; i < eventList().size(); ++i) {
            if (QTestAccessibility_cmpEvent(*eventList().at(i), *ev)) {
                if (i != 0) {
                    qWarning() << " Found event at position " << i;
                    qWarning("%s", qPrintable(msgAccessibilityEventListMismatch(eventList(), ev)));
                }
                delete eventList().takeAt(i);
                return true;
            }
        }

        qWarning("%s", qPrintable(msgAccessibilityEventListMismatch(eventList(), ev)));
        return false;
    }
    static bool containsEvent(QAccessibleEvent *event) {
        for (const QAccessibleEvent *ev : std::as_const(eventList())) {
            if (QTestAccessibility_cmpEvent(*ev, *event))
                return true;
        }
        return false;
    }
    static bool containsEventOfType(QAccessible::Event evtype) {
        for (const QAccessibleEvent *ev : std::as_const(eventList())) {
            if (ev->type() == evtype)
                return true;
        }
        return false;
    }
    static void setUpdateHandler(std::function<void(QAccessibleEvent *event)> updateHandler)
    {
        Q_ASSERT_X(updateHandler, __FUNCTION__, "Update handler cannot be nullptr");
        instance()->m_updateHandler = std::move(updateHandler);
    }

private:
    QTestAccessibility()
    {
        QAccessible::installUpdateHandler(updateHandler);
        QAccessible::installRootObjectHandler(rootObjectHandler);
    }

    ~QTestAccessibility()
    {
        QAccessible::installUpdateHandler(nullptr);
        QAccessible::installRootObjectHandler(nullptr);
    }

    static void rootObjectHandler(QObject *object)
    {
        //    qDebug("rootObjectHandler called %p", object);
        if (object) {
            QGuiApplication* app = qobject_cast<QGuiApplication*>(object);
            if ( !app )
                qWarning("root Object is not a QGuiApplication!");
        } else {
            qWarning("root Object called with 0 pointer");
        }
    }

    static void updateHandler(QAccessibleEvent *event)
    {
        instance()->m_updateHandler(event);

        auto ev = copyEvent(event);
        if (auto obj = ev->object()) {
            QObject::connect(obj, &QObject::destroyed, obj, [&, ev](){
                auto index= eventList().indexOf(ev);
                if (index == -1)
                    return;
                eventList().at(index)->m_object = nullptr;
            });
        }
        eventList().append(ev);
    }
    static QAccessibleEvent *copyEvent(QAccessibleEvent *event)
    {
        QAccessibleEvent *ev;
        if (event->type() == QAccessible::StateChanged) {
            if (event->object())
                ev = new QAccessibleStateChangeEvent(event->object(),
                        static_cast<QAccessibleStateChangeEvent*>(event)->changedStates());
            else
                ev = new QAccessibleStateChangeEvent(event->accessibleInterface(),
                        static_cast<QAccessibleStateChangeEvent*>(event)->changedStates());
        } else if (event->type() == QAccessible::TextCaretMoved) {
            if (event->object())
                ev = new QAccessibleTextCursorEvent(event->object(), static_cast<QAccessibleTextCursorEvent*>(event)->cursorPosition());
            else
                ev = new QAccessibleTextCursorEvent(event->accessibleInterface(), static_cast<QAccessibleTextCursorEvent*>(event)->cursorPosition());
        } else if (event->type() == QAccessible::TextSelectionChanged) {
            const QAccessibleTextSelectionEvent *original = static_cast<QAccessibleTextSelectionEvent*>(event);
            QAccessibleTextSelectionEvent *sel;
            if (event->object())
                sel = new QAccessibleTextSelectionEvent(event->object(), original->selectionStart(), original->selectionEnd());
            else
                sel = new QAccessibleTextSelectionEvent(event->accessibleInterface(), original->selectionStart(), original->selectionEnd());
            sel->setCursorPosition(original->cursorPosition());
            ev = sel;
        } else if (event->type() == QAccessible::TextInserted) {
            const QAccessibleTextInsertEvent *original = static_cast<QAccessibleTextInsertEvent*>(event);
            QAccessibleTextInsertEvent *ins;
            if (original->object())
                ins = new QAccessibleTextInsertEvent(event->object(), original->changePosition(), original->textInserted());
            else
                ins = new QAccessibleTextInsertEvent(event->accessibleInterface(), original->changePosition(), original->textInserted());
            ins->setCursorPosition(original->cursorPosition());
            ev = ins;
        } else if (event->type() == QAccessible::TextRemoved) {
            const QAccessibleTextRemoveEvent *original = static_cast<QAccessibleTextRemoveEvent*>(event);
            QAccessibleTextRemoveEvent *rem;
            if (event->object())
                rem = new QAccessibleTextRemoveEvent(event->object(), original->changePosition(), original->textRemoved());
            else
                rem = new QAccessibleTextRemoveEvent(event->accessibleInterface(), original->changePosition(), original->textRemoved());
            rem->setCursorPosition(original->cursorPosition());
            ev = rem;
        } else if (event->type() == QAccessible::TextUpdated) {
            const QAccessibleTextUpdateEvent *original = static_cast<QAccessibleTextUpdateEvent*>(event);
            QAccessibleTextUpdateEvent *upd;
            if (event->object())
                upd = new QAccessibleTextUpdateEvent(event->object(), original->changePosition(), original->textRemoved(), original->textInserted());
            else
                upd = new QAccessibleTextUpdateEvent(event->accessibleInterface(), original->changePosition(), original->textRemoved(), original->textInserted());
            upd->setCursorPosition(original->cursorPosition());
            ev = upd;
        } else if (event->type() == QAccessible::ValueChanged) {
            if (event->object())
                ev = new QAccessibleValueChangeEvent(event->object(), static_cast<QAccessibleValueChangeEvent*>(event)->value());
            else
                ev = new QAccessibleValueChangeEvent(event->accessibleInterface(), static_cast<QAccessibleValueChangeEvent*>(event)->value());
        } else if (event->type() == QAccessible::TableModelChanged) {
            QAccessibleTableModelChangeEvent *oldEvent = static_cast<QAccessibleTableModelChangeEvent*>(event);
            QAccessibleTableModelChangeEvent *newEvent;
            if (event->object())
                newEvent = new QAccessibleTableModelChangeEvent(event->object(), oldEvent->modelChangeType());
            else
                newEvent = new QAccessibleTableModelChangeEvent(event->accessibleInterface(), oldEvent->modelChangeType());
            newEvent->setFirstRow(oldEvent->firstRow());
            newEvent->setFirstColumn(oldEvent->firstColumn());
            newEvent->setLastRow(oldEvent->lastRow());
            newEvent->setLastColumn(oldEvent->lastColumn());
            ev = newEvent;
        } else if (event->type() == QAccessible::Announcement) {
            QAccessibleAnnouncementEvent *oldEvent =
                    static_cast<QAccessibleAnnouncementEvent *>(event);
            QAccessibleAnnouncementEvent *newEvent;
            if (event->object())
                newEvent = new QAccessibleAnnouncementEvent(event->object(), oldEvent->message());
            else
                newEvent = new QAccessibleAnnouncementEvent(event->accessibleInterface(),
                                                            oldEvent->message());
            newEvent->setPoliteness(oldEvent->politeness());
            ev = newEvent;
        } else {
            if (event->object())
                ev = new QAccessibleEvent(event->object(), event->type());
            else
                ev = new QAccessibleEvent(event->accessibleInterface(), event->type());
        }

        if (ev->type() != QAccessible::ObjectDestroyed)
            ev->setChild(event->child());
        return ev;
    }

    Q_TESTLIB_EXPORT static EventList &eventList();
    Q_TESTLIB_EXPORT static QTestAccessibility *&instance();

private:
    static QString msgAccessibilityEventListMismatch(const EventList &haystack,
                                                     const QAccessibleEvent *needle)
    {
        QString rc;
        QDebug str = QDebug(&rc).nospace();
        str << "Event " << *needle << "\n"
            <<  " not found at head of event list of size " << haystack.size() << " :\n";
        for (const QAccessibleEvent *e : haystack)
            str << ' ' << *e << "\n";
        return rc;
    }
    std::function<void(QAccessibleEvent *event)> m_updateHandler = [](QAccessibleEvent *) { ; };

};

#if QT_DEPRECATED_SINCE(6, 11)
using EventList QT_DEPRECATED_VERSION_X_6_11("Use QTestAccessibility::EventList")
    = QTestAccessibility::EventList;

QT_DEPRECATED_VERSION_X_6_11("This was an internal function for QTestAccessibility. "
                             "Only QAccessibleEvent is allowed to define operator== for itself, "
                             "and it doesn't, so use a named function instead")
inline bool operator==(const QAccessibleEvent &l, const QAccessibleEvent &r)
{
    return QTestAccessibility_cmpEvent(l, r);
}
#endif // QT_DEPRECATED_SINCE(6, 11)

QT_END_NAMESPACE

#endif // QT_CONFIG(accessibility)
#endif // QTESTACCESSIBLE_H
