use adw::subclass::prelude::*;
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};

mod imp {
    use std::cell::RefCell;

    use glib::{subclass::InitializingObject, SignalHandlerId};

    use super::*;

    #[repr(C)]
    pub struct ContextMenuBinClass {
        pub parent_class: glib::object::Class<adw::Bin>,
        pub menu_opened: fn(&super::ContextMenuBin),
    }

    unsafe impl ClassStruct for ContextMenuBinClass {
        type Type = ContextMenuBin;
    }

    pub(super) fn context_menu_bin_menu_opened(this: &super::ContextMenuBin) {
        let klass = this.class();
        (klass.as_ref().menu_opened)(this)
    }

    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
    #[template(resource = "/org/gnome/Fractal/ui/components/context_menu_bin.ui")]
    #[properties(wrapper_type = super::ContextMenuBin)]
    pub struct ContextMenuBin {
        #[template_child]
        pub click_gesture: TemplateChild<gtk::GestureClick>,
        #[template_child]
        pub long_press_gesture: TemplateChild<gtk::GestureLongPress>,
        /// The popover displaying the context menu.
        #[property(get, set = Self::set_popover, explicit_notify, nullable)]
        pub popover: RefCell<Option<gtk::PopoverMenu>>,
        pub signal_handler: RefCell<Option<SignalHandlerId>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for ContextMenuBin {
        const NAME: &'static str = "ContextMenuBin";
        const ABSTRACT: bool = true;
        type Type = super::ContextMenuBin;
        type ParentType = adw::Bin;
        type Class = ContextMenuBinClass;

        fn class_init(klass: &mut Self::Class) {
            Self::bind_template(klass);

            klass.install_action("context-menu.activate", None, move |widget, _, _| {
                widget.open_menu_at(0, 0)
            });
            klass.add_binding_action(
                gdk::Key::F10,
                gdk::ModifierType::SHIFT_MASK,
                "context-menu.activate",
                None,
            );
            klass.add_binding_action(
                gdk::Key::Menu,
                gdk::ModifierType::empty(),
                "context-menu.activate",
                None,
            );

            klass.install_action("context-menu.close", None, move |widget, _, _| {
                if let Some(popover) = widget.popover() {
                    popover.popdown();
                }
            });
        }

        fn instance_init(obj: &InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for ContextMenuBin {
        fn constructed(&self) {
            let obj = self.obj();

            self.long_press_gesture
                .connect_pressed(clone!(@weak obj => move |gesture, x, y| {
                    gesture.set_state(gtk::EventSequenceState::Claimed);
                    gesture.reset();
                    obj.open_menu_at(x as i32, y as i32);
                }));

            self.click_gesture.connect_released(
                clone!(@weak obj => move |gesture, n_press, x, y| {
                    if n_press > 1 {
                        return;
                    }

                    gesture.set_state(gtk::EventSequenceState::Claimed);
                    obj.open_menu_at(x as i32, y as i32);
                }),
            );
            self.parent_constructed();
        }

        fn dispose(&self) {
            if let Some(popover) = self.popover.take() {
                if let Some(handler) = self.signal_handler.take() {
                    popover.disconnect(handler);
                }

                popover.unparent();
            }
        }
    }

    impl WidgetImpl for ContextMenuBin {}
    impl BinImpl for ContextMenuBin {}

    impl ContextMenuBin {
        /// Set the popover displaying the context menu.
        fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
            if *self.popover.borrow() == popover {
                return;
            }

            let obj = self.obj();

            if let Some(popover) = &popover {
                popover.unparent();
                popover.set_parent(&*obj);

                self.signal_handler
                    .replace(Some(popover.connect_parent_notify(
                        clone!(@weak obj => move |popover| {
                            if popover.parent().as_ref() != Some(obj.upcast_ref()) {
                                let imp = obj.imp();
                                if let Some(popover) = imp.popover.take() {
                                    if let Some(signal_handler) = imp.signal_handler.take() {
                                        popover.disconnect(signal_handler)
                                    }
                                }
                            }
                        }),
                    )));
            }

            self.popover.replace(popover);
            obj.notify_popover();
        }
    }
}

glib::wrapper! {
    /// A Bin widget that adds a context menu.
    pub struct ContextMenuBin(ObjectSubclass<imp::ContextMenuBin>)
        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}

impl ContextMenuBin {
    fn open_menu_at(&self, x: i32, y: i32) {
        self.menu_opened();

        if let Some(popover) = self.popover() {
            popover.set_pointing_to(Some(&gdk::Rectangle::new(x, y, 0, 0)));
            popover.popup();
        }
    }
}

pub trait ContextMenuBinExt: 'static {
    /// Get the `PopoverMenu` used in the context menu.
    fn popover(&self) -> Option<gtk::PopoverMenu>;

    /// Set the `PopoverMenu` used in the context menu.
    fn set_popover(&self, popover: Option<gtk::PopoverMenu>);

    /// Called when the menu was requested to open but before the menu is shown.
    fn menu_opened(&self);
}

impl<O: IsA<ContextMenuBin>> ContextMenuBinExt for O {
    fn popover(&self) -> Option<gtk::PopoverMenu> {
        self.upcast_ref().popover()
    }

    fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
        self.upcast_ref().set_popover(popover);
    }

    fn menu_opened(&self) {
        imp::context_menu_bin_menu_opened(self.upcast_ref())
    }
}

/// Public trait that must be implemented for everything that derives from
/// `ContextMenuBin`.
///
/// Overriding a method from this Trait overrides also its behavior in
/// `ContextMenuBinExt`.
pub trait ContextMenuBinImpl: BinImpl {
    /// Called when the menu was requested to open but before the menu is shown.
    ///
    /// This method should be used to set the popover dynamically.
    fn menu_opened(&self) {}
}

unsafe impl<T> IsSubclassable<T> for ContextMenuBin
where
    T: ContextMenuBinImpl,
    T::Type: IsA<ContextMenuBin>,
{
    fn class_init(class: &mut glib::Class<Self>) {
        Self::parent_class_init::<T>(class.upcast_ref_mut());

        let klass = class.as_mut();

        klass.menu_opened = menu_opened_trampoline::<T>;
    }
}

// Virtual method implementation trampolines.
fn menu_opened_trampoline<T>(this: &ContextMenuBin)
where
    T: ObjectSubclass + ContextMenuBinImpl,
    T::Type: IsA<ContextMenuBin>,
{
    let this = this.downcast_ref::<T::Type>().unwrap();
    this.imp().menu_opened()
}
