use crate::app::App;
use crate::gobject_models::{GCategoryID, GFeedID};
use crate::i18n::i18n;
use crate::infrastructure::TokioRuntime;
use crate::main_window::MainWindow;
use gdk4::Texture;
use gio::ListStore;
use glib::{Object, Properties, clone, prelude::*, subclass::*};
use gtk4::{
    Accessible, Buildable, CompositeTemplate, ConstraintTarget, Expression, PropertyExpression, Widget,
    subclass::prelude::*,
};
use libadwaita::{ComboRow, Dialog, prelude::*, subclass::prelude::*};
use news_flash::models::{Category, CategoryID, Feed, FeedID, FeedMapping, NEWSFLASH_TOPLEVEL, PluginCapabilities};
use std::cell::{Cell, RefCell};

mod imp {
    use super::*;

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

        #[template_child]
        pub category_list: TemplateChild<ListStore>,

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

        #[property(get, set = Self::set_feed_id, name = "feed-id")]
        pub feed_id: RefCell<GFeedID>,

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

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

        #[property(get, set, name = "category-id")]
        pub category_id: RefCell<GCategoryID>,

        #[property(get, set, name = "support-mutation")]
        pub support_mutation: Cell<bool>,

        #[property(get, set, name = "support-edit-url")]
        pub support_edit_url: Cell<bool>,

        #[property(get, set, name = "math-delimiter")]
        pub math_delimiter: RefCell<String>,

        #[property(get, set, name = "user-agent")]
        pub user_agent: RefCell<String>,

        #[property(get, set = Self::set_scrape_content, name = "scrape-content")]
        pub scrape_content: Cell<bool>,

        #[property(get, set = Self::set_mute)]
        pub mute: Cell<bool>,
    }

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

        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 EditFeedDialog {
        fn constructed(&self) {
            let expression = PropertyExpression::new(CategorySelectGObject::static_type(), Expression::NONE, "label");
            self.category.set_expression(Some(&expression));

            self.parent_constructed();
        }
    }

    impl WidgetImpl for EditFeedDialog {}

    impl AdwDialogImpl for EditFeedDialog {}

    #[gtk4::template_callbacks]
    impl EditFeedDialog {
        #[template_callback]
        fn on_name_apply(&self) {
            let feed_id = self.feed_id.borrow().as_ref().as_str().to_string();
            let new_title = self.feed_name.borrow().clone();
            MainWindow::activate_action("rename-feed", Some(&(feed_id, new_title).to_variant()));
        }

        #[template_callback]
        fn on_url_apply(&self) {
            let feed_id = self.feed_id.borrow().as_ref().as_str().to_string();
            let new_url = self.feed_url.borrow().clone();
            if new_url.is_empty() {
                return;
            }
            MainWindow::activate_action("edit-feed-url", Some(&(feed_id, new_url).to_variant()));
        }

        #[template_callback]
        fn on_math_delimiter_apply(&self) {
            let feed_id = self.feed_id.borrow().clone();
            let math_delimiter = self.math_delimiter.borrow().clone();
            let mut feed_settings = App::default()
                .settings()
                .get_feed_settings(feed_id.as_ref())
                .unwrap_or_default();

            if math_delimiter.is_empty() {
                feed_settings.inline_math = None;
            } else {
                feed_settings.inline_math = Some(math_delimiter);
            }

            _ = App::default()
                .settings()
                .set_feed_settings(feed_id.as_ref(), &feed_settings);
        }

        #[template_callback]
        fn on_user_agent_apply(&self) {
            let feed_id = self.feed_id.borrow().clone();
            let user_agent = self.user_agent.borrow().clone();
            let mut feed_settings = App::default()
                .settings()
                .get_feed_settings(feed_id.as_ref())
                .unwrap_or_default();

            if user_agent.is_empty() {
                feed_settings.custom_user_agent = None;
            } else {
                feed_settings.custom_user_agent = Some(user_agent);
            }

            _ = App::default()
                .settings()
                .set_feed_settings(feed_id.as_ref(), &feed_settings);
        }

        #[template_callback]
        fn is_texture_some(&self, texture: Option<Texture>) -> bool {
            texture.is_some()
        }

        #[template_callback]
        fn calc_content_height(&self, texture: Option<Texture>) -> i32 {
            if texture.is_some() { 580 } else { 510 }
        }

        fn set_feed_id(&self, g_feed_id: GFeedID) {
            let feed_id = g_feed_id.as_ref().clone();
            glib::spawn_future_local(clone!(
                #[weak(rename_to = imp)]
                self,
                async move { imp.load_favicon(feed_id).await }
            ));
            self.feed_id.replace(g_feed_id);
        }

        fn set_scrape_content(&self, scrape_content: bool) {
            if scrape_content == self.scrape_content.get() {
                return;
            }

            let feed_id = self.feed_id.borrow().clone();
            let mut feed_settings = App::default()
                .settings()
                .get_feed_settings(feed_id.as_ref())
                .unwrap_or_default();

            feed_settings.scrap_content = scrape_content;

            _ = App::default()
                .settings()
                .set_feed_settings(feed_id.as_ref(), &feed_settings);

            self.scrape_content.set(scrape_content);
        }

        fn set_mute(&self, mute: bool) {
            if mute == self.mute.get() {
                return;
            }

            let feed_id = self.feed_id.borrow().clone();
            let mut feed_settings = App::default()
                .settings()
                .get_feed_settings(feed_id.as_ref())
                .unwrap_or_default();

            feed_settings.mute_notifications = mute;

            _ = App::default()
                .settings()
                .set_feed_settings(feed_id.as_ref(), &feed_settings);

            self.mute.set(mute);
        }

        async fn load_favicon(&self, feed_id: FeedID) {
            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()?;
                news_flash.load_icon_from_db(&feed_id).ok()
            });

            let Ok(Some(favicon)) = future.await else {
                return;
            };
            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 image = loader.load().await.ok();
            let frame = if let Some(image) = image {
                image.next_frame().await.ok()
            } else {
                None
            };

            self.obj().set_texture(frame.map(|f| f.texture()).as_ref());
        }
    }
}

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

impl EditFeedDialog {
    pub fn new(feed: Feed, mapping: FeedMapping, categories: Vec<Category>) -> Self {
        let feed_settings = App::default()
            .settings()
            .get_feed_settings(&feed.feed_id)
            .unwrap_or_default();
        let math_delimiter = feed_settings.inline_math.as_deref().unwrap_or("");
        let user_agent = feed_settings.custom_user_agent.as_deref().unwrap_or("");
        let features: PluginCapabilities = App::default().features().into();
        let category_id: GCategoryID = mapping.category_id.clone().into();
        let feed_id: GFeedID = feed.feed_id.into();
        let selected_position = categories
            .iter()
            .enumerate()
            .find_map(move |(i, category)| {
                if category.category_id == mapping.category_id {
                    Some(i + 1) // +1 because of 'None' at the top of the list
                } else {
                    None
                }
            })
            .unwrap_or(0);

        let dialog: EditFeedDialog = Object::builder()
            .property("feed-id", feed_id)
            .property("feed-name", feed.label)
            .property(
                "feed-url",
                feed.feed_url.as_ref().map(ToString::to_string).unwrap_or_default(),
            )
            .property("category-id", category_id)
            .property("support-mutation", features.support_mutation())
            .property(
                "support-edit-url",
                features.contains(PluginCapabilities::EDIT_FEED_URLS),
            )
            .property("math-delimiter", math_delimiter)
            .property("user-agent", user_agent)
            .property("scrape-content", feed_settings.scrap_content)
            .build();

        let imp = dialog.imp();

        // fill category dropdown
        imp.category_list
            .append(&CategorySelectGObject::new(&NEWSFLASH_TOPLEVEL, &i18n("None")));
        for category in &categories {
            imp.category_list.append(&CategorySelectGObject::from(category.clone()));
        }

        // select current category
        imp.category.set_selected(selected_position as u32);

        imp.category.connect_selected_item_notify(clone!(
            #[weak]
            dialog,
            #[upgrade_or_panic]
            move |combo| {
                let Some(selected) = combo.selected_item().and_downcast::<CategorySelectGObject>() else {
                    return;
                };

                let feed_id = dialog.feed_id().as_ref().as_str().to_string();
                let from_id = dialog.category_id().as_ref().as_str().to_string();
                let to_id = selected.id().as_ref().as_str().to_string();

                tracing::debug!(%feed_id, %from_id, %to_id, "move feed");

                MainWindow::activate_action("move-feed", Some(&(feed_id, from_id, to_id, i32::MAX).to_variant()));
            }
        ));

        dialog
    }
}

mod obj_impl {
    use super::*;

    #[derive(Properties)]
    #[properties(wrapper_type = super::CategorySelectGObject)]
    pub struct CategorySelectGObject {
        #[property(get, set)]
        pub id: RefCell<GCategoryID>,

        #[property(get, set)]
        pub label: RefCell<String>,
    }

    impl Default for CategorySelectGObject {
        fn default() -> Self {
            Self {
                id: RefCell::new(NEWSFLASH_TOPLEVEL.clone().into()),
                label: RefCell::new(String::new()),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for CategorySelectGObject {}

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

glib::wrapper! {
    pub struct CategorySelectGObject(ObjectSubclass<obj_impl::CategorySelectGObject>);
}

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

impl CategorySelectGObject {
    pub fn new(id: &CategoryID, label: &str) -> Self {
        let obj = Self::default();
        let imp = obj.imp();
        imp.id.replace(id.clone().into());
        imp.label.replace(label.into());
        obj
    }
}

impl From<Category> for CategorySelectGObject {
    fn from(category: Category) -> Self {
        let category_id: GCategoryID = category.category_id.into();

        Object::builder()
            .property("id", category_id)
            .property("label", category.label)
            .build()
    }
}
