/* tag_list_view.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::cell::{Cell, RefCell};
use std::rc::Rc;

use gettextrs::gettext;
use gtk::prelude::*;
use gtk::{gdk, gio};
use relm4::prelude::*;

use crate::{menus, ui_helpers};

pub struct TagListView {
    string_list: gtk::StringList,
    selection: gtk::SingleSelection,
    empty: bool,
    last_tags: Vec<String>,
    popover: Rc<RefCell<Option<gtk::PopoverMenu>>>,
}

impl std::fmt::Debug for TagListView {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TagListView")
            .field("selection", &self.selection.selected())
            .field("empty", &self.empty)
            .field("n_tags", &self.last_tags.len())
            .finish()
    }
}

#[derive(Debug)]
pub enum TagListViewMsg {
    SetTags(Vec<String>),
    Deselect,
}

#[derive(Debug)]
pub enum TagListViewOutput {
    TagSelected(String),
    CheckoutRequested(String),
    DeleteRequested(String),
    PushRequested(String),
    ArchiveRequested(String),
    CreateBranchRequested(String),
}

#[relm4::component(pub)]
impl Component for TagListView {
    type Init = ();
    type Input = TagListViewMsg;
    type Output = TagListViewOutput;
    type CommandOutput = ();

    view! {
        #[root]
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,

            gtk::Label {
                #[watch]
                set_visible: model.empty,
                set_label: &gettext("No tags"),
                add_css_class: "dim-label",
                add_css_class: "mt-l",
                add_css_class: "mb-l",
            },

            gtk::ScrolledWindow {
                #[watch]
                set_visible: !model.empty,
                set_hscrollbar_policy: gtk::PolicyType::Never,
                set_propagate_natural_height: true,
                set_max_content_height: 300,

                #[name = "list_view"]
                gtk::ListView {
                    add_css_class: "navigation-sidebar",
                    set_model: Some(&model.selection),
                },
            },
        }
    }

    fn init(_init: Self::Init, _root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let menu_target: Rc<Cell<Option<String>>> = Rc::new(Cell::new(None));
        let popover: Rc<RefCell<Option<gtk::PopoverMenu>>> = Rc::new(RefCell::new(None));

        let string_list = gtk::StringList::new(&[]);
        let selection = gtk::SingleSelection::new(Some(string_list.clone()));
        selection.set_autoselect(false);
        selection.set_can_unselect(true);

        {
            let s = sender.output_sender().clone();
            let sl = string_list.clone();
            selection.connect_selection_changed(move |sel, _, _| {
                let pos = sel.selected();

                if pos == gtk::INVALID_LIST_POSITION {
                    return;
                }

                if let Some(tag_name) = sl.string(pos) {
                    let _ = s.send(TagListViewOutput::TagSelected(tag_name.to_string()));
                }
            });
        }

        let factory = setup_tag_factory(menu_target.clone(), popover.clone());

        let model = TagListView {
            string_list,
            selection,
            empty: true,
            last_tags: Vec::new(),
            popover: popover.clone(),
        };

        let widgets = view_output!();
        widgets.list_view.set_factory(Some(&factory));

        let popover_menu = gtk::PopoverMenu::from_model(Some(&menus::tag_context_menu()));
        popover_menu.set_has_arrow(false);
        popover_menu.set_halign(gtk::Align::Start);
        popover_menu.set_parent(&widgets.list_view);

        let action_group = gio::SimpleActionGroup::new();

        {
            let action = gio::SimpleAction::new("checkout", None);
            let s = sender.output_sender().clone();
            let cell = menu_target.clone();

            action.connect_activate(move |_, _| {
                if let Some(name) = cell.take() {
                    let _ = s.send(TagListViewOutput::CheckoutRequested(name));
                }
            });

            action_group.add_action(&action);
        }

        {
            let action = gio::SimpleAction::new("delete", None);
            let s = sender.output_sender().clone();
            let cell = menu_target.clone();

            action.connect_activate(move |_, _| {
                if let Some(name) = cell.take() {
                    let _ = s.send(TagListViewOutput::DeleteRequested(name));
                }
            });

            action_group.add_action(&action);
        }

        {
            let action = gio::SimpleAction::new("push", None);
            let s = sender.output_sender().clone();
            let cell = menu_target.clone();

            action.connect_activate(move |_, _| {
                if let Some(name) = cell.take() {
                    let _ = s.send(TagListViewOutput::PushRequested(name));
                }
            });

            action_group.add_action(&action);
        }

        {
            let action = gio::SimpleAction::new("archive", None);
            let s = sender.output_sender().clone();
            let cell = menu_target.clone();

            action.connect_activate(move |_, _| {
                if let Some(name) = cell.take() {
                    let _ = s.send(TagListViewOutput::ArchiveRequested(name));
                }
            });

            action_group.add_action(&action);
        }

        {
            let action = gio::SimpleAction::new("create-branch", None);
            let s = sender.output_sender().clone();
            let cell = menu_target.clone();

            action.connect_activate(move |_, _| {
                if let Some(name) = cell.take() {
                    let _ = s.send(TagListViewOutput::CreateBranchRequested(name));
                }
            });

            action_group.add_action(&action);
        }

        widgets.list_view.insert_action_group("tag-item", Some(&action_group));
        *popover.borrow_mut() = Some(popover_menu);

        ComponentParts { model, widgets }
    }

    fn shutdown(&mut self, _widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
        if let Some(popover) = self.popover.borrow_mut().take() {
            popover.unparent();
        }
    }

    fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>, _root: &Self::Root) {
        match msg {
            TagListViewMsg::SetTags(tags) => {
                self.empty = tags.is_empty();

                if tags == self.last_tags {
                    return;
                }

                self.last_tags = tags.clone();

                let str_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
                self.string_list.splice(0, self.string_list.n_items(), &str_refs);
            }

            TagListViewMsg::Deselect => {
                self.selection.set_selected(gtk::INVALID_LIST_POSITION);
            }
        }
    }
}

fn setup_tag_factory(
    menu_target: Rc<Cell<Option<String>>>,
    popover: Rc<RefCell<Option<gtk::PopoverMenu>>>,
) -> gtk::SignalListItemFactory {
    let factory = gtk::SignalListItemFactory::new();

    factory.connect_setup(move |_, list_item| {
        let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
            return;
        };

        let content = gtk::Box::new(gtk::Orientation::Horizontal, 8);
        content.add_css_class("mt-s");
        content.add_css_class("mb-s");
        content.add_css_class("ms-s");

        let icon = gtk::Image::new();
        icon.set_icon_name(Some("tag-symbolic"));
        let label = gtk::Label::new(None);
        label.set_widget_name("tag-label");
        label.set_halign(gtk::Align::Start);
        label.set_hexpand(true);
        label.set_use_markup(false);
        label.set_ellipsize(gtk::pango::EllipsizeMode::End);

        content.append(&icon);
        content.append(&label);
        list_item.set_child(Some(&content));

        let gesture = gtk::GestureClick::new();
        gesture.set_button(gdk::BUTTON_SECONDARY);

        let target_cell = menu_target.clone();
        let popover_for_gesture = popover.clone();
        let list_item_for_gesture = list_item.clone();

        gesture.connect_released(move |gesture, _, x, y| {
            let Some(widget) = gesture.widget() else { return };

            let Some(tag_name) = list_item_for_gesture
                .item()
                .and_downcast::<gtk::StringObject>()
                .map(|obj| obj.string().to_string())
            else {
                return;
            };

            gesture.set_state(gtk::EventSequenceState::Claimed);
            target_cell.set(Some(tag_name));

            let Some(popover_menu) = popover_for_gesture.borrow().clone() else {
                return;
            };

            if let Some(parent) = popover_menu.parent()
                && let Some(point) = widget.compute_point(&parent, &gtk::graphene::Point::new(x as f32, y as f32))
            {
                let rect = gdk::Rectangle::new(point.x() as i32, point.y() as i32, 1, 1);
                popover_menu.set_pointing_to(Some(&rect));
            }

            popover_menu.popup();
        });

        content.add_controller(gesture);
    });

    factory.connect_bind(|_, list_item| {
        let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
            return;
        };

        let Some(string_obj) = list_item.item().and_downcast::<gtk::StringObject>() else {
            return;
        };

        let tag_name = string_obj.string();

        let Some(content) = list_item.child().and_downcast::<gtk::Box>() else {
            return;
        };

        let Some(label) = ui_helpers::find_child_by_name(&content, "tag-label").and_downcast::<gtk::Label>() else {
            return;
        };

        label.set_label(&tag_name);
    });

    factory
}
