mod imp {
    use gtk::gio;
    use gtk::glib;
    use gtk::subclass::prelude::*;
    use patchpal::common::git;
    use patchpal::ui::format::Formatted;
    use std::cell::{Cell, OnceCell, RefCell};
    use std::rc::Rc;

    #[derive(Default)]
    pub(crate) struct CommitListWindow {
        pub(crate) mode: Cell<super::WindowMode>,
        pub(crate) descriptions: RefCell<Vec<Rc<git::Description>>>,
        pub(crate) root_model: RefCell<Option<gio::ListStore>>,
        pub(crate) list_view: RefCell<Option<gtk::ListView>>,
        pub(crate) drop_indicator: RefCell<Option<gtk::TreeExpander>>,
        pub(crate) pending_focus: Cell<Option<u32>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for CommitListWindow {
        const NAME: &'static str = "CommitListWindow";
        type Type = super::CommitListWindow;
        type ParentType = gtk::Window;
    }

    impl ObjectImpl for CommitListWindow {}

    impl WidgetImpl for CommitListWindow {}

    impl WindowImpl for CommitListWindow {}

    #[derive(Default)]
    pub(crate) struct CommitItem {
        pub(crate) description: OnceCell<Rc<git::Description>>,
        pub(crate) marked: Cell<bool>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for CommitItem {
        const NAME: &'static str = "CommitItem";
        type Type = super::CommitItem;
        type ParentType = glib::Object;
    }

    impl ObjectImpl for CommitItem {}

    #[derive(Default)]
    pub(crate) struct DetailsItem {
        pub(crate) formatted: OnceCell<Formatted>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DetailsItem {
        const NAME: &'static str = "DetailsItem";
        type Type = super::DetailsItem;
        type ParentType = glib::Object;
    }

    impl ObjectImpl for DetailsItem {}

    #[derive(Default)]
    pub(crate) struct DiffExpanderItem {
        pub(crate) description: OnceCell<Rc<git::Description>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DiffExpanderItem {
        const NAME: &'static str = "DiffExpanderItem";
        type Type = super::DiffExpanderItem;
        type ParentType = glib::Object;
    }

    impl ObjectImpl for DiffExpanderItem {}

    #[derive(Default)]
    pub(crate) struct DiffItem {
        pub(crate) diff_line: OnceCell<Vec<u8>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DiffItem {
        const NAME: &'static str = "DiffItem";
        type Type = super::DiffItem;
        type ParentType = glib::Object;
    }

    impl ObjectImpl for DiffItem {}
}

use bstr::ByteSlice;
use crate::{app::PatchpalApplication, config::GtkFrontendConfig};
use gtk::{glib, glib::Object, gio, prelude::*};
use gtk::gdk;
use gtk::subclass::prelude::*;
use patchpal::common::git;
use patchpal::ui::format::{Formatted, gtk::ToGtkLabel};
use patchpal::ui::git_utils::{DescriptionFormat, ToDescription};
use std::rc::Rc;

#[derive(Clone, Copy, Default, PartialEq)]
pub(crate) enum WindowMode {
    #[default] ViewOnly,
    Modify,
    Select,
}

glib::wrapper! {
    pub(crate) struct CommitItem(ObjectSubclass<imp::CommitItem>);
}

impl CommitItem {
    pub(crate) fn new(description: Rc<git::Description>) -> Self {
        let item: Self = Object::builder().build();
        let _ = item.imp().description.set(description);
        item
    }

    pub(crate) fn description(&self) -> &Rc<git::Description> {
        self.imp().description.get().unwrap()
    }

    fn marked(&self) -> bool {
        self.imp().marked.get()
    }

    fn toggle_marked(&self) -> bool {
        let new_value = !self.marked();
        self.imp().marked.set(new_value);
        new_value
    }
}

glib::wrapper! {
    pub(crate) struct DetailsItem(ObjectSubclass<imp::DetailsItem>);
}

impl DetailsItem {
    pub(crate) fn new(formatted: Formatted) -> Self {
        let item: Self = Object::builder().build();
        let _ = item.imp().formatted.set(formatted);
        item
    }
}

glib::wrapper! {
    pub(crate) struct DiffExpanderItem(ObjectSubclass<imp::DiffExpanderItem>);
}

impl DiffExpanderItem {
    pub(crate) fn new(description: Rc<git::Description>) -> Self {
        let item: Self = Object::builder().build();
        let _ = item.imp().description.set(description);
        item
    }

    pub(crate) fn description(&self) -> &Rc<git::Description> {
        self.imp().description.get().unwrap()
    }
}

glib::wrapper! {
    pub(crate) struct DiffItem(ObjectSubclass<imp::DiffItem>);
}

impl DiffItem {
    pub(crate) fn new(diff_line: Vec<u8>) -> Self {
        let item: Self = Object::builder().build();
        let _ = item.imp().diff_line.set(diff_line);
        item
    }

    pub(crate) fn diff_line(&self) -> &Vec<u8> {
        self.imp().diff_line.get().unwrap()
    }
}

glib::wrapper! {
    pub(crate) struct CommitListWindow(ObjectSubclass<imp::CommitListWindow>)
        @extends gtk::Window, gtk::Widget,
        @implements
            gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root,
            gtk::ShortcutManager;
}

impl CommitListWindow {
    pub(crate) fn new(application: &PatchpalApplication) -> Self {
        let window: Self = Object::builder()
            .property("application", application)
            .property("title", "Patchpal: commit list")
            .build();
        application.config().restore_window_state(window.upcast_ref(), "commit-list-window");
        window.connect_is_active_notify(|window| {
            if !window.is_active() {
                return;
            }
            if let Some(pos) = window.imp().pending_focus.take() {
                let list_view = window.imp().list_view.borrow();
                list_view.as_ref().unwrap().scroll_to(
                    pos, gtk::ListScrollFlags::FOCUS | gtk::ListScrollFlags::SELECT, None,
                );
            }
        });
        window
    }

    fn application(&self) -> PatchpalApplication {
        let parent: &gtk::Window = self.upcast_ref();
        parent.application().unwrap().downcast().unwrap()
    }

    fn is_modifiable(&self) -> bool {
        self.imp().mode.get() == WindowMode::Modify
    }

    fn is_selectable(&self) -> bool {
        self.imp().mode.get() == WindowMode::Select
    }

    /// Replace the window content with a new list of commits. `expand` is an index to the
    /// commit from the list that should be expanded and focused when the window opens.
    pub(crate) fn set_content(&self,
        title: &str,
        descriptions: &[Rc<git::Description>],
        expand: Option<usize>,
        mode: WindowMode,
    ) {
        self.imp().descriptions.borrow_mut().clear();
        self.imp().descriptions.borrow_mut().extend_from_slice(descriptions);
        self.imp().mode.set(mode);
        self.setup_ui(title, expand);
    }

    fn create_children(&self, item: &glib::Object) -> Option<gio::ListModel> {
        // Return children for expandable items
        if let Some(commit_item) = item.downcast_ref::<CommitItem>() {
            let child_model = gio::ListStore::new::<glib::Object>();
            for formatted in commit_item.description().fmt_header() {
                child_model.append(&DetailsItem::new(formatted));
            }
            child_model.append(&DiffExpanderItem::new(commit_item.description().clone()));
            Some(child_model.upcast::<gio::ListModel>())
        } else if let Some(diff_expander_item) = item.downcast_ref::<DiffExpanderItem>() {
            let child_model = gio::ListStore::new::<DiffItem>();
            let app = self.application();
            let upstream_repo = &app.workspace().upstream_repo;
            for line in diff_expander_item.description().diff(upstream_repo).iter() {
                child_model.append(&DiffItem::new(line.trim_end().to_vec()));
            }
            Some(child_model.upcast::<gio::ListModel>())
        } else {
            // This is a DetailsItem or DiffItem. Nothing to expand.
            None
        }
    }

    fn bind_list_item(&self, list_item: &gtk::ListItem) {
        let dark = self.application().config().get_local("dark_theme").unwrap_or(false);
        let tree_list_row: gtk::TreeListRow = list_item.item().and_downcast().unwrap();
        let expander: gtk::TreeExpander = list_item.child().and_downcast().unwrap();
        let hbox: gtk::Box = expander.child().and_downcast().unwrap();
        let copy_button: gtk::Button = hbox.first_child().and_downcast().unwrap();
        let delete_button: gtk::Button = copy_button.next_sibling().and_downcast().unwrap();
        let select_button: gtk::Button = delete_button.next_sibling().and_downcast().unwrap();
        let check_mark: gtk::Label = select_button.next_sibling().and_downcast().unwrap();
        let label: gtk::Label = hbox.last_child().and_downcast().unwrap();

        expander.set_list_row(Some(&tree_list_row));

        if let Some(commit_item) = tree_list_row.item().and_downcast::<CommitItem>() {
            // Render commit title with attributes
            commit_item.description().fmt_oneline().apply_to_label(&label, dark);
            label.set_css_classes(&["commit-overview"]);
            copy_button.set_visible(true);
            delete_button.set_visible(self.is_modifiable());
            select_button.set_visible(self.is_selectable());
            check_mark.set_visible(commit_item.marked());
        } else if let Some(details_item) = tree_list_row.item().and_downcast::<DetailsItem>() {
            // Render commit details
            details_item.imp().formatted.get().unwrap().apply_to_label(&label, dark);
            label.set_css_classes(&["commit-detail"]);
            copy_button.set_visible(false);
            delete_button.set_visible(false);
            select_button.set_visible(false);
            check_mark.set_visible(false);
        } else if let Some(_diff_expander_item) = tree_list_row.item().and_downcast::<DiffExpanderItem>() {
            // Render diff expander
            label.set_text("diff");
            label.set_css_classes(&["diff-expander"]);
            copy_button.set_visible(false);
            delete_button.set_visible(false);
            select_button.set_visible(false);
            check_mark.set_visible(false);
        } else if let Some(diff_item) = tree_list_row.item().and_downcast::<DiffItem>() {
            // Render diff line
            let diff_line = diff_item.diff_line();
            label.set_text(&diff_line.to_str_lossy());
            label.set_css_classes(&[
                "diff-line",
                match diff_line.get(0) {
                    Some(b'-') => "diff-delete",
                    Some(b'+') => "diff-insert",
                    Some(b' ') => "diff-context",
                    Some(b'@') => "diff-hunk-header",
                    _ => "diff-header"
                },
            ]);
            copy_button.set_visible(false);
            delete_button.set_visible(false);
            select_button.set_visible(false);
            check_mark.set_visible(false);
        }
    }

    fn setup_ui(&self, title: &str, expand: Option<usize>) {
        // Create root model with commits
        let root_model = gio::ListStore::new::<CommitItem>();
        let descriptions = self.imp().descriptions.borrow();
        for description in descriptions.iter() {
            root_model.append(&CommitItem::new(description.clone()));
        }
        *self.imp().root_model.borrow_mut() = Some(root_model.clone());

        // Create TreeListModel with children function
        let tree_model = gtk::TreeListModel::new(
            root_model.clone(),
            false,  // no passthrough
            false,  // no autoexpand
            glib::clone!(
                #[weak(rename_to = window)] self,
                #[upgrade_or] None,
                move |item| window.create_children(&item)
            )
        );

        // Create ListView with TreeListModel
        let selection_model = gtk::SingleSelection::new(Some(tree_model));
        let list_view = gtk::ListView::builder()
            .model(&selection_model)
            .css_classes(["commit-list"])
            .build();
        *self.imp().list_view.borrow_mut() = Some(list_view.clone());

        // Create factory for rendering items
        let factory = gtk::SignalListItemFactory::new();
        factory.connect_setup(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_, list_item| {
                let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();

                let copy_button = gtk::Button::builder()
                    .icon_name("edit-copy-symbolic")
                    .tooltip_text("Copy commit ID")
                    .css_classes(["command"])
                    .focusable(false)
                    .build();
                let delete_button = gtk::Button::builder()
                    .icon_name("edit-delete-symbolic")
                    .tooltip_text("Remove commit")
                    .css_classes(["command"])
                    .focusable(false)
                    .build();
                let select_button = gtk::Button::builder()
                    .icon_name("list-add-symbolic")
                    .tooltip_text("Select commit")
                    .css_classes(["command"])
                    .focusable(false)
                    .build();
                let check_mark = gtk::Label::builder()
                    .label("✔")
                    .margin_start(8)
                    .build();
                let label = gtk::Label::builder()
                    .halign(gtk::Align::Start)
                    .margin_start(8)
                    .build();

                let hbox = gtk::Box::builder()
                    .orientation(gtk::Orientation::Horizontal)
                    .build();
                hbox.append(&copy_button);
                hbox.append(&delete_button);
                hbox.append(&select_button);
                hbox.append(&check_mark);
                hbox.append(&label);

                let expander = gtk::TreeExpander::builder()
                    .child(&hbox)
                    .build();
                list_item.set_child(Some(&expander));

                // Set up the signal handlers here. We can't set them repeatedly in
                // the bind callback; the installed handlers accumulate.
                copy_button.connect_clicked(glib::clone!(
                    #[weak] expander,
                    #[weak] window,
                    move |_| window.copy_commit_id(&expander.list_row().unwrap())
                ));
                delete_button.connect_clicked(glib::clone!(
                    #[weak] expander,
                    #[weak] window,
                    move |_| window.delete_commit(&expander.list_row().unwrap())
                ));
                select_button.connect_clicked(glib::clone!(
                    #[weak] expander,
                    #[weak] window,
                    move |_| window.toggle_commit_mark(&expander.list_row().unwrap())
                ));

                if window.is_modifiable() {
                    window.setup_dnd(list_item, &expander);
                }
            }
        ));
        factory.connect_bind(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_, list_item| {
                let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();
                window.bind_list_item(list_item);
            }
        ));
        list_view.set_factory(Some(&factory));

        if self.is_modifiable() {
            let drop_target = gtk::DropTarget::new(u32::static_type(), gdk::DragAction::MOVE);
            drop_target.connect_drop(glib::clone!(
                #[weak] list_view,
                #[weak(rename_to = window)] self,
                #[upgrade_or] false,
                move |_, value, _, y| window.handle_drop(value, y, &list_view).is_some()
            ));
            drop_target.connect_motion(glib::clone!(
                #[weak] list_view,
                #[weak(rename_to = window)] self,
                #[upgrade_or] gdk::DragAction::empty(),
                move |_, _, y| {
                    window.update_drop_indicator(y, &list_view);
                    gdk::DragAction::MOVE
                }
            ));
            drop_target.connect_leave(glib::clone!(
                #[weak(rename_to = window)] self,
                move |_| window.clear_drop_indicator()
            ));
            list_view.add_controller(drop_target);
        }

        // Handle activation to toggle expand/collapse
        list_view.connect_activate(glib::clone!(
            #[weak] selection_model,
            move |_, position| {
                if let Some(item) = selection_model.item(position) {
                    let row: &gtk::TreeListRow = item.downcast_ref().unwrap();
                    // Toggle expansion for CommitItem and DiffExpanderItem
                    let item_obj = row.item().unwrap();
                    if item_obj.is::<CommitItem>() || item_obj.is::<DiffExpanderItem>() {
                        row.set_expanded(!row.is_expanded());
                    }
                }
            }
        ));

        // Add to scrolled window
        let scrolled = gtk::ScrolledWindow::builder()
            .child(&list_view)
            .vexpand(true)
            .hexpand(true)
            .build();

        // Create title and main container
        let header_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .margin_start(12)
            .margin_end(12)
            .margin_top(6)
            .margin_bottom(6)
            .build();

        let title_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .build();
        let title_label = gtk::Label::builder()
            .label(title)
            .css_classes(["commit-list-title"])
            .halign(gtk::Align::Start)
            .build();
        title_box.append(&title_label);

        let button_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .halign(gtk::Align::End)
            .valign(gtk::Align::Center)
            .hexpand(true)
            .spacing(6)
            .build();

        let hint = match self.imp().mode.get() {
            WindowMode::Modify => {
                let add_button = gtk::Button::builder()
                    .icon_name("list-add-symbolic")
                    .tooltip_text("Add commit (a)")
                    .build();
                add_button.connect_clicked(glib::clone!(
                    #[weak(rename_to = window)] self,
                    move |_| window.add_commit_dialog()
                ));
                let sort_button = gtk::Button::builder()
                    .icon_name("view-sort-ascending-symbolic")
                    .tooltip_text("Sort commits (Ctrl+S)")
                    .build();
                sort_button.connect_clicked(glib::clone!(
                    #[weak(rename_to = window)] self,
                    move |_| window.sort_commits()
                ));
                button_box.append(&add_button);
                button_box.append(&sort_button);
                Some("Drag, Ctrl+↑/↓ to reorder; a to add; Del to remove")
            }
            WindowMode::Select => Some("Space to select; Ctrl+↵ to apply"),
            WindowMode::ViewOnly => None,
        };

        if let Some(hint) = hint {
            let hint_label = gtk::Label::builder()
                .label(hint)
                .css_classes(["info-message"])
                .halign(gtk::Align::Start)
                .build();
            title_box.append(&hint_label);

            let apply_button = gtk::Button::builder()
                .label("Apply")
                .build();
            apply_button.connect_clicked(glib::clone!(
                #[weak(rename_to = window)] self,
                move |_| window.apply_changes()
            ));
            button_box.append(&apply_button);
        }

        header_box.append(&title_box);
        header_box.append(&button_box);

        let main_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .build();
        main_box.append(&header_box);
        main_box.append(&scrolled);

        self.set_child(Some(&main_box));

        // Expand and focus the requested item.
        if let Some(expand) = expand {
            self.expand_and_select_row(&selection_model, &list_view, expand as u32);
        }
        list_view.grab_focus();

        // We need to hide the window instead of closing it. After closing, the reference
        // we hold in DiffModel is still valid but cannot be reused, as the window got
        // destroyed. Solve it by not destroying the window once it's created.
        self.connect_close_request(glib::clone!(
            #[weak(rename_to = window)] self,
            #[upgrade_or] glib::Propagation::Proceed,
            move |_| {
                window.application().config()
                    .store_window_state(window.upcast_ref(), "commit-list-window");
                window.set_visible(false);
                glib::Propagation::Stop
            }
        ));

        // Set up keyboard shortcuts
        let keyctrl = gtk::EventControllerKey::builder()
            .propagation_phase(gtk::PropagationPhase::Capture)
            .build();
        keyctrl.connect_key_pressed(glib::clone!(
            #[weak(rename_to = window)] self,
            #[weak] selection_model,
            #[upgrade_or] glib::Propagation::Proceed,
            move |_, keyval, _, modifiers| {
                if window.key_control(keyval, modifiers, &selection_model) {
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            }
        ));
        scrolled.add_controller(keyctrl);
    }

    fn key_control(
        &self,
        keyval: gdk::Key,
        modifiers: gdk::ModifierType,
        selection_model: &gtk::SingleSelection,
    ) -> bool {
        if modifiers == gtk::gdk::ModifierType::empty() {
            match keyval {
                gdk::Key::Escape => self.set_visible(false),
                gdk::Key::Delete => if self.is_modifiable() {
                    if let Some(item) = selection_model.selected_item() {
                        self.delete_commit(&item.downcast().unwrap());
                    }
                }
                gdk::Key::a => if self.is_modifiable() {
                    self.add_commit_dialog();
                }
                gdk::Key::space => if self.is_selectable() {
                    if let Some(item) = selection_model.selected_item() {
                        self.toggle_commit_mark(&item.downcast().unwrap());
                    }
                }
                _ => return false
            }
            return true;
        } else if modifiers == gtk::gdk::ModifierType::CONTROL_MASK {
            let modifiable = self.is_modifiable();
            match keyval {
                gdk::Key::w => self.set_visible(false),
                gdk::Key::s => if modifiable {
                    self.sort_commits();
                }
                gdk::Key::c => if let Some(selected_item) = selection_model.selected_item() {
                    self.copy_commit_id(&selected_item.downcast().unwrap());
                }
                gdk::Key::Up => if modifiable {
                    self.move_selected_commit(selection_model, -1);
                }
                gdk::Key::Down => if modifiable {
                    self.move_selected_commit(selection_model, 1);
                }
                gdk::Key::Return => if modifiable || self.is_selectable() {
                    self.apply_changes();
                }
                _ => return false
            }
            return true;
        }
        false
    }

    fn expand_and_select_row(
        &self,
        selection_model: &gtk::SingleSelection,
        list_view: &gtk::ListView,
        index: u32,
    ) {
        if let Some(item) = selection_model.item(index) {
            if let Some(row) = item.downcast_ref::<gtk::TreeListRow>() {
                row.set_expanded(true);
                selection_model.set_selected(index);
                list_view.scroll_to(
                    index,
                    gtk::ListScrollFlags::FOCUS | gtk::ListScrollFlags::SELECT,
                    None,
                );
            }
        }
    }

    /// Find the root commit item by traversing up the tree
    fn find_parent_commit(tree_list_row: &gtk::TreeListRow) -> Option<CommitItem> {
        let mut current_row = Some(tree_list_row.clone());
        while let Some(row) = current_row {
            if let Some(commit_item) = row.item().and_downcast::<CommitItem>() {
                return Some(commit_item);
            }
            current_row = row.parent();
        }
        None
    }

    fn copy_commit_id(&self, tree_list_row: &gtk::TreeListRow) {
        if let Some(commit_item) = Self::find_parent_commit(tree_list_row) {
            if let Some(clipboard) = gtk::gdk::Display::default()
                .and_then(|display| Some(display.clipboard()))
            {
                clipboard.set_text(&commit_item.description().id.to_str_lossy());
            }
        }
    }

    fn setup_dnd(&self, list_item: &gtk::ListItem, expander: &gtk::TreeExpander) {
        // TreeExpander auto-expands rows on drag hover; disable that.
        let ctrls = expander.observe_controllers();
        for i in 0 .. ctrls.n_items() {
            if let Some(c) = ctrls.item(i) {
                if c.is::<gtk::DropControllerMotion>() {
                    expander.remove_controller(
                        &c.downcast::<gtk::EventController>().unwrap(),
                    );
                    break;
                }
            }
        }

        let drag_source = gtk::DragSource::builder()
            .actions(gdk::DragAction::MOVE)
            .build();
        drag_source.connect_prepare(glib::clone!(
            #[weak] list_item,
            #[weak(rename_to = window)] self,
            #[upgrade_or] None,
            move |_, _, _| {
                let row: gtk::TreeListRow = list_item.item().and_downcast()?;
                let ci = row.item().and_downcast::<CommitItem>()?;
                let pos = window.find_root_position(&ci)?;
                Some(gdk::ContentProvider::for_value(&pos.to_value()))
            }
        ));
        expander.add_controller(drag_source);
    }

    fn root_model_len(&self) -> u32 {
        self.imp().root_model.borrow()
            .as_ref().map_or(0, |m| m.n_items())
    }

    fn find_root_position(&self, commit_item: &CommitItem) -> Option<u32> {
        let model = self.imp().root_model.borrow();
        let model = model.as_ref()?;
        (0 .. model.n_items()).find(|&i| {
            model.item(i)
                .and_downcast::<CommitItem>()
                .is_some_and(|ci| ci == *commit_item)
        })
    }

    /// Map a root-level CommitItem to its position in the flattened tree model.
    fn find_flat_position(selection_model: &gtk::SingleSelection, commit_item: &CommitItem)
        -> Option<u32>
    {
        (0 .. selection_model.n_items()).find(|&i| {
            selection_model.item(i)
                .and_downcast::<gtk::TreeListRow>()
                .and_then(|row| row.item())
                .and_downcast::<CommitItem>()
                .is_some_and(|ci| ci == *commit_item)
        })
    }

    fn find_ancestor<T: IsA<gtk::Widget>>(widget: &gtk::Widget) -> Option<T> {
        let mut current: Option<gtk::Widget> = Some(widget.clone());
        while let Some(w) = current {
            if let Some(found) = w.downcast_ref::<T>() {
                return Some(found.clone());
            }
            current = w.parent();
        }
        None
    }

    fn handle_drop(&self, value: &glib::Value, y: f64, list_view: &gtk::ListView)
        -> Option<()>
    {
        self.clear_drop_indicator();
        let from = value.get::<u32>().ok()?;
        let widget = list_view.pick(0.0, y, gtk::PickFlags::DEFAULT)?;
        let expander = Self::find_ancestor::<gtk::TreeExpander>(&widget)?;
        let row = expander.list_row()?;
        let commit = Self::find_parent_commit(&row)?;
        let mut to = self.find_root_position(&commit)?;
        if row.item().and_downcast::<CommitItem>().is_some() {
            let point = gtk::graphene::Point::new(0.0, y as f32);
            if let Some(p) = list_view.compute_point(&expander, &point) {
                let h = expander.height() as f32;
                if p.y() > h / 2.0 {
                    to += 1;
                }
            }
        } else {
            to += 1;
        }
        self.reorder_commit(from, to);
        Some(())
    }

    fn clear_drop_indicator(&self) {
        if let Some(exp) = self.imp().drop_indicator.borrow_mut().take() {
            exp.remove_css_class("drop-target-above");
            exp.remove_css_class("drop-target-below");
        }
    }

    fn update_drop_indicator(&self, y: f64, list_view: &gtk::ListView) -> Option<()> {
        self.clear_drop_indicator();
        let widget = list_view.pick(0.0, y, gtk::PickFlags::DEFAULT)?;
        let expander = Self::find_ancestor::<gtk::TreeExpander>(&widget)?;
        let row = expander.list_row()?;
        if row.item().and_downcast::<CommitItem>().is_none() {
            return None;
        }
        let point = gtk::graphene::Point::new(0.0, y as f32);
        let class = if list_view.compute_point(&expander, &point)
            .is_some_and(|p| (p.y() as f64) <= expander.height() as f64 / 2.0)
        {
            "drop-target-above"
        } else {
            "drop-target-below"
        };
        expander.add_css_class(class);
        *self.imp().drop_indicator.borrow_mut() = Some(expander);
        Some(())
    }

    fn scroll_to_commit(&self, commit: &CommitItem) {
        let list_view = self.imp().list_view.borrow();
        let Some(list_view) = list_view.as_ref() else { return };
        let sel_model: gtk::SingleSelection = list_view.model().and_downcast().unwrap();
        if let Some(i) = Self::find_flat_position(&sel_model, commit) {
            list_view.scroll_to(
                i, gtk::ListScrollFlags::FOCUS | gtk::ListScrollFlags::SELECT, None,
            );
            if !self.is_active() {
                // Gtk bug: if the commit add dialog is freshly dismissed, the main window
                // may not have yet received the focus event. When it does later, our
                // scroll_to(FOCUS) will get overwritten. We'll need to restore it.
                self.imp().pending_focus.set(Some(i));
            }
        }
    }

    fn delete_commit(&self, tree_list_row: &gtk::TreeListRow) {
        let Some(commit) = Self::find_parent_commit(tree_list_row) else { return };
        if self.root_model_len() <= 1 {
            return;
        }
        let Some(pos) = self.find_root_position(&commit) else { return };
        let focus_commit = {
            let model = self.imp().root_model.borrow();
            let Some(model) = model.as_ref() else { return };
            model.remove(pos);
            let focus_pos = pos.min(model.n_items().saturating_sub(1));
            model.item(focus_pos).and_downcast::<CommitItem>()
        };
        if let Some(ci) = focus_commit {
            self.scroll_to_commit(&ci);
        }
    }

    fn move_selected_commit(&self, selection_model: &gtk::SingleSelection, direction: i32) {
        let selected = selection_model.selected_item();
        let row: gtk::TreeListRow = selected.and_downcast().unwrap();
        let Some(commit) = Self::find_parent_commit(&row) else { return; };
        let Some(pos) = self.find_root_position(&commit) else { return; };
        let n_items = self.root_model_len() as i32;
        let to = if direction > 0 { pos as i32 + 2 } else { pos as i32 - 1 };
        if to < 0 || to > n_items {
            return;
        }
        self.reorder_commit(pos, to as u32);
        self.scroll_to_commit(&commit);
    }

    fn reorder_commit(&self, from: u32, to: u32) -> Option<()> {
        // Dragged to the position right before the source position or right after the source
        // position.
        if from == to || from + 1 == to {
            return None;
        }
        let model = self.imp().root_model.borrow();
        let model = model.as_ref()?;
        let item = model.item(from)?;
        model.remove(from);
        let to = if to > from { to - 1 } else { to };
        let ci: CommitItem = item.downcast().unwrap();
        model.insert(to, &ci);
        Some(())
    }

    fn add_commit_dialog(&self) {
        let entry = gtk::Entry::builder()
            .placeholder_text("commit hash, tag, branch...")
            .activates_default(true)
            .hexpand(true)
            .build();
        let ok_button = gtk::Button::builder()
            .label("Add")
            .build();
        let cancel_button = gtk::Button::builder()
            .label("Cancel")
            .build();
        let button_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .halign(gtk::Align::End)
            .spacing(6)
            .build();
        button_box.append(&ok_button);
        button_box.append(&cancel_button);
        let vbox = gtk::Box::builder()
            .margin_bottom(10)
            .margin_end(10)
            .margin_start(10)
            .margin_top(10)
            .orientation(gtk::Orientation::Vertical)
            .spacing(10)
            .build();
        vbox.append(&entry);
        vbox.append(&button_box);
        let dialog = gtk::Window::builder()
            .child(&vbox)
            .default_widget(&ok_button)
            .modal(true)
            .title("Add commit")
            .transient_for(self)
            .build();
        ok_button.connect_clicked(glib::clone!(
            #[weak] dialog,
            #[weak] entry,
            #[weak(rename_to = window)] self,
            move |_| {
                let text = entry.buffer().text();
                if !text.is_empty() {
                    dialog.close();
                    window.add_commit(text.as_str());
                }
            }
        ));
        cancel_button.connect_clicked(glib::clone!(
            #[weak] dialog,
            move |_| dialog.close()
        ));
        let keyctrl = gtk::EventControllerKey::new();
        keyctrl.connect_key_pressed(glib::clone!(
            #[weak] dialog,
            #[upgrade_or] glib::Propagation::Proceed,
            move |_, keyval, _, _| {
                if keyval == gdk::Key::Escape {
                    dialog.close();
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            }
        ));
        dialog.add_controller(keyctrl);
        dialog.present();
    }

    fn add_commit(&self, commitish: &str) {
        let app = self.application();
        let repo = &app.workspace().upstream_repo;
        match commitish.to_description(repo) {
            Ok(description) => {
                let item = CommitItem::new(description.clone());
                {
                    let model = self.imp().root_model.borrow();
                    let model = model.as_ref().unwrap();
                    let pos = (0 .. model.n_items())
                        .find(|&i|
                            model.item(i).and_downcast::<CommitItem>()
                                .is_some_and(|ci| *ci.description() > description)
                        )
                        .unwrap_or(model.n_items());
                    model.insert(pos, &item);
                }
                self.scroll_to_commit(&item);
            }
            Err(msg) => {
                let alert = gtk::AlertDialog::builder()
                    .detail(&msg)
                    .message("Cannot add commit")
                    .modal(true)
                    .build();
                alert.show(Some(self));
            }
        }
    }

    fn sort_commits(&self) {
        let model = self.imp().root_model.borrow();
        model.as_ref().unwrap().sort(|a, b| {
            let a: &CommitItem = a.downcast_ref().unwrap();
            let b: &CommitItem = b.downcast_ref().unwrap();
            a.description().cmp(b.description())
        });
    }

    fn toggle_commit_mark(&self, tree_list_row: &gtk::TreeListRow) {
        let Some(commit) = Self::find_parent_commit(tree_list_row) else { return; };
        commit.toggle_marked();
        let Some(pos) = self.find_root_position(&commit) else { return; };
        let model = self.imp().root_model.borrow();
        model.as_ref().unwrap().items_changed(pos, 1, 1);
    }

    fn apply_changes(&self) {
        match self.imp().mode.get() {
            WindowMode::Modify => {
                let model = self.imp().root_model.borrow();
                let Some(model) = model.as_ref() else { return };
                let new_commits: Vec<Rc<git::Description>> = model.iter::<CommitItem>()
                    .map(|commit_item_or_err|
                        commit_item_or_err.unwrap().description().clone()
                    )
                    .collect();
                self.set_visible(false);
                self.application().workspace().change_commits(new_commits);
            }
            WindowMode::Select => {
                let model = self.imp().root_model.borrow();
                let Some(model) = model.as_ref() else { return };
                let selected_commits: Vec<Rc<git::Description>> = model.iter::<CommitItem>()
                    .filter(|item| item.as_ref().unwrap().marked())
                    .map(|item| item.unwrap().description().clone())
                    .collect();
                self.set_visible(false);
                self.application().workspace().add_commits(selected_commits);
            }
            WindowMode::ViewOnly => {}
        }
    }
}
