mod article_header;
mod article_row;
mod article_tags;
mod article_update_msg;
mod load_queue;
mod loader;
mod model;
mod scroll;

use self::article_header::ArticleHeader;
use self::article_row::ArticleRow;
use self::scroll::ArticleListScroll;
use crate::app::App;
use crate::content_page::{ArticleListColumn, ArticleListMode, ArticleViewColumn, ContentPage};
use crate::gobject_models::{GArticle, GArticleID, GMarked, GRead, GTag, SidebarSelectionType};
use crate::i18n::{i18n, i18n_f};
use crate::main_window::MainWindow;
use crate::self_stack::{SelfStack, SelfStackTransition};
use crate::settings::{GArticleOrder, GOrderBy};
use crate::sidebar::FeedListItemID;
use crate::sidebar::SideBar;
pub use article_update_msg::{MarkUpdate, OpenArticleInBrowser, ReadUpdate};
use chrono::{DateTime, Local, Utc};
use diffus::Diffable;
use diffus::edit::{Edit, collection};
use gio::ListStore;
use glib::{ControlFlow, Object, Properties, SourceId, clone, subclass};
use gtk4::{
    Accessible, Box, Buildable, CompositeTemplate, ConstraintTarget, CustomSorter, ListHeader, ListItem,
    ListScrollFlags, ListView, NamedAction, Ordering, Shortcut, SingleSelection, Widget, prelude::*,
    subclass::prelude::*,
};
pub use load_queue::ArticleListLoadQueue;
pub use model::ArticleListModel;
use news_flash::models::ArticleID;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::time::Duration;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::ArticleList)]
    #[template(file = "data/resources/ui_templates/article_list/list.blp")]
    pub struct ArticleList {
        #[template_child]
        pub self_stack: TemplateChild<SelfStack>,
        #[template_child]
        pub listview: TemplateChild<ListView>,
        #[template_child]
        pub selection: TemplateChild<SingleSelection>,
        #[template_child]
        pub section_sorter: TemplateChild<CustomSorter>,
        #[template_child]
        pub list_store: TemplateChild<ListStore>,
        #[template_child]
        pub down_shortcut: TemplateChild<Shortcut>,
        #[template_child]
        pub up_shortcut: TemplateChild<Shortcut>,
        #[template_child]
        pub activate_shortcut: TemplateChild<Shortcut>,

        #[property(get, set = Self::set_selected_index, name = "selected-index", default = gtk4::INVALID_LIST_POSITION)]
        pub selected_index: Cell<u32>,

        #[property(get, set = Self::set_selected_model, name = "selected-model", nullable)]
        pub selected_model: RefCell<Option<GArticle>>,

        #[property(get, set, builder(SelfStackTransition::Crossfade))]
        pub transition: Cell<SelfStackTransition>,

        pub articles: RefCell<Vec<GArticle>>,
        pub index: RefCell<HashMap<GArticleID, GArticle>>,

        pub skip_next_activation: Cell<bool>,
        pub delay_next_activation: Cell<bool>,
        pub delayed_selection: RefCell<Option<SourceId>>,
    }

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

        fn class_init(klass: &mut Self::Class) {
            ArticleListScroll::ensure_type();

            klass.bind_template();
            klass.bind_template_callbacks();
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for ArticleList {
        fn constructed(&self) {
            self.section_sorter.set_sort_func(Self::sort_sections);

            self.activate_shortcut
                .set_action(Some(NamedAction::new("win.show-article-view")));
            self.up_shortcut.set_action(Some(NamedAction::new("win.prev-article")));
            self.down_shortcut
                .set_action(Some(NamedAction::new("win.next-article")));

            self.self_stack
                .set_status_icon_name(Some("format-justify-fill-symbolic"));
            self.self_stack.set_status_title(i18n("No Articles"));

            self.parent_constructed();
        }
    }

    impl WidgetImpl for ArticleList {}

    impl BoxImpl for ArticleList {}

    #[gtk4::template_callbacks]
    impl ArticleList {
        #[template_callback]
        pub fn on_header_setup(&self, obj: &Object) {
            let Some(list_header) = obj.downcast_ref::<ListHeader>() else {
                return;
            };
            list_header.set_child(Some(&ArticleHeader::new()));
        }

        #[template_callback]
        fn on_header_bind(&self, obj: &Object) {
            let list_header = obj.downcast_ref::<ListHeader>().unwrap();
            let article = list_header.item().and_downcast::<GArticle>().unwrap();
            let child = list_header.child().and_downcast::<ArticleHeader>().unwrap();
            child.bind_model(&article);
        }

        #[template_callback]
        pub fn on_factory_setup(&self, obj: &Object) {
            let list_item = obj.downcast_ref::<ListItem>().unwrap();

            let row = ArticleRow::new();
            row.connect_activated(clone!(
                #[weak(rename_to = imp)]
                self,
                #[upgrade_or_panic]
                move |row| {
                    let selected_article_id = imp.selected_model.borrow().as_ref().map(GArticle::article_id);
                    let row_article_id = row.model().map(|article| article.article_id());

                    // only activate the article if it is already selected
                    // otherwise the selected-index setter will do the job
                    if selected_article_id.as_ref() == row_article_id.as_ref() {
                        let Some(model) = row.model() else {
                            return;
                        };

                        if model.read() == GRead::Unread {
                            let update = ReadUpdate {
                                article_id: model.article_id(),
                                read: GRead::Read,
                            };
                            MainWindow::activate_action("set-article-read", Some(&update.to_variant()));
                        }

                        ArticleViewColumn::instance().show_article(model.article_id(), true);
                    }
                }
            ));

            list_item.set_child(Some(&row));
        }

        #[template_callback]
        fn on_factory_teardown(&self, obj: &Object) {
            let list_item = obj.downcast_ref::<ListItem>().unwrap();
            list_item.set_child(Widget::NONE);
        }

        #[template_callback]
        fn on_factory_bind(&self, obj: &Object) {
            let list_item = obj.downcast_ref::<ListItem>().unwrap();
            let article = list_item.item().and_downcast::<GArticle>().unwrap();
            let child = list_item.child().and_downcast::<ArticleRow>().unwrap();
            child.set_model(Some(article));
        }

        #[template_callback]
        fn on_factory_unbind(&self, obj: &Object) {
            let list_item = obj.downcast_ref::<ListItem>().unwrap();
            let child = list_item.child().and_downcast::<ArticleRow>().unwrap();
            child.set_model(None::<GArticle>);
        }

        fn set_selected_index(&self, index: u32) {
            self.selected_index.set(index);
            let model = self.selection.item(index).and_downcast::<GArticle>();
            self.obj().set_selected_model(model);
        }

        fn set_selected_model(&self, model: Option<GArticle>) {
            if let Some(model) = &model {
                model.set_selected(true);

                let id = model.article_id();
                let read = model.read();

                if read == GRead::Unread {
                    let update = ReadUpdate {
                        article_id: id.clone(),
                        read: GRead::Read,
                    };
                    MainWindow::activate_action("set-article-read", Some(&update.to_variant()));
                }

                self.maybe_delayed_activation(move |_imp| {
                    if ArticleListColumn::instance().search_focused() {
                        return;
                    }

                    ArticleViewColumn::instance().show_article(id.clone(), true);
                });
            } else {
                ArticleViewColumn::instance().set_article(GArticle::NONE);
            }

            if let Some(old_model) = self.selected_model.replace(model) {
                old_model.set_selected(false);
            }
        }

        fn maybe_delayed_activation<FActivate>(&self, activate: FActivate)
        where
            FActivate: Fn(&Self) + 'static,
        {
            if self.skip_next_activation.get() {
                self.skip_next_activation.set(false);
                return;
            }

            if !self.delay_next_activation.get() {
                activate(self);
                return;
            }

            self.delay_next_activation.set(false);
            if let Some(a) = self.delayed_selection.take() {
                SourceId::remove(a)
            }

            let source_id = glib::timeout_add_local(
                Duration::from_millis(300),
                clone!(
                    #[weak(rename_to = imp)]
                    self,
                    #[upgrade_or]
                    ControlFlow::Break,
                    move || {
                        imp.delayed_selection.take();
                        activate(&imp);
                        ControlFlow::Break
                    }
                ),
            );

            self.delayed_selection.replace(Some(source_id));
        }

        #[template_callback]
        fn on_load_more(&self) {
            ArticleListColumn::instance().extend_list();
        }

        fn sort_sections(obj1: &Object, obj2: &Object) -> Ordering {
            let obj1 = obj1.downcast_ref::<GArticle>().unwrap();
            let obj2 = obj2.downcast_ref::<GArticle>().unwrap();

            let (date_1, date_2) = match App::default().settings().article_list().order_by() {
                GOrderBy::Published => (obj1.date().into(), obj2.date().into()),
                GOrderBy::Updated => {
                    let date_1: DateTime<Utc> = obj1.updated().as_ref().unwrap_or(obj1.date().into());
                    let date_2: DateTime<Utc> = obj2.updated().as_ref().unwrap_or(obj2.date().into());

                    (date_1, date_2)
                }
            };

            let date_1 = date_1.with_timezone(&Local);
            let date_2 = date_2.with_timezone(&Local);

            let date_1 = date_1.date_naive();
            let date_2 = date_2.date_naive();

            match App::default().settings().article_list().order() {
                GArticleOrder::NewestFirst => date_2.cmp(&date_1).into(),
                GArticleOrder::OldestFirst => date_1.cmp(&date_2).into(),
            }
        }

        pub(super) fn select_first(&self) -> Option<GArticleID> {
            self.delay_next_activation.set(true);
            self.selection.select_item(0, true);
            self.listview.scroll_to(0, ListScrollFlags::NONE, None);

            self.list_store
                .item(0)
                .and_downcast::<GArticle>()
                .map(|article| article.article_id())
        }

        pub(super) fn is_empty(&self) -> bool {
            self.list_store.item(0).is_none()
        }

        pub(super) fn compose_empty_message() -> String {
            let sidebar_selection = SideBar::instance().selection();
            let article_list_mode = ArticleListColumn::instance().mode();
            let search_term = ArticleListColumn::instance().search_term();
            let search_term = if search_term.is_empty() {
                None
            } else {
                Some(search_term)
            };
            let title = sidebar_selection.label().unwrap_or("Unknown".to_string());

            match sidebar_selection.selection_type() {
                SidebarSelectionType::None => String::new(),
                SidebarSelectionType::All => match article_list_mode {
                    ArticleListMode::All => match search_term {
                        Some(search) => i18n_f("No articles that fit \"{}\"", &[&search]),
                        None => i18n("No Articles"),
                    },
                    ArticleListMode::Unread => match search_term {
                        Some(search) => i18n_f("No unread articles that fit \"{}\"", &[&search]),
                        None => i18n("No Unread Articles"),
                    },
                    ArticleListMode::Marked => match search_term {
                        Some(search) => i18n_f("No starred articles that fit \"{}\"", &[&search]),
                        None => i18n("No Starred Articles"),
                    },
                },
                SidebarSelectionType::Today => match article_list_mode {
                    ArticleListMode::All => match search_term {
                        Some(search) => i18n_f("No articles today that fit \"{}\"", &[&search]),
                        None => i18n("No Articles today"),
                    },
                    ArticleListMode::Unread => match search_term {
                        Some(search) => i18n_f("No unread articles today that fit \"{}\"", &[&search]),
                        None => i18n("No Unread Articles today"),
                    },
                    ArticleListMode::Marked => match search_term {
                        Some(search) => i18n_f("No starred articles today that fit \"{}\"", &[&search]),
                        None => i18n("No Starred Articles today"),
                    },
                },
                SidebarSelectionType::FeedList => {
                    let item = match sidebar_selection.feedlist_id().as_ref() {
                        FeedListItemID::Category(_) => i18n("category"),
                        FeedListItemID::Feed(..) => i18n("feed"),
                    };
                    match article_list_mode {
                        ArticleListMode::All => match search_term {
                            Some(search) => {
                                i18n_f("No articles that fit \"{}\" in {} \"{}\"", &[&search, &item, &title])
                            }
                            None => i18n_f("No articles in {} \"{}\"", &[&item, &title]),
                        },
                        ArticleListMode::Unread => match search_term {
                            Some(search) => i18n_f(
                                "No unread articles that fit \"{}\" in {} \"{}\"",
                                &[&search, &item, &title],
                            ),
                            None => i18n_f("No unread articles in {} \"{}\"", &[&item, &title]),
                        },
                        ArticleListMode::Marked => match search_term {
                            Some(search) => i18n_f(
                                "No starred articles that fit \"{}\" in {} \"{}\"",
                                &[&search, &item, &title],
                            ),
                            None => i18n_f("No starred articles in {} \"{}\"", &[&item, &title]),
                        },
                    }
                }
                SidebarSelectionType::TagList => match article_list_mode {
                    ArticleListMode::All => match search_term {
                        Some(search) => i18n_f("No articles that fit \"{}\" in tag \"{}\"", &[&search, &title]),
                        None => i18n_f("No articles in tag \"{}\"", &[&title]),
                    },
                    ArticleListMode::Unread => match search_term {
                        Some(search) => i18n_f("No unread articles that fit \"{}\" in tag \"{}\"", &[&search, &title]),
                        None => i18n_f("No unread articles in tag \"{}\"", &[&title]),
                    },
                    ArticleListMode::Marked => match search_term {
                        Some(search) => i18n_f("No starred articles that fit \"{}\" in tag \"{}\"", &[&search, &title]),
                        None => i18n_f("No starred articles in tag \"{}\"", &[&title]),
                    },
                },
            }
        }

        pub(super) fn execute_diff(&self, diff: Edit<'_, Vec<GArticle>>) {
            let mut pos: u32 = 0;
            let mut article_insert_batch: Vec<GArticle> = Vec::new();
            let mut article_remove_batch = 0_u32;

            let Edit::Change(diff) = diff else {
                // no difference
                return;
            };

            let mut iter = diff.into_iter().peekable();

            loop {
                let edit = iter.next();

                let Some(edit) = edit else {
                    break;
                };

                match edit {
                    collection::Edit::Copy(_article) => {
                        // nothing changed
                        pos += 1;
                    }
                    collection::Edit::Insert(article) => {
                        // if the next edit will be an insert as well add it to article_insert_batch
                        // and insert all articles at once as soon as the next edit
                        // is something other than insert
                        if let Some(collection::Edit::Insert(_next_article)) = iter.peek() {
                            article_insert_batch.push(article.clone());
                        } else if !article_insert_batch.is_empty() {
                            article_insert_batch.push(article.clone());

                            self.list_store.splice(pos, 0, &article_insert_batch);
                            pos += article_insert_batch.len() as u32;

                            article_insert_batch.clear();
                        } else {
                            self.list_store.insert(pos, article);
                            pos += 1;
                        }
                    }
                    collection::Edit::Remove(_article) => {
                        // if the next edit will be an remove as well add it to article_remove_batch
                        // and remove all articles at once as soon as the next edit
                        // is something other than remove
                        if let Some(collection::Edit::Remove(_article)) = iter.peek() {
                            article_remove_batch += 1;
                        } else if article_remove_batch > 0 {
                            article_remove_batch += 1;

                            let empty_slice: &[GArticle] = &[];
                            self.list_store.splice(pos, article_remove_batch, empty_slice);
                            article_remove_batch = 0;
                        } else {
                            self.list_store.remove(pos);
                        }
                    }
                    collection::Edit::Change(diff) => {
                        let item = self.list_store.item(pos).and_downcast::<GArticle>();
                        if let Some(article_gobject) = item {
                            if let Some(read) = diff.read {
                                article_gobject.set_read(read);
                            }
                            if let Some(marked) = diff.marked {
                                article_gobject.set_marked(marked);
                            }
                            if let Some(timestamp) = diff.date {
                                article_gobject.set_date(timestamp);
                            }
                            if let Some(timestamp) = diff.updated {
                                article_gobject.set_updated(timestamp);
                            }
                            if let Some(tags) = diff.tags {
                                article_gobject.set_tags(tags);
                            }
                            if let Some(thumbnail) = diff.thumbnail {
                                article_gobject.set_thumbnail(thumbnail);
                            }
                            if let Some(feed_title) = diff.feed_title {
                                article_gobject.set_feed_title(feed_title);
                            }
                        }
                        pos += 1;
                    }
                }
            }
        }

        pub(super) fn get_article_dir(&self, direction: i32) -> Option<GArticle> {
            if self.is_empty() {
                return None;
            }

            let selected_item_pos = self.selection.selected();
            if selected_item_pos == gtk4::INVALID_LIST_POSITION {
                return None;
            }

            let new_item_pos = selected_item_pos as i32 + direction;

            if new_item_pos < 0 || new_item_pos as u32 == gtk4::INVALID_LIST_POSITION {
                return None;
            }

            let new_item_pos = new_item_pos as u32;

            self.list_store.item(new_item_pos).and_downcast::<GArticle>()
        }

        pub(super) fn activate_article_dir(&self, direction: i32) -> Option<GArticleID> {
            if self.is_empty() {
                return None;
            }

            self.listview.grab_focus();

            // don't automatically show article view when navigating via keyboard
            // only show article on activation (enter key)
            ArticleViewColumn::instance().set_block_next_view_switch(true);

            let selected_item_pos = self.selection.selected();
            if selected_item_pos == gtk4::INVALID_LIST_POSITION {
                return self.select_first();
            }

            let new_item_pos = selected_item_pos as i32 + direction;

            if new_item_pos < 0 || new_item_pos as u32 == gtk4::INVALID_LIST_POSITION {
                return self.select_first();
            }

            let new_item_pos = new_item_pos as u32;

            // select now, but activate 300ms later
            let article_list_visible = ContentPage::instance().is_article_list_visible();
            self.delay_next_activation.set(article_list_visible);
            self.selection.select_item(new_item_pos, true);
            self.listview.scroll_to(new_item_pos, ListScrollFlags::NONE, None);

            self.list_store
                .item(new_item_pos)
                .and_downcast::<GArticle>()
                .map(|article| article.article_id())
        }

        pub(super) fn select_article_dir(&self, direction: i32) {
            if self.is_empty() {
                return;
            }

            let selected_item_pos = self.selection.selected();
            let new_item_pos = selected_item_pos as i32 + direction;

            if new_item_pos < 0 || new_item_pos as u32 == gtk4::INVALID_LIST_POSITION {
                return;
            }

            let new_item_pos = new_item_pos as u32;

            self.skip_next_activation.set(true);
            self.selection.select_item(new_item_pos, true);
            self.listview.scroll_to(new_item_pos, ListScrollFlags::NONE, None);
        }

        pub(super) fn build_index(list_store: &ListStore) -> HashMap<GArticleID, GArticle> {
            let mut hashmap = HashMap::new();
            let mut index = 0;

            while let Some(article) = list_store.item(index).and_downcast::<GArticle>() {
                hashmap.insert(article.article_id(), article);
                index += 1;
            }

            hashmap
        }
    }
}

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

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

impl ArticleList {
    pub fn instance() -> Self {
        ArticleListColumn::instance().imp().article_list.get()
    }

    pub fn clear(&self) {
        let imp = self.imp();
        let message = imp::ArticleList::compose_empty_message();
        let message = glib::markup_escape_text(&message);

        imp.self_stack.freeze();
        imp.self_stack.set_status_description(Some(message.as_str()));
        imp.self_stack.show_status_full(SelfStackTransition::Crossfade);
        imp.list_store.remove_all();

        imp.index.take();
        imp.articles.take();
    }

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

        let require_new_list = is_new_list || imp.is_empty();

        // check if list model is empty and display a message
        if new_list.is_empty() {
            self.clear();
        } else {
            let new_articles = new_list.articles();

            // check if a new list is reqired or current list should be updated
            if require_new_list {
                // compare with empty model
                let empty_list = Vec::new();
                let diff = empty_list.diff(&new_articles);

                imp.self_stack.freeze();
                imp.list_store.remove_all();
                imp.execute_diff(diff);
                imp.self_stack.show_content_full(imp.transition.get());
            } else {
                let old_articles = imp.articles.borrow();
                let diff = old_articles.diff(&new_articles);

                imp.self_stack.freeze();
                imp.execute_diff(diff);
                imp.self_stack.show_content_full(imp.transition.get());
            }

            // generate new index from list store
            // so actual GArticle instances can be looked up
            // to modify the correct model instance
            let new_index = imp::ArticleList::build_index(&imp.list_store);
            imp.index.replace(new_index);
            imp.articles.replace(new_articles);
        }

        // reset transition to default for next update
        imp.transition.set(SelfStackTransition::Crossfade);
    }

    pub fn extend(&self, list_to_append: ArticleListModel) {
        let imp = self.imp();
        let mut models = list_to_append.articles();
        imp.list_store.splice(imp.list_store.n_items(), 0, &models);

        let new_index = imp::ArticleList::build_index(&imp.list_store);
        imp.index.replace(new_index);
        imp.articles.borrow_mut().append(&mut models);
    }

    pub fn activate_next_article(&self) -> Option<GArticleID> {
        self.imp().activate_article_dir(1)
    }

    pub fn activate_prev_article(&self) -> Option<GArticleID> {
        self.imp().activate_article_dir(-1)
    }

    pub fn select_next_article(&self) {
        self.imp().select_article_dir(1)
    }

    pub fn select_prev_article(&self) {
        self.imp().select_article_dir(-1)
    }

    pub fn can_go_next(&self) -> bool {
        let imp = self.imp();
        imp.selected_index.get() < imp.list_store.n_items().saturating_sub(1)
    }

    pub fn can_go_prev(&self) -> bool {
        self.imp().selected_index.get() > 0
    }

    pub fn next_article(&self) -> Option<GArticle> {
        self.imp().get_article_dir(1)
    }

    pub fn prev_article(&self) -> Option<GArticle> {
        self.imp().get_article_dir(-1)
    }

    pub fn select_and_scroll_to(&self, article_id: GArticleID) {
        let imp = self.imp();

        let borrow = imp.index.borrow();
        let Some(article) = borrow.get(&article_id) else {
            return;
        };
        if let Some(pos) = imp.list_store.find(article) {
            imp.listview.scroll_to(pos, ListScrollFlags::SELECT, None);
        }
    }

    pub fn set_article_row_state(&self, article_id: &GArticleID, read: Option<GRead>, marked: Option<GMarked>) {
        let imp = self.imp();

        if imp.is_empty() {
            return;
        }

        let index = imp.index.borrow();
        let Some(article_gobject) = index.get(article_id) else {
            return;
        };

        if let Some(read) = read {
            article_gobject.set_read(read);
        }
        if let Some(marked) = marked {
            article_gobject.set_marked(marked);
        }
    }

    pub fn article_row_update_tags(&self, article_id: &GArticleID, add: Option<&GTag>, remove: Option<&GTag>) {
        let imp = self.imp();

        if imp.is_empty() {
            return;
        }

        let index = imp.index.borrow();
        let Some(article_gobject) = index.get(article_id) else {
            return;
        };

        let mut tags = article_gobject.tags();

        if let Some(remove) = remove
            && let Some((index, _tag)) = tags
                .as_ref()
                .iter()
                .enumerate()
                .find(|(_i, t)| t.tag_id() == remove.tag_id())
        {
            tags.remove(index);
        }
        if let Some(add) = add {
            tags.push(add.clone());
        }

        article_gobject.set_tags(tags);
    }

    pub fn get_last_row_model(&self) -> Option<GArticle> {
        let imp = self.imp();
        let count = imp.selection.n_items();
        let last_index = if count == 0 { 0 } else { count - 1 };
        imp.selection.item(last_index).and_downcast::<GArticle>()
    }

    pub fn loaded_article_ids(&self) -> Vec<ArticleID> {
        self.imp().index.borrow().keys().cloned().map(ArticleID::from).collect()
    }
}
