/* tree_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::collections::HashMap;
use std::rc::Rc;

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

use crate::menus::{local_branch_context_menu, remote_branch_context_menu};
use crate::preferences;
use crate::tree_node::{TreeNode, create_children_model};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TreeListViewKind {
    LocalBranches,
    RemoteBranches,
}

pub struct TreeListViewInit {
    pub repo_id: String,
    pub settings_key: &'static str,
    pub empty_label: String,
    pub top_level_collapsed: bool,
    pub kind: TreeListViewKind,
}

const ACTION_SWITCH: &str = "switch";
const ACTION_DELETE: &str = "delete";
const ACTION_CREATE_FROM: &str = "create-from";
const ACTION_COPY_NAME: &str = "copy-name";
const ACTION_RENAME: &str = "rename";
const ACTION_PUSH: &str = "push";
const ACTION_FETCH: &str = "fetch";
const ACTION_ARCHIVE: &str = "archive";
const ACTION_MERGE: &str = "merge";

type ContextMenuTarget = Rc<Cell<Option<String>>>;

struct BranchActions {
    kind: TreeListViewKind,
    switch: gio::SimpleAction,
    delete: gio::SimpleAction,
    create_from: gio::SimpleAction,
    copy_name: gio::SimpleAction,
    rename: Option<gio::SimpleAction>,
    push: Option<gio::SimpleAction>,
    fetch: Option<gio::SimpleAction>,
    archive: gio::SimpleAction,
    merge: gio::SimpleAction,
}

impl BranchActions {
    fn iter(&self) -> impl Iterator<Item = &gio::SimpleAction> {
        [&self.switch, &self.delete, &self.create_from, &self.copy_name]
            .into_iter()
            .chain(self.rename.as_ref())
            .chain(self.push.as_ref())
            .chain(self.fetch.as_ref())
            .chain(Some(&self.archive))
            .chain(Some(&self.merge))
    }
}

pub struct TreeListView {
    repo_id: String,
    settings_key: &'static str,
    empty_label: String,
    top_level_collapsed: bool,
    kind: TreeListViewKind,
    empty: bool,
    selection: gtk::SingleSelection,
    handler_map: Rc<RefCell<HashMap<gtk::TreeListRow, glib::SignalHandlerId>>>,
    popover: Rc<RefCell<Option<gtk::PopoverMenu>>>,
    actions: Rc<BranchActions>,
}

#[derive(Debug)]
pub enum TreeListViewMsg {
    SetStore(gio::ListStore),
    Deselect,
    GroupToggled(String, bool),
}

#[derive(Debug)]
pub enum TreeListViewOutput {
    ItemSelected(String),
    SwitchRequested(String),
    DeleteRequested(String),
    RenameRequested(String),
    CreateBranchFromRequested(String),
    CopyNameRequested(String),
    PushRequested(String),
    FetchRequested(String),
    ArchiveRequested(String),
    MergeRequested(String),
}

#[relm4::component(pub)]
impl Component for TreeListView {
    type Init = TreeListViewInit;
    type Input = TreeListViewMsg;
    type Output = TreeListViewOutput;
    type CommandOutput = ();

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

            gtk::Label {
                #[watch]
                set_visible: model.empty,
                set_label: &model.empty_label,
                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 handler_map: Rc<RefCell<HashMap<gtk::TreeListRow, glib::SignalHandlerId>>> =
            Rc::new(RefCell::new(HashMap::new()));

        let menu_target: ContextMenuTarget = Rc::new(Cell::new(None));
        let actions = Rc::new(build_branch_actions(init.kind, sender.output_sender().clone(), menu_target.clone()));
        let popover: Rc<RefCell<Option<gtk::PopoverMenu>>> = Rc::new(RefCell::new(None));

        let selection = gtk::SingleSelection::new(None::<gtk::TreeListModel>);
        selection.set_autoselect(false);
        selection.set_can_unselect(true);

        let factory = setup_tree_list_factory(
            sender.input_sender().clone(),
            sender.output_sender().clone(),
            &handler_map,
            menu_target.clone(),
            actions.clone(),
            popover.clone(),
            selection.clone(),
        );

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

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

                if let Some(tree_row) = selection.selected_item().and_downcast::<gtk::TreeListRow>()
                    && let Some(node) = tree_row.item().and_downcast::<TreeNode>()
                {
                    if node.is_group() {
                        tree_row.set_expanded(!tree_row.is_expanded());
                        selection.set_selected(gtk::INVALID_LIST_POSITION);
                        return;
                    }

                    let _ = s.send(TreeListViewOutput::ItemSelected(node.full_ref()));
                }
            });
        }

        let model = TreeListView {
            repo_id: init.repo_id,
            settings_key: init.settings_key,
            empty_label: init.empty_label,
            top_level_collapsed: init.top_level_collapsed,
            kind: init.kind,
            empty: true,
            selection,
            handler_map,
            popover: popover.clone(),
            actions,
        };

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

        let menu_model = match model.kind {
            TreeListViewKind::LocalBranches => local_branch_context_menu(),
            TreeListViewKind::RemoteBranches => remote_branch_context_menu(),
        };

        let popover_menu = gtk::PopoverMenu::from_model(Some(&menu_model));
        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();

        for action in model.actions.iter() {
            action_group.add_action(action);
        }

        widgets.list_view.insert_action_group("tree-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 {
            TreeListViewMsg::SetStore(store) => {
                self.empty = store.n_items() == 0;

                // autoexpand=true so sub-groups always default to expanded; we explicitly
                // override top-level rows below according to the persisted list
                let tree_model = gtk::TreeListModel::new(store, false, true, create_children_model);

                let settings = preferences::repo_settings(&self.repo_id);
                let stored: Vec<String> = settings.strv(self.settings_key).iter().map(|s| s.to_string()).collect();

                let n_root_items = tree_model.n_items();
                let mut to_apply: Vec<(gtk::TreeListRow, bool)> = Vec::new();

                for i in 0..n_root_items {
                    if let Some(row) = tree_model.row(i)
                        && let Some(node) = row.item().and_downcast::<TreeNode>()
                        && node.is_group()
                    {
                        let in_list = stored.contains(&node.name());
                        let should_expand = if self.top_level_collapsed { in_list } else { !in_list };

                        to_apply.push((row, should_expand));
                    }
                }

                // Disconnect all tracked handlers before replacing the model
                for (row, handler_id) in self.handler_map.borrow_mut().drain() {
                    row.disconnect(handler_id);
                }

                self.selection.set_model(Some(&tree_model));

                for (row, should_expand) in to_apply {
                    row.set_expanded(should_expand);
                }
            }

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

            TreeListViewMsg::GroupToggled(group_name, expanded) => {
                let settings = preferences::repo_settings(&self.repo_id);
                let mut stored: Vec<String> = settings.strv(self.settings_key).iter().map(|s| s.to_string()).collect();

                // top_level_collapsed=true -> list tracks explicitly-expanded groups
                // top_level_collapsed=false -> list tracks explicitly-collapsed groups
                // yes, confusing, but the UI is much better if we collapse the remotes by default
                let should_be_in_list = expanded == self.top_level_collapsed;

                if should_be_in_list {
                    if !stored.contains(&group_name) {
                        stored.push(group_name);
                    }
                } else {
                    stored.retain(|s| s != &group_name);
                }

                let strs: Vec<&str> = stored.iter().map(|s| s.as_str()).collect();
                let _ = settings.set_strv(self.settings_key, strs);
            }
        }
    }
}

fn setup_tree_list_factory(
    sender: relm4::Sender<TreeListViewMsg>,
    output: relm4::Sender<TreeListViewOutput>,
    handler_map: &Rc<RefCell<HashMap<gtk::TreeListRow, glib::SignalHandlerId>>>,
    menu_target: ContextMenuTarget,
    actions: Rc<BranchActions>,
    popover: Rc<RefCell<Option<gtk::PopoverMenu>>>,
    selection: gtk::SingleSelection,
) -> 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 expander = gtk::TreeExpander::new();
        expander.set_focusable(false);

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

        let icon = gtk::Image::new();
        icon.set_accessible_role(gtk::AccessibleRole::Presentation);
        let label = gtk::Label::new(None);
        label.set_halign(gtk::Align::Start);
        label.set_hexpand(true);
        label.set_ellipsize(gtk::pango::EllipsizeMode::End);

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

        if matches!(actions.kind, TreeListViewKind::LocalBranches) {
            let double_click = gtk::GestureClick::new();
            double_click.set_button(gdk::BUTTON_PRIMARY);

            let output_for_double_click = output.clone();

            double_click.connect_pressed(move |gesture, n_press, _, _| {
                if n_press != 2 {
                    return;
                }

                let Some(widget) = gesture.widget() else { return };

                let rslt = widget
                    .ancestor(gtk::TreeExpander::static_type())
                    .and_downcast::<gtk::TreeExpander>();

                let Some(expander) = rslt else { return };
                let Some(tree_row) = expander.list_row() else { return };
                let Some(node) = tree_row.item().and_downcast::<TreeNode>() else {
                    return;
                };

                if node.is_group() || node.is_head() {
                    return;
                }

                gesture.set_state(gtk::EventSequenceState::Claimed);
                let _ = output_for_double_click.send(TreeListViewOutput::SwitchRequested(node.full_ref()));
            });

            content.add_controller(double_click);
        }

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

        let target_cell = menu_target.clone();
        let actions_for_gesture = actions.clone();
        let popover_for_gesture = popover.clone();
        let selection_for_gesture = selection.clone();

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

            let rslt = widget
                .ancestor(gtk::TreeExpander::static_type())
                .and_downcast::<gtk::TreeExpander>();

            let Some(expander) = rslt else {
                return;
            };

            let Some(tree_row) = expander.list_row() else { return };
            let Some(node) = tree_row.item().and_downcast::<TreeNode>() else {
                return;
            };

            if node.is_group() {
                return;
            }

            gesture.set_state(gtk::EventSequenceState::Claimed);

            let is_head = node.is_head();
            target_cell.set(Some(node.full_ref()));

            actions_for_gesture.switch.set_enabled(!is_head);
            actions_for_gesture.delete.set_enabled(!is_head);

            let head_present = match actions_for_gesture.kind {
                TreeListViewKind::LocalBranches => tree_has_head_branch(&selection_for_gesture),
                TreeListViewKind::RemoteBranches => true,
            };
            actions_for_gesture.merge.set_enabled(head_present && !is_head);

            if let Some(fetch_action) = &actions_for_gesture.fetch {
                let fetch_enabled = match actions_for_gesture.kind {
                    TreeListViewKind::RemoteBranches => true,
                    TreeListViewKind::LocalBranches => node.has_upstream(),
                };

                fetch_action.set_enabled(fetch_enabled);
            }

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

    let bind_sender = sender;
    let bind_map = handler_map.clone();
    factory.connect_bind(move |_, list_item| {
        let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
            return;
        };
        let Some(tree_list_row) = list_item.item().and_downcast::<gtk::TreeListRow>() else {
            return;
        };
        let Some(node) = tree_list_row.item().and_downcast::<TreeNode>() else {
            return;
        };

        let Some(expander) = list_item.child().and_downcast::<gtk::TreeExpander>() else {
            return;
        };
        expander.set_list_row(Some(&tree_list_row));

        let Some(content) = expander.child().and_downcast::<gtk::Box>() else {
            return;
        };
        let Some(icon) = content.first_child().and_downcast::<gtk::Image>() else {
            return;
        };
        let Some(label) = icon.next_sibling().and_downcast::<gtk::Label>() else {
            return;
        };

        label.set_label(&node.name());
        icon.set_icon_name(Some(&node.icon()));

        if node.is_group() {
            label.add_css_class("heading");

            // Only persist toggle state for top-level groups; we don't support that for nested groups and it
            // would be more confusing than helpful to do so
            if tree_list_row.depth() == 0 {
                let s = bind_sender.clone();
                let group_name = node.name();
                let handler_id = tree_list_row.connect_expanded_notify(move |row| {
                    s.emit(TreeListViewMsg::GroupToggled(group_name.clone(), row.is_expanded()));
                });

                bind_map.borrow_mut().insert(tree_list_row.clone(), handler_id);
            }
        } else {
            label.remove_css_class("heading");
        }

        if node.is_head() {
            content.add_css_class("current-branch");
        } else {
            content.remove_css_class("current-branch");
        }
    });

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

        if let Some(tree_list_row) = list_item.item().and_downcast::<gtk::TreeListRow>()
            && let Some(handler_id) = unbind_map.borrow_mut().remove(&tree_list_row)
        {
            tree_list_row.disconnect(handler_id);
        }
    });

    factory
}

fn build_branch_actions(
    kind: TreeListViewKind,
    output: relm4::Sender<TreeListViewOutput>,
    target_cell: ContextMenuTarget,
) -> BranchActions {
    let make_action = |name: &'static str, build: fn(String) -> TreeListViewOutput| -> gio::SimpleAction {
        let action = gio::SimpleAction::new(name, None);
        let s = output.clone();
        let cell = target_cell.clone();

        action.connect_activate(move |_, _| {
            if let Some(target) = cell.take() {
                let _ = s.send(build(target));
            }
        });

        action
    };

    BranchActions {
        kind,
        switch: make_action(ACTION_SWITCH, TreeListViewOutput::SwitchRequested),
        delete: make_action(ACTION_DELETE, TreeListViewOutput::DeleteRequested),
        create_from: make_action(ACTION_CREATE_FROM, TreeListViewOutput::CreateBranchFromRequested),
        copy_name: make_action(ACTION_COPY_NAME, TreeListViewOutput::CopyNameRequested),
        rename: matches!(kind, TreeListViewKind::LocalBranches)
            .then(|| make_action(ACTION_RENAME, TreeListViewOutput::RenameRequested)),
        push: matches!(kind, TreeListViewKind::LocalBranches)
            .then(|| make_action(ACTION_PUSH, TreeListViewOutput::PushRequested)),
        fetch: Some(make_action(ACTION_FETCH, TreeListViewOutput::FetchRequested)),
        archive: make_action(ACTION_ARCHIVE, TreeListViewOutput::ArchiveRequested),
        merge: make_action(ACTION_MERGE, TreeListViewOutput::MergeRequested),
    }
}

fn tree_has_head_branch(selection: &gtk::SingleSelection) -> bool {
    let Some(tree_model) = selection.model().and_downcast::<gtk::TreeListModel>() else {
        return false;
    };

    let root = tree_model.model();
    root_has_head(&root)
}

fn root_has_head(model: &gio::ListModel) -> bool {
    for i in 0..model.n_items() {
        let Some(obj) = model.item(i) else { continue };

        let Some(node) = obj.downcast_ref::<TreeNode>() else {
            continue;
        };

        if node.is_head() {
            return true;
        }

        if node.is_group()
            && let Some(child_model) = create_children_model(&obj)
            && root_has_head(&child_model)
        {
            return true;
        }
    }

    false
}
