use crate::App;
use crate::gobject_models::GArticle;
use crate::i18n::i18n;
use crate::util::{DateUtil, GtkUtil};
use glib::{Object, Properties, clone, closure, prelude::*, subclass::prelude::*, subclass::*};
use gtk4::pango::{EllipsizeMode, WrapMode};
use gtk4::{
    Accessible, Buildable, CompositeTemplate, ConstraintTarget, ExpressionWatch, Label, Widget, prelude::*,
    subclass::prelude::*,
};
use html2gtk::{FontStyle, LabelWithWidgets};
use libadwaita::{Bin, prelude::*};
use std::cell::{Cell, RefCell};

mod imp {
    use super::*;

    #[derive(Default, Debug, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::ArticleHeader)]
    #[template(file = "data/resources/ui_templates/article_view/header.blp")]
    pub struct ArticleHeader {
        #[template_child]
        pub title_bin: TemplateChild<Bin>,
        #[template_child]
        pub feed_title: TemplateChild<Label>,
        #[template_child]
        pub author: TemplateChild<Label>,
        #[template_child]
        pub date: TemplateChild<Label>,
        #[template_child]
        pub updated: TemplateChild<Label>,

        #[property(get, set = Self::set_article, nullable)]
        pub article: RefCell<Option<GArticle>>,

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

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

        #[property(get, set = Self::set_selectabel)]
        pub selectable: Cell<bool>,

        pub expression_watch: RefCell<Option<ExpressionWatch>>,
        pub title_label: RefCell<Option<Widget>>,
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_callbacks();
            klass.set_css_name("articleheader");
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for ArticleHeader {
        fn constructed(&self) {
            let obj = self.obj();

            App::default()
                .settings()
                .article_view()
                .bind_property("selectable", &*obj, "selectable")
                .build();

            let title_to_css_classes = closure!(|_: Option<Object>, title: Option<String>| {
                if title.is_some() {
                    vec!["body".to_string(), "dim-label".to_string()]
                } else {
                    vec!["heading".to_string(), "dim-label".to_string()]
                }
            });

            let title_expression = obj
                .property_expression("article")
                .chain_property::<GArticle>("title")
                .chain_closure::<Vec<String>>(title_to_css_classes)
                .bind(&*obj, "feed-name-classes", Widget::NONE);
            self.expression_watch.replace(Some(title_expression));

            let settings = App::default().settings().article_view();
            settings.connect_font_size_notify(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    imp.update_labels();
                }
            ));
            settings.connect_font_style_notify(clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    imp.update_labels();
                }
            ));

            self.parent_constructed();
        }
    }

    impl WidgetImpl for ArticleHeader {
        fn unrealize(&self) {
            if let Some(watch) = self.expression_watch.take() {
                watch.unwatch();
            }

            self.parent_unrealize()
        }
    }

    impl BoxImpl for ArticleHeader {}

    #[gtk4::template_callbacks]
    impl ArticleHeader {
        fn update_labels(&self) {
            let Some(article) = self.article.borrow().clone() else {
                self.title_bin.set_child(Widget::NONE);
                self.title_label.replace(None);
                return;
            };

            let Some(title) = article.title() else {
                self.title_bin.set_child(Widget::NONE);
                self.title_label.replace(None);
                return;
            };

            let url = article.url().unwrap_or_default();
            let is_markup = article.title_is_markup();
            let title = if is_markup {
                title
            } else {
                glib::markup_escape_text(&title).into()
            };

            let settings = App::default().settings().article_view();
            let font_family = match settings.font_style() {
                FontStyle::SansSerif => String::new(),
                FontStyle::Serif => format!("face=\"{}\"", FontStyle::Serif.as_css_property()),
            };
            let font_size = settings.font_size();
            let title_size = font_size * 2;

            let url = glib::markup_escape_text(&url);
            let text = format!(
                "<span line_height=\"normal\" size=\"{title_size}pt\" font_weight=\"700\" {font_family}><a href=\"{url}\">{title}</a></span>"
            );

            let is_selectable = App::default().settings().article_view().selectable();

            let widget = if let Some(label_with_math) = App::default()
                .settings()
                .get_merged_feed_setting(article.feed_id().as_ref())
                .inline_math
                .and_then(|math_delimiter| GtkUtil::label_with_math(&text, &math_delimiter, title_size))
            {
                let label = label_with_math.child();
                label.set_lines(4);
                label.set_css_classes(&["title"]);
                label.set_selectable(is_selectable);

                label_with_math.upcast()
            } else {
                Label::builder()
                    .use_markup(true)
                    .selectable(is_selectable)
                    .wrap(true)
                    .wrap_mode(WrapMode::WordChar)
                    .ellipsize(EllipsizeMode::End)
                    .lines(4)
                    .xalign(0.0)
                    .css_classes(["title"])
                    .label(text)
                    .build()
                    .upcast()
            };

            self.title_bin.set_child(Some(&widget));
            self.title_label.replace(Some(widget));

            let feed_title = glib::markup_escape_text(&article.feed_title());
            let feed_title = format!("<span size=\"{font_size}pt\" {font_family}>{feed_title}</span>");
            self.feed_title.set_label(&feed_title);

            let author = article.author();
            self.author.set_visible(author.is_some());
            if let Some(author) = author {
                let posted_by = i18n("posted by: ");
                let author = glib::markup_escape_text(&author);
                let author = format!("<span size=\"{font_size}pt\" {font_family}>{posted_by}{author}, </span>");
                self.author.set_label(&author);
            }

            let article_date = article.date();
            let date = DateUtil::format_date(article_date.as_ref());
            let time = DateUtil::format_time(article_date.as_ref());
            let at = i18n("at");
            let date_label = format!("<span size=\"{font_size}pt\" {font_family}>{date} {at} {time}</span>");
            self.date.set_label(&date_label);

            let updated = article.updated();
            let updated_ref = updated.as_ref();
            self.updated.set_visible(updated_ref.is_some());
            if let Some(updated) = updated_ref {
                let updated_prefix = i18n("updated");
                let updated_date = DateUtil::format_date(updated);
                let updated_time = DateUtil::format_time(updated);
                let updated_label = format!(
                    "<span size=\"{font_size}pt\" {font_family}>{updated_prefix}: {updated_date} {at} {updated_time}</span>"
                );
                self.updated.set_label(&updated_label);
            }
        }

        fn set_article(&self, article: Option<GArticle>) {
            self.article.replace(article.clone());
            self.update_labels();
        }

        fn set_selectabel(&self, selectable: bool) {
            if let Some(title_widget) = self.title_label.borrow().as_ref() {
                if let Some(label) = title_widget.downcast_ref::<Label>() {
                    label.set_selectable(selectable);
                } else if let Some(label_with_widgets) = title_widget.downcast_ref::<LabelWithWidgets>() {
                    label_with_widgets.child().set_selectable(selectable);
                }
            }

            self.selectable.set(selectable);
        }

        #[template_callback]
        fn no_title_spacing(&self, title: Option<String>) -> i32 {
            if title.is_some() { 0 } else { 8 }
        }
    }
}

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

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