// SPDX-License-Identifier: GPL-3.0-or-later
use std::ffi::OsStr;

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

use crate::{
    application::Application,
    config,
    model::QRCodeModel,
    qrcode::QRCode,
    utils,
    widgets::{CameraPage, HistoryPage, QRCodeCreatePage, QRScannedPage},
};

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

    use super::*;

    #[derive(Debug, gtk::CompositeTemplate)]
    #[template(resource = "/com/belmoussaoui/Decoder/ui/window.ui")]
    pub struct Window {
        pub settings: gio::Settings,
        pub model: QRCodeModel,
        pub last_toast: RefCell<Option<adw::Toast>>,

        #[template_child]
        pub toolbar_view: TemplateChild<adw::ToolbarView>,
        #[template_child]
        pub camera: TemplateChild<CameraPage>,
        #[template_child]
        pub navigation_view: TemplateChild<adw::NavigationView>,
        #[template_child]
        pub main_stack: TemplateChild<adw::ViewStack>,
        #[template_child]
        pub create_page: TemplateChild<QRCodeCreatePage>,
        #[template_child]
        pub scanned_page: TemplateChild<QRScannedPage>,
        #[template_child]
        pub history_page: TemplateChild<HistoryPage>,
        #[template_child]
        pub switcher_bar: TemplateChild<adw::ViewSwitcherBar>,
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
    }

    impl Default for Window {
        fn default() -> Self {
            Self {
                settings: gio::Settings::new(config::APP_ID),
                model: QRCodeModel::default(),
                toolbar_view: TemplateChild::default(),
                camera: TemplateChild::default(),
                navigation_view: TemplateChild::default(),
                main_stack: TemplateChild::default(),
                history_page: TemplateChild::default(),
                create_page: TemplateChild::default(),
                scanned_page: TemplateChild::default(),
                switcher_bar: TemplateChild::default(),
                toast_overlay: TemplateChild::default(),
                last_toast: RefCell::default(),
            }
        }
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_callbacks();

            klass.install_action("win.back", None, |obj, _, _| {
                let imp = obj.imp();
                imp.navigation_view.pop();

                if imp
                    .main_stack
                    .visible_child_name()
                    .is_some_and(|child_name| child_name == "scan")
                {
                    imp.camera.start();
                }
            });

            klass.install_action("win.scan-qr", None, |obj, _, _| {
                let imp = obj.imp();
                imp.main_stack.set_visible_child_name("scan");
            });
        }

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

    impl ObjectImpl for Window {
        fn constructed(&self) {
            let obj = self.obj();
            self.parent_constructed();
            if config::PROFILE == "Devel" {
                obj.add_css_class("devel");
            }
            obj.load_window_state();

            obj.init();

            obj.connect_suspended_notify(|window| {
                let imp = window.imp();
                if window.is_suspended() {
                    tracing::debug!("Window suspended: stopping stream");
                    imp.camera.stop();
                } else {
                    tracing::debug!("Window un-suspended: starting stream");
                    imp.camera.start();
                }
            });

            let target = gtk::DropTarget::builder()
                .name("file-drop-target")
                .actions(gdk::DragAction::COPY | gdk::DragAction::MOVE)
                .formats(&gdk::ContentFormats::for_type(gdk::FileList::static_type()))
                .build();

            target.connect_drop(glib::clone!(
                #[weak]
                obj,
                #[upgrade_or]
                false,
                move |_, value, _, _| {
                    if let Ok(file_list) = value.get::<gdk::FileList>()
                        && let Some(file) = file_list.files().first()
                    {
                        match utils::scan(file) {
                            Ok(code) => {
                                glib::spawn_future_local(glib::clone!(
                                    #[weak]
                                    obj,
                                    async move {
                                        if let Err(err) = obj.show_code_detected(&code).await {
                                            tracing::error!(
                                                "Could not show code {}: {err:?}",
                                                &code
                                            );
                                        }
                                    }
                                ));
                                return true;
                            }
                            Err(err) => tracing::error!("Could not open file: {err:?}"),
                        }
                    }
                    false
                }
            ));

            obj.add_controller(target);
        }
    }
    impl WidgetImpl for Window {}
    impl WindowImpl for Window {
        fn close_request(&self) -> glib::Propagation {
            let window = self.obj();
            if let Err(err) = window.save_window_size() {
                tracing::warn!("Failed to save window state, {}", &err);
            }
            self.parent_close_request()
        }
    }
    impl ApplicationWindowImpl for Window {}
    impl AdwApplicationWindowImpl for Window {}

    #[gtk::template_callbacks]
    impl Window {
        #[template_callback]
        async fn on_code_detected(_page: &CameraPage, code: &str, window: &super::Window) {
            if let Err(err) = window.show_code_detected(code).await {
                tracing::error!("Could not detect code: {err}");
            }
        }

        #[template_callback]
        fn on_scanned_page_created(_page: &gtk::Widget, code: &QRCode, window: &super::Window) {
            window.imp().model.append(code);
        }

        #[template_callback]
        fn on_create_page_created(_page: &gtk::Widget, code: &QRCode, window: &super::Window) {
            window.imp().model.append(code);
        }

        #[template_callback]
        fn on_visible_page_notify(window: &super::Window) {
            window.update_toolbar_style();
        }

        #[template_callback]
        async fn on_exported(window: &super::Window, content: &str) {
            if let Err(err) = window.export(content).await {
                tracing::error!("Could not export QR code: {err}");
            }
        }

        #[template_callback]
        async fn on_scanned_page_exported(window: &super::Window, content: &str) {
            if let Err(err) = window.export(content).await {
                tracing::error!("Could not export QR code: {err}");
            }
            window.imp().navigation_view.pop();
            window.imp().camera.start();
        }

        #[template_callback]
        fn on_notify_streaming(window: &super::Window) {
            window.update_toolbar_style();
        }

        #[template_callback]
        fn on_visible_child_name_notify(
            stack: &adw::ViewStack,
            _param: glib::ParamSpec,
            window: &super::Window,
        ) {
            if stack
                .visible_child_name()
                .is_some_and(|name| name == "scan")
            {
                window.imp().camera.start();
            } else {
                window.imp().camera.stop();
            }
            window.update_toolbar_style();
        }
    }
}

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

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

    pub async fn export(&self, content: &str) -> anyhow::Result<()> {
        use crate::qrcode::img;

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

        let svg_filter = gtk::FileFilter::new();
        svg_filter.add_mime_type("image/svg+xml");
        svg_filter.set_name(Some("SVG"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&png_filter);
        filters.append(&svg_filter);

        let dialog = gtk::FileDialog::new();
        dialog.set_filters(Some(&filters));
        dialog.set_accept_label(Some(&gettext("_Select")));
        // TRANSLATORS This goes into a file name
        let basename = gettext("QR Code");
        // Taken from gnome-shell's ui/screnshoot.js.
        let timestamp = glib::DateTime::now_local()?.format("%Y-%m-%d %H-%M-%S")?;
        dialog.set_initial_name(Some(&format!("{basename} {timestamp}.png")));

        let output = match dialog.save_future(Some(self)).await {
            Err(err) if err.matches(gtk::DialogError::Dismissed) => return Ok(()),
            res => res?,
        };
        let format = if output
            .path()
            .is_some_and(|path| path.extension() == Some(OsStr::new("svg")))
        {
            img::Format::Svg
        } else {
            img::Format::Png
        };
        img::save_as(content, format, &output).await?;
        self.add_toast(&gettext("QR code saved"));

        Ok(())
    }

    pub async fn show_code_detected(&self, code_content: &str) -> anyhow::Result<()> {
        let imp = self.imp();
        imp.scanned_page.set_scanned_code(code_content).await?;
        imp.navigation_view.push_by_tag("code");
        self.update_toolbar_style();

        Ok(())
    }

    fn init(&self) {
        let imp = self.imp();

        imp.history_page.set_model(&imp.model);

        glib::spawn_future_local(clone!(
            #[weak(rename_to = window)]
            self,
            async move {
                window.imp().model.init().await;
            }
        ));
    }

    fn save_window_size(&self) -> Result<(), glib::BoolError> {
        let imp = self.imp();

        let (width, height) = self.default_size();

        imp.settings.set_int("window-width", width)?;
        imp.settings.set_int("window-height", height)?;

        imp.settings
            .set_boolean("is-maximized", self.is_maximized())?;

        Ok(())
    }

    fn load_window_state(&self) {
        let imp = self.imp();

        let width = imp.settings.int("window-width");
        let height = imp.settings.int("window-height");
        let is_maximized = imp.settings.boolean("is-maximized");

        self.set_default_size(width, height);

        if is_maximized {
            self.maximize();
        }
    }

    pub fn add_toast(&self, text: &str) {
        let toast = adw::Toast::new(text);
        self.imp().toast_overlay.add_toast(toast.clone());

        // Since this is used for error reporting, we make sure to dismiss the
        // last toast to avoid spam.
        if let Some(old_toast) = self.imp().last_toast.replace(Some(toast)) {
            old_toast.dismiss();
        }
    }

    fn update_toolbar_style(&self) {
        let imp = self.imp();
        let manager = adw::StyleManager::default();

        if imp
            .main_stack
            .visible_child_name()
            .is_some_and(|name| name == "scan")
            && imp.camera.is_streaming()
            && imp
                .navigation_view
                .visible_page()
                .is_some_and(|page| page.tag().is_some_and(|tag| tag != "code"))
        {
            imp.toolbar_view.add_css_class("camera-view");
            manager.set_color_scheme(adw::ColorScheme::PreferDark);
        } else {
            imp.toolbar_view.remove_css_class("camera-view");
            manager.set_color_scheme(adw::ColorScheme::PreferLight);
        }
    }
}
