// SPDX-License-Identifier: GPL-3.0-or-later
use gtk::glib::Properties;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};

use gettextrs::gettext;
use rand::Rng;

use crate::application::Application;
use crate::config::{APP_ID, PROFILE};

#[derive(Default, Debug, Copy, Clone, glib::Enum, PartialEq)]
#[repr(u32)]
#[enum_type(name = "Unit")]
pub enum Unit {
    #[default]
    Paragraphs,
    Words,
}

impl Unit {
    fn to_translatable_string(self) -> String {
        match self {
            Self::Paragraphs => gettext("Paragraphs"),
            Self::Words => gettext("Words"),
        }
    }
}

impl From<u32> for Unit {
    fn from(u: u32) -> Self {
        match u {
            0 => Self::Paragraphs,
            1 => Self::Words,
            _ => Self::default(),
        }
    }
}

mod imp {
    use super::*;

    use std::cell::Cell;

    use adw::subclass::prelude::*;
    use gtk::CompositeTemplate;

    #[derive(Debug, Properties, CompositeTemplate)]
    #[template(resource = "/org/gnome/design/Lorem/ui/window.ui")]
    #[properties(wrapper_type = super::Window)]
    pub struct Window {
        #[template_child]
        pub lorem_ipsum: TemplateChild<gtk::Label>,
        #[template_child]
        pub spin_button: TemplateChild<gtk::SpinButton>,
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,

        #[property(get, set = Self::set_unit, explicit_notify, builder(Unit::default()))]
        pub unit: Cell<Unit>,
        #[property(get, set = Self::set_length, explicit_notify, default_value = 3)]
        pub length: Cell<u32>,

        pub settings: gio::Settings,
    }

    impl Default for Window {
        fn default() -> Self {
            Self {
                lorem_ipsum: TemplateChild::default(),
                spin_button: TemplateChild::default(),
                toast_overlay: TemplateChild::default(),

                unit: Cell::default(),
                length: Cell::new(3),
                settings: gio::Settings::new(APP_ID),
            }
        }
    }

    #[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("copy", None, |obj, _, _| {
                obj.copy();
            });
        }

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

    #[gtk::template_callbacks]
    impl Window {
        fn set_length(&self, length: u32) {
            if length != self.length.replace(length) {
                self.obj().generate();
                self.obj().notify_length();
            }
        }

        fn set_unit(&self, unit: Unit) {
            if unit != self.unit.replace(unit) {
                self.obj().notify_unit();
            }
        }

        #[template_callback]
        fn unit_name(item: adw::EnumListItem) -> String {
            let type_ = Unit::from(item.value() as u32);
            type_.to_translatable_string()
        }

        #[template_callback]
        fn on_notify_selected(drop_down: gtk::DropDown, _: glib::ParamSpec, window: super::Window) {
            let n = match drop_down.selected().into() {
                Unit::Paragraphs => 3,
                Unit::Words => 50,
            };
            window.set_length(n);
            window.generate();
        }
    }

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

            // Devel Profile
            if PROFILE == "Devel" {
                obj.add_css_class("devel");
            }

            self.settings.bind("unit", obj.as_ref(), "unit").build();

            obj.generate();

            // Load latest window state
            obj.load_window_size();
        }
    }

    impl WidgetImpl for Window {}
    impl WindowImpl for Window {
        // Save window state on delete event
        fn close_request(&self) -> glib::Propagation {
            let window = self.obj();
            if let Err(err) = window.save_window_size() {
                log::warn!("Failed to save window state, {err}");
            }

            // Pass close request on to the parent
            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, gtk::ApplicationWindow, adw::ApplicationWindow,
        @implements gio::ActionMap, gio::ActionGroup, gtk::ConstraintTarget, gtk::Accessible, gtk::Buildable, gtk::ShortcutManager, gtk::Native, gtk::Root;
}

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

    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_size(&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();
        }
    }

    fn generate(&self) {
        let n = self.length() as usize;

        let label = match self.unit() {
            Unit::Paragraphs => {
                let mut rng = rand::thread_rng();

                let texts: Vec<String> = (0..n)
                    .map(|i| {
                        let m = rng.gen_range(30..70);
                        match i {
                            0 => lipsum::lipsum(m),
                            _ => lipsum::lipsum_words_with_rng(&mut rng, m),
                        }
                    })
                    .collect();

                texts.join("\n\n")
            }
            Unit::Words => lipsum::lipsum(n),
        };

        self.imp().lorem_ipsum.set_label(&label);
    }

    fn copy(&self) {
        let clipboard = self.clipboard();
        let toast = if let Some((start, end)) = self.imp().lorem_ipsum.selection_bounds() {
            let label = self.imp().lorem_ipsum.label();
            clipboard.set_text(&label[start as usize..end as usize]);

            adw::Toast::new(&gettext("Selection copied"))
        } else {
            let text = self.imp().lorem_ipsum.label();
            clipboard.set_text(&text);

            adw::Toast::new(&gettext("Copied"))
        };

        self.imp().toast_overlay.add_toast(toast);
    }
}
