use std::time::Duration;

use gdk::{
    glib::{self, clone},
    subclass::prelude::ObjectSubclassIsExt,
};
use gtk::prelude::AdjustmentExt;

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

impl JourneysPage {
    fn is_auto_scroll(&self) -> bool {
        self.auto_scroll()
    }

    fn scroll_down(&self) {
        if self.is_auto_scroll() {
            gspawn!(clone!(
                #[weak(rename_to = s)]
                self,
                async move {
                    // Need to sleep a little to make sure the scrolled window saw the changed
                    // child.
                    glib::timeout_future(Duration::from_millis(50)).await;
                    let adjustment = s.imp().scrolled_window.vadjustment();
                    adjustment.set_value(adjustment.upper());
                }
            ));
        }
    }

    fn scroll_up(&self) {
        if self.is_auto_scroll() {
            gspawn!(clone!(
                #[weak(rename_to = s)]
                self,
                async move {
                    // Need to sleep a little to make sure the scrolled window saw the changed
                    // child.
                    glib::timeout_future(Duration::from_millis(50)).await;
                    let adjustment = s.imp().scrolled_window.vadjustment();
                    adjustment.set_value(adjustment.lower());
                }
            ));
        }
    }
}

pub mod imp {
    use std::cell::Cell;
    use std::cell::RefCell;

    use chrono::Duration;
    use gdk::gio::Settings;
    use gdk::glib::MainContext;
    use gdk::glib::Properties;
    use gdk::glib::clone;
    use gdk::glib::subclass::Signal;
    use glib::subclass::InitializingObject;
    use gtk::CompositeTemplate;
    use gtk::ListItem;
    use gtk::PositionType;
    use gtk::SignalListItemFactory;
    use gtk::Widget;
    use gtk::glib;
    use gtk::prelude::*;
    use gtk::subclass::prelude::*;
    use once_cell::sync::Lazy;
    use rcore::JourneysOptions;
    use rcore::LoyaltyCard;
    use rcore::TariffClass;
    use rcore::TransferOptions;

    use crate::backend::Client;
    use crate::backend::Journey;
    use crate::backend::JourneysResult;
    use crate::backend::TimeType;
    use crate::config;
    use crate::gui::journey_list_item::JourneyListItem;
    use crate::gui::time_divider::TimeDivider;
    use crate::gui::utility::Utility;
    use crate::gui::window::Window;

    #[derive(CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::JourneysPage)]
    #[template(resource = "/ui/journeys_page.ui")]
    pub struct JourneysPage {
        #[template_child]
        pub(super) scrolled_window: TemplateChild<gtk::ScrolledWindow>,

        #[template_child]
        list_journeys: TemplateChild<gtk::ListView>,

        destination_alignment_group: gtk::SizeGroup,

        #[property(get, set)]
        journeys_result: RefCell<Option<JourneysResult>>,

        settings: Settings,
        #[property(get, set)]
        client: RefCell<Option<Client>>,

        #[property(name = "loading-earlier", get, set)]
        loading_earlier: Cell<bool>,
        #[property(name = "loading-later", get, set)]
        loading_later: Cell<bool>,
        #[property(get, set)]
        auto_scroll: Cell<bool>,
        #[property(get, set)]
        compact: Cell<bool>,
        #[property(get, set)]
        scrolled_up: Cell<bool>,
    }

    impl Default for JourneysPage {
        fn default() -> Self {
            Self {
                scrolled_window: Default::default(),
                list_journeys: Default::default(),
                destination_alignment_group: gtk::SizeGroup::new(gtk::SizeGroupMode::Horizontal),
                journeys_result: Default::default(),
                settings: Settings::new(config::BASE_ID),
                client: Default::default(),
                loading_earlier: Default::default(),
                loading_later: Default::default(),
                auto_scroll: Cell::new(true),
                compact: Cell::new(false),
                scrolled_up: Cell::new(false),
            }
        }
    }

    #[gtk::template_callbacks]
    impl JourneysPage {
        /// Every time when the page is not yet filled with the journeys, load more.
        fn connect_initial_loading(&self) {
            let obj = self.obj();
            self.scrolled_window.vadjustment().connect_changed(clone!(
                #[weak]
                obj,
                move |adj| {
                    // This means the page is not yet filled.
                    if adj.upper() <= adj.page_size() {
                        // Do not scroll for the initial loading.
                        obj.set_auto_scroll(false);
                        if obj
                            .journeys_result()
                            .expect("JourneysPage to have a JourneysResult when loading")
                            .time_type()
                            == TimeType::Departure
                        {
                            obj.imp().handle_later()
                        } else {
                            obj.imp().handle_earlier()
                        }
                    } else {
                        // Scroll if the page is already filled and more is manually requested.
                        obj.set_auto_scroll(true);
                    }
                }
            ));
        }

        #[template_callback]
        fn handle_edge_reached(&self, position: PositionType) {
            match position {
                PositionType::Top => self.handle_earlier(),
                PositionType::Bottom => self.handle_later(),
                _ => (),
            }
        }

        #[template_callback]
        fn handle_earlier(&self) {
            let obj = self.obj();

            // Skip if already loading.
            if obj.loading_earlier() {
                return;
            }
            obj.set_loading_earlier(true);

            if self.auto_scroll.get() && !self.scrolled_up.get() {
                obj.set_scrolled_up(true);
            }

            let main_context = MainContext::default();
            let window = self.obj().root().and_downcast::<Window>().expect(
                "search page must be mapped and realised when a template callback is called",
            );
            main_context.spawn_local(clone!(
                #[strong]
                obj,
                #[strong(rename_to = settings)]
                self.settings,
                #[strong]
                window,
                async move {
                    let Some(journeys_result) = obj.journeys_result() else {
                        return;
                    };

                    let client = obj.client().expect("Client of JourneysPage to be set up");
                    let result_journeys_result = client
                        .journeys(
                            journeys_result.source().expect("Journey to have a source"),
                            journeys_result
                                .destination()
                                .expect("Journey to have a destination"),
                            journeys_result.time_type(),
                            JourneysOptions {
                                earlier_than: journeys_result.earlier_ref(),
                                language: Some(Utility::language_code()),
                                stopovers: true,
                                loyalty_card: LoyaltyCard::from_id(
                                    settings
                                        .enum_("bahncard")
                                        .try_into()
                                        .expect("Failed to convert setting `bahncard` to u8"),
                                ),
                                bike_friendly: settings.boolean("bike-accessible"),
                                transfers: if settings.boolean("direct-only") {
                                    TransferOptions::Limited(0)
                                } else {
                                    TransferOptions::Unlimited
                                },
                                // Value clamped in the settings; default should never happen.
                                transfer_time: Duration::try_minutes(
                                    settings.int("transfer-time").into(),
                                )
                                .unwrap_or_default(),
                                tariff_class: if settings.boolean("first-class") {
                                    TariffClass::First
                                } else {
                                    TariffClass::Second
                                },
                                products: Utility::products_selection_from_setting(&settings),
                                ..Default::default()
                            },
                        )
                        .await;
                    if let Ok(result_journeys_result) = result_journeys_result {
                        journeys_result.merge_prepend(&result_journeys_result)
                    } else {
                        window.display_error_toast(
                            result_journeys_result.expect_err("Error to be present"),
                        );
                    }
                    obj.set_loading_earlier(false);
                    obj.scroll_up();
                }
            ));
        }

        #[template_callback]
        fn handle_later(&self) {
            let obj = self.obj();

            // Skip if already loading.
            if obj.loading_later() {
                return;
            }
            obj.set_loading_later(true);

            let main_context = MainContext::default();
            let window = self.obj().root().and_downcast::<Window>().expect(
                "search page must be mapped and realised when a template callback is called",
            );
            main_context.spawn_local(clone!(
                #[strong]
                obj,
                #[strong(rename_to = settings)]
                self.settings,
                #[strong]
                window,
                async move {
                    let journeys_result = obj.property::<JourneysResult>("journeys-result");

                    let result_journeys_result = obj
                        .property::<Client>("client")
                        .journeys(
                            journeys_result.source().expect("Journey to have a source"),
                            journeys_result
                                .destination()
                                .expect("Journey to have a destination"),
                            journeys_result.time_type(),
                            JourneysOptions {
                                later_than: journeys_result.later_ref(),
                                language: Some(Utility::language_code()),
                                stopovers: true,
                                loyalty_card: LoyaltyCard::from_id(
                                    settings
                                        .enum_("bahncard")
                                        .try_into()
                                        .expect("Failed to convert setting `bahncard` to u8"),
                                ),
                                bike_friendly: settings.boolean("bike-accessible"),
                                transfers: if settings.boolean("direct-only") {
                                    TransferOptions::Limited(0)
                                } else {
                                    TransferOptions::Unlimited
                                },
                                // Value clamped in the settings; default should never happen.
                                transfer_time: Duration::try_minutes(
                                    settings.int("transfer-time").into(),
                                )
                                .unwrap_or_default(),
                                tariff_class: if settings.boolean("first-class") {
                                    TariffClass::First
                                } else {
                                    TariffClass::Second
                                },
                                products: Utility::products_selection_from_setting(&settings),
                                ..Default::default()
                            },
                        )
                        .await;
                    if let Ok(result_journeys_result) = result_journeys_result {
                        journeys_result.merge_append(&result_journeys_result);
                    } else {
                        window.display_error_toast(
                            result_journeys_result.expect_err("Error to be present"),
                        );
                    }
                    obj.set_loading_later(false);
                    obj.scroll_down();
                }
            ));
        }

        fn setup_model(&self, obj: &super::JourneysPage) {
            let factory = SignalListItemFactory::new();
            factory.connect_setup(clone!(
                #[weak]
                obj,
                #[weak(rename_to = size_group)]
                self.destination_alignment_group,
                move |_, list_item| {
                    let journey_item = JourneyListItem::new();
                    let list_item = list_item
                        .downcast_ref::<ListItem>()
                        .expect("The factory item to be a `ListItem`");

                    list_item.set_child(Some(&journey_item));
                    list_item.property_expression("item").bind(
                        &journey_item,
                        "journey",
                        Widget::NONE,
                    );
                    obj.property_expression("compact")
                        .bind(&journey_item, "compact", Widget::NONE);

                    size_group.add_widget(&journey_item.get_destination_box());

                    journey_item.connect_parent_notify(|journey_item| {
                        if let Some(parent) = journey_item.property::<Option<Widget>>("parent") {
                            parent.update_relation(&[gtk::accessible::Relation::DescribedBy(&[
                                journey_item.get_indicators().upcast_ref(),
                            ])]);
                        }
                    });
                }
            ));
            factory.connect_bind(|_, list_item| {
                let list_item = list_item
                    .downcast_ref::<ListItem>()
                    .expect("The factory item to be a `ListItem`");
                let journey_item = list_item
                    .child()
                    .expect("The ListItem to have a child")
                    .downcast_ref::<JourneyListItem>()
                    .expect("The ListItem's child to be a `JourneyListItem`")
                    .clone();

                list_item.set_accessible_label(&journey_item.format_trip_description());
            });

            let header_factory = SignalListItemFactory::new();
            header_factory.connect_setup(clone!(
                #[weak]
                obj,
                move |_, object| {
                    let widget = TimeDivider::default();
                    let header_item = object.downcast_ref::<gtk::ListHeader>().unwrap();
                    header_item.set_child(Some(&widget));
                    header_item.bind_property("item", &widget, "item").build();
                    header_item.bind_property("start", &widget, "start").build();
                    obj.bind_property("scrolled-up", &widget, "initial")
                        .invert_boolean()
                        .sync_create()
                        .build();
                    obj.bind_property("journeys-result", &widget, "journeys-result")
                        .sync_create()
                        .build();
                }
            ));

            self.list_journeys.set_factory(Some(&factory));
            self.list_journeys.set_header_factory(Some(&header_factory));
            self.list_journeys.set_single_click_activate(true);

            self.list_journeys.connect_activate(clone!(
                #[strong]
                obj,
                move |list_view, position| {
                    let model = list_view.model().expect("The model has to exist.");
                    let journey_object = model
                        .item(position)
                        .expect("The item has to exist.")
                        .downcast::<Journey>()
                        .expect("The item has to be an `Journey`.");

                    obj.emit_by_name::<()>("select", &[&journey_object]);
                }
            ));
        }
    }

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

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

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

    #[glib::derived_properties]
    impl ObjectImpl for JourneysPage {
        fn constructed(&self) {
            self.parent_constructed();
            self.setup_model(&self.obj());
            self.connect_initial_loading();
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("select")
                        .param_types([Journey::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }
    }

    impl WidgetImpl for JourneysPage {}
    impl BoxImpl for JourneysPage {}
}
