use crate::{DEFAULT_FONT_SIZE, HtmlWidget, Image, ImageDownloaderExt};
use article_scraper::{DownloadProgress, ImageDownloadError};
use bytesize::ByteSize;
use futures_util::{FutureExt, StreamExt};
use gtk4::gdk::{Rectangle, Texture};
use gtk4::gio::{File, FileCreateFlags, ListStore};
use gtk4::glib::{
    self, Bytes, Enum, MainContext, Object, Priority, Properties, SignalHandlerId, clone,
    derived_properties, object_subclass, prelude::*, subclass::*, wrapper,
};
use gtk4::{
    Accessible, Align, Allocation, Buildable, CompositeTemplate, ConstraintTarget, ContentFit,
    FileDialog, FileFilter, Orientation, Overflow, Picture, PopoverMenu, SizeRequestMode, Stack,
    Widget, Window, prelude::*, subclass::prelude::*,
};
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
use std::sync::Arc;

#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Enum)]
#[repr(u32)]
#[enum_type(name = "ImageState")]
pub enum ImageState {
    #[default]
    Loading,
    Finished,
    Missing,
}

#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Enum)]
#[repr(u32)]
#[enum_type(name = "ImageType")]
pub enum ImageType {
    Inline,
    #[default]
    Single,
    Gallery,
    VideoThumbnail,
}

const INLINE_SIZE: i32 = 24;
const SPINNER_SIZE: i32 = 64;
const SPINNER_MARGIN: i32 = 20;
const MAX_HEIGHT: i32 = 500;

mod imp {
    use super::*;

    #[derive(Debug, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::ImageWidget)]
    #[template(file = "src/widget_factory/image/widget.blp")]
    pub struct ImageWidget {
        #[template_child]
        pub stack: TemplateChild<Stack>,
        #[template_child]
        pub picture: TemplateChild<Picture>,
        #[template_child]
        pub menu_popover: TemplateChild<PopoverMenu>,

        #[property(get, set, builder(ImageState::Loading))]
        pub state: Cell<ImageState>,

        #[property(get, set = Self::set_image_type, builder(ImageType::Single))]
        pub image_type: Cell<ImageType>,

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

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

        #[property(get, set, name = "progress-text")]
        pub progress_text: RefCell<String>,

        #[property(get, set, builder(ContentFit::Contain))]
        pub fit: Cell<ContentFit>,

        #[property(get, set, name = "font-size")]
        pub font_size: Cell<i32>,

        #[property(get = Self::image_width_prop, name = "image-width")]
        #[property(get = Self::image_height, name = "image-height")]
        #[property(get, set, name = "layout-width")]
        pub layout_width: Cell<i32>,

        #[property(get, set, name = "layout-height")]
        pub layout_height: Cell<i32>,
    }

    impl Default for ImageWidget {
        fn default() -> Self {
            Self {
                stack: TemplateChild::default(),
                picture: TemplateChild::default(),
                menu_popover: TemplateChild::default(),
                state: Cell::new(ImageState::Loading),
                image_type: Cell::new(ImageType::Single),
                parent: RefCell::new(None),
                texture: RefCell::new(None),
                progress_text: RefCell::new(String::new()),
                fit: Cell::new(ContentFit::Contain),
                font_size: Cell::new(DEFAULT_FONT_SIZE),
                layout_width: Cell::new(-1),
                layout_height: Cell::new(-1),
            }
        }
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_callbacks();
            klass.set_css_name("image-widget");

            klass.install_action("image.save", None, |obj, _, _| {
                let Some(window) = obj.root().and_downcast::<Window>() else {
                    return;
                };

                tracing::info!("save image");

                let filter = FileFilter::new();
                filter.add_pattern("*.PNG");
                filter.add_pattern("*.png");
                filter.add_mime_type("image/png");
                filter.set_name(Some("PNG"));

                let filter_list = ListStore::new::<FileFilter>();
                filter_list.append(&filter);

                let dialog = FileDialog::builder()
                    .accept_label("_Save")
                    .title("Save Image")
                    .modal(true)
                    .initial_folder(&File::for_path(glib::home_dir()))
                    .initial_name("Image.png")
                    .filters(&filter_list)
                    .build();

                let texture = obj.texture();

                glib::spawn_future_local(async move {
                    let Ok(file) = dialog.save_future(Some(&window)).await else {
                        return;
                    };
                    let Some(texture) = texture else {
                        return;
                    };

                    let bytes = texture.save_to_png_bytes();

                    let Ok(stream) = file
                        .replace_future(
                            None,
                            false,
                            FileCreateFlags::REPLACE_DESTINATION,
                            Priority::DEFAULT,
                        )
                        .await
                        .inspect_err(|error| tracing::error!(?error, "Failed to open file stream"))
                    else {
                        return;
                    };
                    if let Err(error) = stream.write_all_future(bytes, Priority::DEFAULT).await {
                        tracing::error!(?error, "Failed to save png");
                    }
                });
            });
        }

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

    #[derived_properties]
    impl ObjectImpl for ImageWidget {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("display-image")
                        .param_types([
                            super::ImageWidget::static_type(),
                            Texture::static_type(),
                            Option::<String>::static_type(),
                        ])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }

        fn dispose(&self) {
            self.menu_popover.unparent();
            self.stack.unparent();
        }
    }

    impl WidgetImpl for ImageWidget {
        fn request_mode(&self) -> SizeRequestMode {
            SizeRequestMode::HeightForWidth
        }

        fn measure(&self, orientation: Orientation, for_size: i32) -> (i32, i32, i32, i32) {
            tracing::trace!(?orientation, for_size);
            let aspect_ratio = self
                .texture
                .borrow()
                .as_ref()
                .map(|texture| texture.width() as f32 / texture.height() as f32);
            tracing::trace!(?aspect_ratio);

            let width = self.image_width();
            let height = self.image_height();

            if orientation == Orientation::Vertical {
                let height = if let Some(aspect_ratio) = aspect_ratio {
                    if for_size > 0 {
                        height.min((for_size as f32 / aspect_ratio) as i32)
                    } else {
                        height
                    }
                } else if self.image_type.get() == ImageType::Inline {
                    self.inline_size()
                } else {
                    SPINNER_SIZE + 2 * SPINNER_MARGIN
                };
                tracing::trace!(height);
                (height.min(for_size), height, -1, -1)
            } else {
                let width = if self.image_type.get() == ImageType::Inline {
                    self.inline_size()
                } else if let Some(aspect_ratio) = aspect_ratio {
                    if for_size > 0 {
                        let w = (for_size as f32 * aspect_ratio) as i32;
                        let width = width.unwrap_or(w);
                        width.min(w)
                    } else {
                        width.unwrap_or(SPINNER_SIZE)
                    }
                } else {
                    SPINNER_SIZE + 2 * SPINNER_MARGIN
                };
                tracing::trace!(width);
                let natural_width = match self.state.get() {
                    ImageState::Missing => SPINNER_SIZE + 2 * SPINNER_MARGIN,
                    _ => match self.image_type.get() {
                        ImageType::Single | ImageType::VideoThumbnail => width,
                        ImageType::Inline => self.inline_size(),
                        ImageType::Gallery => 9999,
                    },
                };
                (width.min(for_size), natural_width, -1, -1)
            }
        }

        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
            tracing::trace!(width, height, "allocate");
            self.stack
                .size_allocate(&Allocation::new(0, 0, width, height), baseline);
            self.parent_size_allocate(width, height, baseline)
        }
    }

    #[gtk4::template_callbacks]
    impl ImageWidget {
        fn image_height(&self) -> i32 {
            let layout_height = self.layout_height.get();
            if layout_height > 0 {
                layout_height
            } else {
                self.texture
                    .borrow()
                    .as_ref()
                    .map(TextureExt::height)
                    .map(|w| w / self.obj().scale_factor())
                    .unwrap_or(MAX_HEIGHT)
                    .min(MAX_HEIGHT)
            }
        }

        fn image_width(&self) -> Option<i32> {
            let layout_width = self.layout_width.get();
            if layout_width > 0 {
                Some(layout_width)
            } else {
                self.texture
                    .borrow()
                    .as_ref()
                    .map(TextureExt::width)
                    .map(|w| w / self.obj().scale_factor())
            }
        }

        fn image_width_prop(&self) -> i32 {
            self.image_width().unwrap_or(-1)
        }

        fn set_image_type(&self, image_type: ImageType) {
            let obj = self.obj();

            if image_type != ImageType::Inline {
                self.picture.add_css_class("rounded-edges");
            }

            if image_type == ImageType::Single {
                obj.set_overflow(Overflow::Hidden);
                obj.set_halign(Align::Center);
            } else if image_type == ImageType::Gallery {
                obj.set_fit(ContentFit::ScaleDown);
            } else if image_type == ImageType::VideoThumbnail {
                obj.set_fit(ContentFit::Cover);
                self.picture.set_valign(Align::Fill);
                self.picture.set_halign(Align::Fill);
            }

            self.image_type.set(image_type);
        }

        fn inline_size(&self) -> i32 {
            let size = INLINE_SIZE as f64 * self.font_size.get() as f64 / DEFAULT_FONT_SIZE as f64;
            size as i32
        }

        #[template_callback]
        fn on_released(&self) {
            if !self.is_clickable() {
                return;
            }

            let obj = self.obj();
            let widget = obj.upcast_ref();
            let parent = obj.parent();

            let Some(texture) = self.texture.borrow().clone() else {
                return;
            };
            let Some(html_widget) = HtmlWidget::find_parent(widget) else {
                tracing::trace!("not decendent of HtmlWidget");
                obj.emit_display_image(&texture, parent);
                return;
            };

            html_widget.emit_display_image(&texture, widget, parent);
        }

        #[template_callback]
        fn on_alt_released(&self, _n_times: i32, x: f64, y: f64) {
            let rect = Rectangle::new(x as i32, y as i32, 0, 0);
            self.menu_popover.set_pointing_to(Some(&rect));
            self.menu_popover.popup();
        }

        #[template_callback]
        pub fn on_long_press(&self, x: f64, y: f64) {
            let rect = Rectangle::new(x as i32, y as i32, 0, 0);
            self.menu_popover.set_pointing_to(Some(&rect));
            self.menu_popover.popup();
        }

        #[template_callback]
        fn on_enter(&self) {
            if !self.is_clickable() {
                return;
            }

            self.obj().add_css_class("highlight");
        }

        #[template_callback]
        fn on_leave(&self) {
            self.obj().remove_css_class("highlight");
        }

        #[template_callback]
        fn visible_child_name(&self, state: ImageState) -> &'static str {
            match state {
                ImageState::Loading => "spinner",
                ImageState::Finished => "picture",
                ImageState::Missing => "error",
            }
        }

        #[template_callback]
        fn is_progress_label_visible(&self, text: String) -> bool {
            !text.is_empty()
        }

        #[template_callback]
        fn spinner_size(&self, image_type: ImageType) -> i32 {
            if image_type == ImageType::Inline {
                self.inline_size()
            } else {
                SPINNER_SIZE
            }
        }

        #[template_callback]
        fn is_homogeneous(&self, image_type: ImageType) -> bool {
            image_type != ImageType::Inline
        }

        #[template_callback]
        fn size_request(&self, image_type: ImageType) -> i32 {
            if image_type == ImageType::Inline {
                self.inline_size()
            } else {
                -1
            }
        }

        fn is_clickable(&self) -> bool {
            if self.image_type.get() == ImageType::VideoThumbnail {
                return false;
            }

            if self.parent.borrow().is_some() {
                return true;
            }

            if let Some(texture) = self.texture.borrow().as_ref() {
                let image_widget = self.obj();

                let image_width = texture.width();
                let image_height = texture.height();
                let widget_width = image_widget.width();
                let widget_height = image_widget.height();

                if image_width < 200 || image_height < 200 || image_width * image_height < 120000 {
                    tracing::trace!(image_width, image_height, "image too small");
                    return false;
                }

                if widget_width >= image_width && widget_height >= image_height {
                    tracing::trace!(
                        image_width,
                        image_height,
                        widget_width,
                        widget_height,
                        "image already fully visible"
                    );
                    return false;
                }

                true
            } else {
                false
            }
        }
    }
}

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

impl ImageWidget {
    pub fn new(image_type: ImageType, font_size: i32) -> Self {
        Object::builder()
            .property("image_type", image_type)
            .property("font-size", font_size)
            .build()
    }

    pub async fn load(
        &self,
        content_id: &str,
        image: Image,
        downloader: Arc<Box<dyn ImageDownloaderExt>>,
    ) {
        tracing::trace!(?image, "load");

        self.set_tooltip_text(image.alt.as_deref());
        self.set_parent(image.parent);
        self.set_layout_width(image.width.unwrap_or(-1));
        self.set_layout_height(image.height.unwrap_or(-1));

        if let Some(image_data) = image.data {
            self.decode_data(image_data).await;
            return;
        }

        let Some(url) = image.url else {
            self.set_state(ImageState::Missing);
            return;
        };

        let (progress_sender, mut progress_receiver) =
            futures::channel::mpsc::channel::<DownloadProgress>(2);

        MainContext::default().spawn_local(clone!(
            #[weak(rename_to = widget)]
            self,
            async move {
                while let Some(progress) = progress_receiver.next().await {
                    let downloaded = ByteSize(progress.downloaded as u64);
                    let total = ByteSize(progress.total_size as u64);
                    let progress_text = format!("{downloaded} of {total}");
                    widget.set_progress_text(progress_text);
                }
            }
        ));

        let url = url.as_str().replace("&amp;", "&").to_string();
        let content_id = content_id.to_string();
        let (sender, receiver) =
            futures::channel::oneshot::channel::<Result<Texture, ImageDownloadError>>();

        MainContext::default().spawn(async move {
            let result = downloader
                .download(&content_id, &url, Some(progress_sender))
                .await;

            sender.send(result).unwrap();
        });

        receiver
            .map(clone!(
                #[weak(rename_to = widget)]
                self,
                move |res| {
                    let Ok(res) = res else {
                        widget.set_state(ImageState::Missing);
                        return;
                    };

                    match res {
                        Ok(texture) => {
                            widget.set_state(ImageState::Finished);
                            widget.set_texture(Some(texture));
                            widget.queue_resize();

                            widget.notify_image_width();
                            widget.notify_image_height();
                        }
                        Err(_error) => {
                            widget.set_state(ImageState::Missing);
                            widget.queue_resize();
                        }
                    }
                }
            ))
            .await;
    }

    async fn decode_data(&self, data: Vec<u8>) {
        let bytes = Bytes::from_owned(data);
        let loader = glycin::Loader::new_bytes(bytes);
        let Ok(image) = loader.load().await else {
            self.set_state(ImageState::Missing);
            return;
        };
        let Ok(frame) = image.next_frame().await else {
            self.set_state(ImageState::Missing);
            return;
        };

        let texture = frame.texture();
        self.set_texture(Some(texture));
        self.set_state(ImageState::Finished);

        self.notify_image_width();
        self.notify_image_height();
    }

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

    pub fn connect_display_image<F: Fn(&Self, 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 `ImageWidget`");
            let texture = args[2]
                .get::<Texture>()
                .expect("The value needs to be of type `Texture`");
            let parent = args[3]
                .get::<Option<String>>()
                .expect("The value needs to be of type `Option<String>`");
            f(&this, texture, parent);
            None
        })
    }
}
