// 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::glib;

mod imp {
    use super::*;

    use crate::{widgets::SearchBar, Application};

    /// Private part of [`Editor`].
    ///
    /// [`Editor`]: super::Editor
    #[derive(Debug, Default, gtk::CompositeTemplate)]
    #[template(resource = "/io/gitlab/liferooter/TextPieces/ui/editor.ui")]
    pub struct Editor {
        /// Source view widget.
        #[template_child]
        pub view: TemplateChild<sourceview::View>,

        /// Scrolled window.
        #[template_child]
        pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,

        /// Source view buffer.
        #[template_child]
        pub buffer: TemplateChild<sourceview::Buffer>,

        /// Search bar.
        #[template_child]
        pub search_bar: TemplateChild<SearchBar>,
    }

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

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

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

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

            // Bind editor style scheme to application's color scheme
            adw::StyleManager::default()
                .bind_property("dark", &self.buffer.get(), "style-scheme")
                .sync_create()
                .transform_to(|_, is_dark| {
                    let scheme_id = match is_dark {
                        false => "Adwaita",
                        true => "Adwaita-dark",
                    };
                    sourceview::StyleSchemeManager::default().scheme(scheme_id)
                })
                .build();

            // Bind settings to editor's properties
            let settings = Application::settings();
            let view = &self.view.get();

            settings
                .bind_wrap_lines(view, "wrap-mode")
                .get_only()
                .mapping(|v, _| {
                    if v.get().unwrap_or_default() {
                        Some(gtk::WrapMode::WordChar.into())
                    } else {
                        Some(gtk::WrapMode::None.into())
                    }
                })
                .build();
            settings
                .bind("tabs-to-spaces", view, "insert-spaces-instead-of-tabs")
                .get_only()
                .build();
            settings
                .bind("spaces-in-tab", view, "tab-width")
                .get_only()
                .build();
        }
    }

    impl WidgetImpl for Editor {
        fn grab_focus(&self) -> bool {
            // Forward focus to the view
            self.view.grab_focus()
        }
    }

    impl BinImpl for Editor {}

    #[gtk::template_callbacks]
    impl Editor {
        #[template_callback]
        fn update_top_margin(&self) {
            let editor = self.obj().to_owned();

            glib::idle_add_local_full(glib::Priority::LOW, move || {
                let imp = editor.imp();
                let search_bar = &imp.search_bar;

                let search_enabled = search_bar.search_active();

                let margin = 10
                    + if search_enabled {
                        search_bar.size(gtk::Orientation::Vertical)
                    } else {
                        0
                    };
                let margin_delta = margin - imp.view.top_margin();

                let adjustment = imp.scrolled_window.vadjustment();

                adjustment.set_value(adjustment.value() + f64::from(margin_delta));

                imp.view.set_top_margin(margin);

                glib::ControlFlow::Break
            });
        }
    }
}

glib::wrapper! {
    /// Text Pieces editor.
    pub struct Editor(ObjectSubclass<imp::Editor>)
        @extends gtk::Widget, adw::Bin,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl Editor {
    /// Returns selection bounds, or whole text bounds if there's no selection.
    pub fn selection_or_whole(&self) -> (gtk::TextIter, gtk::TextIter) {
        let buffer = &self.imp().buffer;
        buffer.selection_bounds().unwrap_or_else(|| buffer.bounds())
    }

    /// Returns given range of the text of the editor.
    ///
    /// If no range is given, returns whole text.
    pub fn text(&self, range: Option<(gtk::TextIter, gtk::TextIter)>) -> glib::GString {
        let buffer = &self.imp().buffer;
        let (start, end) = range.unwrap_or_else(|| buffer.bounds());
        buffer.text(&start, &end, true)
    }

    /// Replace given range with given text.
    ///
    /// If no range is given, replaces whole text.
    pub fn set_text(
        &self,
        text: &str,
        range: Option<(gtk::TextIter, gtk::TextIter)>,
        select: bool,
    ) {
        let buffer = &self.imp().buffer;
        if let Some((mut start, mut end)) = range {
            let start_offset = start.offset();

            buffer.begin_user_action();

            buffer.delete(&mut start, &mut end);
            buffer.insert(&mut start, text);

            buffer.end_user_action();

            let end_offset = start_offset
                + i32::try_from(text.len())
                    .expect("Text is long enough not to fit into 32-bit sized integer type, wow");

            if select {
                let start = buffer.iter_at_offset(start_offset);
                let end = buffer.iter_at_offset(end_offset);

                buffer.select_range(&start, &end);
            }
        } else {
            buffer.set_text(text);
        }
    }

    /// Returns editor's buffer.
    pub fn buffer(&self) -> sourceview::Buffer {
        self.imp().buffer.to_owned()
    }

    /// Retuns editor's text view.
    pub fn view(&self) -> sourceview::View {
        self.imp().view.to_owned()
    }
}
