mod imp {
    use crate::editor::Editor;
    use gtk::glib;
    use gtk::subclass::prelude::*;
    use std::cell::{Cell, OnceCell};

    #[derive(Default)]
    pub(crate) struct PatchpalAppWindow {
        pub(crate) editor: OnceCell<Editor>,
        pub(crate) viewer: OnceCell<gtk::ColumnView>,
        pub(crate) commit_widgets: OnceCell<super::CommitWidgets>,
        pub(crate) file_widgets: OnceCell<super::FileWidgets>,
        pub(crate) commit_select: OnceCell<gtk::DropDown>,
        pub(crate) commit_label: OnceCell<gtk::Label>,
        pub(crate) started: Cell<bool>,
        pub(crate) updating_commit: Cell<bool>,
    }

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

    impl ObjectImpl for PatchpalAppWindow {}
    impl WidgetImpl for PatchpalAppWindow {}
    impl WindowImpl for PatchpalAppWindow {}
    impl ApplicationWindowImpl for PatchpalAppWindow {}
}

use crate::{
    app::PatchpalApplication, commit_list_window::WindowMode, commits_model,
    config::GtkFrontendConfig, diff_model, diff_widget, editor::Editor, files_model,
};
use gtk::{gdk::Key, gdk::ModifierType, glib};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use patchpal::ui::{
    blocks, format::gtk::ToGtkLabel, frontend::ListChange, workspace::StoreMode,
};
use std::ffi::OsString;

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

#[derive(Debug)]
pub(crate) struct CommitWidgets {
    button_next_box: gtk::Box,
    button_commit: gtk::Button,
}

#[derive(Debug)]
pub(crate) struct FileWidgets {
    select: gtk::DropDown,
    label_files: gtk::Label,
    label_hunks: gtk::Label,
    button_next_unacked: gtk::Button,
    button_ack_clean: gtk::Button,
    button_ack_menu: gtk::MenuButton,
}

impl PatchpalAppWindow {
    pub(crate) fn new(app: &PatchpalApplication) -> Self {
        glib::Object::builder()
            .property("application", app)
            .build()
    }

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

    fn viewer(&self) -> &gtk::ColumnView {
        self.imp().viewer.get().unwrap()
    }

    fn commit_widgets(&self) -> &CommitWidgets {
        &self.imp().commit_widgets.get().unwrap()
    }

    fn file_widgets(&self) -> &FileWidgets {
        &self.imp().file_widgets.get().unwrap()
    }

    fn model(&self) -> Option<diff_model::DiffModel> {
        let sel_model: gtk::SingleSelection = self.viewer()
            .model().unwrap().downcast().unwrap();
        sel_model.model().map(|model| model.downcast().unwrap())
    }

    fn max_number_width(&self) -> i32 {
        let label = gtk::Label::builder()
            .label("999999")
            .single_line_mode(true)
            .css_classes(["number"])
            .build();
        label.measure(gtk::Orientation::Horizontal, -1).1
    }

    fn viewer_content_factory(&self, backport_side: bool) -> gtk::SignalListItemFactory {
        let factory = gtk::SignalListItemFactory::new();
        factory.connect_setup(move |_, list_item| {
            let list_item: &gtk::ColumnViewCell = list_item.downcast_ref().unwrap();
            list_item.set_child(Some(&diff_widget::DiffWidget::new(backport_side)));
        });
        factory.connect_bind(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_, list_item| {
                let list_item: &gtk::ColumnViewCell = list_item.downcast_ref().unwrap();
                let widget: diff_widget::DiffWidget =
                    list_item.child().unwrap().downcast().unwrap();
                let obj: diff_model::DiffModelItem =
                    list_item.item().unwrap().downcast().unwrap();
                widget.set_content(&window.model().unwrap(), &obj);
            }
        ));
        factory.connect_unbind(move |_, list_item| {
            let list_item: &gtk::ColumnViewCell = list_item.downcast_ref().unwrap();
            let widget: diff_widget::DiffWidget = list_item.child().unwrap().downcast().unwrap();
            widget.unset_content();
        });
        factory
    }

    fn create_viewer(&self, column_width: i32) -> gtk::ColumnView {
        let sel_model = gtk::SingleSelection::builder()
            .autoselect(true)
            .can_unselect(false)
            .build();
        let viewer = gtk::ColumnView::builder()
            .model(&sel_model)
            .show_column_separators(true)
            .reorderable(false)
            .vexpand(true)
            .tab_behavior(gtk::ListTabBehavior::Cell)
            .build();
        sel_model.connect_selected_notify(move |sel_model| {
            if let Some(model) = sel_model.model() {
                let model: diff_model::DiffModel = model.downcast().unwrap();
                model.record_focus(sel_model.selected() as usize);
            }
        });
        viewer.connect_activate(glib::clone!(
            #[weak] sel_model,
            move |_, idx| {
                if let Some(model) = sel_model.model() {
                    let model: diff_model::DiffModel = model.downcast().unwrap();
                    model.activate_row_handler(idx)
                }
            }
        ));

        let factory_number = gtk::SignalListItemFactory::new();
        factory_number.connect_setup(move |_, list_item| {
            let list_item: &gtk::ColumnViewCell = list_item.downcast_ref().unwrap();
            let label = gtk::Label::builder()
                .single_line_mode(true)
                .halign(gtk::Align::End)
                .css_classes(["number"])
                .build();
            list_item.set_child(Some(&label));
        });
        factory_number.connect_bind(move |_, list_item| {
            let list_item: &gtk::ColumnViewCell = list_item.downcast_ref().unwrap();
            let label: gtk::Label = list_item.child().unwrap().downcast().unwrap();
            let obj: diff_model::DiffModelItem = list_item.item().unwrap().downcast().unwrap();
            if let Some(number) = obj.line_number() {
                label.set_label(&number.to_string());
            } else {
                label.set_label("");
            }
        });
        let col_number = gtk::ColumnViewColumn::builder()
            .factory(&factory_number)
            .fixed_width(self.max_number_width())
            .build();
        viewer.append_column(&col_number);

        let col_content = gtk::ColumnViewColumn::builder()
            .factory(&self.viewer_content_factory(true))
            .fixed_width(column_width)
            .resizable(true)
            .title("target")
            .build();
        viewer.append_column(&col_content);
        let col_content = gtk::ColumnViewColumn::builder()
            .factory(&self.viewer_content_factory(false))
            .fixed_width(1)
            .expand(true)
            .resizable(false)
            .title("original")
            .build();
        viewer.append_column(&col_content);

        viewer
    }

    fn create_commit_list(&self, dark: bool) -> gtk::Box {
        let app = self.application();
        let commits: gtk::Widget = if !app.workspace().multicommit_capable() {
            let title = app.workspace().commit_title("");
            let label_commit = title.to_label(dark);
            label_commit.set_margin_start(10);
            label_commit.set_margin_end(10);
            label_commit.set_hexpand(true);
            label_commit.set_halign(gtk::Align::Start);
            label_commit.upcast()
        } else {
            let list = commits_model::CommitsModel::new(app.workspace(), 1);

            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(move |_, list_item| {
                let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();
                let label = gtk::Label::builder()
                    .margin_end(10)
                    .hexpand(true)
                    .halign(gtk::Align::Start)
                    .build();
                list_item.set_child(Some(&label));
            });
            factory.connect_bind(glib::clone!(
                #[weak] list,
                move |_, list_item| {
                    let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();
                    let obj: commits_model::CommitsModelItem =
                        list_item.item().unwrap().downcast().unwrap();
                    let label: gtk::Label = list_item.child().unwrap().downcast().unwrap();
                    obj.title().apply_to_label(&label, dark);
                    let sel = list_item.position() < list.selectable_count();
                    list_item.set_activatable(sel);
                    label.set_sensitive(sel);
                }
            ));

            let select = gtk::DropDown::builder()
                .model(&list)
                .factory(&factory)
                .selected(0)
                .hexpand(true)
                .build();

            select.connect_selected_notify(glib::clone!(
                #[weak] list,
                #[weak] app,
                #[weak(rename_to = window)] self,
                move |sel| {
                    let sc = list.selectable_count();
                    if sel.selected() >= sc {
                        sel.set_selected(sc - 1);
                        return;
                    }
                    // Do not call jump_to_commit when we're changing the selection in
                    // response to commit_list_changed. The selected item may not be accurate.
                    if window.imp().updating_commit.get() {
                        return;
                    }
                    let index = sel.selected() as usize;
                    if let Err(msg) = app.workspace().jump_to_commit(index) {
                        eprintln!("{msg}");
                        app.workspace().abort();
                    }
                }
            ));

            self.imp().commit_select.set(select.clone()).unwrap();
            select.upcast()
        };

        let has_description = if let Ok(commit) = app.workspace().commit() {
            commit.has_metadata()
        } else {
            false
        };

        let button_next = gtk::Button::builder()
            .label("⏩ next")
            .tooltip_text("Advance to next commit")
            .build();
        let button_commit = gtk::Button::builder()
            .label(match app.workspace().store_mode.get() {
                StoreMode::Commit => "🏃 commit",
                StoreMode::Stage(_) | StoreMode::Save => "💾 save",
            })
            .tooltip_text(match app.workspace().store_mode.get() {
                StoreMode::Commit => "Commit changes to repository",
                StoreMode::Stage(_) | StoreMode::Save => "Save merged changes",
            })
            .build();
        let button_commit_list = gtk::Button::builder()
            .label("🗐")
            .css_classes(["narrow"])
            .visible(has_description)
            .tooltip_text("Show and manipulate the commit list")
            .build();
        let button_kbd_help = gtk::Button::builder()
            .label("⌨❓")
            .css_classes(["narrow"])
            .tooltip_text("Show keyboard shortcuts")
            .build();
        let button_settings = gtk::Button::builder()
            .icon_name("applications-system-symbolic")
            .css_classes(["narrow"])
            .tooltip_text("Open settings")
            .build();

        button_next.connect_clicked(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_| window.goto_next_commit()
        ));
        button_commit.connect_clicked(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_| window.save_and_commit()
        ));

        let ff_action = gtk::gio::SimpleAction::new("fast-forward", None);
        ff_action.connect_activate(glib::clone!(
            #[weak] app,
            move |_, _| app.fast_forward(false)
        ));
        app.add_action(&ff_action);

        let ff_commit_action = gtk::gio::SimpleAction::new("fast-forward-commit", None);
        ff_commit_action.connect_activate(glib::clone!(
            #[weak] app,
            move |_, _| app.fast_forward(true)
        ));
        app.add_action(&ff_commit_action);

        let next_menu = gtk::gio::Menu::new();
        next_menu.append(Some("fast forward"), Some("app.fast-forward"));
        next_menu.append(Some("fast forward and commit"), Some("app.fast-forward-commit"));
        let button_next_menu = gtk::MenuButton::builder()
            .icon_name("pan-down-symbolic")
            .menu_model(&next_menu)
            .css_classes(["narrow"])
            .tooltip_text("More commit options")
            .build();
        let popover = button_next_menu.popover().unwrap();
        popover.set_has_arrow(false);
        popover.set_halign(gtk::Align::Start);
        if let Ok(w) = popover.child().unwrap().downcast::<gtk::ScrolledWindow>() {
            w.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Never);
        }

        button_commit_list.connect_clicked(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_| window.show_commit_list()
        ));
        button_kbd_help.connect_clicked(glib::clone!(
            #[weak] app,
            move |_| app.keyboard_help()
        ));
        button_settings.connect_clicked(glib::clone!(
            #[weak] app,
            move |_| app.show_settings()
        ));

        let hbox = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .build();
        if app.workspace().multicommit_capable() {
            let label = gtk::Label::builder()
                .margin_start(10)
                .margin_end(10)
                .build();
            self.imp().commit_label.set(label.clone()).unwrap();
            hbox.append(&label);
        }
        let button_next_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .css_classes(["linked"])
            .build();
        button_next_box.append(&button_next);
        button_next_box.append(&button_next_menu);

        hbox.append(&button_next_box);
        hbox.append(&button_commit);
        hbox.append(&button_commit_list);
        hbox.append(&commits);
        hbox.append(&button_kbd_help);
        hbox.append(&button_settings);

        let commit_widgets = CommitWidgets {
            button_next_box,
            button_commit,
        };
        self.imp().commit_widgets.set(commit_widgets).unwrap();
        hbox
    }

    fn create_file_list(&self, dark: bool) -> gtk::Box {
        let app = self.application();
        let factory = gtk::SignalListItemFactory::new();
        factory.connect_setup(move |_, list_item| {
            let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();
            let hbox = gtk::Box::builder()
                .orientation(gtk::Orientation::Horizontal)
                .build();
            let status = gtk::Label::builder()
                .build();
            let label = gtk::Label::builder()
                .margin_start(10)
                .build();
            hbox.append(&status);
            hbox.append(&label);
            list_item.set_child(Some(&hbox));
        });
        factory.connect_bind(move |_, list_item| {
            let list_item: &gtk::ListItem = list_item.downcast_ref().unwrap();
            let obj: files_model::FilesModelItem = list_item.item().unwrap().downcast().unwrap();
            let hbox: gtk::Box = list_item.child().unwrap().downcast().unwrap();
            let status: gtk::Label = hbox.first_child().unwrap().downcast().unwrap();
            let label: gtk::Label = status.next_sibling().unwrap().downcast().unwrap();
            let unacked = obj.unacked();
            if unacked > 0 {
                status.set_label(&unacked.to_string());
                status.set_css_classes(&["unacked"])
            } else {
                status.set_label("✓");
                status.set_css_classes(&["all-acked"])
            }
            obj.name().apply_to_label(&label, dark);
        });

        let select = gtk::DropDown::builder()
            .factory(&factory)
            .selected(0)
            .hexpand(true)
            .build();
        select.connect_selected_notify(glib::clone!(
            #[weak(rename_to = window)] self,
            move |sel| {
                if let Some(model) = window.model() {
                    let index = model.switch(sel.selected() as usize);
                    window.viewer().scroll_to(
                        index as u32,
                        None,
                        gtk::ListScrollFlags::FOCUS | gtk::ListScrollFlags::SELECT,
                        None
                    );
                }
            }
        ));

        let label_files = gtk::Label::builder()
            .margin_start(10)
            .margin_end(10)
            .build();
        let label_hunks = gtk::Label::builder()
            .margin_start(5)
            .margin_end(10)
            .build();
        let button_next_unacked = gtk::Button::builder()
            .label("🮶")
            .css_classes(["narrow"])
            .tooltip_text("Go to next unaccepted hunk")
            .build();
        let button_ack_clean = gtk::Button::builder()
            .label("🧹 accept clean")
            .css_classes(["narrow"])
            .tooltip_text("Accept all clean hunks")
            .build();

        button_next_unacked.connect_clicked(glib::clone!(
            #[weak(rename_to = window)] self,
            move |_| window.goto_next_unacked(false)
        ));
        button_ack_clean.connect_clicked(glib::clone!(
            #[weak] app,
            move |_| app.workspace().command_ack_all_clean()
        ));

        let auto_accept_clean_action = gtk::gio::SimpleAction::new_stateful(
            "auto-accept-clean",
            None,
            &app.get_auto_accept_clean().to_variant(),
        );
        auto_accept_clean_action.connect_activate(glib::clone!(
            #[weak] app,
            move |action, _| {
                let new_state = !action.state().unwrap().get::<bool>().unwrap();
                action.set_state(&new_state.to_variant());
                app.set_auto_accept_clean(new_state);
            }
        ));
        app.add_action(&auto_accept_clean_action);

        let menu = gtk::gio::Menu::new();
        menu.append(Some("auto accept clean"), Some("app.auto-accept-clean"));
        let button_ack_menu = gtk::MenuButton::builder()
            .label("🧹")
            .menu_model(&menu)
            .css_classes(["narrow"])
            .tooltip_text("More acceptance options")
            .build();
        let popover = button_ack_menu.popover().unwrap();
        popover.set_has_arrow(false);
        popover.set_halign(gtk::Align::Start);
        // disable scrollbars as they mess up the rendering
        if let Ok(w) = popover.child().unwrap().downcast::<gtk::ScrolledWindow>() {
            w.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Never);
        }

        let button_ack_box = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .css_classes(["linked"])
            .build();
        button_ack_box.append(&button_ack_clean);
        button_ack_box.append(&button_ack_menu);

        let hbox = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .build();
        hbox.append(&label_files);
        hbox.append(&label_hunks);
        hbox.append(&button_next_unacked);
        hbox.append(&button_ack_box);
        hbox.append(&select);

        let file_widgets = FileWidgets {
            select,
            label_files,
            label_hunks,
            button_next_unacked,
            button_ack_clean,
            button_ack_menu,
        };
        self.imp().file_widgets.set(file_widgets).unwrap();
        hbox
    }

    fn update_labels_and_buttons(&self) {
        let file_widgets = self.file_widgets();
        let count = self.application().workspace().count_unacked();
        let is_last = self.application().workspace().is_last_commit();
        file_widgets.label_hunks.set_label(
            &format!("{} hunk{} to review", count, if count == 1 { "" } else { "s" })
        );
        file_widgets.label_hunks.set_visible(count > 0);
        file_widgets.button_next_unacked.set_visible(count > 0);
        let has_clean = self.application().workspace().count_unacked_clean() > 0;
        file_widgets.button_ack_clean.set_visible(has_clean);
        if has_clean {
            file_widgets.button_ack_menu.set_icon_name("pan-down-symbolic");
        } else {
            file_widgets.button_ack_menu.set_label("🧹");
        }
        let commit_widgets = self.commit_widgets();
        commit_widgets.button_next_box.set_visible(count == 0 && !is_last);
        commit_widgets.button_commit.set_visible(count == 0 && is_last);
    }

    fn goto_next_commit(&self) {
        if !self.application().workspace().is_resolved() {
            return;
        }
        let app = self.application();
        if let Err(msg) = app.workspace().apply_next() {
            eprintln!("{msg}");
            app.workspace().abort();
        }
    }

    fn goto_prev_commit(&self) {
        let app = self.application();
        if let Err(msg) = app.workspace().apply_prev() {
            eprintln!("{msg}");
            app.workspace().abort();
        }
    }

    fn save_and_commit(&self) {
        let app = self.application();
        if !app.workspace().is_resolved() || !app.workspace().is_last_commit() {
            return;
        }
        if let Err(msg) = app.workspace().save() {
            gtk::AlertDialog::builder()
                .modal(true)
                .message("Cannot commit")
                .detail(msg)
                .build()
                .show(Some(self));
        } else {
            self.stop();
        }
    }

    /// Start the window if it has not been already started, or reinit it if it has been. The
    /// `commit_index` specifies which commit is being shown.
    pub(crate) fn show_commit(&self, commit_index: usize) {
        if self.imp().started.replace(true) {
            self.reset_view(commit_index);
            return;
        }
        let app = self.application();
        app.workspace().telemetry().tele_set("gui", true);
        let cfg = app.config();
        cfg.restore_window_state(self.upcast_ref(), "window");
        self.imp().editor.set(Editor::new(&app)).unwrap();
        let viewer = self.create_viewer(cfg.get("window-column").unwrap_or(500));
        self.imp().viewer.set(viewer.clone()).unwrap();
        let dark = cfg.get_local("dark_theme").unwrap_or(false);
        let file_list = self.create_file_list(dark);
        let commit_list = self.create_commit_list(dark);

        self.connect_close_request(glib::clone!(
            #[weak] app,
            #[upgrade_or] glib::Propagation::Proceed,
            move |window| {
                let modified = app.workspace().modified();
                if modified {
                    gtk::AlertDialog::builder()
                        .modal(true)
                        .message("Exit?")
                        .detail("You have uncommitted changes. Discard them and exit?")
                        .buttons(["_Yes", "_No"])
                        .default_button(0)
                        .cancel_button(1)
                        .build()
                        .choose(Some(window), None::<&gtk::gio::Cancellable>, glib::clone!(
                            #[weak] app,
                            #[weak] window,
                            move |result| {
                                if matches!(result, Ok(button) if button == 0) {
                                    app.workspace().clear_modified();
                                    window.close();
                                }
                            }));
                } else {
                    app.workspace().abort();
                    window.stop();
                }
                glib::Propagation::Stop
            }
        ));

        let root = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .build();
        root.append(&commit_list);
        root.append(&file_list);
        root.append(&gtk::ScrolledWindow::builder()
            .child(&viewer)
            .overlay_scrolling(false)
            .build()
        );

        // Gtk bug: if the selected (= focused) item in the ColumnView's model is changed
        // (and emits items-changed) while another window has focus, the current window focus
        // is reset. In our case, it means the first button in the top bar gets focused.
        // There doesn't seem to be a good workaround. Just refocus to `viewer` whenever our
        // window gets focus. While it may be a bit unexpected in certain situations, it's
        // still better than losing focus when e.g. starting an editor while the missing
        // commits line is still in progress.
        self.connect_is_active_notify(glib::clone!(
            #[weak] viewer,
            move |window| {
                if window.is_active() {
                    viewer.grab_focus();
                }
            }
        ));

        let keyctrl = gtk::EventControllerKey::builder()
            .propagation_phase(gtk::PropagationPhase::Capture)
            .build();
        keyctrl.connect_key_pressed(glib::clone!(
            #[weak(rename_to = window)] self,
            #[upgrade_or] glib::Propagation::Proceed,
            move |_, keyval, _, modifiers| {
                if window.key_control(keyval, modifiers) ||
                        window.key_movement(keyval, modifiers) ||
                        window.key_commands(keyval, modifiers) ||
                        window.key_fold(keyval, modifiers) ||
                        window.key_switch(keyval, modifiers) {
                    glib::Propagation::Stop
                } else {
                    glib::Propagation::Proceed
                }
            }
        ));
        root.add_controller(keyctrl);

        self.set_child(Some(&root));
        self.settings().set_gtk_enable_animations(true);

        self.reset_view(commit_index);
    }

    /// Reset the viewer, file widgets and labels after displaying a new commit.
    fn reset_view(&self, commit_index: usize) {
        // update the commit dropdown
        self.commit_list_changed(None, Some(commit_index));
        // (re)init the diff_model
        let model = diff_model::DiffModel::new(&self.application());
        let viewer = self.viewer();
        let sel_model: gtk::SingleSelection = viewer.model().unwrap().downcast().unwrap();
        sel_model.set_model(Some(&model));
        // (re)init the file select dropdown
        let list = files_model::FilesModel::new(&model);
        let file_widgets = self.file_widgets();
        file_widgets.select.set_model(Some(&list));
        file_widgets.select.set_selected(0);
        // set/update the labels
        let count = list.n_items();
        file_widgets.label_files.set_label(&format!(
            "{} file{}",
            count,
            if count == 1 { "" } else { "s" },
        ));
        self.update_commit_label(commit_index);
        self.update_labels_and_buttons();

        // set/update the window title and present the window
        self.set_title(Some(
            self.application().workspace().commit_title("Patchpal:").as_str()
        ));
        self.present();

        viewer.grab_focus();
        // Gtk bug: although autoselect on the selection model will select the first item, it
        // won't get focus. The only exported function that sets the internal ColumnView's
        // focus position is scroll_to. Use it to workaround this.
        viewer.scroll_to(0, None, gtk::ListScrollFlags::FOCUS, None);
    }

    /// Prepare for resetting the view. This will deinitialize models, so we can change their
    /// underlying data safely.
    pub(crate) fn hide_commit(&self) {
        // disable the file select dropdown
        self.file_widgets().select.set_model(None::<&gtk::gio::ListModel>);
        // disable the code viewer by unsetting the diff_model
        let sel_model: gtk::SingleSelection = self.viewer()
            .model().unwrap().downcast().unwrap();
        sel_model.set_model(None::<&gtk::gio::ListModel>);
    }

    fn stop(&self) {
        if !self.imp().started.replace(false) {
            return;
        }
        let app = self.application();
        let cfg = app.config();
        cfg.store_window_state(self.upcast_ref(), "window");
        let column: gtk::ColumnViewColumn =
            self.viewer().columns().item(1).unwrap().downcast().unwrap();
        cfg.set("window-column", column.fixed_width().to_string());
        let _ = cfg.save();
        for w in app.windows() {
            w.set_visible(false);
        }
    }

    fn goto_next_unacked(&self, backwards: bool) {
        let Some(model) = self.model() else { return; };
        let app = self.application();
        let (file_index, block_index) = model.current_focus();
        let data = app.workspace().find_next_unacked(file_index, block_index, backwards);
        if let Some((file_index, block_index)) = data {
            let line_index_range = app.workspace()
                .blocks(file_index).unwrap()
                .block_to_rendered_index_range(block_index);
            self.file_widgets().select.set_selected(file_index as u32);
            let viewer = self.viewer();
            viewer.scroll_to(
                line_index_range.end.saturating_sub(1) as u32,
                None,
                gtk::ListScrollFlags::NONE,
                None
            );
            glib::idle_add_local_once(glib::clone!(
                #[strong] viewer,
                move || {
                    viewer.scroll_to(
                        line_index_range.start as u32,
                        None,
                        gtk::ListScrollFlags::FOCUS | gtk::ListScrollFlags::SELECT,
                        None
                    );
                }
            ));
        }
    }

    fn show_commit_list(&self) {
        let app = self.application();
        let Some((descriptions, current)) = app.workspace().commit_descriptions()
            else { return };
        let window = app.commit_list_window();
        let mode = if app.workspace().multicommit_capable() {
            WindowMode::Modify
        } else {
            WindowMode::ViewOnly
        };
        window.set_content("Commits", &descriptions, Some(current), mode);
        window.present();
    }

    fn key_control(&self, keyval: Key, modifiers: ModifierType) -> bool {
        if modifiers == ModifierType::empty() {
            match keyval {
                Key::c => self.show_commit_list(),
                _ => return false,
            }
        } else if modifiers & !ModifierType::SHIFT_MASK == ModifierType::empty() {
            match keyval {
                Key::question => self.application().keyboard_help(),
                // Keep Shift+I for backwards compatibility.
                Key::I => self.show_commit_list(),
                _ => return false,
            }
        } else if modifiers == ModifierType::CONTROL_MASK {
            match keyval {
                Key::q => self.close(),
                Key::a => self.application().workspace().command_ack_all_clean(),
                Key::Return => self.save_and_commit(),
                Key::bracketright => self.goto_next_commit(),
                Key::bracketleft => self.goto_prev_commit(),
                Key::f => self.application().activate_action("fast-forward", None),
                _ => return false,
            }
        } else if modifiers == (ModifierType::CONTROL_MASK | ModifierType::SHIFT_MASK) {
            match keyval {
                Key::A => {
                    let app = self.application();
                    if !app.action_state("auto-accept-clean").unwrap().get::<bool>().unwrap()
                    {
                        app.activate_action("auto-accept-clean", None);
                    }
                }
                Key::Return =>
                    self.application().activate_action("fast-forward-commit", None),
                _ => return false,
            }
        } else {
            return false;
        }
        true
    }

    fn key_movement(&self, keyval: Key, modifiers: ModifierType) -> bool {
        if modifiers != ModifierType::empty() {
            return false;
        }
        match keyval {
            Key::period => self.goto_next_unacked(false),
            Key::comma => self.goto_next_unacked(true),
            _ => return false,
        };
        true
    }

    fn key_commands(&self, keyval: Key, modifiers: ModifierType) -> bool {
        let commands = if modifiers == ModifierType::empty() {
            match keyval {
                Key::a => blocks::Commands::ACK,
                Key::u => blocks::Commands::AI,
                Key::r => blocks::Commands::RETRY,
                Key::p => blocks::Commands::PREV,
                Key::n => blocks::Commands::NEXT,
                Key::e => blocks::Commands::EDIT,
                Key::x => blocks::Commands::REMOVE,
                Key::Delete => blocks::Commands::REMOVE,
                _ => return false,
            }
        } else if modifiers == ModifierType::CONTROL_MASK {
            match keyval {
                Key::z => blocks::Commands::UNDO,
                _ => return false,
            }
        } else {
            return false;
        };
        if let Some(model) = self.model() {
            model.key_command_handler(commands);
        }
        true
    }

    fn key_fold(&self, keyval: Key, modifiers: ModifierType) -> bool {
        if modifiers != ModifierType::empty() {
            return false;
        }
        let expand = match keyval {
            Key::plus | Key::KP_Add => true,
            Key::minus | Key::KP_Subtract => false,
            _ => return false,
        };
        if let Some(model) = self.model() {
            model.key_fold_handler(expand);
        }
        true
    }

    fn key_switch(&self, keyval: Key, modifiers: ModifierType) -> bool {
        if modifiers != ModifierType::empty() {
            return false;
        }
        let amount = match keyval {
            Key::bracketleft => -1,
            Key::bracketright => 1,
            _ => return false,
        };
        let select = &self.file_widgets().select;
        if let Some(model) = select.model() {
            let new_selected = select.selected() as isize + amount;
            let len = model.n_items() as isize;
            if new_selected >= 0 && new_selected < len {
                select.set_selected(new_selected as u32);
            }
        }
        true
    }

    pub(crate) fn file_changed(&self, file_index: usize) {
        if let Some(model) = self.file_widgets().select.model() {
            let list: files_model::FilesModel = model.downcast().unwrap();
            list.update_unacked(file_index);
            self.update_labels_and_buttons();
        }
    }

    pub(crate) fn commit_list_changed(
        &self, change: Option<ListChange>,
        selected: Option<usize>,
    ) {
        // commit_select is not set for mergetool/mergeport
        let Some(select) = self.imp().commit_select.get() else { return; };
        self.imp().updating_commit.set(true);
        let list: commits_model::CommitsModel = select.model().unwrap().downcast().unwrap();
        if let Some(change) = change {
            list.items_changed(
                change.position as u32,
                change.removed as u32,
                change.added as u32,
            );
        }
        if let Some(selected) = selected {
            list.set_selectable_count((selected + 1) as u32);
            select.set_selected(selected as u32);
        } else {
            list.set_selectable_count(0);
        }
        self.imp().updating_commit.set(false);
    }

    pub(crate) fn update_commit_label(&self, commit_index: usize) {
        if let Some(label) = self.imp().commit_label.get() {
            let total = self.application().workspace().commit_count();
            label.set_label(&format!("commit {}/{total}", commit_index + 1));
        }
    }

    pub(crate) fn view_changed(&self, file_index: usize, change: ListChange) {
        let Some(model) = self.model() else { return; };
        if file_index == model.current_file_index() {
            model.items_changed(
                change.position as u32,
                change.removed as u32,
                change.added as u32,
            );
        }
    }

    pub(crate) fn run_editor(
        &self,
        path: OsString,
        data: Vec<u8>,
        done: Box<dyn FnOnce(Option<Vec<u8>>)>,
    ) {
        self.imp().editor.get().unwrap().launch(path, data, done);
    }
}
