use crate::application::Application;
use crate::config::{APP_ID, PROFILE};
use crate::widgets::drawing_area::{DrawingArea, Filter};
use adw::subclass::prelude::*;
use anyhow::Result;
use gettextrs::gettext;
use gtk::{
    gdk, gio,
    glib::{self, clone},
    prelude::*,
};

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

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

    #[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 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(),
                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 {
                widget.save_file().await;
            });
        }

        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.filled_btn.bind_property("active", &*self.blur_btn, "active").invert_boolean().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) -> gtk::Inhibit {
            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),
        };
    }

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

        let info = file.query_info(gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE)?;
        let display_name = info.attribute_as_string(gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);

        imp.window_title.set_subtitle(&display_name.unwrap());
        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);
    }

    #[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) {
        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
                .unwrap();
            let display_name = info.attribute_as_string(gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
            let extension = file.path().and_then(|p| p.extension().map(|e| e.to_str().unwrap().to_owned())).unwrap_or_else(|| String::from("png"));

            format!("{}-obfuscated.{}", display_name.unwrap().trim_end_matches(&extension).trim_end_matches('.'), extension)
        } else {
            "obfuscated.png".to_owned()
        };

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

        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 dialog = gtk::FileDialog::builder()
            .title(gettext("Save File"))
            .accept_label(gettext("Save"))
            .modal(true)
            .initial_name(initial_name)
            .filters(&filters_model)
            .build();

        //let xdg_pictures = glib::user_special_dir(glib::UserDirectory::Pictures).map(gio::File::for_path);
        //let _ = dialog.set_current_folder(xdg_pictures.as_ref());

        match dialog.save_future(Some(self)).await {
            Ok(file) => {
                if let Err(err) = imp.drawing_area.save_to(&file) {
                    log::error!("Failed to save the image: {}", err);
                }
            }
            Err(err) => {
                log::error!("Failed to get a file {err}")
            }
        }
    }

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

        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 dialog = gtk::FileDialog::builder()
            .accept_label(gettext("Open"))
            .title(gettext("Open File"))
            .modal(true)
            .filters(&filters_model)
            .build();

        //let xdg_pictures = glib::user_special_dir(glib::UserDirectory::Pictures).map(gio::File::for_path);
        //let _ = open_dialog.set_current_folder(xdg_pictures.as_ref());

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