// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

// cSpell: ignore quintptr
#include "app-window.h"

#include <slint-platform.h>

#include <QtGui/QtGui>
#include <QtGui/qpa/qplatformnativeinterface.h>
#include <QtWidgets/QApplication>

static void update_timer()
{
    static QTimer timer;
    static auto init = [] {
        timer.callOnTimeout([] {
            slint::platform::update_timers_and_animations();
            update_timer();
        });
        return true;
    }();
    if (auto timeout = slint::platform::duration_until_next_timer_update()) {
        timer.start(*timeout);
    } else {
        timer.stop();
    }
}

slint::PointerEventButton convert_button(Qt::MouseButtons b)
{
    switch (b) {
    case Qt::LeftButton:
        return slint::PointerEventButton::Left;
    case Qt::RightButton:
        return slint::PointerEventButton::Right;
    case Qt::MiddleButton:
        return slint::PointerEventButton::Middle;
    default:
        return slint::PointerEventButton::Other;
    }
}

static slint::platform::NativeWindowHandle window_handle_for_qt_window(QWindow *window)
{
    // Ensure that the native window surface exists
    window->create();
#ifdef __APPLE__
    QPlatformNativeInterface *native = qApp->platformNativeInterface();
    NSView *nsview = reinterpret_cast<NSView *>(
            native->nativeResourceForWindow(QByteArray("nsview"), window));
    NSWindow *nswindow = reinterpret_cast<NSWindow *>(
            native->nativeResourceForWindow(QByteArray("nswindow"), window));
    return slint::platform::NativeWindowHandle::from_appkit(nsview, nswindow);
#elif defined Q_OS_WIN
    auto wid = Qt::HANDLE(window->winId());
    return slint::platform::NativeWindowHandle::from_win32(wid, GetModuleHandle(nullptr));
#else
    // Try Wayland first, then XLib, then Xcb
    auto wid = window->winId();
    auto visual_id = 0; // FIXME
    QPlatformNativeInterface *native = qApp->platformNativeInterface();
    auto screen = quintptr(native->nativeResourceForWindow(QByteArray("x11screen"), window));
    if (auto *wayland_display = reinterpret_cast<wl_display *>(
                native->nativeResourceForIntegration(QByteArray("wl_display")))) {
        auto *wayland_surface = reinterpret_cast<wl_surface *>(
                native->nativeResourceForWindow(QByteArray("surface"), window));
        return slint::platform::NativeWindowHandle::from_wayland(wayland_surface, wayland_display);
    } else if (auto *x11_display = native->nativeResourceForWindow(QByteArray("display"), window)) {
        return slint::platform::NativeWindowHandle::from_x11_xlib(wid, wid, x11_display, screen);
    } else if (auto *xcb_connection = reinterpret_cast<xcb_connection_t *>(
                       native->nativeResourceForWindow(QByteArray("connection"), window))) {
        return slint::platform::NativeWindowHandle::from_x11_xcb(wid, wid, xcb_connection, screen);
    } else {
        throw "Unsupported windowing system (tried wayland, xlib, and xcb)";
    }
#endif
}

static slint::SharedString key_event_text(QKeyEvent *e)
{
    switch (e->key()) {
    case Qt::Key::Key_Backspace:
        return slint::platform::key_codes::Backspace;
    case Qt::Key::Key_Tab:
        return slint::platform::key_codes::Tab;
    case Qt::Key::Key_Enter:
    case Qt::Key::Key_Return:
        return slint::platform::key_codes::Return;
    case Qt::Key::Key_Escape:
        return slint::platform::key_codes::Escape;
    case Qt::Key::Key_Backtab:
        return slint::platform::key_codes::Backtab;
    case Qt::Key::Key_Delete:
        return slint::platform::key_codes::Delete;
    case Qt::Key::Key_Shift:
        return slint::platform::key_codes::Shift;
    case Qt::Key::Key_Control:
        return slint::platform::key_codes::Control;
    case Qt::Key::Key_Alt:
        return slint::platform::key_codes::Alt;
    case Qt::Key::Key_AltGr:
        return slint::platform::key_codes::AltGr;
    case Qt::Key::Key_CapsLock:
        return slint::platform::key_codes::CapsLock;
    case Qt::Key::Key_Meta:
        return slint::platform::key_codes::Meta;
    case Qt::Key::Key_Up:
        return slint::platform::key_codes::UpArrow;
    case Qt::Key::Key_Down:
        return slint::platform::key_codes::DownArrow;
    case Qt::Key::Key_Left:
        return slint::platform::key_codes::LeftArrow;
    case Qt::Key::Key_Right:
        return slint::platform::key_codes::RightArrow;
    case Qt::Key::Key_F1:
        return slint::platform::key_codes::F1;
    case Qt::Key::Key_F2:
        return slint::platform::key_codes::F2;
    case Qt::Key::Key_F3:
        return slint::platform::key_codes::F3;
    case Qt::Key::Key_F4:
        return slint::platform::key_codes::F4;
    case Qt::Key::Key_F5:
        return slint::platform::key_codes::F5;
    case Qt::Key::Key_F6:
        return slint::platform::key_codes::F6;
    case Qt::Key::Key_F7:
        return slint::platform::key_codes::F7;
    case Qt::Key::Key_F8:
        return slint::platform::key_codes::F8;
    case Qt::Key::Key_F9:
        return slint::platform::key_codes::F9;
    case Qt::Key::Key_F10:
        return slint::platform::key_codes::F10;
    case Qt::Key::Key_F11:
        return slint::platform::key_codes::F11;
    case Qt::Key::Key_F12:
        return slint::platform::key_codes::F12;
    case Qt::Key::Key_F13:
        return slint::platform::key_codes::F13;
    case Qt::Key::Key_F14:
        return slint::platform::key_codes::F14;
    case Qt::Key::Key_F15:
        return slint::platform::key_codes::F15;
    case Qt::Key::Key_F16:
        return slint::platform::key_codes::F16;
    case Qt::Key::Key_F17:
        return slint::platform::key_codes::F17;
    case Qt::Key::Key_F18:
        return slint::platform::key_codes::F18;
    case Qt::Key::Key_F19:
        return slint::platform::key_codes::F19;
    case Qt::Key::Key_F20:
        return slint::platform::key_codes::F20;
    case Qt::Key::Key_F21:
        return slint::platform::key_codes::F21;
    case Qt::Key::Key_F22:
        return slint::platform::key_codes::F22;
    case Qt::Key::Key_F23:
        return slint::platform::key_codes::F23;
    case Qt::Key::Key_F24:
        return slint::platform::key_codes::F24;
    case Qt::Key::Key_Insert:
        return slint::platform::key_codes::Insert;
    case Qt::Key::Key_Home:
        return slint::platform::key_codes::Home;
    case Qt::Key::Key_End:
        return slint::platform::key_codes::End;
    case Qt::Key::Key_PageUp:
        return slint::platform::key_codes::PageUp;
    case Qt::Key::Key_PageDown:
        return slint::platform::key_codes::PageDown;
    case Qt::Key::Key_ScrollLock:
        return slint::platform::key_codes::ScrollLock;
    case Qt::Key::Key_Pause:
        return slint::platform::key_codes::Pause;
    case Qt::Key::Key_SysReq:
        return slint::platform::key_codes::SysReq;
    case Qt::Key::Key_Stop:
        return slint::platform::key_codes::Stop;
    case Qt::Key::Key_Menu:
        return slint::platform::key_codes::Menu;
    default:
        if (e->modifiers() & Qt::ControlModifier) {
            // e->text() is not the key when Ctrl is pressed
            return QKeySequence(e->key()).toString().toLower().toUtf8().data();
        }
        return e->text().toUtf8().data();
    }
}

class MyWindow : public QWindow, public slint::platform::WindowAdapter
{
    std::optional<slint::platform::SkiaRenderer> m_renderer;
    bool m_visible = false;

public:
    MyWindow(QWindow *parentWindow = nullptr) : QWindow(parentWindow)
    {
        resize(640, 480);
        m_renderer.emplace(window_handle_for_qt_window(this), size());
    }

    slint::platform::AbstractRenderer &renderer() override { return m_renderer.value(); }

    void paintEvent(QPaintEvent *ev) override
    {
        slint::platform::update_timers_and_animations();

        m_renderer->render();

        if (window().has_active_animations()) {
            requestUpdate();
        }
        update_timer();
    }

    void closeEvent(QCloseEvent *event) override
    {
        if (m_visible) {
            event->ignore();
            window().dispatch_close_requested_event();
        }
    }

    bool event(QEvent *e) override
    {
        if (e->type() == QEvent::UpdateRequest) {
            paintEvent(static_cast<QPaintEvent *>(e));
            return true;
        } else if (e->type() == QEvent::KeyPress) {
            auto ke = static_cast<QKeyEvent *>(e);
            if (ke->isAutoRepeat())
                window().dispatch_key_press_repeat_event(key_event_text(ke));
            else
                window().dispatch_key_press_event(key_event_text(ke));
            return true;
        } else if (e->type() == QEvent::KeyRelease) {
            window().dispatch_key_release_event(key_event_text(static_cast<QKeyEvent *>(e)));
            return true;
        } else if (e->type() == QEvent::WindowActivate) {
            window().dispatch_window_active_changed_event(true);
            return true;
        } else if (e->type() == QEvent::WindowDeactivate) {
            window().dispatch_window_active_changed_event(false);
            return true;
        } else {
            return QWindow::event(e);
        }
    }

    void set_visible(bool visible) override
    {
        m_visible = visible;
        if (visible) {
            window().dispatch_scale_factor_change_event(devicePixelRatio());
            QWindow::show();
        } else {
            QWindow::close();
        }
    }

    void set_size(slint::PhysicalSize size) override
    {
        float scale_factor = devicePixelRatio();
        resize(size.width / scale_factor, size.height / scale_factor);
    }

    slint::PhysicalSize size() override
    {
        auto windowSize = slint::LogicalSize({ float(width()), float(height()) });
        float scale_factor = devicePixelRatio();
        return slint::PhysicalSize({ uint32_t(windowSize.width * scale_factor),
                                     uint32_t(windowSize.height * scale_factor) });
    }

    void set_position(slint::PhysicalPosition position) override
    {
        float scale_factor = devicePixelRatio();
        setFramePosition(QPointF(position.x / scale_factor, position.y / scale_factor).toPoint());
    }

    std::optional<slint::PhysicalPosition> position() override
    {
        auto pos = framePosition();
        float scale_factor = devicePixelRatio();
        return { slint::PhysicalPosition(
                { int32_t(pos.x() * scale_factor), int32_t(pos.y() * scale_factor) }) };
    }

    void request_redraw() override { requestUpdate(); }

    void update_window_properties(const WindowProperties &props) override
    {
        QWindow::setTitle(QString::fromUtf8(props.title().data()));
        auto c = props.layout_constraints();
        QWindow::setMaximumSize(c.max ? QSize(c.max->width, c.max->height)
                                      : QSize(1 << 15, 1 << 15));
        QWindow::setMinimumSize(c.min ? QSize(c.min->width, c.min->height) : QSize());

        Qt::WindowStates states = windowState() & Qt::WindowActive;
        if (props.is_fullscreen())
            states |= Qt::WindowFullScreen;
        if (props.is_minimized())
            states |= Qt::WindowMinimized;
        if (props.is_maximized())
            states |= Qt::WindowMaximized;
        setWindowStates(states);
    }

    void resizeEvent(QResizeEvent *ev) override
    {
        auto logicalSize = ev->size();
        window().dispatch_resize_event(
                slint::LogicalSize({ float(logicalSize.width()), float(logicalSize.height()) }));
    }

    void mousePressEvent(QMouseEvent *event) override
    {
        slint::platform::update_timers_and_animations();
        window().dispatch_pointer_press_event(
                slint::LogicalPosition({ float(event->pos().x()), float(event->pos().y()) }),
                convert_button(event->button()));
        update_timer();
    }
    void mouseReleaseEvent(QMouseEvent *event) override
    {
        slint::platform::update_timers_and_animations();
        window().dispatch_pointer_release_event(
                slint::LogicalPosition({ float(event->pos().x()), float(event->pos().y()) }),
                convert_button(event->button()));
        update_timer();
    }
    void mouseMoveEvent(QMouseEvent *event) override
    {
        slint::platform::update_timers_and_animations();
        window().dispatch_pointer_move_event(
                slint::LogicalPosition({ float(event->pos().x()), float(event->pos().y()) }));
        update_timer();
    }
};

struct MyPlatform : public slint::platform::Platform
{
    std::unique_ptr<QWindow> parentWindow;

    std::unique_ptr<slint::platform::WindowAdapter> create_window_adapter() override
    {
        return std::make_unique<MyWindow>(parentWindow.get());
    }

    void set_clipboard_text(const slint::SharedString &str,
                            slint::platform::Platform::Clipboard clipboard) override
    {
        switch (clipboard) {
        case slint::platform::Platform::Clipboard::DefaultClipboard:
            qApp->clipboard()->setText(QString::fromUtf8(str.data()), QClipboard::Clipboard);
            break;
        case slint::platform::Platform::Clipboard::SelectionClipboard:
            qApp->clipboard()->setText(QString::fromUtf8(str.data()), QClipboard::Selection);
            break;
        }
    }

    std::optional<slint::SharedString> clipboard_text(Clipboard clipboard) override
    {
        QString text;
        switch (clipboard) {
        case slint::platform::Platform::Clipboard::DefaultClipboard:
            text = qApp->clipboard()->text(QClipboard::Clipboard);
            break;
        case slint::platform::Platform::Clipboard::SelectionClipboard:
            text = qApp->clipboard()->text(QClipboard::Selection);
            break;
        default:
            return {};
        }
        if (text.isNull()) {
            return {};
        } else {
            return slint::SharedString(text.toUtf8().data());
        }
    }
};

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    static MyPlatform *platform = [] {
        auto platform = std::make_unique<MyPlatform>();
        auto p2 = platform.get();
        slint::platform::set_platform(std::move(platform));
        return p2;
    }();

    slint::platform::update_timers_and_animations();

    auto my_ui = App::create();
    // mu_ui->set_property(....);
    my_ui->show();

    return app.exec();
}
