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

    #[derive(Default)]
    pub(crate) struct DiffWidget {
        pub(crate) block_index: Cell<u32>,
        pub(crate) op: OnceCell<gtk::Label>,
        pub(crate) content: OnceCell<gtk::Label>,
        pub(crate) extra_widget: RefCell<Option<gtk::Widget>>,
        pub(crate) backport_side: Cell<bool>,
    }

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

    impl ObjectImpl for DiffWidget {}

    impl WidgetImpl for DiffWidget {}

    impl BoxImpl for DiffWidget {}
}

use crate::diff_model::{DiffModel, DiffModelItem};
use gtk::{glib, glib::Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use patchpal::ui::blocks::{
    Commands, FormatHalfLine, ProgressIndicator, Status, SuggestionSearchState,
};
use patchpal::ui::format::gtk::ToGtkLabel;
use patchpal::ui::git_utils::DescriptionFormat;
use patchpal::ui::interdiff::OptionDiffLineHalf;

glib::wrapper! {
    pub(crate) struct DiffWidget(ObjectSubclass<imp::DiffWidget>)
        @extends gtk::Box, gtk::Widget,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}

impl DiffWidget {
    pub(crate) fn new(backport_side: bool) -> Self {
        let result: Self = Object::builder()
            //.property("spacing", 5)
            .build();

        let op = gtk::Label::builder()
            .single_line_mode(true)
            .halign(gtk::Align::End)
            .css_classes(["op"])
            .build();

        let content = gtk::Label::builder()
            .single_line_mode(true)
            .halign(gtk::Align::Start)
            .css_classes(["code"])
            .build();

        let imp = result.imp();
        imp.op.set(op.clone()).unwrap();
        imp.content.set(content.clone()).unwrap();
        imp.backport_side.replace(backport_side);

        result.append(&op);
        result.append(&content);

        result
    }

    pub(crate) fn set_content(&self, model: &DiffModel, item: &DiffModelItem) {
        let block_index = item.imp().block_index.get();
        let block_line_index = item.imp().block_line_index.get();
        let file_index = model.current_file_index();
        let block = model.workspace().block(file_index, block_index).unwrap();
        let diff_line = &block.rendered_lines()[block_line_index];
        let half_line = if self.imp().backport_side.get() {
            &diff_line.backport
        } else {
            &diff_line.original
        };
        let op = self.imp().op.get().unwrap();
        let content = self.imp().content.get().unwrap();
        let dark = model.config().get_local("dark_theme").unwrap_or(false);

        self.imp().block_index.set(block_index as u32);
        match half_line {
            OptionDiffLineHalf::Empty => {
                op.set_label("");
                op.set_attributes(None);
                op.set_css_classes(&["op"]);
                content.set_label("");
                content.set_attributes(None);
                content.set_css_classes(&["code"]);
                self.set_css_classes(&["empty"]);
            }
            OptionDiffLineHalf::Missing => {
                op.set_label("⦚");
                op.set_attributes(None);
                op.set_css_classes(&["op"]);
                let content = self.imp().content.get().unwrap();
                content.set_label("");
                content.set_attributes(None);
                content.set_css_classes(&["code"]);
                self.set_css_classes(&[]);
            }
            OptionDiffLineHalf::Present(half_line) => {
                let (indicator, css_class) = half_line.format_indicator();
                indicator.apply_to_label(&op, dark);
                op.set_css_classes(&["op", css_class]);
                let line_content = half_line.format_content();
                line_content.apply_to_label(&content, dark);
                content.set_css_classes(&["code", css_class]);
                self.set_css_classes(
                    if css_class == "diff-folded" { &["empty"] } else { &[] }
                );
            }
            OptionDiffLineHalf::Status(Status::Commands) => {
                op.set_label("");
                op.set_attributes(None);
                op.set_css_classes(&["op"]);
                content.set_label(&block.get_state_label());
                content.set_attributes(None);
                content.set_css_classes(&["status"]);
                self.set_css_classes(&[]);

                let extra_widget = match block.get_progress_indicator() {
                    ProgressIndicator::AI =>
                        gtk::Spinner::builder()
                            .spinning(true)
                            .build()
                            .upcast(),
                    ProgressIndicator::Edit =>
                        gtk::Image::builder()
                            .icon_name("sync-synchronizing")
                            .build()
                            .upcast(),
                    ProgressIndicator::None =>
                        self.create_command_buttons(
                            model,
                            block.get_available_commands(),
                            file_index,
                            block_index,
                        ).upcast(),
                };
                self.insert_child_after(&extra_widget, self.first_child().as_ref());
                self.imp().extra_widget.replace(Some(extra_widget));
            }
            OptionDiffLineHalf::Status(Status::Suggestions) => {
                op.set_label("");
                op.set_attributes(None);
                op.set_css_classes(&["op"]);
                content.set_label("");
                content.set_attributes(None);
                content.set_css_classes(&["status"]);
                self.set_css_classes(&[]);

                let extra_widget = self.create_suggestions_widget(model, item).upcast();
                self.insert_child_after(&extra_widget, self.first_child().as_ref());
                self.imp().extra_widget.replace(Some(extra_widget));
            }
        }
    }

    pub(crate) fn unset_content(&self) {
        if let Some(extra_widget) = self.imp().extra_widget.replace(None) {
            self.remove(&extra_widget);
        }
        self.set_css_classes(&[]);
    }

    fn create_command_buttons(
        &self,
        model: &DiffModel,
        commands: Commands,
        file_index: usize,
        block_index: usize,
    ) -> gtk::Box {
        let command_buttons = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .build();
        for command in commands {
            let label = match command {
                Commands::ACK => "✔ accept",
                Commands::AI => "use AI",
                Commands::RETRY => "retry",
                Commands::PREV => "◀ prev",
                Commands::NEXT => "next ▶",
                Commands::EDIT => "✎ edit",
                Commands::REMOVE => "🗙 remove",
                Commands::UNDO => "⎌ undo",
                _ => panic!()
            };
            let button = gtk::Button::builder()
                .label(label)
                .build();
            if command == Commands::ACK {
                button.set_css_classes(&["command", "accept"]);
            } else if command == Commands::UNDO {
                button.set_css_classes(&["command", "undo"]);
            } else {
                button.set_css_classes(&["command"]);
            }
            button.connect_clicked(glib::clone!(
                #[weak]
                model,
                move |_| {
                    model.handle_command(file_index, block_index, command);
                }
            ));
            command_buttons.append(&button);
        }
        command_buttons
    }

    fn create_suggestions_widget(&self, model: &DiffModel, item: &DiffModelItem) -> gtk::Box {
        let widget = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .hexpand(true)
            .css_classes(["suggestions"])
            .build();
        let (state, missing) = item.suggestions(model);
        if let SuggestionSearchState::Err(msg) = &*state {
            widget.append(
                &gtk::Label::builder()
                    .label(msg.trim_end())
                    .halign(gtk::Align::Start)
                    .build()
            );
            widget.add_css_class("error");
            return widget;
        }

        let count = missing.len();
        if count == 0 {
            if *state == SuggestionSearchState::InProgress {
                widget.append(&gtk::Label::builder()
                    .label("Looking for missing dependencies…")
                    .halign(gtk::Align::Start)
                    .build()
                );
            } else {
                widget.append(&gtk::Label::builder()
                    .label("No dependencies found.")
                    .css_classes(["info-message"])
                    .halign(gtk::Align::Start)
                    .build()
                );
            }
        } else {
            let label = if *state == SuggestionSearchState::InProgress {
                // search in progress
                gtk::Label::builder()
                    .label("Suggested dependencies:")
                    .css_classes(["info-message"])
                    .halign(gtk::Align::Start)
                    .build()
            } else {
                // search finished
                let label = gtk::Label::builder()
                    .use_markup(true)
                    .label("Suggested dependencies (<a href=\"#\">open</a>):")
                    .css_classes(["info-message"])
                    .halign(gtk::Align::Start)
                    .build();
                let unique_id = model.workspace().get_unique_id(
                    model.current_file_index(),
                    item.imp().block_index.get(),
                ).unwrap();
                label.connect_activate_link(glib::clone!(
                    #[weak] model,
                    #[upgrade_or] glib::Propagation::Stop,
                    move |_, _| {
                        model.open_suggestions(unique_id);
                        glib::Propagation::Stop
                    }
                ));
                label
            };
            widget.append(&label);

            let dark = model.config().get_local("dark_theme").unwrap_or(false);
            for description in missing.iter().take(3) {
                widget.append(&description.fmt_oneline().to_label(dark));
            }
            if count > 3 {
                widget.append(&gtk::Label::builder()
                    .label(format!("and {} more", count - 3))
                    .halign(gtk::Align::Start)
                    .build()
                );
            }
        }
        if *state == SuggestionSearchState::InProgress {
            widget.add_css_class("in-progress");
        }
        widget
    }
}
