mod model;
mod tag_row;

use crate::color::ColorRGBA;
use crate::gobject_models::{GSidebarSelection, GTag};
use crate::util::{GtkUtil, constants};
use diffus::edit::{Edit, collection};
use gdk4::Display;
use gio::ListStore;
use glib::{SignalHandlerId, subclass::*};
use gtk4::{
    Accessible, Buildable, CompositeTemplate, ConstraintTarget, CssProvider, ListBox, ListBoxRow,
    STYLE_PROVIDER_PRIORITY_APPLICATION, Widget, prelude::*, subclass::prelude::*,
};
use libadwaita::{Bin, subclass::prelude::*};
pub use model::TagListModel;
use once_cell::sync::Lazy;
use std::cell::RefCell;
use tag_row::TagRow;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(file = "data/resources/ui_templates/sidebar/tag_list.blp")]
    pub struct TagList {
        #[template_child]
        pub list_box: TemplateChild<ListBox>,
        #[template_child]
        pub list_store: TemplateChild<ListStore>,

        pub list_model: RefCell<TagListModel>,
        pub style_provider: RefCell<CssProvider>,
    }

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

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

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

    impl ObjectImpl for TagList {
        fn constructed(&self) {
            self.list_box.bind_model(Some(&*self.list_store), |obj| {
                let tag = obj.downcast_ref::<GTag>().unwrap();
                let row = TagRow::new();
                row.bind_model(tag);
                row.upcast()
            });

            self.list_box.remove_css_class("separators");
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("activated")
                        .action()
                        .param_types([super::TagList::static_type()])
                        .build(),
                    Signal::builder("selection-changed")
                        .param_types([super::TagList::static_type(), GSidebarSelection::static_type()])
                        .build(),
                ]
            });

            SIGNALS.as_ref()
        }
    }

    impl WidgetImpl for TagList {}

    impl BinImpl for TagList {}

    #[gtk4::template_callbacks]
    impl TagList {
        #[template_callback]
        fn on_list_activate(&self, _row: ListBoxRow) {
            self.obj().activate();
        }

        #[template_callback]
        fn on_selection_changed(&self, row: Option<ListBoxRow>) {
            let Some(row) = row else {
                return;
            };

            let pos = row.index() as u32;
            let selection = self
                .tag_at_index(pos)
                .map(|tag| GSidebarSelection::from_tag_list(tag, pos));

            if let Some(selection) = selection {
                let obj = self.obj();
                obj.emit_by_name::<()>("selection-changed", &[&*obj, &selection])
            }
        }

        pub(super) fn tag_at_index(&self, index: u32) -> Option<GTag> {
            let tag_row = self.list_box.row_at_index(index as i32).and_downcast::<TagRow>()?;
            let tag = tag_row.model();
            Some(tag)
        }
    }
}

glib::wrapper! {
    pub struct TagList(ObjectSubclass<imp::TagList>)
        @extends Widget, Bin,
        @implements Accessible, Buildable, ConstraintTarget;
}

impl Default for TagList {
    fn default() -> Self {
        glib::Object::new::<Self>()
    }
}

impl TagList {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn update(&self, new_list: TagListModel) {
        let imp = self.imp();

        self.update_style_provider(&new_list);

        let old_list = imp.list_model.replace(new_list);

        let model_guard = imp.list_model.borrow();
        let diff = old_list.generate_diff(&model_guard);
        let mut pos = 0;

        match diff {
            Edit::Copy(_list) => {
                // no difference
            }
            Edit::Change(diff) => {
                let _ = diff
                    .into_iter()
                    .map(|edit| {
                        match edit {
                            collection::Edit::Copy(_tag) => {
                                // nothing changed
                                pos += 1;
                            }
                            collection::Edit::Insert(tag) => {
                                imp.list_store.insert(pos, tag);
                                pos += 1;
                            }
                            collection::Edit::Remove(_tag) => {
                                imp.list_store.remove(pos);
                            }
                            collection::Edit::Change(diff) => {
                                let item = imp.list_store.item(pos).and_downcast::<GTag>();
                                if let Some(tag_gobject) = item {
                                    if let Some(label) = diff.label {
                                        tag_gobject.set_label(label.as_str().to_owned());
                                    }
                                    if let Some(color) = diff.color {
                                        tag_gobject.set_color(Some(color.as_str().to_owned()));
                                    }
                                }
                                pos += 1;
                            }
                        }
                    })
                    .collect::<Vec<_>>();
            }
        };
    }

    pub fn clear_selection(&self) {
        self.imp().list_box.select_row(ListBoxRow::NONE);
    }

    pub fn set_selection(&self, pos: u32) {
        let imp = self.imp();
        let row = imp.list_box.row_at_index(pos as i32);
        imp.list_box.select_row(row.as_ref());
    }

    pub fn next_item(&self) -> Option<GSidebarSelection> {
        let imp = self.imp();
        let selected_row = imp.list_box.selected_row()?;
        let selected_pos = selected_row.index();
        tracing::debug!(selected_pos, "next item");

        let next_pos = selected_pos + 1;
        let row = imp.list_box.row_at_index(next_pos).and_downcast::<TagRow>()?;

        imp.list_box.select_row(Some(&row));

        Some(GSidebarSelection::from_tag_list(row.model(), next_pos as u32))
    }

    pub fn prev_item(&self) -> Option<GSidebarSelection> {
        let imp = self.imp();
        let selected_row = imp.list_box.selected_row()?;
        let selected_pos = selected_row.index();
        tracing::debug!(selected_pos, "prev item");

        let prev_pos = selected_pos - 1;
        let row = imp.list_box.row_at_index(prev_pos).and_downcast::<TagRow>()?;

        imp.list_box.select_row(Some(&row));

        Some(GSidebarSelection::from_tag_list(row.model(), prev_pos as u32))
    }

    pub fn first_item(&self) -> Option<GSidebarSelection> {
        let imp = self.imp();
        let tag = imp.tag_at_index(0)?;
        Some(GSidebarSelection::from_tag_list(tag, 0))
    }

    pub fn last_item(&self) -> Option<GSidebarSelection> {
        let imp = self.imp();
        let count = imp.list_store.n_items();

        if count > 0 {
            let pos = count - 1;
            let tag = imp.tag_at_index(pos)?;
            Some(GSidebarSelection::from_tag_list(tag, pos))
        } else {
            None
        }
    }

    pub fn activate(&self) {
        self.emit_by_name::<()>("activated", &[&self.clone()]);
    }

    pub fn connect_activated<F: Fn(&Self) + 'static>(&self, f: F) -> SignalHandlerId {
        self.connect_local("activated", false, move |args| {
            let feed_list = args[1].get::<Self>().expect("The value needs to be of type `TagList`");
            f(&feed_list);
            None
        })
    }

    fn update_style_provider(&self, list_model: &TagListModel) {
        let imp = self.imp();
        gtk4::style_context_remove_provider_for_display(&Display::default().unwrap(), &*imp.style_provider.borrow());

        let mut css_string = String::new();
        for tag_model in list_model.tags() {
            let css_selector = tag_model
                .tag_id()
                .as_ref()
                .as_str()
                .chars()
                .filter(|c| c.is_alphanumeric() && !c.is_whitespace())
                .collect::<String>();
            let css_selector = format!("tag-style-{css_selector}");

            css_string.push_str(&Self::generate_css(&css_selector, tag_model.color().as_deref()));
        }

        let provider = CssProvider::new();
        provider.load_from_string(&css_string);

        gtk4::style_context_add_provider_for_display(
            &Display::default().unwrap(),
            &provider,
            STYLE_PROVIDER_PRIORITY_APPLICATION,
        );
        imp.style_provider.replace(provider);
    }

    fn generate_css(tag_id: &str, color_str: Option<&str>) -> String {
        let tag_color = match ColorRGBA::parse_string(color_str.unwrap_or(constants::TAG_DEFAULT_COLOR)) {
            Ok(color) => color,
            Err(_) => ColorRGBA::parse_string(constants::TAG_DEFAULT_COLOR)
                .expect("Failed to parse default outer RGBA string."),
        };
        let gradient_upper = GtkUtil::adjust_lightness(&tag_color, constants::TAG_GRADIENT_SHIFT, None);
        let gradient_lower = GtkUtil::adjust_lightness(&tag_color, -constants::TAG_GRADIENT_SHIFT, None);

        let gradient_upper_hex = gradient_upper.as_string_no_alpha();
        let gradient_lower_hex = gradient_lower.as_string_no_alpha();

        let background_luminance = tag_color.luminance_normalized();
        let text_shift = if background_luminance > 0.8 {
            background_luminance - 1.3
        } else {
            1.2 - background_luminance
        };
        let mut text_color = tag_color;
        text_color.adjust_lightness_absolute(text_shift).unwrap();
        text_color
            .adjust_saturation_absolute(constants::TAG_TEXT_SATURATION_SHIFT)
            .unwrap();
        let text_color_hex = text_color.as_string_no_alpha();

        let css_str = format!(
            ".{tag_id} {{
                background-image: linear-gradient({gradient_upper_hex}, {gradient_lower_hex});
                color: {text_color_hex};
                border-radius: 12px;
                min-width: 12px;
                padding: 2px 5px 1px;
                opacity: 1.0;
            }}

            "
        );
        css_str
    }
}
