use adw::{prelude::*, subclass::prelude::*};
use anyhow::Result;
use gettextrs::gettext;
use gtk::{
    gdk, gio,
    glib::{self, clone},
};

use crate::{
    application::Application,
    config::{APP_ID, PROFILE},
    widgets::drawing_area::{DrawingArea, Filter},
};

#[derive(PartialEq, Eq, Debug)]
pub enum View {
    Empty,
    Image,
}

mod imp {
    use std::cell::RefCell;

    use super::*;

    #[derive(Debug, gtk::CompositeTemplate)]
    #[template(resource = "/com/belmoussaoui/Obfuscate/window.ui")]
    pub struct Window {
        pub settings: gio::Settings,
        #[template_child]
        pub drawing_area: TemplateChild<DrawingArea>,
        #[template_child]
        pub stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub window_title: TemplateChild<adw::WindowTitle>,
        #[template_child]
        pub zoom_level: TemplateChild<gtk::Label>,
        #[template_child]
        pub status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        pub blur_btn: TemplateChild<gtk::ToggleButton>,
        #[template_child]
        pub filled_btn: TemplateChild<gtk::ToggleButton>,
        #[template_child]
        pub blur_btn2: TemplateChild<gtk::ToggleButton>,
        #[template_child]
        pub filled_btn2: TemplateChild<gtk::ToggleButton>,
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
        pub open_file: RefCell<Option<gio::File>>,
        pub current_toast: RefCell<Option<adw::Toast>>,
    }

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

        fn new() -> Self {
            Self {
                settings: gio::Settings::new(APP_ID),
                drawing_area: TemplateChild::default(),
                stack: TemplateChild::default(),
                window_title: TemplateChild::default(),
                zoom_level: TemplateChild::default(),
                status_page: TemplateChild::default(),
                blur_btn: TemplateChild::default(),
                filled_btn: TemplateChild::default(),
                blur_btn2: TemplateChild::default(),
                filled_btn2: TemplateChild::default(),
                toast_overlay: TemplateChild::default(),
                open_file: RefCell::default(),
                current_toast: RefCell::default(),
            }
        }

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_instance_callbacks();
            klass.install_action("win.zoom_in", None, |widget, _, _| {
                let drawing_area = widget.imp().drawing_area.get();
                drawing_area.set_zoom(drawing_area.zoom() + 0.1);
            });

            klass.install_action("win.zoom_out", None, |widget, _, _| {
                let drawing_area = widget.imp().drawing_area.get();
                drawing_area.set_zoom(drawing_area.zoom() - 0.1);
            });

            klass.install_action("win.zoom_reset", None, |widget, _, _| {
                widget.imp().drawing_area.set_zoom(1.0);
            });

            klass.install_action_async("win.open", None, |widget, _, _| async move {
                widget.open_file().await;
            });

            klass.install_action("win.copy", None, |widget, _, _| {
                widget.imp().drawing_area.copy();
            });

            klass.install_action_async("win.paste", None, |widget, _, _| async move {
                widget.imp().drawing_area.paste().await;
            });
            // Undo
            klass.install_action("win.undo", None, |widget, _, _| {
                widget.imp().drawing_area.undo();
            });
            // Redo
            klass.install_action("win.redo", None, |widget, _, _| {
                widget.imp().drawing_area.redo();
            });

            klass.install_action_async("win.save", None, |widget, _, _| async move {
                if let Err(err) = widget.save_file().await {
                    log::error!("Failed to get a file {err}")
                }
            });
        }

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

    impl ObjectImpl for Window {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            if PROFILE == "Devel" {
                obj.add_css_class("devel");
            }
            gtk::Window::set_default_icon_name(APP_ID);
            obj.set_icon_name(Some(APP_ID));
            self.status_page.set_icon_name(Some(APP_ID));

            obj.action_set_enabled("win.zoom_reset", false);
            obj.action_set_enabled("win.undo", false);
            obj.action_set_enabled("win.redo", false);

            self.blur_btn
                .bind_property("active", &*self.filled_btn, "active")
                .invert_boolean()
                .build();
            self.blur_btn
                .bind_property("active", &*self.blur_btn2, "active")
                .bidirectional()
                .build();
            self.filled_btn
                .bind_property("active", &*self.blur_btn, "active")
                .invert_boolean()
                .build();
            self.filled_btn
                .bind_property("active", &*self.filled_btn2, "active")
                .bidirectional()
                .build();

            obj.set_view(View::Empty);
            obj.load_state();

            let drop_target = gtk::DropTarget::new(gio::File::static_type(), gdk::DragAction::COPY);
            drop_target.connect_drop(
                clone!(@strong obj => @default-return false, move |_, value, _, _| {
                    if let Ok(file) = value.get::<gio::File>() {
                        obj.set_open_file(&file);
                        true
                    } else {
                        false
                    }
                }),
            );
            self.status_page.add_controller(drop_target);
        }
    }

    impl WidgetImpl for Window {}
    impl WindowImpl for Window {
        fn close_request(&self) -> glib::Propagation {
            if !self.drawing_area.saved() {
                let window = self.obj();
                return match glib::MainContext::default()
                    .block_on(async move { window.confirm_save_changes().await })
                {
                    Ok(p) => p,
                    Err(e) => {
                        log::error!("Failed to save changed {e}");
                        glib::Propagation::Stop
                    }
                };
            }

            if let Err(err) = self.obj().save_state() {
                log::warn!("Failed to save window state {}", err);
            }
            self.parent_close_request()
        }
    }
    impl ApplicationWindowImpl for Window {}
    impl AdwApplicationWindowImpl for Window {}
}

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::Widget, gtk::Window, adw::ApplicationWindow, gtk::ApplicationWindow,
        @implements gtk::Root, gio::ActionMap;
}

#[gtk::template_callbacks]
impl Window {
    pub fn new(app: &Application) -> Self {
        glib::Object::builder().property("application", app).build()
    }

    pub fn set_view(&self, view: View) {
        let imp = self.imp();

        match view {
            View::Empty => {
                imp.stack.set_visible_child_name("empty");
            }
            View::Image => {
                imp.stack.set_visible_child_name("image");
            }
        }
        self.action_set_enabled("win.undo", false);
        self.action_set_enabled("win.redo", false);
        self.action_set_enabled("win.copy", view == View::Image);
        self.action_set_enabled("win.save", view == View::Image);
        self.action_set_enabled("win.zoom_in", view == View::Image);
        self.action_set_enabled("win.zoom_out", view == View::Image);
    }

    pub fn set_open_file(&self, file: &gio::File) {
        self.set_view(View::Empty);
        match self.set_open_file_inner(file) {
            Ok(_) => self.set_view(View::Image),
            Err(err) => log::error!("Failed to open file {}", err),
        };
    }

    async fn confirm_save_changes(&self) -> Result<glib::Propagation> {
        const RESPONSE_CANCEL: &str = "cancel";
        const RESPONSE_DISCARD: &str = "discard";
        const RESPONSE_SAVE: &str = "save";
        let dialog = adw::AlertDialog::builder()
            .heading(gettext("Save Changes?"))
            .body(gettext("Open image contain unsaved changes. Changes which are not saved will be permanently lost"))
            .close_response(RESPONSE_CANCEL)
            .default_response(RESPONSE_SAVE)
            .build();
        dialog.add_response(RESPONSE_CANCEL, &gettext("Cancel"));
        dialog.add_response(RESPONSE_DISCARD, &gettext("Discard"));
        dialog.set_response_appearance(RESPONSE_DISCARD, adw::ResponseAppearance::Destructive);
        dialog.add_response(RESPONSE_SAVE, &gettext("Save"));
        dialog.set_response_appearance(RESPONSE_SAVE, adw::ResponseAppearance::Suggested);

        match &*dialog.clone().choose_future(self).await {
            RESPONSE_CANCEL => {
                dialog.close();
                Ok(glib::Propagation::Stop)
            }
            RESPONSE_DISCARD => Ok(glib::Propagation::Proceed),
            RESPONSE_SAVE => {
                self.save_file().await?;
                Ok(glib::Propagation::Stop)
            }
            _ => unreachable!(),
        }
    }

    fn set_open_file_inner(&self, file: &gio::File) -> Result<()> {
        let imp = self.imp();
        imp.open_file.replace(Some(file.clone()));

        self.update_title()?;
        imp.drawing_area.load_file(file)?;
        Ok(())
    }

    #[template_callback]
    fn on_notify_zoom(&self) {
        let zoom = self.imp().drawing_area.zoom();

        self.imp()
            .zoom_level
            .set_text(&format!("{:.0}%", zoom * 100.0));
        self.action_set_enabled("win.zoom_in", zoom < 8.0);
        self.action_set_enabled("win.zoom_out", zoom > 0.1);
        self.action_set_enabled("win.zoom_reset", (zoom - 1.0).abs() > std::f64::EPSILON);
    }

    fn update_title(&self) -> Result<()> {
        let imp = self.imp();
        let Some(file) = imp.open_file.borrow().clone() else {
            return Ok(());
        };
        let info = file.query_info(
            gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
            gio::FileQueryInfoFlags::NONE,
            gio::Cancellable::NONE,
        )?;
        let Some(display_name) =
            info.attribute_as_string(gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME)
        else {
            return Ok(());
        };
        if imp.drawing_area.saved() {
            imp.window_title.set_subtitle(&display_name);
        } else {
            imp.window_title.set_subtitle(&format!("*{}", display_name));
        }

        Ok(())
    }

    #[template_callback]
    fn on_notify_saved(&self) {
        if let Err(err) = self.update_title() {
            log::warn!("Failed to update title {err}");
        }
    }

    #[template_callback]
    fn on_notify_can_redo(&self) {
        self.action_set_enabled("win.redo", self.imp().drawing_area.can_redo());
    }

    #[template_callback]
    fn on_notify_can_undo(&self) {
        self.action_set_enabled("win.undo", self.imp().drawing_area.can_undo());
    }

    #[template_callback]
    fn on_image_loaded(&self) {
        self.set_view(View::Image);
    }

    #[template_callback]
    fn on_blur_toggled(&self, toggle_btn: &gtk::ToggleButton) {
        if toggle_btn.is_active() {
            let imp = self.imp();
            if let Some(toast) = imp.current_toast.take() {
                toast.dismiss();
            }
            let toast = adw::Toast::new(&gettext("The blur tool is not secure. Please do not use it to share sensitive information in a public forum"));
            imp.toast_overlay.add_toast(toast.clone());
            imp.current_toast.replace(Some(toast));
            imp.drawing_area.set_filter(Filter::Blur);
        }
    }

    #[template_callback]
    fn on_filled_toggled(&self, toggle_btn: &gtk::ToggleButton) {
        if toggle_btn.is_active() {
            let imp = self.imp();
            if let Some(toast) = imp.current_toast.take() {
                toast.dismiss();
            }
            imp.drawing_area.set_filter(Filter::Filled);
        }
    }

    fn load_state(&self) {
        let is_maximized = self.imp().settings.boolean("is-maximized");

        if is_maximized {
            self.maximize();
        }
    }

    fn save_state(&self) -> Result<(), glib::BoolError> {
        self.imp()
            .settings
            .set_boolean("is-maximized", self.is_maximized())?;
        Ok(())
    }

    async fn save_file(&self) -> Result<()> {
        let imp = self.imp();
        let open_file = imp.open_file.borrow().clone();
        let initial_name = if let Some(file) = open_file {
            let info = file
                .query_info_future(
                    gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
                    gio::FileQueryInfoFlags::NONE,
                    glib::Priority::default(),
                )
                .await?;

            let extension = file
                .path()
                .and_then(|p| p.extension().and_then(|e| e.to_str()).map(|p| p.to_owned()));
            let display_name = info.attribute_as_string(gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
            match (display_name, extension) {
                (Some(name), Some(ext)) => {
                    format!(
                        "{}-obfuscated.{}",
                        name.trim_end_matches(&ext).trim_end_matches('.'),
                        ext
                    )
                }
                (Some(name), None) => {
                    format!(
                        "{}-obfuscated.png",
                        name.trim_end_matches("png").trim_end_matches('.')
                    )
                }
                (None, Some(ext)) => {
                    format!("obfuscated.{}", ext)
                }
                (None, None) => "obfuscated.png".to_owned(),
            }
        } else {
            "obfuscated.png".to_owned()
        };

        let filters_model = gio::ListStore::new::<gtk::FileFilter>();

        let png_filter = gtk::FileFilter::new();
        png_filter.set_name(Some("image/png"));
        png_filter.add_mime_type("image/png");

        filters_model.append(&png_filter);

        let mut builder = gtk::FileDialog::builder()
            .title(gettext("Save File"))
            .accept_label(gettext("Save"))
            .modal(true)
            .initial_name(initial_name)
            .filters(&filters_model);
        if let Some(xdg_pictures) =
            glib::user_special_dir(glib::UserDirectory::Pictures).map(gio::File::for_path)
        {
            builder = builder.initial_folder(&xdg_pictures);
        }
        let dialog = builder.build();

        let file = dialog.save_future(Some(self)).await?;
        imp.drawing_area.save_to(&file).await?;
        Ok(())
    }

    async fn open_file(&self) {
        let filters_model = gio::ListStore::new::<gtk::FileFilter>();

        let all_filters = gtk::FileFilter::new();
        all_filters.set_name(Some(&gettext("All Images")));
        all_filters.add_mime_type("image/png");
        all_filters.add_mime_type("image/jpeg");
        all_filters.add_mime_type("image/bmp");
        filters_model.append(&all_filters);

        let png_filter = gtk::FileFilter::new();
        png_filter.set_name(Some("image/png"));
        png_filter.add_mime_type("image/png");
        filters_model.append(&png_filter);

        let jpg_filter = gtk::FileFilter::new();
        jpg_filter.set_name(Some("image/jpeg"));
        jpg_filter.add_mime_type("image/jpeg");
        filters_model.append(&jpg_filter);

        let bpm_filter = gtk::FileFilter::new();
        bpm_filter.set_name(Some("image/bmp"));
        bpm_filter.add_mime_type("image/bmp");
        filters_model.append(&bpm_filter);

        let mut builder = gtk::FileDialog::builder()
            .accept_label(gettext("Open"))
            .title(gettext("Open File"))
            .modal(true)
            .filters(&filters_model);

        if let Some(xdg_pictures) =
            glib::user_special_dir(glib::UserDirectory::Pictures).map(gio::File::for_path)
        {
            builder = builder.initial_folder(&xdg_pictures);
        }
        let dialog = builder.build();

        match dialog.open_future(Some(self)).await {
            Ok(file) => {
                self.set_open_file(&file);
            }
            Err(err) => {
                log::error!("Failed to open a file {err}");
            }
        }
    }
}
