// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore imgarray listviewitem
/*!

This module contains all the native Qt widget implementation that forwards to QStyle.

Same as in i_slint_core::items, when When adding an item or a property,
it needs to be kept in sync with different place.

 - It needs to be changed in this module
 - the Widget list in lib.rs
 - In the compiler: builtins.slint
 - For the C++ code (new item only): the build.rs to export the new item, and the `using` declaration in slint.h
 - Don't forget to update the documentation
*/

#![allow(non_upper_case_globals)]

use crate::qt_window::QPainterPtr;
use const_field_offset::FieldOffsets;
use core::pin::Pin;
use cpp::{cpp, cpp_class};
use i_slint_core::graphics::Color;
use i_slint_core::input::{
    FocusEvent, InputEventFilterResult, InputEventResult, KeyEventResult, MouseEvent,
};
use i_slint_core::item_rendering::{CachedRenderingData, ItemRenderer};
use i_slint_core::items::{Item, ItemConsts, ItemRc, ItemVTable, RenderingResult, VoidArg};
use i_slint_core::layout::{LayoutInfo, Orientation};
use i_slint_core::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize};
#[cfg(feature = "rtti")]
use i_slint_core::rtti::*;
use i_slint_core::window::{WindowAdapter, WindowAdapterRc, WindowInner};
use i_slint_core::{
    Callback, Coord, ItemVTable_static, Property, SharedString, SharedVector, declare_item_vtable,
};
use i_slint_core_macros::*;
use std::ptr::NonNull;
use std::rc::Rc;

type ItemRendererRef<'a> = &'a mut dyn ItemRenderer;

/// Helper macro to get the size from the width and height property,
/// and return Default::default in case the size is too small
macro_rules! get_size {
    ($self:ident) => {{
        let geo = $self.geometry();
        let width = geo.width();
        let height = geo.height();
        if width < 1. || height < 1. {
            return Default::default();
        };
        qttypes::QSize { width: width as _, height: height as _ }
    }};
}

macro_rules! fn_render {
    ($this:ident $dpr:ident $size:ident $painter:ident $widget:ident $initial_state:ident => $($tt:tt)*) => {
        fn render(self: Pin<&Self>, backend: &mut &mut dyn ItemRenderer, item_rc: &ItemRc, size: LogicalSize) -> RenderingResult {
            self.animation_tracker();
            let $dpr: f32 = backend.scale_factor().get();

            let active: bool = backend.window().active();
            // This should include self.enabled() as well, but not every native widget
            // has that property right now.
            let $initial_state = cpp!(unsafe [ active as "bool" ] -> i32 as "int" {
                QStyle::State state(QStyle::State_None);
                if (active)
                    state |= QStyle::State_Active;
                return (int)state;
            });

            let $widget: NonNull<()> = SlintTypeErasedWidgetPtr::qwidget_ptr(&self.widget_ptr);

            if let Some(painter) = backend.as_any().and_then(|any| <dyn std::any::Any>::downcast_mut::<QPainterPtr>(any)) {
                let width = size.width * $dpr;
                let height = size.height * $dpr;
                if width < 1. || height < 1. {
                    return Default::default();
                };
                let $size = qttypes::QSize { width: width as _, height: height as _ };
                let $this = self;
                let _workaround = unsafe { $crate::qt_widgets::PainterClipWorkaround::new(painter) };
                painter.save();
                let $painter = painter;
                $($tt)*
                $painter.restore();
            } else {
                // Fallback: this happen when the Qt backend is not used and the gl backend is used instead
                backend.draw_cached_pixmap(
                    item_rc,
                    &|callback| {
                        let geo = item_rc.geometry();
                        let width = geo.width() * $dpr;
                        let height = geo.height() * $dpr;
                        if width < 1. || height < 1. {
                            return Default::default();
                        };
                        let $size = qttypes::QSize { width: width as _, height: height as _ };
                        let mut imgarray = QImageWrapArray::new($size, $dpr);
                        let img = &mut imgarray.img;
                        let mut painter = cpp!(unsafe [img as "QImage*"] -> QPainterPtr as "std::unique_ptr<QPainter>" {
                            auto painter = std::make_unique<QPainter>(img);
                            painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
                            return painter;
                        });
                        let $painter = &mut painter;
                        let $this = self;
                        $($tt)*
                        drop(painter);
                        imgarray.draw(callback);
                    },
                );
            }
            RenderingResult::ContinueRenderingChildren
        }
    };
}

struct QImageWrapArray {
    /// The image reference the array, so the array must outlive the image without being detached or accessed
    img: qttypes::QImage,
    array: SharedVector<u8>,
}

impl QImageWrapArray {
    pub fn new(size: qttypes::QSize, dpr: f32) -> Self {
        let mut array = SharedVector::default();
        array.resize((size.width * size.height * 4) as usize, 0);
        let array_ptr = array.make_mut_slice().as_mut_ptr();
        let img = cpp!(unsafe [size as "QSize", array_ptr as "uchar*", dpr as "float"] -> qttypes::QImage as "QImage" {
            ensure_initialized();
            QImage img(array_ptr, size.width(), size.height(), size.width() * 4, QImage::Format_RGBA8888_Premultiplied);
            img.setDevicePixelRatio(dpr);
            return img;
        });
        QImageWrapArray { img, array }
    }

    pub fn draw(&self, callback: &mut dyn FnMut(u32, u32, &[u8])) {
        let size = self.img.size();
        callback(size.width, size.height, self.array.as_slice());
    }
}

cpp! {{
    // Note: Do not include <QtWidgets> to avoid inclusion of gl.h (see #10989).
    #include <QtWidgets/QApplication>
    #include <QtWidgets/QStyle>
    #include <QtWidgets/QStyleOption>
    #include <QtWidgets/QStyleFactory>
    #include <QtGui/QPainter>
    #include <QtGui/QClipboard>
    #include <QtGui/QPaintEngine>
    #include <QtCore/QMimeData>
    #include <QtCore/QDebug>
    #include <QtCore/QScopeGuard>

    using QPainterPtr = std::unique_ptr<QPainter>;

    static bool g_lastWindowClosed = false; // global to track window closure when using processEvents().

    /// Make sure there is an instance of QApplication.
    /// The `from_qt_backend` argument specifies if we know that we are running
    /// the Qt backend, or if we are just drawing widgets
    void ensure_initialized(bool from_qt_backend = false)
    {
        if (qApp) {
            return;
        }
        if (!from_qt_backend) {
            // When not using the Qt backend, Qt is not in control of the event loop
            // so we should set this flag.
            QCoreApplication::setAttribute(Qt::AA_PluginApplication, true);
        }

        static QByteArray executable = rust!(Slint_get_executable_name [] -> qttypes::QByteArray as "QByteArray" {
            std::env::args().next().unwrap_or_default().as_bytes().into()
        });

        static int argc  = 1;
        static char *argv[] = { executable.data() };
        // Leak the QApplication, otherwise it crashes on exit
        // (because the QGuiApplication destructor access some Q_GLOBAL_STATIC which are already gone)
        new QApplication(argc, argv);
        qApp->setQuitOnLastWindowClosed(false);
    }

    // HACK ALERT: This struct declaration is duplicated in api/cpp/bindgen.rs - keep in sync.
    struct SlintTypeErasedWidget
    {
        virtual ~SlintTypeErasedWidget() = 0;
        SlintTypeErasedWidget() = default;
        SlintTypeErasedWidget(const SlintTypeErasedWidget&) = delete;
        SlintTypeErasedWidget& operator=(const SlintTypeErasedWidget&) = delete;

        virtual void *qwidget() = 0;
    };

    SlintTypeErasedWidget::~SlintTypeErasedWidget() = default;

    template <typename Base>
    struct SlintAnimatedWidget: public Base, public SlintTypeErasedWidget {
        void *animation_update_property_ptr;
        bool event(QEvent *event) override {
            // QEvent::StyleAnimationUpdate is sent by QStyleAnimation used by Qt builtin styles
            // And we hacked some attribute so that QWidget::update() will emit UpdateLater
            if (event->type() == QEvent::StyleAnimationUpdate  || event->type() == QEvent::UpdateLater) {
                rust!(Slint_AnimatedWidget_update [animation_update_property_ptr: Pin<&Property<i32>> as "void*"] {
                    animation_update_property_ptr.set(animation_update_property_ptr.get() + 1);
                });
                event->accept();
                return true;
            } else {
                return Base::event(event);
            }
        }
        // This seemingly useless cast is needed to adjust the this pointer correctly to point to Base.
        void *qwidget() override { return static_cast<QWidget*>(this); }
    };

    template <typename Base>
    std::unique_ptr<SlintTypeErasedWidget> make_unique_animated_widget(void *animation_update_property_ptr)
    {
        ensure_initialized();
        auto ptr = std::make_unique<SlintAnimatedWidget<Base>>();
        // For our hacks to work, we need to have some invisible parent widget.
        static QWidget globalParent;
        ptr->setParent(&globalParent);
        // Let Qt thinks the widget is visible even if it isn't so update() from animation is forwarded
        ptr->setAttribute(Qt::WA_WState_Visible, true);
        // Hack so update() send a UpdateLater event
        ptr->setAttribute(Qt::WA_WState_InPaintEvent, true);
        ptr->animation_update_property_ptr = animation_update_property_ptr;
        return ptr;
    }
}}

cpp_class!(pub unsafe struct SlintTypeErasedWidgetPtr as "std::unique_ptr<SlintTypeErasedWidget>");

impl SlintTypeErasedWidgetPtr {
    fn qwidget_ptr(this: &std::cell::Cell<Self>) -> NonNull<()> {
        let widget_ptr: *mut SlintTypeErasedWidgetPtr = this.as_ptr();
        cpp!(unsafe [widget_ptr as "std::unique_ptr<SlintTypeErasedWidget>*"] -> NonNull<()> as "void*" {
            return (*widget_ptr)->qwidget();
        })
    }
}

cpp! {{
    // Some style function calls setClipRect or setClipRegion on the painter and replace the clips.
    // eg CE_ItemViewItem, CE_Header, or CC_GroupBox in QCommonStyle (#3541).
    // We do workaround that by setting the clip as a system clip so it cant be overwritten
    struct PainterClipWorkaround {
        QPainter *painter;
        QRegion old_clip;
        explicit PainterClipWorkaround(QPainter *painter) : painter(painter) {
            auto engine = painter->paintEngine();
            old_clip = engine->systemClip();
            auto new_clip = painter->clipRegion() * painter->transform();
            if (!old_clip.isNull())
                new_clip &= old_clip;
            engine->setSystemClip(new_clip);
        }
        ~PainterClipWorkaround() {
            auto engine = painter->paintEngine();
            engine->setSystemClip(old_clip);
            // Qt is seriously bugged, setSystemClip will be scaled by the scale factor
            auto actual_clip = engine->systemClip();
            if (actual_clip != old_clip) {
                QSizeF s2 = actual_clip.boundingRect().size();
                QSizeF s1 = old_clip.boundingRect().size();
                engine->setSystemClip(old_clip * QTransform::fromScale(s1.width() / s2.width(), s1.height() / s2.height()));
            }
        }
        PainterClipWorkaround(const PainterClipWorkaround&) = delete;
        PainterClipWorkaround& operator=(const PainterClipWorkaround&) = delete;
    };
}}
cpp_class!(pub(crate) unsafe struct PainterClipWorkaround as "PainterClipWorkaround");
impl PainterClipWorkaround {
    /// Safety: the painter must outlive us
    pub unsafe fn new(painter: &QPainterPtr) -> Self {
        cpp!(unsafe [painter as "const QPainterPtr*"] -> PainterClipWorkaround as "PainterClipWorkaround" {
            return PainterClipWorkaround(painter->get());
        })
    }
}

mod button;
pub use button::*;

mod checkbox;
pub use checkbox::*;

mod spinbox;
pub use spinbox::*;

mod slider;
pub use slider::*;

mod progress_indicator;
pub use progress_indicator::*;

mod groupbox;
pub use groupbox::*;

mod lineedit;
pub use lineedit::*;

mod scrollview;
pub use scrollview::*;

mod listviewitem;
pub use listviewitem::*;

mod combobox;
pub use combobox::*;

mod tabwidget;
pub use tabwidget::*;

mod stylemetrics;
pub use stylemetrics::*;

mod palette;
pub use palette::*;

mod tableheadersection;
pub use tableheadersection::*;
