mod article_list_column;
mod article_list_mode;
mod article_view_column;
mod pre_fullscreen_state;
mod sidebar_column;

pub use self::article_list_column::ArticleListColumn;
pub use self::article_list_mode::ArticleListMode;
pub use self::article_view_column::ArticleViewColumn;
pub use self::sidebar_column::SidebarColumn;

use self::pre_fullscreen_state::PreFullscreenState;
use crate::app::App;
use crate::article_list::ArticleList;
use crate::error::NewsFlashGtkError;
use crate::global_actions::{ArticleActions, EditActions};
use crate::gobject_models::{GSidebarSelection, SidebarSelectionType};
use crate::i18n::{i18n, i18n_f};
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use crate::media::MediaViewer;
use crate::settings::SyncIntervalType;
use crate::sidebar::{FeedListTree, SideBar, SidebarLoader, TagListModel};
use crate::themes::StyleManager;
use crate::undo_action::UndoAction;
use crate::util::{Elapsed, JOIN_ERROR, constants};
use gdk4::Texture;
use glib::{Binding, Object, Properties, clone, closure, subclass};
use gtk4::{
    Accessible, Box, Buildable, CompositeTemplate, ConstraintTarget, ExpressionWatch, Widget,
    prelude::*,
    prelude::{GObjectPropertyExpressionExt, ObjectExt},
    subclass::prelude::*,
};
use libadwaita::prelude::NavigationPageExt;
use libadwaita::{MultiLayoutView, NavigationSplitView, OverlaySplitView, Toast, ToastOverlay};
use news_flash::error::NewsFlashError;
use std::cell::{Cell, RefCell};
use tokio_util::sync::CancellationToken;

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::ContentPage)]
    #[template(file = "data/resources/ui_templates/content_page.blp")]
    pub struct ContentPage {
        #[template_child]
        pub content_overlay: TemplateChild<ToastOverlay>,
        #[template_child]
        pub multi_layout: TemplateChild<MultiLayoutView>,
        #[template_child]
        pub default_outer: TemplateChild<OverlaySplitView>,
        #[template_child]
        pub default_inner: TemplateChild<NavigationSplitView>,
        #[template_child]
        pub medium_outer: TemplateChild<NavigationSplitView>,
        #[template_child]
        pub medium_inner: TemplateChild<NavigationSplitView>,
        #[template_child]
        pub article_list_column: TemplateChild<ArticleListColumn>,
        #[template_child]
        pub sidebar_column: TemplateChild<SidebarColumn>,
        #[template_child]
        pub articleview_column: TemplateChild<ArticleViewColumn>,
        #[template_child]
        pub media_viewer: TemplateChild<MediaViewer>,

        #[property(get, set)]
        pub executing_undo_action: Cell<bool>,

        pub toast: RefCell<Option<Toast>>,
        pub undo_acton: RefCell<Option<UndoAction>>,
        pub pre_fullscreen_state: RefCell<Option<PreFullscreenState>>,
        pub cancellation_token: RefCell<CancellationToken>,
        pub expression_watch: RefCell<Option<ExpressionWatch>>,
    }

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

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

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

    #[glib::derived_properties]
    impl ObjectImpl for ContentPage {
        fn constructed(&self) {
            App::default()
                .settings()
                .feed_list()
                .connect_only_show_relevant_notify(|_settings| {
                    super::ContentPage::instance().update_sidebar();
                });

            App::default().settings().feed_list().connect_order_notify(|settings| {
                glib::spawn_future_local(SideBar::set_order(settings.order()));
            });

            App::default()
                .settings()
                .article_list()
                .connect_order_notify(|_settings| {
                    ArticleListColumn::instance().new_list();
                });

            App::default()
                .settings()
                .article_list()
                .connect_order_by_notify(|_settings| {
                    ArticleListColumn::instance().new_list();
                });

            App::default()
                .settings()
                .article_list()
                .connect_show_thumbnails_notify(|_settings| {
                    ArticleListColumn::instance().new_list();
                });

            App::default()
                .settings()
                .article_list()
                .connect_hide_future_articles_notify(|_settings| {
                    super::ContentPage::instance().update_sidebar();
                    ArticleListColumn::instance().update_list();
                });

            App::default().settings().general().connect_theme_notify(|settings| {
                StyleManager::apply_theme(&settings.theme());
            });

            App::default()
                .settings()
                .general()
                .connect_keep_running_in_background_notify(|settings| {
                    if !settings.keep_running_in_background() {
                        settings.set_autostart(false);
                    }
                });

            App::default()
                .settings()
                .general()
                .connect_autostart_notify(|settings| {
                    App::request_background_permission(settings.autostart());
                });

            App::default()
                .settings()
                .general()
                .connect_custom_sync_interval_notify(|settings| {
                    if settings.sync_type() == SyncIntervalType::Custom {
                        App::default().schedule_sync();
                    }
                });

            App::default()
                .settings()
                .general()
                .connect_predefined_sync_interval_notify(|settings| {
                    if settings.sync_type() == SyncIntervalType::Predefined {
                        App::default().schedule_sync();
                    }
                });

            App::default()
                .settings()
                .general()
                .connect_sync_type_notify(|_settings| {
                    App::default().schedule_sync();
                });

            self.sidebar_column
                .imp()
                .sidebar
                .bind_property("selection", &*self.article_list_column, "title")
                .transform_to(Self::selection_to_title)
                .build();

            let count_to_subtitle = closure!(|_: Option<Object>, count: u32| {
                if count == 0 {
                    None
                } else {
                    let prefix = match ArticleListColumn::instance().mode() {
                        ArticleListMode::All | ArticleListMode::Unread => i18n("Unread"),
                        ArticleListMode::Marked => i18n("Marked"),
                    };
                    Some(format!("{prefix}: {count}"))
                }
            });

            let selection_watch = self
                .sidebar_column
                .imp()
                .sidebar
                .property_expression("selection")
                .chain_property::<GSidebarSelection>("count")
                .chain_closure::<Option<String>>(count_to_subtitle)
                .bind(&*self.article_list_column, "subtitle", Widget::NONE);
            self.expression_watch.replace(Some(selection_watch));

            self.parent_constructed();
        }
    }

    impl WidgetImpl for ContentPage {
        fn unrealize(&self) {
            if let Some(watch) = self.expression_watch.take() {
                watch.unwatch();
            }

            self.parent_unrealize()
        }
    }

    impl BoxImpl for ContentPage {}

    impl ContentPage {
        fn selection_to_title(_binding: &Binding, selection: GSidebarSelection) -> Option<String> {
            match selection.selection_type() {
                SidebarSelectionType::None => None,
                SidebarSelectionType::All => Some(i18n("All Articles")),
                SidebarSelectionType::Today => Some(i18n("Today")),
                _ => selection.label(),
            }
        }

        pub(super) fn is_layout(&self, layout_name: &str) -> bool {
            self.multi_layout
                .layout_name()
                .map(|name| name == layout_name)
                .unwrap_or(false)
        }
    }
}

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

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

impl ContentPage {
    pub fn instance() -> Self {
        MainWindow::instance().imp().content_page.get()
    }

    pub fn save_pre_fullscreen_state(&self) {
        let imp = self.imp();

        let state = PreFullscreenState {
            outer_shows_sidebar: imp.default_outer.shows_sidebar(),
            outer_collapsed: imp.default_outer.is_collapsed(),
            inner_shows_content: imp.default_inner.shows_content(),
            inner_collapsed: imp.default_inner.is_collapsed(),
            medium_outer_shows_content: imp.medium_outer.shows_content(),
            medium_outer_collapsed: imp.medium_outer.is_collapsed(),
            medium_inner_shows_content: imp.medium_inner.shows_content(),
            medium_inner_collapsed: imp.medium_inner.is_collapsed(),
        };
        imp.pre_fullscreen_state.replace(Some(state));

        imp.default_outer.set_pin_sidebar(true);
    }

    pub fn restore_pre_fullscreen_state(&self) {
        let imp = self.imp();
        let Some(state) = imp.pre_fullscreen_state.take() else {
            return;
        };

        imp.default_outer.set_pin_sidebar(false);
        imp.default_outer.set_show_sidebar(state.outer_shows_sidebar);
        imp.default_outer.set_collapsed(state.outer_collapsed);
        imp.default_inner.set_show_content(state.inner_shows_content);
        imp.default_inner.set_collapsed(state.inner_collapsed);
        imp.medium_outer.set_show_content(state.medium_outer_shows_content);
        imp.medium_outer.set_collapsed(state.medium_outer_collapsed);
        imp.medium_inner.set_collapsed(state.medium_inner_collapsed);
    }

    pub fn show_article_view(&self, show: bool) {
        let imp = self.imp();

        if self.is_default_layout() {
            imp.default_inner.set_show_content(show);
        } else {
            imp.medium_outer.set_show_content(show);
        }
    }

    pub fn show_only_article_view(&self) {
        let imp = self.imp();

        // default layout
        imp.default_inner.set_collapsed(true);
        imp.default_inner.set_show_content(true);
        imp.default_outer.set_collapsed(true);
        imp.default_outer.set_show_sidebar(false);

        // medium layout
        imp.medium_outer.set_show_content(true);
    }

    pub fn set_article_view_can_pop(&self, can_pop: bool) {
        if let Some(article_navigation_page) = self.imp().default_inner.content() {
            article_navigation_page.set_can_pop(can_pop);
        }
    }

    pub fn is_default_layout(&self) -> bool {
        self.imp().is_layout(constants::CONTENT_PAGE_LAYOUT_DEFAULT)
    }

    pub fn is_medium_layout(&self) -> bool {
        self.imp().is_layout(constants::CONTENT_PAGE_LAYOUT_MEDIUM)
    }

    pub fn is_article_view_visible(&self) -> bool {
        let imp = self.imp();

        if self.is_default_layout() {
            imp.default_inner.shows_content()
        } else {
            imp.medium_outer.shows_content()
        }
    }

    pub fn is_article_view_hidden(&self) -> bool {
        let imp = self.imp();

        if self.is_default_layout() {
            imp.default_inner.is_collapsed() && !imp.default_inner.shows_content()
        } else {
            imp.medium_outer.is_collapsed() && !imp.medium_outer.shows_content()
        }
    }

    pub fn is_article_list_visible(&self) -> bool {
        let imp = self.imp();

        if self.is_default_layout() {
            !imp.default_inner.is_collapsed() || !imp.default_inner.shows_content()
        } else {
            !imp.medium_outer.is_collapsed() || !imp.medium_outer.shows_content()
        }
    }

    pub fn is_outer_collapsed(&self) -> bool {
        let imp = self.imp();

        if self.is_default_layout() {
            imp.default_outer.is_collapsed()
        } else {
            imp.medium_outer.is_collapsed()
        }
    }

    pub fn show_video(&self, url: &str, source: &Widget) {
        self.imp().media_viewer.show_video(url, source);
    }

    pub fn display_image(&self, texture: &Texture, source: &Widget, parent_url: Option<String>) {
        self.imp().media_viewer.show_image(texture, source, parent_url);
    }

    pub fn is_media_view_visible(&self) -> bool {
        self.imp().media_viewer.is_visible()
    }

    pub fn simple_message(&self, message: &str) {
        let imp = self.imp();
        let toast = Toast::new(message);
        toast.connect_dismissed(clone!(
            #[weak]
            imp,
            #[upgrade_or_panic]
            move |toast| {
                let remove = imp.toast.borrow().as_ref() == Some(toast);
                if remove {
                    _ = imp.toast.take();
                }
            }
        ));
        imp.content_overlay.add_toast(toast.clone());
        imp.toast.replace(Some(toast));
    }

    pub fn newsflash_error(&self, message: &str, error: NewsFlashError) {
        let imp = self.imp();
        let toast = Toast::new(message);
        toast.set_button_label(Some(&i18n("details")));
        App::default().set_newsflash_error(NewsFlashGtkError::NewsFlash {
            source: error,
            context: message.into(),
        });
        toast.set_action_name(Some("win.show-error-dialog"));
        toast.connect_dismissed(clone!(
            #[weak]
            imp,
            #[upgrade_or_panic]
            move |toast| {
                let remove = imp.toast.borrow().as_ref() == Some(toast);
                if remove {
                    _ = imp.toast.take();
                }
            }
        ));
        imp.content_overlay.add_toast(toast.clone());
        imp.toast.replace(Some(toast));
    }

    pub fn dismiss_notification(&self) {
        if let Some(toast) = self.imp().toast.take() {
            toast.dismiss();
        }
    }

    pub fn add_undo_notification(&self, action: UndoAction) {
        let imp = self.imp();

        self.dismiss_notification();

        let message = match &action {
            UndoAction::MarkRead(_ids) => i18n("Mark all read"),
            UndoAction::DeleteCategory(_id, label) => i18n_f("Deleted Category '{}'", &[label]),
            UndoAction::DeleteFeed(_id, label) => i18n_f("Deleted Feed '{}'", &[label]),
            UndoAction::DeleteTag(_id, label) => i18n_f("Deleted Tag '{}'", &[label]),
        };

        let toast = Toast::new(&message);
        toast.set_button_label(Some(&i18n("Undo")));

        toast.connect_button_clicked(clone!(
            #[weak]
            imp,
            move |_toast| {
                glib::spawn_future_local(clone!(
                    #[weak(rename_to = obj)]
                    imp.obj(),
                    async move {
                        let Some(action) = imp.undo_acton.take() else {
                            return;
                        };

                        if let Some(toast) = imp.toast.take() {
                            toast.dismiss();
                        }

                        match action {
                            UndoAction::MarkRead(ids) => ArticleActions::set_articles_unread(ids).await,
                            _ => {
                                obj.update_sidebar();
                                ArticleListColumn::instance().update_list();
                            }
                        }
                    }
                ));
            }
        ));

        toast.connect_dismissed(clone!(
            #[weak]
            imp,
            #[strong]
            action,
            #[upgrade_or_panic]
            move |toast| {
                let remove = imp.toast.borrow().as_ref() == Some(toast);
                if remove {
                    _ = imp.toast.take();
                }

                tracing::debug!(%action, "remove current action");

                glib::spawn_future_local(clone!(
                    #[weak]
                    imp,
                    async move {
                        let obj = imp.obj();
                        obj.set_executing_undo_action(true);

                        let Some(action) = imp.undo_acton.take() else {
                            return;
                        };

                        match action {
                            UndoAction::MarkRead(_ids) => {}
                            UndoAction::DeleteFeed(feed_id, _label) => EditActions::delete_feed(feed_id.clone()).await,
                            UndoAction::DeleteCategory(category_id, _label) => {
                                EditActions::delete_category(category_id.clone()).await
                            }
                            UndoAction::DeleteTag(tag_id, _label) => EditActions::delete_tag(tag_id.clone()).await,
                        }

                        obj.set_executing_undo_action(false);
                    }
                ));
            }
        ));

        imp.content_overlay.add_toast(toast.clone());
        imp.toast.replace(Some(toast));
        imp.undo_acton.replace(Some(action));
    }

    pub fn get_current_undo_action(&self) -> Option<UndoAction> {
        self.imp().undo_acton.borrow().clone()
    }

    pub fn load_branding(&self) {
        glib::spawn_future_local(async move {
            let future = TokioRuntime::spawn(async move {
                if let Some(news_flash) = App::news_flash().read().await.as_ref() {
                    (news_flash.id().await, news_flash.user_name().await)
                } else {
                    (None, None)
                }
            });

            let (id, user) = future.await.expect(JOIN_ERROR);
            if let Some(id) = id {
                _ = SidebarColumn::instance().set_account(&id, user.as_deref()).await;
            } else {
                // in case of failure show 'welcome page'
                MainWindow::instance().show_welcome_page();
            }
        });
    }

    pub fn clear(&self) {
        ArticleList::instance().clear();

        let feed_tree_model = FeedListTree::new();
        let tag_list_model = TagListModel::new();

        SideBar::instance().update(0, 0, 0, feed_tree_model, tag_list_model);
    }

    pub fn update_sidebar(&self) {
        let new_token = CancellationToken::new();
        let old_token = self.imp().cancellation_token.replace(new_token.clone());
        old_token.cancel();

        let loader = SidebarLoader::default()
            .hide_future_articles(App::default().settings().article_list().hide_future_articles())
            .article_list_mode(ArticleListColumn::instance().mode())
            .undo_action(self.get_current_undo_action());

        glib::spawn_future_local(async move {
            let news_flash = App::news_flash();

            let future = TokioRuntime::spawn(async move {
                let news_flash = tokio::select! {
                    news_flash = news_flash.read() => { Ok(news_flash) }
                    _ = new_token.cancelled() => {
                        tracing::debug!("loading was cancelled");
                        Err(Elapsed::new())
                    }
                };

                if let Ok(Some(news_flash)) = news_flash.as_deref().as_ref() {
                    loader.load(news_flash)
                } else {
                    Err(NewsFlashError::NotLoggedIn)
                }
            });

            match future.await.expect(JOIN_ERROR) {
                Ok(load_result) => {
                    let (total_count, today_count, selected_count, feed_list_model, tag_list_model) =
                        load_result.build_list_models(SideBar::instance().selection());

                    SideBar::instance().update(
                        total_count,
                        today_count,
                        selected_count,
                        feed_list_model,
                        tag_list_model,
                    );
                }
                Err(error) => {
                    Self::instance().simple_message(&i18n_f("Failed to update sidebar: '{}'", &[&error.to_string()]))
                }
            }
        });
    }
}
