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

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

use futures::channel::oneshot;
use gettextrs::pgettext;
use gtk::glib;
use textpieces_core::Action;

use crate::application::Application;
use crate::utils::ActionModel;

mod imp {
    use super::*;

    use std::{
        cell::{OnceCell, RefCell},
        collections::VecDeque,
    };

    use textpieces_core::Parameter;
    use tracing::debug;

    use crate::{utils::TemplateFunctions, widgets::ParameterRow};

    /// Private part of [`ActionPage`].
    ///
    /// [`ActionPage`]: super::ActionPage
    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
    #[template(resource = "/io/gitlab/liferooter/TextPieces/ui/action-page.ui")]
    #[properties(wrapper_type = super::ActionPage)]
    pub struct ActionPage {
        // One side of oneshot channel used for waiting for the editing to finish.
        pub result_sender: RefCell<Option<oneshot::Sender<EditOperation>>>,

        /// ID of edited action.
        #[property(name = "is-new", type = bool, get = Self::is_new)]
        pub edited_action: OnceCell<Option<String>>,

        /// Action parameters.
        pub param_rows: RefCell<Vec<ParameterRow>>,

        /// Status page that says the action has no parameters.
        #[template_child]
        no_params: TemplateChild<adw::StatusPage>,

        /// Parameters list widget.
        #[template_child]
        pub param_listbox: TemplateChild<adw::PreferencesGroup>,

        /// Entry row that contains action name.
        #[template_child]
        pub name_entry: TemplateChild<adw::EntryRow>,

        /// Entry row that contains action description.
        #[template_child]
        pub description_entry: TemplateChild<adw::EntryRow>,
    }

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

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

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

    #[glib::derived_properties]
    impl ObjectImpl for ActionPage {
        fn constructed(&self) {
            self.parent_constructed();

            // It's a dirty hack. I know.
            // But I just don't want to reimplement AdwStatusPage
            // just because it has no good way to disable scrolling.
            let mut children = VecDeque::<gtk::Widget>::new();
            children.push_back(self.no_params.get().upcast());

            while let Some(widget) = children.pop_front() {
                if let Some(scrolled_window) = widget.downcast_ref::<gtk::ScrolledWindow>() {
                    scrolled_window.set_vscrollbar_policy(gtk::PolicyType::Never);
                }

                widget
                    .observe_children()
                    .iter::<glib::Object>()
                    .map(Result::unwrap)
                    .map(glib::Object::downcast)
                    .map(Result::unwrap)
                    .for_each(|w| children.push_back(w));
            }

            // Set correct initial variant of parameters widget
            self.update_parameters_widget();

            // Save on close when needed
            self.obj().connect_hidden(|obj| {
                let page = obj.imp();

                // The action is being edited, not created,
                // so closing the page is just a way to save changes
                if page.edited_action.get().unwrap().is_some() {
                    page.send_action();
                } else {
                    // Cancel result sender
                    page.result_sender.take();
                }
            });
        }
    }

    impl WidgetImpl for ActionPage {}
    impl NavigationPageImpl for ActionPage {}

    impl ActionPage {
        /// Shows corrent variant of parameters widget.
        ///
        /// If there are any parameters, shows parameters list.
        /// Otherwise shows a placeholder.
        pub fn update_parameters_widget(&self) {
            self.param_listbox
                .set_visible(!self.param_rows.borrow().is_empty());
        }

        /// Returns whether the page is used to create new action.
        pub fn is_new(&self) -> bool {
            self.edited_action
                .get()
                .map(Option::as_ref)
                .unwrap_or_default()
                .is_none()
        }

        /// Adds new parameter to the action and return its row.
        pub fn new_parameter(&self, parameter: Option<&Parameter>) -> ParameterRow {
            let new_row = parameter
                .map(ParameterRow::from_parameter)
                .unwrap_or_default();

            let page = self.obj().to_owned();
            new_row.connect_local(
                "delete",
                true,
                glib::clone!(
                    #[weak(rename_to = row)]
                    new_row,
                    #[upgrade_or]
                    None,
                    move |_| {
                        let page = page.imp();

                        page.param_rows.borrow_mut().remove(row.index() as usize);
                        page.param_listbox.remove(&row);
                        page.update_parameters_widget();

                        None
                    }
                ),
            );

            self.param_rows.borrow_mut().push(new_row.clone());
            self.param_listbox.add(&new_row);

            self.update_parameters_widget();

            new_row
        }

        /// Sends [`EditOperation::Change`] value as the result of editing.
        fn send_action(&self) {
            if let Some(sender) = self.result_sender.take() {
                let _ = sender.send(EditOperation::Change(Action {
                    name: self.name_entry.text().into(),
                    description: self.description_entry.text().into(),
                    parameters: self
                        .param_rows
                        .borrow()
                        .iter()
                        .map(ParameterRow::parameter)
                        .collect(),
                }));
            }
        }

        /// Creates a oneshot channel to send the result of editing.
        ///
        /// Returns receiver part.
        pub fn create_channel(&self) -> oneshot::Receiver<EditOperation> {
            let (sender, receiver) = oneshot::channel();

            self.result_sender.replace(Some(sender));

            receiver
        }
    }

    #[gtk::template_callbacks]
    impl ActionPage {
        /// Called when user creates a parameter by button.
        #[template_callback]
        fn add_parameter_clicked(&self) {
            debug!("Parameter is added");

            self.new_parameter(None).grab_focus();
        }

        /// Called when user clicks "Create" button.
        ///
        /// Only possible while editing new action.
        #[template_callback]
        fn on_create_clicked(&self) {
            self.send_action();
        }

        /// Called when user clicks "Delete" button.
        ///
        /// Only possible while editing existing action.
        #[template_callback]
        fn on_delete_clicked(&self) {
            if let Some(sender) = self.result_sender.take() {
                let _ = sender.send(EditOperation::Delete);
            }
        }
    }
}

glib::wrapper! {
    pub struct ActionPage(ObjectSubclass<imp::ActionPage>)
        @extends gtk::Widget, adw::NavigationPage,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

/// Edit operation done by action page.
#[derive(Debug, Clone)]
pub enum EditOperation {
    /// Action was modified.
    Change(Action),
    /// Action was deleted.
    Delete,
}

impl ActionPage {
    /// Edits given action using action page.
    ///
    /// Takes callback to call with the page as an argument after page
    /// is created.
    pub async fn edit(
        action: &ActionModel,
        page_callback: impl FnOnce(&Self),
    ) -> Option<EditOperation> {
        let obj: Self = glib::Object::new();

        let imp = obj.imp();

        // Set name and description
        imp.name_entry.set_text(&action.name());
        imp.description_entry.set_text(&action.description());

        // Get action parameters
        let parameters = if action.is_builtin() {
            Application::get().builtin_actions().await.actions()[&action.id()]
                .parameters
                .clone()
        } else {
            Application::get().custom_actions().await.actions()[&action.id()]
                .parameters
                .clone()
        };

        // Create parameter rows for all parameters
        let parameter_rows = parameters
            .iter()
            .map(|row| imp.new_parameter(Some(row)))
            .collect();

        // Store parameter rows
        imp.param_rows.replace(parameter_rows);

        // Set correct page title
        obj.set_title(&pgettext("action settings", "Edit Action"));

        // Set edited action model
        imp.edited_action.set(Some(action.id())).unwrap();
        obj.notify_is_new();

        page_callback(&obj);

        imp.create_channel().await.ok()
    }

    /// Edits new action using action page.
    ///
    /// Takes callback to call with the page as an argument after page
    /// is created.
    pub async fn create_action(page_callback: impl FnOnce(&Self)) -> Option<Action> {
        let obj: Self = glib::Object::new();

        let imp = obj.imp();

        // Set correct page title
        obj.set_title(&pgettext("action settings", "Edit Action"));

        // Set no edited action model
        imp.edited_action.set(None).unwrap();

        page_callback(&obj);

        match imp.create_channel().await {
            Ok(EditOperation::Change(action)) => Some(action),
            _ => None,
        }
    }
}
