use super::loader::{ArticleListLoader, LoadType};
use crate::app::App;
use crate::article_list::{ArticleList, loader::ArticleListLoadResult, model::ArticleListModel};
use crate::content_page::{ArticleListColumn, ContentPage};
use crate::infrastructure::TokioRuntime;
use crate::settings::GOrderBy;
use crate::sidebar::SideBar;
use crate::util::{Elapsed, JOIN_ERROR};
use glib::{Object, Properties, clone, prelude::*, subclass::*};
use gtk4::{prelude::*, subclass::prelude::*};
use news_flash::error::NewsFlashError;
use news_flash::models::ArticleID;
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
use tokio_util::sync::CancellationToken;

mod imp {

    use super::*;

    #[derive(Debug, Default, Properties)]
    #[properties(wrapper_type = super::ArticleListLoadQueue)]
    pub struct ArticleListLoadQueue {
        pub custom_page_size: Cell<Option<i64>>,
        pub custom_article_to_load: RefCell<Option<ArticleID>>,
        pub cancellation_token: RefCell<CancellationToken>,

        #[property(get, set, default = true)]
        pub enabled: Cell<bool>,
    }

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

    #[glib::derived_properties]
    impl ObjectImpl for ArticleListLoadQueue {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("new")
                        .param_types([ArticleListModel::static_type()])
                        .build(),
                    Signal::builder("update")
                        .param_types([ArticleListModel::static_type()])
                        .build(),
                    Signal::builder("extend")
                        .param_types([ArticleListModel::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }

        fn dispose(&self) {
            self.cancellation_token.borrow().cancel();
        }
    }

    impl ArticleListLoadQueue {
        pub(super) async fn update_force(&self, force_new: bool) {
            let new_token = CancellationToken::new();
            let old_token = self.cancellation_token.replace(new_token.clone());
            old_token.cancel();

            let page_size = self.custom_page_size.take();
            let custom_article = self.custom_article_to_load.take();
            let loader = Self::build_loader(
                if force_new { LoadType::New } else { LoadType::Update },
                page_size,
                custom_article,
            );

            let future = TokioRuntime::spawn(async move {
                let load_future = Self::load_impl(loader);

                tokio::select! {
                    load_result = load_future => {
                        Ok(load_result)
                    }
                    _ = new_token.cancelled() => {
                        tracing::debug!("loading was cancelled");
                        Err(Elapsed::new())
                    }
                }
            });

            let load_result = match future.await.expect(JOIN_ERROR) {
                Ok(load_result) => load_result,
                Err(_elapsed) => return,
            };

            if let Ok(load_result) = load_result {
                let load_type = load_result.load_type();
                let list_model = load_result.build_list_model();

                if load_type == LoadType::New {
                    self.emit_new(list_model);
                } else {
                    self.emit_update(list_model);
                }
            }
        }

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

            let page_size = self.custom_page_size.take();
            let custom_article = self.custom_article_to_load.take();
            let loader = Self::build_loader(LoadType::Extend, page_size, custom_article);

            let future = TokioRuntime::spawn(async move {
                let load_future = Self::load_impl(loader);

                tokio::select! {
                    load_result = load_future => {
                        Ok(load_result)
                    }
                    _ = new_token.cancelled() => {
                        tracing::debug!("loading was cancelled");
                        Err(Elapsed::new())
                    }
                }
            });

            let load_result = match future.await.expect(JOIN_ERROR) {
                Ok(load_result) => load_result,
                Err(_elapsed) => return,
            };

            if let Ok(load_result) = load_result {
                self.emit_extend(load_result.build_list_model());
            }
        }

        fn build_loader(
            load_type: LoadType,
            page_size: Option<i64>,
            custom_article: Option<ArticleID>,
        ) -> ArticleListLoader {
            let article_list_mode = ArticleListColumn::instance().mode();
            tracing::debug!(?load_type, ?article_list_mode, "build loader");

            let search_term = ArticleListColumn::instance().search_term();
            let search_term = if search_term.is_empty() {
                None
            } else {
                Some(search_term)
            };

            let last_article_date = if App::default().settings().article_list().order_by() == GOrderBy::Published {
                ArticleList::instance()
                    .get_last_row_model()
                    .map(|model| model.date().into())
            } else {
                let model = ArticleList::instance().get_last_row_model();
                model.map(|model| model.updated().as_ref().unwrap_or(*model.date().as_ref()))
            };

            ArticleListLoader::default()
                .load_type(load_type)
                .hide_furure_articles(App::default().settings().article_list().hide_future_articles())
                .search_term(search_term)
                .order(App::default().settings().article_list().order().into())
                .order_by(App::default().settings().article_list().order_by().into())
                .mode(article_list_mode)
                .page_size(page_size)
                .list_height(ArticleList::instance().height() as i64)
                .sidebar_selection(SideBar::instance().selection())
                .undo_action(ContentPage::instance().get_current_undo_action())
                .last_article_date(last_article_date)
                .loaded_article_ids(ArticleList::instance().loaded_article_ids())
                .restore_selected_id(custom_article)
        }

        async fn load_impl(loader: ArticleListLoader) -> Result<ArticleListLoadResult, NewsFlashError> {
            let news_flash = App::news_flash();
            let guard = news_flash.read().await;
            let news_flash = guard.as_ref().ok_or(NewsFlashError::NotLoggedIn)?;
            loader.load(news_flash)
        }

        fn emit_new(&self, model: ArticleListModel) {
            tracing::debug!("emit new article list");
            self.obj().emit_by_name::<()>("new", &[&model]);
        }

        fn emit_update(&self, model: ArticleListModel) {
            tracing::debug!("emit update article list");
            self.obj().emit_by_name::<()>("update", &[&model]);
        }

        fn emit_extend(&self, model: ArticleListModel) {
            tracing::debug!("emit extend article list");
            self.obj().emit_by_name::<()>("extend", &[&model]);
        }
    }
}

glib::wrapper! {
    pub struct ArticleListLoadQueue(ObjectSubclass<imp::ArticleListLoadQueue>);
}

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

impl ArticleListLoadQueue {
    pub fn update(&self) {
        if self.enabled() {
            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self.imp(),
                async move {
                    imp.update_force(false).await;
                }
            ));
        } else {
            tracing::debug!("reject update because load queue is disabled");
        }
    }

    pub fn update_forced(&self) {
        if self.enabled() {
            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self.imp(),
                async move {
                    imp.update_force(true).await;
                }
            ));
        } else {
            tracing::debug!("reject update_forced because load queue is disabled");
        }
    }

    pub fn extend(&self) {
        if self.enabled() {
            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self.imp(),
                async move {
                    imp.extend().await;
                }
            ));
        } else {
            tracing::debug!("reject extend because load queue is disabled");
        }
    }

    pub fn set_custom_page_size(&self, page_size: Option<i64>) {
        self.imp().custom_page_size.replace(page_size);
    }

    pub fn set_custom_article_to_load(&self, article_id: Option<ArticleID>) {
        self.imp().custom_article_to_load.replace(article_id);
    }
}
