// SPDX-FileCopyrightText: 2023 Gleb Smirnov glebsmirnov0708@gmail.com
//
// SPDX-License-Identifier: GPL-3.0-or-later

use adw::prelude::*;
use adw::subclass::prelude::*;

use gtk::{gio, glib, pango};

mod imp {
    use std::{collections::BTreeSet, fs, path::PathBuf};

    use anyhow::Result;
    use gettextrs::pgettext;

    use super::*;

    use crate::{
        config::PKGDATADIR,
        utils::{open_file, spawn_async_local, to_entries, ActionModel, GActions},
        widgets::{action_page::EditOperation, ActionPage},
        Application,
    };

    /// Private part of [`Preferences`].
    ///
    /// [`Preferences`]: super::Preferences
    #[derive(Default, Debug, gtk::CompositeTemplate)]
    #[template(resource = "/io/gitlab/liferooter/TextPieces/ui/preferences.ui")]
    pub struct Preferences {
        /// Color scheme setting row.
        #[template_child]
        color_scheme_row: TemplateChild<adw::ComboRow>,

        /// An adjustment of how many spaces are in tab.
        #[template_child]
        spaces_in_tab: TemplateChild<gtk::Adjustment>,

        /// An adjustment of font size.
        #[template_child]
        font_size: TemplateChild<gtk::Adjustment>,

        /// Font description label
        #[template_child]
        font_label: TemplateChild<gtk::Label>,

        /// Editable list box for custom actions settings.
        #[template_child]
        custom_actions_listbox: TemplateChild<gtk::ListBox>,

        /// Placeholder to show when there's no custom actions.
        #[template_child]
        custom_actions_placeholder: TemplateChild<adw::StatusPage>,
    }

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

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

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

    impl ObjectImpl for Preferences {
        fn constructed(&self) {
            self.parent_constructed();

            let settings = Application::settings();

            settings
                .bind_color_scheme(&self.color_scheme_row.get(), "selected")
                .get()
                .mapping(|color_scheme, _| {
                    Some(
                        match color_scheme.get::<String>().as_deref() {
                            Some("follow") => 0,
                            Some("light") => 1,
                            Some("dark") => 2,
                            _ => unreachable!(),
                        }
                        .into(),
                    )
                })
                .set()
                .set_mapping(|color_scheme_id, _| {
                    Some(
                        match color_scheme_id.get::<u32>() {
                            Ok(0) => "follow",
                            Ok(1) => "light",
                            Ok(2) => "dark",
                            _ => unreachable!(),
                        }
                        .into(),
                    )
                })
                .build();

            settings
                .bind_font_size(&self.font_size.get(), "value")
                .get()
                .set()
                .build();

            settings
                .bind_font_family(&self.font_label.get(), "label")
                .get_only()
                .build();

            settings
                .bind_spaces_in_tab(&self.spaces_in_tab.get(), "value")
                .get()
                .set()
                .build();

            // Create actions for some settings
            let action_group = gio::SimpleActionGroup::new();

            action_group.add_action(&settings.create_wrap_lines_action());
            action_group.add_action(&settings.create_tabs_to_spaces_action());

            self.obj()
                .insert_action_group("settings", Some(&action_group));

            // Create model for custom actions
            let filter = gtk::CustomFilter::new(|obj| {
                !obj.downcast_ref::<ActionModel>()
                    .expect("Action model contains non-action somehow")
                    .is_builtin()
            });

            let prefs = self.obj().to_owned();

            let model = gtk::FilterListModel::new(Some(Application::action_list()), Some(filter));
            model.connect_items_changed(glib::clone!(
                #[weak]
                prefs,
                move |model, _, _, _| prefs.imp().on_action_model_changed(model.upcast_ref())
            ));
            self.on_action_model_changed(model.upcast_ref());

            self.custom_actions_listbox
                .bind_model(Some(&model), move |obj| {
                    let action = obj.downcast_ref::<ActionModel>().expect(
                        "Type check in filter function hasn't failed with wrong types somehow",
                    );

                    let row = adw::ActionRow::builder()
                        .title(action.name())
                        .subtitle(action.description())
                        .build();

                    let row_actions = gio::SimpleActionGroup::new();
                    let action_entries: GActions<(Self::Type, ActionModel)> = &[
                        ("edit-script", |(prefs, action)| {
                            spawn_async_local(glib::clone!(
                                #[weak]
                                prefs,
                                #[weak]
                                action,
                                async move {
                                    let app = Application::get();
                                    let actions = app.custom_actions().await;
                                    let script_path = actions.provider().script_file(&action.id());

                                    if let Err(err) =
                                        open_file(script_path, &prefs.imp().window()).await
                                    {
                                        prefs.add_toast(adw::Toast::new(&err.to_string()))
                                    }
                                }
                            ));
                        }),
                        ("edit-metadata", |(prefs, action)| {
                            spawn_async_local(glib::clone!(
                                #[weak]
                                prefs,
                                #[weak]
                                action,
                                async move {
                                    if let Err(err) = prefs.imp().edit_action(&action).await {
                                        prefs.add_toast(adw::Toast::new(&err.to_string()))
                                    }
                                }
                            ));
                        }),
                        ("delete", |(prefs, action)| {
                            spawn_async_local(glib::clone!(
                                #[weak]
                                prefs,
                                #[weak]
                                action,
                                async move {
                                    if let Err(err) = prefs.imp().delete_action(&action).await {
                                        prefs.add_toast(adw::Toast::new(&err.to_string()))
                                    }
                                }
                            ));
                        }),
                    ];

                    row_actions.add_action_entries(to_entries(
                        action_entries,
                        &(prefs.to_owned(), action.to_owned()),
                    ));

                    row.insert_action_group("action", Some(&row_actions));

                    let options_menu = gio::Menu::new();
                    options_menu.append(
                        Some(&pgettext("preferences", "Edit _Script")),
                        Some("action.edit-script"),
                    );
                    options_menu.append(
                        Some(&pgettext("preferences", "Edit _Metadata")),
                        Some("action.edit-metadata"),
                    );
                    options_menu.append(
                        Some(&pgettext("preferences", "_Delete")),
                        Some("action.delete"),
                    );

                    let options_button = gtk::MenuButton::builder()
                        .valign(gtk::Align::Center)
                        .icon_name("view-more-symbolic")
                        .tooltip_text(pgettext("preferences", "Options"))
                        .menu_model(&options_menu)
                        .build();

                    options_button.add_css_class("flat");
                    options_button.add_css_class("circular");

                    row.add_suffix(&options_button);

                    row.upcast()
                })
        }
    }

    impl WidgetImpl for Preferences {}
    impl AdwDialogImpl for Preferences {}
    impl PreferencesDialogImpl for Preferences {}

    impl Preferences {
        async fn create_action(&self) -> Result<()> {
            let action = ActionPage::create_action(|page| self.obj().push_subpage(page)).await;

            let Some(action) = action else {
                // Creation is cancelled by user
                return Ok(());
            };

            self.obj().pop_subpage();

            let app = Application::get();
            let mut actions = app.custom_actions_mut().await;

            let action_names: BTreeSet<String> = actions.actions().keys().cloned().collect();

            let normalized_name = action.name.to_lowercase().replace([' ', '/'], "-");
            let action_id = (1..)
                .find_map(move |i| {
                    let suffix = if i == 1 {
                        String::new()
                    } else {
                        format!("-{i}")
                    };
                    let id = normalized_name.clone() + &suffix;

                    (!action_names.contains(&id)).then_some(id)
                })
                .expect("Infinite list was too short");

            let scripts_dir = actions.provider().scripts_dir();
            if !scripts_dir.exists() {
                fs::create_dir_all(scripts_dir)?;
            }

            let script_path = actions.provider().script_file(&action_id);
            let template_path = PathBuf::from_iter([PKGDATADIR, "action_template"]);

            fs::copy(&template_path, &script_path)?;

            actions.add(&action_id, action).await?;

            // Explicitly drop custom actions lock to prevent deadlock
            drop(actions);

            app.update_actions_model().await;

            open_file(script_path, &self.window()).await?;

            Ok(())
        }

        async fn edit_action(&self, action: &ActionModel) -> anyhow::Result<()> {
            assert!(!action.is_builtin());

            let operation = ActionPage::edit(action, |page| self.obj().push_subpage(page)).await;

            let Some(operation) = operation else {
                return Ok(());
            };

            self.obj().pop_subpage();

            match operation {
                EditOperation::Change(new_action) => {
                    let app = Application::get();
                    let mut custom_actions = app.custom_actions_mut().await;

                    custom_actions.update(&action.id(), new_action).await?;

                    drop(custom_actions);
                    app.update_actions_model().await;
                }
                EditOperation::Delete => {
                    self.delete_action(action).await?;
                }
            }

            Ok(())
        }

        async fn delete_action(&self, action: &ActionModel) -> anyhow::Result<()> {
            assert!(!action.is_builtin());

            let app = Application::get();
            let mut custom_actions = app.custom_actions_mut().await;
            custom_actions.remove(&action.id()).await?;

            let script_path = custom_actions.provider().script_file(&action.id());
            fs::remove_file(script_path)?;

            drop(custom_actions);

            app.update_actions_model().await;

            Ok(())
        }

        fn window(&self) -> gtk::Window {
            self.obj().root().and_downcast().unwrap()
        }
    }

    #[gtk::template_callbacks]
    impl Preferences {
        #[template_callback]
        async fn on_font_row_activated(&self) {
            let settings = Application::settings();

            let font = gtk::FontDialog::builder()
                .modal(true)
                .build()
                .choose_family_future(Some(&self.window()), Option::<&pango::FontFamily>::None)
                .await;

            if let Ok(font) = font {
                settings.set_font_family(&font.name());
            }
        }

        #[template_callback]
        async fn on_create_action_clicked(&self) {
            if let Err(err) = self.create_action().await {
                self.obj().add_toast(
                    adw::Toast::builder()
                        .priority(adw::ToastPriority::High)
                        .use_markup(false)
                        .title(
                            pgettext("preferences", "Failed to create an action: ")
                                + &err.to_string(),
                        )
                        .build(),
                );
            }
        }

        fn on_action_model_changed(&self, model: &gio::ListModel) {
            self.custom_actions_placeholder
                .set_visible(model.n_items() == 0);
        }
    }
}

glib::wrapper! {
    /// Text Pieces preferences window.
    pub struct Preferences(ObjectSubclass<imp::Preferences>)
        @extends gtk::Widget, adw::Dialog, adw::PreferencesDialog,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl Preferences {
    pub fn new() -> Self {
        glib::Object::new()
    }
}

impl Default for Preferences {
    fn default() -> Self {
        Self::new()
    }
}
