use crate::app::App;
use crate::content_page::ContentPage;
use crate::edit_feed_dialog::CategorySelectGObject;
use crate::gobject_models::GFeed;
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use crate::util::JOIN_ERROR;
use gdk4::Texture;
use gio::ListStore;
use glib::{Properties, clone, prelude::*, subclass::*};
use gtk4::{
    Accessible, Box, Buildable, CompositeTemplate, ConstraintTarget, PropertyExpression, Widget, subclass::prelude::*,
};
use libadwaita::{ComboRow, prelude::*};
use news_flash::models::{Feed, NEWSFLASH_TOPLEVEL};
use once_cell::sync::Lazy;
use reqwest::header::HeaderMap;
use std::cell::{Cell, RefCell};

mod imp {

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::AddFeedWidget)]
    #[template(file = "data/resources/ui_templates/add_dialog/feed_widget.blp")]
    pub struct AddFeedWidget {
        #[template_child]
        pub category_combo: TemplateChild<ComboRow>,

        #[property(get, set, name = "feed-url")]
        pub feed_url: RefCell<String>,

        #[property(get, set, name = "feed-title")]
        pub feed_title: RefCell<String>,

        #[property(get, set = Self::set_feed)]
        pub feed: RefCell<GFeed>,

        #[property(get, set, nullable)]
        pub texture: RefCell<Option<Texture>>,

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

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

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

        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 AddFeedWidget {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| vec![Signal::builder("feed-added").build()]);
            SIGNALS.as_ref()
        }

        fn constructed(&self) {
            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move { imp.fill_category_dropdown().await }
            ));
            self.parent_constructed();
        }
    }

    impl WidgetImpl for AddFeedWidget {}

    impl BoxImpl for AddFeedWidget {}

    #[gtk4::template_callbacks]
    impl AddFeedWidget {
        #[template_callback]
        fn is_string_not_empty(&self, string: String) -> bool {
            !string.is_empty()
        }

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

            let feed_url = obj.feed_url();
            if feed_url.is_empty() {
                tracing::error!("Failed to add feed: No url");
                ContentPage::instance().simple_message(&i18n("Failed to add feed: No valid url"));
                return;
            }
            let feed_title = obj.feed_title();
            let category_id = self
                .category_combo
                .selected_item()
                .and_downcast::<CategorySelectGObject>()
                .as_ref()
                .map(CategorySelectGObject::id)
                .map(|id| id.as_ref().to_string())
                .unwrap_or_default();
            let scrape = self.scrape.get();

            let variant = (feed_url, feed_title, category_id, scrape).to_variant();
            MainWindow::activate_action("add-feed", Some(&variant));
            obj.emit_by_name::<()>("feed-added", &[]);
        }

        async fn fill_category_dropdown(&self) {
            let future = TokioRuntime::spawn(async move {
                let news_flash = App::news_flash();
                let news_flash_guad = news_flash.read().await;
                let news_flash = news_flash_guad.as_ref()?;
                let (categories, _mappings) = news_flash.get_categories().ok()?;
                Some(categories)
            });

            let categories = future.await.expect(JOIN_ERROR);

            let list_store = ListStore::new::<CategorySelectGObject>();
            list_store.append(&CategorySelectGObject::new(&NEWSFLASH_TOPLEVEL, &i18n("None")));

            if let Some(categories) = categories {
                for category in &categories {
                    list_store.append(&CategorySelectGObject::from(category.clone()));
                }
            }

            let expression = PropertyExpression::new(
                CategorySelectGObject::static_type(),
                None::<&PropertyExpression>,
                "label",
            );
            self.category_combo.set_expression(Some(&expression));
            self.category_combo.set_model(Some(&list_store));
            self.category_combo.set_selected(0);
        }

        fn set_feed(&self, feed: GFeed) {
            let obj = self.obj();
            obj.set_feed_title(feed.label());
            obj.set_feed_url(feed.feed_url().unwrap_or_default());

            let feed: Feed = feed.into();
            let header_map = App::default().settings().get_feed_header_map(&feed.feed_id);

            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move { imp.fetch_favicon(feed, header_map).await }
            ));
        }

        async fn fetch_favicon(&self, feed: Feed, header_map: HeaderMap) {
            let future = TokioRuntime::spawn(async move {
                let client = App::client().await;
                news_flash::util::favicons::FavIconLoader::fetch_new_icon(
                    None, &feed, &client, header_map,
                    None, // image is 64x64 so try to fetch a 128px icon because of 2x scaling
                )
                .await
            });

            let favicon = future.await.expect(JOIN_ERROR);
            let data = if favicon.highres.is_some() {
                favicon.highres
            } else {
                favicon.lowres
            };
            let Some(data) = data else {
                return;
            };
            let bytes = glib::Bytes::from_owned(data);
            let loader = glycin::Loader::new_bytes(bytes);
            let Ok(image) = loader.load().await else {
                return;
            };
            let frame = image.next_frame().await.ok();
            let texture = frame.map(|f| f.texture());
            self.obj().set_texture(texture.as_ref());
        }
    }
}

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

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

impl AddFeedWidget {
    pub fn reset(&self) {
        self.set_feed_title("");
        self.set_feed_url("");
        self.set_texture(Texture::NONE);
    }
}
