use std::time::Instant;

use adw::prelude::*;
use gettextrs::gettext;
use gtk::glib;
use gtk::subclass::prelude::*;

use crate::application::HieroglyphicApplication;
use crate::widgets::{BoxedStrokes, SymbolItem, SymbolObject};
use crate::{classify, config};

// GTK is single-threaded
thread_local! {
    static SETTINGS: gio::Settings = gio::Settings::new(config::APP_ID);
}

#[derive(Default, Debug, Clone, Copy)]
pub enum MarkupLanguageMode {
    #[default]
    Latex,
    Typst,
}

impl MarkupLanguageMode {
    fn from_settings() -> Self {
        let language_value = SETTINGS.with(|settings| settings.string("markup-language-mode"));

        // see the gschema file for the value definitions
        match language_value.as_str() {
            "latex" => MarkupLanguageMode::Latex,
            "typst" => MarkupLanguageMode::Typst,
            _ => unreachable!(),
        }
    }
}

mod imp {
    use std::{
        cell::{Cell, OnceCell, RefCell},
        sync::mpsc::Sender,
    };

    use adw::subclass::application_window::AdwApplicationWindowImpl;
    use gio::ActionEntry;

    use crate::{
        classify::Symbol,
        config,
        widgets::{self, IndicatorButton},
    };

    use super::*;

    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
    #[template(resource = "/io/github/finefindus/Hieroglyphic/ui/window.ui")]
    #[properties(wrapper_type = super::HieroglyphicWindow)]
    pub struct HieroglyphicWindow {
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
        #[template_child]
        pub drawing_area: TemplateChild<widgets::DrawingArea>,
        #[template_child]
        pub symbol_list: TemplateChild<gtk::ListBox>,
        #[template_child]
        pub indicator_button: TemplateChild<IndicatorButton>,
        #[template_child]
        pub preview_symbol: TemplateChild<SymbolItem>,
        #[property(get, set)]
        pub stack_page: RefCell<String>,
        pub markup_language: Cell<MarkupLanguageMode>,
        pub symbols: OnceCell<gio::ListStore>,
        pub symbol_strokes: RefCell<Option<Vec<classify::Stroke>>>,
        pub classifier: OnceCell<Sender<Vec<classify::Stroke>>>,
        pub symbol_filter: RefCell<Vec<&'static str>>,
    }

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

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

            klass.install_action("win.show-contribution-dialog", None, move |win, _, _| {
                let builder = gtk::Builder::from_resource(
                    "/io/github/finefindus/Hieroglyphic/ui/contribution-dialog.ui",
                );
                let switch: adw::SwitchRow = builder.object("switch_row").unwrap();
                SETTINGS.with(|settings| {
                    settings.bind("contribute-data", &switch, "active").build();
                    // only show nudge once, i.e. hide it after clicking the button
                    settings
                        .set_boolean("show-contribution-nudge", false)
                        .expect("Failed to set `show-contribution-nudge`");
                });
                let dialog: adw::Dialog = builder.object("contribution_dialog").unwrap();
                dialog.present(Some(win));
            });

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

            klass.install_action(
                "win.copy-symbol",
                Some(glib::VariantTy::STRING),
                move |win, _, var| {
                    let Some(symbol) = var
                        .and_then(|v| v.get::<String>())
                        .and_then(|id| Symbol::from_id(&id))
                    else {
                        return;
                    };

                    win.copy_symbol(&SymbolItem::new(
                        &symbol,
                        &MarkupLanguageMode::from_settings(),
                    ));
                },
            );
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for HieroglyphicWindow {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            // Devel Profile
            if config::PROFILE == "Devel" {
                obj.add_css_class("devel");
                SETTINGS.with(|settings| {
                    settings
                        .set_boolean("show-contribution-nudge", true)
                        .expect("Failed to set `show-contribution-nudge`");
                });
            }
            obj.settings().set_gtk_icon_theme_name(Some("Adwaita"));

            tracing::debug!("Loaded {} symbols", classify::SYMBOL_COUNT);

            let settings = SETTINGS.with(|s| s.clone());
            settings
                .bind(
                    "show-contribution-nudge",
                    &*self.indicator_button,
                    "show-indicator",
                )
                .build();
            self.markup_language
                .set(MarkupLanguageMode::from_settings());

            let current_language_mode = settings.string("markup-language-mode");
            let language_mode_action = ActionEntry::builder("markup-language-mode")
                .parameter_type(Some(&String::static_variant_type()))
                .state(current_language_mode.to_variant())
                .activate(move |window: &Self::Type, action, var| {
                    let Some(mode) = var.and_then(|v| v.get::<String>()) else {
                        return;
                    };

                    action.set_state(&mode.to_variant());
                    SETTINGS.with(|settings| {
                        settings
                            .set_string("markup-language-mode", &mode)
                            .expect("Failed to set `markup-language-mode`");
                    });
                    window
                        .imp()
                        .markup_language
                        .set(MarkupLanguageMode::from_settings());

                    let symbols = window.imp().symbols.get().unwrap();
                    let objects = symbols
                        .snapshot()
                        .iter()
                        .filter_map(|obj| obj.downcast_ref::<SymbolObject>())
                        .map(|sym| sym.symbol().id())
                        .collect::<Vec<_>>();
                    window.display_symbols(objects);
                })
                .build();
            obj.add_action_entries([language_mode_action]);

            obj.setup_symbol_list();
            obj.setup_classifier();
        }

        fn dispose(&self) {
            self.dispose_template();
        }
    }

    impl WidgetImpl for HieroglyphicWindow {}
    impl WindowImpl for HieroglyphicWindow {}

    impl ApplicationWindowImpl for HieroglyphicWindow {}
    impl AdwApplicationWindowImpl for HieroglyphicWindow {}
}

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

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

    /// Shows a basic toast with the given text.
    fn show_toast(&self, text: impl AsRef<str>) {
        let toast = adw::Toast::builder()
            .title(text.as_ref())
            .use_markup(false)
            .build();
        // dismiss and replace the previous toasts if they exists
        // this ensures that the user only sees the most recent copy toast
        self.imp().toast_overlay.dismiss_all();
        self.imp().toast_overlay.add_toast(toast);
    }

    fn setup_symbol_list(&self) {
        let model = gio::ListStore::new::<SymbolObject>();

        self.imp()
            .symbols
            .set(model.clone())
            .expect("Failed to set symbol model");

        let selection_model = gtk::NoSelection::new(Some(model));
        self.imp().symbol_list.bind_model(
            Some(&selection_model),
            glib::clone!(
                #[weak(rename_to = window)]
                self,
                #[upgrade_or_panic]
                move |obj| {
                    let symbol_object = obj
                        .downcast_ref::<SymbolObject>()
                        .expect("Object should be of type `SymbolObject`");
                    let symbol_item = SymbolItem::new(
                        symbol_object.symbol(),
                        &window.imp().markup_language.get(),
                    );

                    symbol_item.upcast()
                }
            ),
        );
    }

    fn setup_classifier(&self) {
        let imp = self.imp();
        let (req_tx, req_rx) = std::sync::mpsc::channel();
        let (res_tx, res_rx) = async_channel::bounded(1);
        imp.classifier.set(req_tx).expect("Failed to set tx");

        gio::spawn_blocking(move || {
            tracing::info!("Classifier thread started");
            let mut classifier = classify::Classifier::new().expect("Failed to setup classifier");

            loop {
                let Some(strokes) = req_rx.iter().next() else {
                    // channel has hung up, cleanly exit
                    tracing::info!("Exiting classifier thread");
                    return;
                };

                if strokes.is_empty() {
                    tracing::warn!("Skipping classification on empty strokes");
                    continue;
                }

                let classifications: Option<Vec<&'static str>> = 'classify: {
                    let start = Instant::now();
                    let Some(results) = classifier.classify(strokes) else {
                        tracing::warn!("Classifier returned None");
                        break 'classify None;
                    };
                    tracing::info!(
                        "Classification complete in {}ms",
                        start.elapsed().as_millis()
                    );
                    Some(results)
                };

                res_tx
                    .send_blocking(classifications)
                    .expect("Failed to send classifications");
            }
        });

        glib::spawn_future_local(glib::clone!(
            #[weak(rename_to = window)]
            self,
            async move {
                tracing::debug!("Listening for classifications");
                while let Ok(Some(classifications)) = res_rx.recv().await {
                    window.display_symbols(classifications);
                }
            }
        ));
    }

    fn display_symbols(&self, mut classifications: Vec<&str>) {
        self.set_stack_page("symbols");
        let imp = self.imp();

        let mut symbols = imp
            .symbols
            .get()
            .cloned()
            .expect("`symbols` should be initialized in `setup_symbol_list`");

        let filter_symbols = imp.symbol_filter.borrow();
        if !filter_symbols.is_empty() {
            // if `--show-only` symbols are set, show only those symbols
            // this is intended, for development/debug to directly improve new/less recognized
            // symbols
            classifications = filter_symbols.clone();
        }

        let language_mode = MarkupLanguageMode::from_settings();
        // use the first symbol as the symbol displayed in the bottom bar in
        // bottom-sheet mode
        if let Some(symbol) = classifications
            .iter()
            .filter_map(|id| classify::Symbol::from_id(id))
            .find(|sym| sym.command(&language_mode).is_some())
        {
            imp.preview_symbol.set_symbol(symbol, &language_mode);
        }

        symbols.remove_all();
        // switching out all 1k symbols takes too long, so only display the first 25
        symbols.extend(
            classifications
                .into_iter()
                .filter_map(classify::Symbol::from_id)
                // only display symbols supported by the current language
                .filter(|symbol| symbol.command(&language_mode).is_some())
                .map(SymbolObject::new)
                .take(25),
        );
        // scroll to top after updating symbols, so that the most likely symbols are
        // visible first
        imp.symbol_list
            .adjustment()
            .expect("Failed to get symbol list adjustment")
            .set_value(0.0);
    }

    /// Classify the given strokes.
    #[template_callback]
    fn classify(&self, BoxedStrokes(strokes): BoxedStrokes) {
        // we clone the strokes to the window, so we can upload them later on
        self.imp().symbol_strokes.replace(Some(strokes.clone()));
        self.imp()
            .classifier
            .get()
            .unwrap()
            .send(strokes)
            .expect("Failed to send strokes");
    }

    #[template_callback]
    fn on_item_activated(&self, row: Option<&gtk::ListBoxRow>) {
        let binding = row.and_then(|row| row.child());
        let Some(symbol) = binding.and_downcast_ref::<SymbolItem>() else {
            return;
        };

        self.copy_symbol(symbol);
    }

    #[template_callback]
    pub fn copy_symbol(&self, symbol: &SymbolItem) {
        let command = symbol.command();

        self.clipboard().set_text(&command);
        tracing::debug!("Selected: {} ({})", &command, symbol.id());
        self.show_toast(gettext("Copied “{}”").replace("{}", &command));

        if let Some(strokes) = self.imp().symbol_strokes.take() {
            self.try_upload_data(symbol.id(), strokes);
        }
    }

    fn try_upload_data(&self, label: String, strokes: Vec<classify::Stroke>) {
        // skip uploads always on debug mode, to avoid accidental uploads
        if SETTINGS.with(|s| !s.boolean("contribute-data")) || config::PROFILE == "Devel" {
            tracing::debug!("Skipping data upload: user has not opted into data contribution");
            return;
        }

        // skip uploading the data if the user is on a metered network connection
        // see https://gitlab.gnome.org/GNOME/Initiatives/-/issues/42
        let network_monitor = gio::NetworkMonitor::default();
        if network_monitor.is_network_metered() {
            tracing::debug!("Skipping data upload: network is metered");
            return;
        }

        // skip uploading data whilst the user has power saving enabled
        // see https://gitlab.gnome.org/GNOME/Initiatives/-/issues/43
        let power_monitor = gio::PowerProfileMonitor::get_default();
        if power_monitor.is_power_saver_enabled() {
            tracing::debug!("Skipping data upload: power saver is active");
            return;
        }

        tracing::info!("Uploading strokes...");
        // spawn a new thread to avoid blocking the UI thread while uploading
        std::thread::spawn(move || {
            match ureq::post(&format!(
                "https://hieroglyphic-server.apps.openshift.gnome.org/v1/upload/{}",
                label
            ))
            .header("User-Agent", format!("Hieroglyphic/{}", config::VERSION))
            .send_json(strokes)
            {
                Ok(_) => {
                    tracing::info!("Successfully uploaded data");
                }
                Err(err) => {
                    tracing::warn!("Failed to upload strokes: {}", err);
                }
            }
        });
    }
}
