use crate::image_downloader::ImageDownloaderExt;
#[cfg(feature = "video-thumbnails")]
use crate::video_thumbnail_extractor::VideoThumbnailExtractorExt;
use crate::{
    DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT, DEFAULT_PARAGRAPH_SPACING, DEFAULT_SELECTABLE_TEXT,
    Element, Error, FontStyle, Justification, MathRendererExt, Parser, WidgetBuilder, WidgetConfig,
};
use gtk4::gdk::Texture;
use gtk4::glib::{
    self, GString, MainContext, Object, Properties, SignalHandlerId, prelude::*,
    subclass::prelude::*, subclass::*,
};
use gtk4::{
    Accessible, Buildable, CompositeTemplate, ConstraintTarget, PickFlags, Widget, prelude::*,
    subclass::prelude::*
};
use libadwaita::{Bin, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;

mod imp {
    use super::*;

    #[derive(CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::HtmlWidget)]
    #[template(file = "src/widget.blp")]
    pub struct HtmlWidget {
        #[property(get, set = Self::set_font_size, name = "font-size", default_value = DEFAULT_FONT_SIZE)]
        pub font_size: Cell<i32>,

        #[property(get, set = Self::set_font_style, name = "font-style", builder(FontStyle::SansSerif))]
        pub font_style: Cell<FontStyle>,

        #[property(get, set = Self::set_line_height, name = "line-height", default_value = DEFAULT_LINE_HEIGHT)]
        pub line_height: Cell<f64>,

        #[property(get, set = Self::set_element_margin, name = "paragraph-spacing", default_value = DEFAULT_PARAGRAPH_SPACING)]
        pub paragraph_spacing: Cell<f64>,

        #[property(get, set = Self::set_justification, builder(Justification::Left))]
        pub justification: Cell<Justification>,

        #[property(get, set = Self::set_select_text, name = "select-text", default_value = DEFAULT_SELECTABLE_TEXT)]
        pub select_text: Cell<bool>,

        #[property(get, set, name = "hovered-url", nullable)]
        pub hovered_url: RefCell<Option<String>>,

        pub image_downloader: RefCell<Option<Arc<Box<dyn ImageDownloaderExt>>>>,
        pub math_renderer: RefCell<Option<Arc<Box<dyn MathRendererExt>>>>,
        pub element: Rc<RefCell<Element>>,
        pub id_map: Rc<RefCell<HashMap<String, Widget>>>,

        #[cfg(feature = "video-thumbnails")]
        pub video_thumbnail_extractor: RefCell<Option<crate::VideoThumbnailExtractor>>,
    }

    impl Default for HtmlWidget {
        fn default() -> Self {
            Self {
                font_size: Cell::new(DEFAULT_FONT_SIZE),
                font_style: Cell::new(FontStyle::SansSerif),
                line_height: Cell::new(DEFAULT_LINE_HEIGHT),
                justification: Cell::new(Justification::Left),
                select_text: Cell::new(DEFAULT_SELECTABLE_TEXT),
                paragraph_spacing: Cell::new(DEFAULT_PARAGRAPH_SPACING),
                hovered_url: RefCell::new(None),
                image_downloader: RefCell::new(None),
                math_renderer: RefCell::new(None),
                element: Rc::new(RefCell::new(Element::default())),
                id_map: Rc::new(RefCell::new(HashMap::new())),

                #[cfg(feature = "video-thumbnails")]
                video_thumbnail_extractor: RefCell::new(None),
            }
        }
    }

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

        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 HtmlWidget {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("play-video-url")
                        .param_types([
                            super::HtmlWidget::static_type(),
                            Widget::static_type(),
                            GString::static_type(),
                        ])
                        .build(),
                    Signal::builder("display-image")
                        .param_types([
                            super::HtmlWidget::static_type(),
                            Widget::static_type(),
                            Texture::static_type(),
                            Option::<String>::static_type(),
                        ])
                        .build(),
                    Signal::builder("activate-link")
                        .param_types([super::HtmlWidget::static_type(), String::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }

        fn constructed(&self) {
            crate::load_css();
        }
    }

    impl WidgetImpl for HtmlWidget {}

    impl BinImpl for HtmlWidget {}

    #[gtk4::template_callbacks]
    impl HtmlWidget {
        #[template_callback]
        fn on_motion(&self, x: f64, y: f64) {
            let href = Self::get_href(self.obj().upcast_ref(), x, y);
            self.obj().set_hovered_url(href);
        }

        #[template_callback]
        fn on_released(&self, times: i32, x: f64, y: f64) {
            if times != 1 {
                return;
            }

            if let Some(href) = Self::get_href(self.obj().upcast_ref(), x, y) {
                let launcher = gtk4::UriLauncher::new(&href);

                let ctx = MainContext::default();
                ctx.spawn_local(async move {
                    _ = launcher.launch_future(gtk4::Window::NONE).await;
                });
            }
        }

        fn get_href(widget: &Widget, x: f64, y: f64) -> Option<String> {
            let Some(picked) = &widget.pick(x, y, PickFlags::DEFAULT) else {
                return None;
            };

            // let window_point = Point::new(x as f32, y as f32);
            // let widget_point = widget.compute_point(picked, &window_point)?;

            if let Some(label) = picked.downcast_ref::<gtk4::Label>()
                && let Some(hovered_uri) = label.current_uri()
            {
                tracing::debug!("{hovered_uri}");
                return Some(hovered_uri.to_string());
            }

            None
        }

        fn set_line_height(&self, line_height: f64) {
            self.line_height.set(line_height);
            self.rebuild(None);
        }

        fn set_element_margin(&self, line_height: f64) {
            self.paragraph_spacing.set(line_height);
            self.rebuild(None);
        }

        fn set_justification(&self, justification: Justification) {
            self.justification.set(justification);
            self.rebuild(None);
        }

        fn set_select_text(&self, select_text: bool) {
            self.select_text.set(select_text);
            self.rebuild(None);
        }

        fn set_font_size(&self, font_size: i32) {
            self.font_size.set(font_size);
            self.rebuild(None);
        }

        fn set_font_style(&self, font_style: FontStyle) {
            self.font_style.replace(font_style);
            self.rebuild(None);
        }

        pub(super) fn rebuild(&self, height: Option<i32>) -> bool {
            let config = WidgetConfig {
                line_height: self.line_height.get(),
                paragraph_spacing: self.paragraph_spacing.get(),
                inline_math_delimiter: None,
                font_size: self.font_size.get(),
                font_style: self.font_style.get(),
                justification: self.justification.get(),
                select_text: self.select_text.get(),
            };
            self.id_map.replace(HashMap::new());
            let builder = WidgetBuilder::default()
                .config(config)
                .scale_factor(self.obj().scale_factor())
                .image_downloader(self.image_downloader.borrow().as_ref())
                .math_renderer(self.math_renderer.borrow().as_ref())
                .id_map(self.id_map.clone());

            let (widget, completed) = if let Some(height) = height {
                builder.build_partial(self.element.borrow().clone(), height)
            } else {
                (builder.build(self.element.borrow().clone()), true)
            };

            self.obj().set_child(Some(&widget));
            completed
        }
    }
}

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

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

impl HtmlWidget {
    pub fn set_image_downloader(
        &self,
        image_downloader: Option<&Arc<Box<dyn ImageDownloaderExt>>>,
    ) {
        self.imp()
            .image_downloader
            .replace(image_downloader.cloned());
    }

    pub fn image_downloader(&self) -> Option<Arc<Box<dyn ImageDownloaderExt>>> {
        self.imp().image_downloader.borrow().clone()
    }

    pub fn set_math_renderer(&self, math_renderer: Option<&Arc<Box<dyn MathRendererExt>>>) {
        self.imp().math_renderer.replace(math_renderer.cloned());
    }

    pub fn math_renderer(&self) -> Option<Arc<Box<dyn MathRendererExt>>> {
        self.imp().math_renderer.borrow().clone()
    }

    #[cfg(feature = "video-thumbnails")]
    pub fn set_video_thumbnail_extractor(
        &self,
        video_thumbnail_extractor: Option<crate::VideoThumbnailExtractor>,
    ) {
        self.imp()
            .video_thumbnail_extractor
            .replace(video_thumbnail_extractor);
    }

    pub fn load_element(&self, root: Element) {
        let imp = self.imp();

        imp.element.replace(root);
        imp.rebuild(None);
    }

    pub fn load_element_partial(&self, root: Element, height: i32) -> bool {
        let imp = self.imp();

        imp.element.replace(root);
        imp.rebuild(Some(height))
    }

    pub async fn load_html(
        &self,
        html: String,
        base_url: Option<&str>,
        inline_math_delimiter: Option<&str>,
    ) -> Result<(), Error> {
        let mut result = Parser::default().parse_fragment(html, base_url, inline_math_delimiter)?;

        #[cfg(feature = "video-thumbnails")]
        {
            let extractor = self.imp().video_thumbnail_extractor.borrow().clone();
            if let Some(extractor) = extractor {
                extractor.add_missing_thumbnails(&mut result.root).await;
            }
        }

        #[cfg(debug_assertions)]
        {
            let json = serde_json::to_string_pretty(&result.root).unwrap();
            std::fs::write("output.json", &json).unwrap();
        }

        self.load_element(result.root);
        Ok(())
    }

    pub fn rebuild(&self) {
        self.imp().rebuild(None);
    }

    pub fn emit_play_video_url(&self, url: &str, source: &Widget) {
        self.emit_by_name::<()>(
            "play-video-url",
            &[&self.clone(), source, &GString::from(url)],
        )
    }

    pub fn connect_play_video_url<F: Fn(&Self, Widget, GString) + 'static>(
        &self,
        f: F,
    ) -> SignalHandlerId {
        self.connect_local("play-video-url", false, move |args| {
            let this = args[1]
                .get::<Self>()
                .expect("The value needs to be of type `HtmlWidget`");
            let source = args[2]
                .get::<Widget>()
                .expect("The value needs to be of type `Widget`");
            let url = args[3]
                .get::<GString>()
                .expect("The value needs to be of type `GString`");
            f(&this, source, url);
            None
        })
    }

    pub fn emit_display_image(&self, texture: &Texture, source: &Widget, parent: Option<String>) {
        self.emit_by_name::<()>("display-image", &[&self.clone(), source, &texture, &parent])
    }

    pub fn connect_display_image<F: Fn(&Self, Widget, Texture, Option<String>) + 'static>(
        &self,
        f: F,
    ) -> SignalHandlerId {
        self.connect_local("display-image", false, move |args| {
            let this = args[1]
                .get::<Self>()
                .expect("The value needs to be of type `HtmlWidget`");
            let widget = args[2]
                .get::<Widget>()
                .expect("The value needs to be of type `Widget`");
            let texture = args[3]
                .get::<Texture>()
                .expect("The value needs to be of type `Texture`");
            let parent = args[4]
                .get::<Option<String>>()
                .expect("The value needs to be of type `Option<String>`");
            f(&this, widget, texture, parent);
            None
        })
    }

    pub fn emit_activate_link(&self, link: &str) {
        self.emit_by_name::<()>("activate-link", &[&self.clone(), &link.to_string()])
    }

    pub fn connect_activate_link<F: Fn(&Self, String) + 'static>(&self, f: F) -> SignalHandlerId {
        self.connect_local("activate-link", false, move |args| {
            let this = args[1]
                .get::<Self>()
                .expect("The value needs to be of type `HtmlWidget`");
            let link = args[2]
                .get::<String>()
                .expect("The value needs to be of type `String`");
            f(&this, link);
            None
        })
    }

    pub fn get_widget_for_id(&self, id: &str) -> Option<Widget> {
        self.imp().id_map.borrow().get(id).cloned()
    }

    #[cfg(feature = "video-thumbnails")]
    pub fn add_thumbnail_extractor(
        &self,
        extractor_impl: Arc<Box<dyn VideoThumbnailExtractorExt>>,
    ) {
        let extractor = self.imp().video_thumbnail_extractor.borrow().clone();
        if let Some(mut extractor) = extractor {
            extractor.add_extractor(extractor_impl);
        }
    }

    pub(crate) fn find_parent(source: &Widget) -> Option<Self> {
        let mut parent = source.parent();

        while let Some(p) = parent.as_ref() {
            if let Some(html_widget) = p.downcast_ref::<Self>() {
                return Some(html_widget.clone());
            }

            parent = p.parent();
        }

        None
    }
}
