mod imp {
    use crate::app::PatchpalApplication;
    use gtk::{gio, glib};
    use gtk::prelude::*;
    use gtk::subclass::prelude::*;
    use std::cell::{Cell, OnceCell, RefCell};
    use std::num::NonZeroU32;

    #[derive(Default)]
    pub(crate) struct DiffModelItem {
        pub(crate) block_index: Cell<usize>,
        pub(crate) block_line_index: Cell<usize>,
        pub(crate) line_number: Cell<Option<NonZeroU32>>,
    }

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

    impl ObjectImpl for DiffModelItem {}

    #[derive(Default)]
    pub(crate) struct DiffModel {
        pub(crate) application: OnceCell<PatchpalApplication>,
        pub(crate) current_file_index: Cell<usize>,
        /// The line with keyboard focus. It is stored separately for each `blocks`.
        /// The fields in the tuple are (block_index, line_index_in_the_block).
        pub(crate) focused: RefCell<Vec<(usize, usize)>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for DiffModel {
        const NAME: &'static str = "BackportDiffModel";
        type Type = super::DiffModel;
        type ParentType = glib::Object;
        type Interfaces = (gio::ListModel,);
    }

    impl ObjectImpl for DiffModel {}

    impl ListModelImpl for DiffModel {
        fn item_type(&self) -> glib::Type {
            glib::Object::static_type()
        }

        fn n_items(&self) -> u32 {
            let file_index = self.current_file_index.get();
            self.application.get().unwrap().workspace().blocks(file_index).unwrap()
                .rendered_len() as u32
        }

        fn item(&self, index: u32) -> Option<glib::Object> {
            let file_index = self.current_file_index.get();
            let blocks = &self.application.get().unwrap()
                .workspace().blocks(file_index).unwrap();
            let (block_index, block_line_index, line_number) =
                blocks.rendered_index_to_block_details(index as usize)?;
            Some(super::DiffModelItem::new(
                block_index,
                block_line_index,
                line_number,
            ).upcast())
        }
    }
}


use crate::{app::PatchpalApplication, commit_list_window::WindowMode};
use gtk::{gio, glib};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use patchpal::common::git;
use patchpal::ui::{config::Config, interdiff::OptionDiffLineHalf, unique_id::UniqueId};
use patchpal::ui::blocks::{Commands, Status, SuggestionSearchState};
use patchpal::ui::workspace::Workspace;
use std::{cell::Ref, num::NonZeroU32, rc::Rc};

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

impl DiffModelItem {
    fn new(
        block_index: usize,
        block_line_index: usize,
        line_number: Option<NonZeroU32>,
    ) -> Self {
        let result: Self = glib::Object::builder().build();
        let imp = result.imp();
        imp.block_index.set(block_index);
        imp.block_line_index.set(block_line_index);
        imp.line_number.set(line_number);
        result
    }

    pub(crate) fn line_number(&self) -> Option<NonZeroU32> {
        let imp = self.imp();
        imp.line_number.get()
    }

    pub(crate) fn suggestions<'a>(&self, model: &'a DiffModel)
        -> (Ref<'a, SuggestionSearchState>, Ref<'a, [Rc<git::Description>]>)
    {
        let file_index = model.current_file_index();
        let block_index = self.imp().block_index.get();
        Ref::map_split(
            model.workspace().block(file_index, block_index).unwrap(),
            |block| (
                block.suggestion_search_state(),
                block.as_change().missing.as_slice(),
            )
        )
    }
}


glib::wrapper! {
    pub(crate) struct DiffModel(ObjectSubclass<imp::DiffModel>)
        @implements gio::ListModel;
}

impl DiffModel {
    pub(crate) fn new(application: &PatchpalApplication) -> Self {
        let result: Self = glib::Object::builder().build();
        let _ = result.imp().application.set(application.clone());
        result.init_focused();
        result.switch(0);
        result
    }

    fn init_focused(&self) {
        let focused = &mut self.imp().focused.borrow_mut();
        self.workspace().for_each_blocks(|blocks|
            focused.push(blocks.rendered_index_to_block_pos(0).unwrap_or((0, 0)))
        );
    }

    pub(crate) fn switch(&self, file_index: usize) -> usize {
        let old_len = self.imp().n_items();
        self.imp().current_file_index.set(file_index);
        let new_len = self.imp().n_items();

        let (block_index, pos) = self.imp().focused.borrow()[file_index];
        let result = self.workspace()
            .blocks(file_index).unwrap()
            .block_pos_to_rendered_index(block_index, pos);

        self.items_changed(0, old_len, new_len);
        result
    }

    pub(crate) fn len(&self) -> usize {
        self.workspace().commit().unwrap().file_count()
    }

    fn application(&self) -> &PatchpalApplication {
        self.imp().application.get().unwrap()
    }

    pub(crate) fn config(&self) -> &Config {
        self.application().config()
    }

    pub(crate) fn workspace(&self) -> &Workspace<PatchpalApplication> {
        self.application().workspace()
    }

    pub(crate) fn current_file_index(&self) -> usize {
        self.imp().current_file_index.get()
    }

    /// Return the currently focused `(file_index, block_index)`.
    pub(crate) fn current_focus(&self) -> (usize, usize) {
        let file_index = self.current_file_index();
        let block_index = self.imp().focused.borrow()[file_index].0;
        (file_index, block_index)
    }

    pub(crate) fn record_focus(&self, line_index: usize) {
        let file_index = self.current_file_index();
        let focused = &mut self.imp().focused.borrow_mut()[file_index];
        *focused = self.workspace()
            .blocks(file_index).unwrap()
            .rendered_index_to_block_pos(line_index).unwrap();
    }

    fn open_suggestions_at(&self, file_index: usize, block_index: usize) {
        // Check if suggestions search has finished before launching window
        let block = self.workspace().block(file_index, block_index).unwrap();
        let change = block.as_change();
        if *block.suggestion_search_state() == SuggestionSearchState::Done &&
            !change.missing.is_empty()
        {
            let window = self.application().commit_list_window();
            let mode = if self.workspace().multicommit_capable() {
                WindowMode::Select
            } else {
                WindowMode::ViewOnly
            };
            window.set_content("Suggested commits", &change.missing, None, mode);
            window.present();
        }
        // If not finished, do nothing (search still in progress)
    }

    pub(crate) fn open_suggestions(&self, unique_id: UniqueId) {
        let res = self.workspace().resolve_unique_id(unique_id);
        if let Some((file_index, block_index)) = res {
            self.open_suggestions_at(file_index, block_index);
        }
    }

    pub(crate) fn activate_row_handler(&self, index: u32) {
        let file_index = self.current_file_index();
        let (block_index, status) = {
            let blocks = self.workspace().blocks(file_index).unwrap();
            let index = index as usize;
            let block_pos = blocks.rendered_index_to_block_pos(index);
            let (block_index, block_line_index) = match block_pos {
                Some(block_pos) => block_pos,
                None => return
            };
            let block = blocks.get(block_index);
            let diff_line = &block.rendered_lines()[block_line_index];
            let status = if let OptionDiffLineHalf::Status(status) = &diff_line.backport {
                Some(status.clone())
            } else if let OptionDiffLineHalf::Status(status) = &diff_line.original {
                Some(status.clone())
            } else {
                None
            };
            (block_index, status)
        };
        if matches!(status, Some(status) if status == Status::Suggestions) {
            self.open_suggestions_at(file_index, block_index);
        } else {
            self.workspace().toggle_fold_block(file_index, block_index);
        }
    }

    pub(crate) fn key_fold_handler(&self, expand: bool) {
        let (file_index, block_index) = self.current_focus();
        self.workspace().set_fold_block(file_index, block_index, !expand);
    }

    /// Executes the given command in the block which contains keyboard focus.
    /// `possible_commands` contain one or more commands. The first command from
    /// `possible_commands` that is allowed in the block will be executed.
    pub(crate) fn key_command_handler(&self, possible_commands: Commands) {
        let (file_index, block_index) = self.current_focus();
        let command = {
            let block = self.workspace().block(file_index, block_index).unwrap();
            let mut result = Commands::empty();
            let block_commands = block.get_available_commands();
            for command in possible_commands {
                if block_commands.contains(command) {
                    result = command;
                    break;
                }
            }
            result
        };
        self.handle_command(file_index, block_index, command);
    }

    pub(crate) fn handle_command(
        &self,
        file_index: usize,
        block_index: usize,
        command: Commands,
    ) {
        if command == Commands::empty() {
            return;
        }
        if let Err(msg) = self.workspace().handle_command(file_index, block_index, command) {
            let alert = gtk::AlertDialog::builder()
                .detail(&msg)
                .message("Error")
                .modal(true)
                .build();
            alert.show(Some(self.application().app_window()));
        }
    }
}
