use glib::{ControlFlow, Properties, SourceId, clone, subclass::*};
use gtk4::{
    Accessible, Adjustment, Buildable, CompositeTemplate, ConstraintTarget, Widget, prelude::*, subclass::prelude::*,
};
use libadwaita::{Bin, subclass::prelude::*};
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::time::Duration;

const LOAD_MORE_THREASHOLD: f64 = 200.0;
const LIST_BOTTOM: f64 = LOAD_MORE_THREASHOLD / 4.0;
const TIMEOUT_MS: u64 = 400;
const LOAD_MORE_SIGNAL_NAME: &str = "load-more";

mod imp {
    use super::*;

    #[derive(Default, Debug, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::ArticleListScroll)]
    #[template(file = "data/resources/ui_templates/article_list/scroll.blp")]
    pub struct ArticleListScroll {
        #[property(get, set, nullable)]
        pub child: RefCell<Option<Widget>>,

        pub timout_source_id: RefCell<Option<SourceId>>,
    }

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

        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 ArticleListScroll {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder(LOAD_MORE_SIGNAL_NAME)
                        .param_types([super::ArticleListScroll::static_type()])
                        .build(),
                ]
            });

            SIGNALS.as_ref()
        }
    }

    impl WidgetImpl for ArticleListScroll {}

    impl BinImpl for ArticleListScroll {}

    #[gtk4::template_callbacks]
    impl ArticleListScroll {
        #[template_callback]
        fn on_vadjustment_changed(&self, vadj: &Adjustment) {
            if !self.check_threshold(vadj, LOAD_MORE_THREASHOLD) {
                return;
            }

            if self.timout_source_id.borrow().is_some() {
                return;
            }

            self.emit_load_more();
            self.start_timeout(vadj);
        }

        fn start_timeout(&self, vadj: &Adjustment) {
            let value_before = vadj.value();
            let page_size_before = vadj.page_size();

            let source_id = glib::source::timeout_add_local(
                Duration::from_millis(TIMEOUT_MS),
                clone!(
                    #[weak]
                    vadj,
                    #[weak(rename_to = imp)]
                    self,
                    #[upgrade_or_panic]
                    move || {
                        imp.timout_source_id.take();

                        if vadj.page_size() <= page_size_before && vadj.value() <= value_before {
                            return ControlFlow::Break;
                        }

                        if imp.check_threshold(&vadj, LIST_BOTTOM) {
                            imp.emit_load_more();
                            imp.start_timeout(&vadj);
                        }
                        ControlFlow::Break
                    }
                ),
            );

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

        fn check_threshold(&self, vadj: &Adjustment, threshold: f64) -> bool {
            let max = vadj.upper() - vadj.page_size();
            max > 0.0 && vadj.value() > (max - threshold)
        }

        fn emit_load_more(&self) {
            let obj = self.obj();
            obj.emit_by_name::<()>(LOAD_MORE_SIGNAL_NAME, &[&*obj]);
        }
    }
}

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

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