// SPDX-License-Identifier: GPL-3.0-or-later

use regex::{Regex, RegexBuilder, Captures};

use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::{gio, glib};

use gettextrs::gettext;

use crate::i18n::ngettext_f;

use crate::application::Application;
use crate::config::{APP_ID, PROFILE, VERSION};
use crate::flags_dialog::FlagsDialog;
use crate::quick_reference_dialog::QuickReferenceDialog;
use crate::capture::{Capture, CaptureKind};

mod imp {
    use super::*;

    #[derive(Debug, gtk::CompositeTemplate)]
    #[template(resource = "/net/ffkkinos/Wildcard/ui/window.ui")]
    pub struct Window {
        pub settings: gio::Settings,

        #[template_child]
        pub split_view: TemplateChild<adw::OverlaySplitView>,
        #[template_child]
        pub bottom_sheet: TemplateChild<adw::BottomSheet>,

        #[template_child]
        pub quick_reference_button: TemplateChild<gtk::Button>,

        #[template_child]
        pub matches_information_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
        #[template_child]
        pub examples_list_view: TemplateChild<gtk::ListView>,

        #[template_child]
        pub matches_information_status_page: TemplateChild<adw::StatusPage>,
        #[template_child]
        pub test_view: TemplateChild<gtk::TextView>,
        #[template_child]
        pub test_buffer: TemplateChild<gtk::TextBuffer>,
        #[template_child]
        pub regex_row: TemplateChild<adw::EntryRow>,

        #[template_child]
        pub matches_label: TemplateChild<gtk::Label>,
    }

    impl Default for Window {
        fn default() -> Self {
            Self {
                settings: gio::Settings::new(APP_ID),

                split_view: TemplateChild::default(),
                bottom_sheet: TemplateChild::default(),

                quick_reference_button: TemplateChild::default(),

                matches_information_scrolled_window: TemplateChild::default(),
                examples_list_view: TemplateChild::default(),

                matches_information_status_page: TemplateChild::default(),
                test_view: TemplateChild::default(),
                test_buffer: TemplateChild::default(),
                regex_row: TemplateChild::default(),

                matches_label: TemplateChild::default(),
            }
        }
    }

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

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

    impl ObjectImpl for Window {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

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

            // obj.populate_examples_list();
            obj.setup_text_views();
            obj.load_regex_state();
            obj.load_window_size();
        }
    }

    #[gtk::template_callbacks]
    impl Window {
        fn update_matches_information_list(
            &self,
            captures_list: Vec<Capture>
        ) {
            self.matches_information_status_page.set_visible(false);
            self.matches_information_scrolled_window.set_visible(true);

            let model = gio::ListStore::new::<Capture>();
            model.extend_from_slice(&captures_list);

            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(move |_, list_item| {
                let row = gtk::ListBoxRow::new();

                list_item
                    .downcast_ref::<gtk::ListItem>()
                    .expect("")
                    .set_selectable(false);

                list_item
                    .downcast_ref::<gtk::ListItem>()
                    .expect("Needs to be ListItem")
                    .set_child(Some(&row));
            });

            factory.connect_bind(move |_, list_item| {
                let example = list_item
                    .downcast_ref::<gtk::ListItem>()
                    .expect("Needs to be ListItem")
                    .item()
                    .and_downcast::<Capture>()
                    .expect("The item has to be an `Capture`.");

                let row = list_item
                    .downcast_ref::<gtk::ListItem>()
                    .expect("Needs to be ListItem")
                    .child()
                    .and_downcast::<gtk::ListBoxRow>()
                    .expect("The child has to be a `GtkListBoxRow`.");

                row.set_activatable(false);

                let overall_string = &format!("Match {}", example.position());
                let group_string = match example.group_name() {
                    None => &format!("Group {}", example.group_index()),
                    Some(name) => &format!("Group {}", name),
                };

                let container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
                container.set_homogeneous(true);
                container.set_hexpand(true);

                //

                let left_container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
                left_container.set_hexpand(true);
                left_container.set_halign(gtk::Align::Fill);

                let range_container = gtk::Box::new(gtk::Orientation::Horizontal, 0);
                range_container.set_hexpand(true);
                range_container.set_halign(gtk::Align::End);

                //

                let right_container = gtk::Box::new(gtk::Orientation::Horizontal, 0);

                let id_string = match example.kind() {
                    CaptureKind::Overall => overall_string,
                    CaptureKind::Group => group_string,
                };

                let id_label = gtk::Label::builder()
                    .label(id_string)
                    .tooltip_text(id_string)
                    .ellipsize(gtk::pango::EllipsizeMode::End)
                    .margin_end(6)
                    .css_classes(["id-label"])
                    .build();

                let range_label = gtk::Label::builder()
                    .label(&format!("{}-{}", example.start(), example.end()))
                    .margin_end(6)
                    .css_classes(["dimmed", "numeric"])
                    .build();

                range_container.append(&range_label);

                left_container.append(&id_label);
                left_container.append(&range_container);

                let string_label = gtk::Label::builder()
                    .halign(gtk::Align::Start)
                    .valign(gtk::Align::Center)
                    .hexpand(true)
                    .label(if example.string().is_empty() { gettext("empty string") } else { example.string() } )
                    .wrap(true)
                    .wrap_mode(gtk::pango::WrapMode::Char)
                    .build();

                right_container.append(&string_label);

                container.append(&left_container);
                container.append(&right_container);

                row.set_child(Some(&container));
            });

            let selection_model = gtk::SingleSelection::builder()
                .autoselect(false)
                .selected(0)
                .model(&model)
                .build();

            self.examples_list_view.set_factory(Some(&factory));
            self.examples_list_view.set_model(Some(&selection_model));
        }

        #[template_callback]
        fn on_list_view_activated(&self, position: u32) {
            let obj = self.obj();
            let imp = obj.imp();

            let list_view = imp.examples_list_view.get();
            let model = list_view.model().expect("The model has to exist.");

            let capture = model
                .item(position)
                .and_downcast::<Capture>()
                .expect("The item has to be an `Capture`.");

            let start = capture.start();
            let end = capture.end();

            let test_buffer = imp.test_buffer.get();
            let mut start_iter = test_buffer.start_iter();
            start_iter.set_offset(start as i32);
            let mut end_iter = test_buffer.end_iter();
            end_iter.set_offset(end as i32);

            test_buffer.select_range(
                &start_iter,
                &end_iter,
            );
        }

        #[template_callback]
        fn on_buffer_changed(&self) {
            let regex_string = self.regex_row.text();
            let test_string = self.test_buffer.text(
                &self.test_buffer.start_iter(),
                &self.test_buffer.end_iter(),
                false,
            );

            let re: Result<Regex, regex::Error> = RegexBuilder::new(&regex_string)
                .multi_line(self.settings.boolean("multiline-flag"))
                .case_insensitive(self.settings.boolean("case-insensitive-flag"))
                .ignore_whitespace(self.settings.boolean("ignore-whitespace-flag"))
                .dot_matches_new_line(self.settings.boolean("dot-matches-newline-flag"))
                .unicode(self.settings.boolean("unicode-flag"))
                .swap_greed(self.settings.boolean("greed-flag"))
                .build();

            match re {
                Err(_err) => {
                    self.regex_row.add_css_class("warning");
                    self.matches_information_status_page.set_visible(true);
                    self.matches_information_scrolled_window.set_visible(false);
                }
                Ok(regex) => {
                    self.regex_row.remove_css_class("warning");

                    self.test_buffer
                        .remove_all_tags(&self.test_buffer.start_iter(), &self.test_buffer.end_iter());

                    let captures: Vec<(usize, Captures)> = regex.captures_iter(&test_string).enumerate().collect();

                    let group_names: Vec<_> = regex.capture_names().collect();

                    captures.iter().for_each(|(capture_index, caps)| {
                        caps.iter().enumerate().for_each(|(group_index, c)| {
                            match c {
                                None => {},
                                Some(m) => {
                                    let mut start_iter = self.test_buffer.start_iter();
                                    start_iter.set_offset(m.start() as i32);

                                    let mut end_iter = self.test_buffer.start_iter();
                                    end_iter.set_offset(m.end() as i32);

                                    if group_index == 0 {
                                        let marker = if capture_index % 2 == 0 {
                                            "marked_first"
                                        } else {
                                            "marked_second"
                                        };

                                        self.test_buffer
                                            .apply_tag_by_name(marker, &start_iter, &end_iter);
                                    } else {
                                        let group_marker = if group_index % 2 == 0 {
                                            "group_first"
                                        } else {
                                            "group_second"
                                        };

                                        self.test_buffer
                                            .apply_tag_by_name(group_marker, &start_iter, &end_iter);
                                    }
                                }
                            }
                        });
                    });

                    if regex_string.is_empty() || captures.len() == 0 {
                        self.matches_label.set_label(&gettext("no matches"));
                        return;
                    }

                    self.matches_label.set_label(&ngettext_f(
                        "{matches} match",
                        "{matches} matches",
                        captures.len() as u32,
                        &[("matches", &captures.len().to_string())],
                    ));

                    let captures_list = captures.iter().flat_map(|(i, c)| {
                        let mut result = vec![];

                        c.iter().enumerate().for_each(|(idx, g)| {
                            if let Some(group_match) = g {
                                result.push(
                                    Capture::new(
                                        if idx == 0 { CaptureKind::Overall } else { CaptureKind::Group },
                                        (*i as u32) + 1,
                                        idx as u32,
                                        *group_names.get(idx).unwrap(),
                                        group_match.into(),
                                        group_match.start() as u32,
                                        group_match.end() as u32,
                                    )
                                );
                            }
                        });

                        result
                    }).collect::<Vec<_>>();

                    self.update_matches_information_list(captures_list);
                }
            }
        }

        #[template_callback]
        fn on_flags_row_activated(&self) {
            self.obj().show_flags_dialog();
        }

        #[template_callback]
        fn on_info_button_clicked(&self) {
            self.obj().show_info_dialog();
        }

        #[template_callback]
        fn on_matches_row_clicked(&self) {
            self.bottom_sheet.set_open(true);
        }

        #[template_callback]
        fn on_quick_reference_button_clicked(&self) {
            self.obj().show_quick_reference_window();
        }
    }

    impl WidgetImpl for Window {}
    impl WindowImpl for Window {
        fn close_request(&self) -> glib::Propagation {
            let window = self.obj();

            if let Err(err) = window.save_regex_state() {
                log::error!("Failed to save regex state, {}", &err);
            }

            if let Err(err) = window.save_window_size() {
                log::error!("Failed to save window state, {}", &err);
            }

            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::ActionGroup, gio::ActionMap, gtk::Root;
}

impl Window {
    pub fn new(application: &Application) -> Self {
        glib::Object::builder()
            .property("application", application)
            .build()
    }

    fn setup_text_views(&self) {
        let imp = self.imp();

        imp.regex_row.grab_focus();

        imp.test_buffer.create_tag(
            Some("marked_first"),
            &[("background", &"#99c1f1"), ("foreground", &"#000000")],
        );
        imp.test_buffer.create_tag(
            Some("marked_second"),
            &[("background", &"#62a0ea"), ("foreground", &"#000000")],
        );
        imp.test_buffer.create_tag(
            Some("group_first"),
            &[("background", &"#d8f4cf"), ("foreground", &"#000000")],
        );
        imp.test_buffer.create_tag(
            Some("group_second"),
            &[("background", &"#dfc4a9"), ("foreground", &"#000000")],
        );
        imp.test_buffer
            .create_tag(Some("marked_highlight"), &[("background", &"#f9f06b")]);
    }

    fn save_regex_state(&self) -> Result<(), glib::BoolError> {
        let imp = self.imp();

        let regex_string = imp.regex_row.text();
        let test_string = imp.test_buffer.text(
            &imp.test_buffer.start_iter(),
            &imp.test_buffer.end_iter(),
            false,
        );

        imp.settings.set_string("last-regex", &regex_string)?;
        imp.settings.set_string("last-test", &test_string)?;

        Ok(())
    }

    fn load_regex_state(&self) {
        let imp = self.imp();

        let regex_string = imp.settings.string("last-regex");
        let test_string = imp.settings.string("last-test");

        imp.regex_row.set_text(&regex_string);
        imp.test_buffer.set_text(&test_string);
    }

    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)?;

        Ok(())
    }

    fn load_window_size(&self) {
        let imp = self.imp();

        let width = imp.settings.int("window-width");
        let height = imp.settings.int("window-height");

        self.set_default_size(width, height);
    }

    fn show_flags_dialog(&self) {
        let dialog = FlagsDialog::new();

        dialog.connect_local(
            "flags-changed",
            false,
            glib::clone!(
                #[strong(rename_to=this)]
                self,
                move |_| {
                    this.imp().test_buffer.emit_by_name::<()>("changed", &[]);

                    None
                }
            ),
        );

        dialog.present(Some(self));
    }

    fn show_info_dialog(&self) {
        let builder =
            gtk::Builder::from_resource("/net/ffkkinos/Wildcard/ui/info_dialog.ui");
        let dialog: adw::Dialog = builder.object("info_dialog").unwrap();

        dialog.present(Some(self));
    }

    fn show_quick_reference_window(&self) {
        let dialog = QuickReferenceDialog::new();
        dialog.present(Some(self));
    }

    fn show_about_dialog(&self) {
        let dialog = adw::AboutDialog::builder()
            .application_icon(APP_ID)
            .application_name(gettext("Wildcard"))
            .license_type(gtk::License::Gpl30)
            .comments(gettext("Test your regular expressions"))
            .website("https://gitlab.gnome.org/World/Wildcard")
            .issue_url("https://gitlab.gnome.org/World/Wildcard/-/issues/new")
            .version(VERSION)
            .translator_credits(gettext("translator-credits"))
            .developer_name("Felipe Kinoshita")
            .developers(vec!["Felipe Kinoshita <kinofhek@gmail.com>"])
            .artists(vec!["kramo https://kramo.hu"])
            .copyright("© 2026 Felipe Kinoshita.")
            .release_notes_version(VERSION)
            .release_notes("<p>Bigger release:</p><ul><li>Added highlighting of matched regex groups in text view.</li><li>Added match information (overall match and groups) to the sidebar</li><li>Quick reference panel now available as a dialog</li><li>Updated translations</li></ul><p>If you would like to come with suggestions, report bugs, translate the app, or contribute otherwise, feel free to reach out!</p>")
            .build();

        dialog.present(Some(self));
    }
}
