mod row;

use crate::app::App;
use crate::article_list::ArticleList;
use crate::content_page::{ArticleViewColumn, ContentPage};
use crate::gobject_models::{GArticle, GArticleID, GTag};
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::undo_action::UndoAction;
use crate::util::{JOIN_ERROR, constants};
use gdk4::ModifierType;
use gio::ListStore;
use glib::{ControlFlow, Object, Propagation, Properties, clone, subclass::prelude::*, subclass::*};
use gtk4::{
    Accessible, Buildable, CompositeTemplate, ConstraintTarget, CustomSorter, Expression, Label, ListBox, ListBoxRow,
    Native, Ordering, Popover, PropertyExpression, ScrolledWindow, SearchEntry, ShortcutManager, SortListModel, Stack,
    StringFilter, Widget, prelude::*, subclass::prelude::*,
};
use news_flash::error::NewsFlashError;
use news_flash::models::{ArticleID, TagID};
use row::PopoverTagRow;
use std::cell::RefCell;
use std::collections::HashSet;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::TagPopover)]
    #[template(file = "data/resources/ui_templates/tagging/popover.blp")]
    pub struct TagPopover {
        #[template_child]
        pub stack: TemplateChild<Stack>,
        #[template_child]
        pub create_label: TemplateChild<Label>,
        #[template_child]
        pub list_box: TemplateChild<ListBox>,

        #[template_child]
        pub scroll: TemplateChild<ScrolledWindow>,
        #[template_child]
        pub sorter: TemplateChild<CustomSorter>,
        #[template_child]
        pub filter: TemplateChild<StringFilter>,
        #[template_child]
        pub list_store: TemplateChild<ListStore>,
        #[template_child]
        pub sort_model: TemplateChild<SortListModel>,

        #[template_child]
        pub entry: TemplateChild<SearchEntry>,

        #[property(get, set = Self::set_article, nullable)]
        pub article: RefCell<Option<GArticle>>,

        pub assigned_tags: RefCell<HashSet<TagID>>,
        pub ctrl_pressed: RefCell<bool>,
    }

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

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

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

    #[glib::derived_properties]
    impl ObjectImpl for TagPopover {
        fn constructed(&self) {
            let obj = self.obj();

            let title_expression = PropertyExpression::new(GTag::static_type(), Expression::NONE, "label");
            self.filter.set_expression(Some(&title_expression));
            self.sorter.set_sort_func(Self::sort_func);

            self.list_box.bind_model(Some(&*self.sort_model), |obj| {
                let row = PopoverTagRow::default();
                let tag = obj.downcast_ref::<GTag>().unwrap();
                row.set_tag(tag.clone());
                row.upcast()
            });

            obj.connect_show(|this| {
                this.imp().update();
            });

            self.parent_constructed();
        }
    }

    impl WidgetImpl for TagPopover {}

    impl PopoverImpl for TagPopover {}

    #[gtk4::template_callbacks]
    impl TagPopover {
        #[template_callback]
        fn on_search_changed(&self) {
            let query = self.entry.text();
            let query = query.as_str().trim();
            self.filter.set_search(Some(query));
            self.create_label
                .set_text(&format!("Press Enter to create and assign '{query}'"));

            if self.sort_model.item(0).is_none() {
                let stack_page = if query.is_empty() { "empty" } else { "create" };
                self.stack.set_visible_child_name(stack_page);
            } else {
                self.stack.set_visible_child_name("list");
            }
        }

        #[template_callback]
        fn on_search_activate(&self) {
            let query = self.entry.text();
            let query = query.as_str().trim();

            if query.is_empty() && self.sort_model.n_items() == 0 {
                return;
            }

            let Some(article) = self.article.borrow().clone() else {
                return;
            };

            let article_id = article.article_id();
            let ctrl_pressed = *self.ctrl_pressed.borrow();
            let no_matches = self.sort_model.item(0).is_none();
            let exact_match = self.exact_search_match(query);

            if exact_match.is_none() && (ctrl_pressed || no_matches) {
                self.create_tag(constants::TAG_DEFAULT_COLOR.into(), query.into(), article_id);
            } else if let Some(best_match) = self.best_search_match() {
                // Assign selected tag
                let is_assigned = best_match.assigned();
                best_match.set_assigned(!is_assigned);

                if is_assigned {
                    self.untag_article(article_id, best_match);
                } else {
                    self.tag_article(article_id, best_match);
                }
            }
        }

        #[template_callback]
        fn on_key_pressed(&self, key: gdk4::Key, _keyval: u32, state: ModifierType) -> Propagation {
            let ctrl_pressed = state.contains(ModifierType::CONTROL_MASK);
            self.ctrl_pressed.replace(ctrl_pressed);

            if ctrl_pressed && key == gdk4::Key::Return {
                self.entry.emit_activate();
            } else if key == gdk4::Key::Escape {
                self.obj().popdown();
            }
            Propagation::Proceed
        }

        #[template_callback]
        fn on_list_activate(&self, row: ListBoxRow) {
            let Ok(tag_row) = row.downcast::<PopoverTagRow>() else {
                return;
            };
            let tag_gobject = tag_row.tag();

            let is_assigned = tag_gobject.assigned();
            tag_gobject.set_assigned(!is_assigned);

            let Some(article) = self.article.borrow().clone() else {
                return;
            };

            let article_id = article.article_id();

            if is_assigned {
                self.untag_article(article_id.clone(), tag_gobject);
            } else {
                self.tag_article(article_id.clone(), tag_gobject);
            }
        }

        fn set_article(&self, article: Option<GArticle>) {
            self.article.replace(article);
            self.update();
        }

        fn exact_search_match(&self, search_query: &str) -> Option<GTag> {
            let mut index = 0;
            let mut item = self.sort_model.item(index);

            while let Some(tag_gobject) = item.and_downcast::<GTag>() {
                if tag_gobject.label() == search_query {
                    return Some(tag_gobject);
                }

                index += 1;
                item = self.sort_model.item(index);
            }

            None
        }

        fn best_search_match(&self) -> Option<GTag> {
            let selected = if let Some(row) = self.list_box.selected_row() {
                row.index()
            } else {
                0
            };
            self.sort_model.item(selected as u32).and_downcast::<GTag>()
        }

        fn sort_func(obj1: &Object, obj2: &Object) -> Ordering {
            let tag_1: &GTag = obj1.downcast_ref::<GTag>().unwrap();
            let tag_2: &GTag = obj2.downcast_ref::<GTag>().unwrap();

            let assigned_1 = tag_1.assigned();
            let assigned_2 = tag_2.assigned();

            let title_1 = tag_1.label();
            let title_2 = tag_2.label();

            if assigned_1 == assigned_2 {
                title_1.cmp(&title_2).into()
            } else {
                assigned_1.cmp(&assigned_2).reverse().into()
            }
        }

        fn reset_view(&self) {
            let obj = self.obj();

            self.entry.set_text("");

            if obj.is_visible() {
                let _ = self.entry.grab_focus();
            }

            if self.list_store.item(0).is_none() {
                self.stack.set_visible_child_name("empty");
            } else {
                self.stack.set_visible_child_name("list");
            }

            let scroll = self.scroll.clone();
            glib::timeout_add_local(std::time::Duration::from_millis(20), move || {
                scroll.vadjustment().set_value(0.0);
                ControlFlow::Break
            });
        }

        fn update(&self) {
            self.list_store.remove_all();
            self.assigned_tags.borrow_mut().clear();

            let Some(article) = self.article.borrow().clone() else {
                return;
            };

            let blacklisted_tag = ContentPage::instance().get_current_undo_action().and_then(|undo| {
                if let UndoAction::DeleteTag(tag_id, _) = undo {
                    Some(tag_id.clone())
                } else {
                    None
                }
            });

            let article_id: ArticleID = article.article_id().into();
            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move {
                    let future = TokioRuntime::spawn(async move {
                        let news_flash = App::news_flash();
                        let news_flash_guard = news_flash.read().await;
                        let news_flash = news_flash_guard.as_ref()?;

                        let article_tags = news_flash.get_tags_of_article(&article_id).ok()?;
                        let all_tags = news_flash.get_tags().ok().map(|(tags, _taggings)| tags)?;

                        Some((article_tags, all_tags))
                    });

                    if let Some((article_tags, tags)) = future.await.expect(JOIN_ERROR) {
                        for tag in &article_tags {
                            if Some(&tag.tag_id) == blacklisted_tag.as_ref() {
                                continue;
                            }

                            imp.assigned_tags.borrow_mut().insert(tag.tag_id.clone());
                        }

                        for tag in tags {
                            if Some(&tag.tag_id) == blacklisted_tag.as_ref() {
                                continue;
                            }

                            let assigned = imp.assigned_tags.borrow().contains(&tag.tag_id);
                            let gtag: GTag = tag.clone().into();
                            gtag.set_assigned(assigned);
                            imp.list_store.append(&gtag);
                        }
                    }

                    imp.reset_view();
                }
            ));
        }

        fn create_tag(&self, color: String, title: String, assign_to_article: GArticleID) {
            let article_id: ArticleID = assign_to_article.clone().into();

            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move {
                    let future = TokioRuntime::spawn(async move {
                        let news_flash = App::news_flash();
                        let news_flash_guard = news_flash.read().await;
                        let news_flash = news_flash_guard.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
                        let client = App::client().await;
                        let tag = news_flash.add_tag(&title, Some(color), &client).await?;
                        news_flash.tag_article(&article_id, &tag.tag_id, &client).await?;
                        Ok(tag)
                    });

                    match future.await.expect(JOIN_ERROR) {
                        Ok(tag) => {
                            let gtag: GTag = tag.into();
                            gtag.set_assigned(true);

                            ContentPage::instance().update_sidebar();
                            ArticleViewColumn::instance().refresh_article_metadata_from_db();
                            ArticleList::instance().article_row_update_tags(&assign_to_article, Some(&gtag), None);

                            imp.list_store.append(&gtag);
                            imp.reset_view();
                        }
                        Err(error) => {
                            tracing::error!(%error, "Failed to add tag");
                            ContentPage::instance().newsflash_error(&i18n("Failed to add tag"), error);
                        }
                    }
                }
            ));
        }

        fn tag_article(&self, article_id: GArticleID, tag: GTag) {
            let article_id_clone: ArticleID = article_id.clone().into();
            let tag_id: TagID = tag.tag_id().into();

            glib::spawn_future_local(async move {
                let future = TokioRuntime::spawn(async move {
                    let news_flash = App::news_flash();
                    let news_flash_guard = news_flash.read().await;
                    let news_flash = news_flash_guard.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
                    let client = App::client().await;
                    news_flash.tag_article(&article_id_clone, &tag_id, &client).await?;
                    Ok(())
                });

                match future.await.expect(JOIN_ERROR) {
                    Ok(()) => {
                        ArticleList::instance().article_row_update_tags(&article_id, Some(&tag), None);
                    }
                    Err(error) => {
                        tracing::error!(%error, "Failed to tag article");
                        ContentPage::instance().newsflash_error(&i18n("Failed to tag article"), error);
                    }
                };
            });
        }

        pub fn untag_article(&self, article_id: GArticleID, tag: GTag) {
            let article_id_clone: ArticleID = article_id.clone().into();
            let tag_id: TagID = tag.tag_id().into();

            glib::spawn_future_local(async move {
                let future = TokioRuntime::spawn(async move {
                    let news_flash = App::news_flash();
                    let news_flash_guard = news_flash.read().await;
                    let news_flash = news_flash_guard.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
                    let client = App::client().await;
                    news_flash.untag_article(&article_id_clone, &tag_id, &client).await?;
                    Ok(())
                });

                match future.await.expect(JOIN_ERROR) {
                    Ok(()) => {
                        ArticleList::instance().article_row_update_tags(&article_id, None, Some(&tag));
                    }
                    Err(error) => {
                        tracing::error!(%error, "Failed to untag article");
                        ContentPage::instance().newsflash_error(&i18n("Failed to untag article"), error);
                    }
                };
            });
        }
    }
}

glib::wrapper! {
    pub struct TagPopover(ObjectSubclass<imp::TagPopover>)
        @extends Widget, Popover,
        @implements Accessible, Buildable, ConstraintTarget, ShortcutManager, Native;
}

impl Default for TagPopover {
    fn default() -> Self {
        Object::new()
    }
}
