/* commit_search_bar.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use adw::prelude::*;
use gettextrs::gettext;
use gtk::glib;
use relm4::prelude::*;

use crate::git::{self, CommitInfo};
use crate::search_query::SearchQuery;

pub struct CommitSearchBar {
    path: PathBuf,
    current_ref: Option<String>,
    visible: bool,
    query: SearchQuery,
    cancel: Arc<AtomicBool>,
    searching: bool,
    result_count: usize,
    generation: u64,
    debounce_source: Option<glib::SourceId>,
}

#[derive(Debug)]
pub enum CommitSearchBarMsg {
    Toggle,
    Cancel,
    Hide,
    SetRef(Option<String>),
    SearchInputChanged,
    DebounceFired,
    SearchResultBatch { generation: u64, commits: Vec<CommitInfo> },
    SearchComplete { generation: u64 },
    SearchError { generation: u64, error: String },
}

#[derive(Debug)]
pub enum CommitSearchBarOutput {
    ResultBatch(Vec<CommitInfo>),
    SearchStarted,
    SearchComplete,
    Cleared,
    ShowToast(String),
}

#[relm4::component(pub)]
impl Component for CommitSearchBar {
    type Init = PathBuf;
    type Input = CommitSearchBarMsg;
    type Output = CommitSearchBarOutput;
    type CommandOutput = CommitSearchBarMsg;

    view! {
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,
            add_css_class: "gap-xs",

            gtk::Box {
                set_orientation: gtk::Orientation::Horizontal,
                add_css_class: "gap-xs",
                #[watch]
                set_visible: model.visible,

                #[name = "search_entry"]
                gtk::SearchEntry {
                    set_placeholder_text: Some("file:path author:name since:date text"),
                    update_property: &[gtk::accessible::Property::Label(&gettext("Search Commits"))],
                    set_hexpand: true,
                },

                gtk::MenuButton {
                    set_icon_name: "dialog-information-symbolic",
                    add_css_class: "flat",
                    add_css_class: "circular",
                    set_tooltip_text: Some(&gettext("Search syntax help")),
                    set_valign: gtk::Align::Center,

                    #[wrap(Some)]
                    set_popover = &gtk::Popover {
                        gtk::Box {
                            set_orientation: gtk::Orientation::Vertical,
                            add_css_class: "gap-l",
                            add_css_class: "m-l",
                            set_width_request: 320,

                            gtk::Label {
                                set_label: &gettext("Search Syntax"),
                                add_css_class: "title-4",
                                set_halign: gtk::Align::Start,
                            },

                            gtk::Box {
                                set_orientation: gtk::Orientation::Vertical,
                                add_css_class: "gap-m",

                                gtk::Label {
                                    set_markup: &gettext("<tt>file:src/main.rs</tt>: commits that changed this file"),
                                    set_halign: gtk::Align::Start,
                                    set_wrap: true,
                                },

                                gtk::Label {
                                    set_markup: &gettext("<tt>author:Name</tt>: commits by author (substring match)"),
                                    set_halign: gtk::Align::Start,
                                    set_wrap: true,
                                },

                                gtk::Label {
                                    set_markup: &gettext("<tt>since:2024-01-15</tt>: commits after this date"),
                                    set_halign: gtk::Align::Start,
                                    set_wrap: true,
                                },

                                gtk::Label {
                                    set_markup: &gettext("<tt>until:2024-06-01</tt>: commits before this date"),
                                    set_halign: gtk::Align::Start,
                                    set_wrap: true,
                                },

                                gtk::Label {
                                    set_markup: &gettext("<tt>since:2w</tt> / <tt>3months</tt> / <tt>1year</tt>: relative dates"),
                                    set_halign: gtk::Align::Start,
                                    set_wrap: true,
                                },

                                gtk::Separator {},

                                gtk::Label {
                                    set_markup: &gettext("Free text searches commit messages. Use <tt>author:\"First Last\"</tt> for names with spaces. Multiple <tt>file:</tt> or <tt>author:</tt> values are OR-combined; different keywords are AND-combined."),
                                    set_halign: gtk::Align::Start,
                                    set_wrap: true,
                                },
                            },
                        },
                    },
                },
            },

            gtk::Box {
                set_orientation: gtk::Orientation::Horizontal,
                add_css_class: "gap-m",
                #[watch]
                set_visible: model.searching || (model.visible && !model.query.is_empty()),

                gtk::Spinner {
                    #[watch]
                    set_spinning: model.searching,
                    #[watch]
                    set_visible: model.searching,
                },

                gtk::Label {
                    set_hexpand: true,
                    set_halign: gtk::Align::Start,
                    add_css_class: "dim-label",
                    add_css_class: "caption",
                    #[watch]
                    set_label: &if model.searching {
                        format!("{}… ({})", gettext("Searching"), model.result_count)
                    } else if !model.query.is_empty() {
                        format!("{}: {}", gettext("Results"), model.result_count)
                    } else {
                        String::new()
                    },
                },

                gtk::Button {
                    set_icon_name: "process-stop-symbolic",
                    add_css_class: "flat",
                    add_css_class: "circular",
                    set_tooltip_text: Some(&gettext("Stop search")),
                    #[watch]
                    set_visible: model.searching,
                    connect_clicked[sender] => move |_| {
                        sender.input(CommitSearchBarMsg::Cancel);
                    },
                },
            },
        }
    }

    fn init(path: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let model = CommitSearchBar {
            path,
            current_ref: None,
            visible: false,
            query: SearchQuery::default(),
            cancel: Arc::new(AtomicBool::new(false)),
            searching: false,
            result_count: 0,
            generation: 0,
            debounce_source: None,
        };

        let widgets = view_output!();

        {
            let s = sender.input_sender().clone();
            widgets.search_entry.connect_search_changed(move |_| {
                s.emit(CommitSearchBarMsg::SearchInputChanged);
            });

            let s = sender.input_sender().clone();
            widgets.search_entry.connect_stop_search(move |_| {
                s.emit(CommitSearchBarMsg::Toggle);
            });
        }

        ComponentParts { model, widgets }
    }

    fn update_with_view(
        &mut self,
        widgets: &mut Self::Widgets,
        msg: Self::Input,
        sender: ComponentSender<Self>,
        _root: &Self::Root,
    ) {
        match msg {
            CommitSearchBarMsg::Toggle => {
                self.visible = !self.visible;

                if self.visible {
                    widgets.search_entry.grab_focus();
                } else {
                    self.cancel_search();
                    widgets.search_entry.set_text("");
                    if !self.query.is_empty() {
                        self.query = SearchQuery::default();
                        self.result_count = 0;
                        sender.output(CommitSearchBarOutput::Cleared).ok();
                    }
                }
            }

            CommitSearchBarMsg::Cancel => {
                self.cancel_search();
            }

            CommitSearchBarMsg::Hide => {
                self.cancel_search();
                self.visible = false;
                self.query = SearchQuery::default();
                self.result_count = 0;
                widgets.search_entry.set_text("");
            }

            CommitSearchBarMsg::SetRef(ref_name) => {
                self.current_ref = ref_name;
            }

            CommitSearchBarMsg::SearchInputChanged => {
                if let Some(source_id) = self.debounce_source.take() {
                    source_id.remove();
                }

                let s = sender.input_sender().clone();
                let id = glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || {
                    s.emit(CommitSearchBarMsg::DebounceFired);
                });
                self.debounce_source = Some(id);
            }

            CommitSearchBarMsg::DebounceFired => {
                self.debounce_source = None;
                let text = widgets.search_entry.text();
                let query = SearchQuery::parse(&text);
                if query.is_empty() {
                    self.cancel_search();
                    self.query = SearchQuery::default();
                    self.result_count = 0;
                    sender.output(CommitSearchBarOutput::Cleared).ok();
                    return;
                }

                self.cancel_search();
                self.query = query.clone();
                self.result_count = 0;
                self.searching = true;

                sender.output(CommitSearchBarOutput::SearchStarted).ok();

                self.generation = self.generation.wrapping_add(1);
                let generation = self.generation;
                let cancel = Arc::new(AtomicBool::new(false));
                self.cancel = cancel.clone();

                let p = self.path.clone();
                let ref_name = self.current_ref.clone().unwrap_or_else(|| "HEAD".to_string());

                sender.spawn_command(move |cmd_sender| {
                    let result = git::search_commits(&p, &ref_name, &query, &cancel, |batch| {
                        cmd_sender.emit(CommitSearchBarMsg::SearchResultBatch {
                            generation,
                            commits: batch,
                        });
                    });

                    match result {
                        Ok(_) => {
                            cmd_sender.emit(CommitSearchBarMsg::SearchComplete { generation });
                        }
                        Err(error) => {
                            cmd_sender.emit(CommitSearchBarMsg::SearchError { generation, error });
                        }
                    }
                });
            }

            CommitSearchBarMsg::SearchResultBatch { generation, commits } => {
                if generation != self.generation {
                    return;
                }

                self.result_count += commits.len();
                sender.output(CommitSearchBarOutput::ResultBatch(commits)).ok();
            }

            CommitSearchBarMsg::SearchComplete { generation } => {
                if generation != self.generation {
                    return;
                }
                self.searching = false;
                sender.output(CommitSearchBarOutput::SearchComplete).ok();
            }

            CommitSearchBarMsg::SearchError { generation, error } => {
                if generation != self.generation {
                    return;
                }
                self.searching = false;
                sender.output(CommitSearchBarOutput::ShowToast(error)).ok();
            }
        }

        self.update_view(widgets, sender);
    }

    fn update_cmd(&mut self, msg: Self::CommandOutput, sender: ComponentSender<Self>, _root: &Self::Root) {
        sender.input(msg);
    }
}

impl CommitSearchBar {
    fn cancel_search(&mut self) {
        self.cancel.store(true, Ordering::Relaxed);
        self.generation = self.generation.wrapping_add(1);
        self.searching = false;
    }
}
