/* working_copy_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::path::PathBuf;

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

use crate::changed_file_row::{
    ChangedFileRow, ChangedFileRowInit, ChangedFileRowInput, ChangedFileRowOutput, RowSection,
};
use crate::confirm_dialog::ConfirmDialog;
use crate::diff_view::{DiffView, DiffViewOutput};
use crate::text_prompt_dialog::TextPromptDialog;
use crate::{git, preferences, ui_helpers};

mod commit;
mod delete;
mod discard;
mod filter;
mod gitignore;
mod helpers;
mod rebase;
mod refresh;
mod selection;
mod staging;

pub use commit::CommitAction;
pub use delete::DeleteAction;
pub use discard::DiscardAction;
pub use filter::FilterAction;
pub use gitignore::GitignoreAction;
pub use rebase::RebaseAction;
pub use refresh::RefreshAction;
pub use selection::SelectionAction;
pub use staging::StagingAction;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewLayout {
    Unified,
    Split,
}

impl ViewLayout {
    fn from_setting(value: &str) -> Self {
        match value {
            "split" => ViewLayout::Split,
            _ => ViewLayout::Unified,
        }
    }

    fn stack_name(self) -> &'static str {
        match self {
            ViewLayout::Unified => "unified",
            ViewLayout::Split => "split",
        }
    }
}

use helpers::{
    attach_list_keys, attach_row_selection, forward_row_output, setup_commit_area_controllers, state_banner_title,
};

pub struct WorkingCopyView {
    path: PathBuf,
    files_factory: FactoryVecDeque<ChangedFileRow>,
    staged_factory: FactoryVecDeque<ChangedFileRow>,
    unstaged_factory: FactoryVecDeque<ChangedFileRow>,
    last_files: Vec<git::ChangedFile>,
    diff_view: Controller<DiffView>,
    all_staged: bool,
    has_staged_files: bool,
    commit_subject_empty: bool,
    commit_body_visible: bool,
    amend: bool,
    repo_state: git::RepoState,
    rebase_has_conflicts: bool,
    confirm_dialog: Option<Controller<ConfirmDialog>>,
    text_prompt_dialog: Option<Controller<TextPromptDialog>>,
    max_line_length: u32,
    wrapping_body: bool,
    filter_query: String,
    view_layout: ViewLayout,
    repo_settings: gio::Settings,
    app_settings: gio::Settings,
}

#[derive(Debug)]
pub enum WorkingCopyViewOutput {
    ShowToast(String),
    DataChanged,
    FileCountChanged(usize),
    RebaseContinue,
    RebaseAbort,
}

#[derive(Debug)]
pub enum WorkingCopyViewMsg {
    Staging(StagingAction),
    Discard(DiscardAction),
    Delete(DeleteAction),
    Commit(CommitAction),
    Filter(FilterAction),
    LayoutChanged(ViewLayout),
    Selection(SelectionAction),
    Refresh(RefreshAction),
    Gitignore(GitignoreAction),
    Rebase(RebaseAction),
    RepoStateChanged(git::RepoState, bool),
    ShowToast(String),
    ConfirmDialogDismissed,
}

#[relm4::component(pub)]
impl Component for WorkingCopyView {
    type Init = (PathBuf, String);
    type Input = WorkingCopyViewMsg;
    type Output = WorkingCopyViewOutput;
    type CommandOutput = WorkingCopyViewMsg;

    view! {
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,

            #[name = "state_banner"]
            gtk::Revealer {
                set_transition_type: gtk::RevealerTransitionType::SlideDown,
                #[watch]
                set_reveal_child: model.repo_state != git::RepoState::Clean,

                gtk::Box {
                    set_orientation: gtk::Orientation::Horizontal,
                    add_css_class: "gap-l",
                    add_css_class: "toolbar",
                    add_css_class: "rebase-banner",

                    gtk::Label {
                        #[watch]
                        set_label: &state_banner_title(model.repo_state, model.rebase_has_conflicts),
                        set_hexpand: true,
                        set_halign: gtk::Align::Start,
                        set_wrap: true,
                    },

                    gtk::Button {
                        #[watch]
                        set_visible: model.repo_state == git::RepoState::Rebase,
                        set_label: &gettext("Continue Rebase"),
                        add_css_class: "suggested-action",
                        connect_clicked => WorkingCopyViewMsg::Rebase(RebaseAction::ContinueClicked),
                    },

                    gtk::Button {
                        #[watch]
                        set_visible: model.repo_state == git::RepoState::Rebase,
                        set_label: &gettext("Abort Rebase"),
                        add_css_class: "destructive-action",
                        connect_clicked => WorkingCopyViewMsg::Rebase(RebaseAction::AbortClicked),
                    },
                },
            },

            #[name = "main_paned"]
            gtk::Paned {
                set_orientation: gtk::Orientation::Horizontal,
                set_shrink_start_child: false,
                set_shrink_end_child: false,
                set_vexpand: true,

                #[wrap(Some)]
                set_start_child = &gtk::ScrolledWindow {
                    set_hscrollbar_policy: gtk::PolicyType::Never,
                    set_width_request: 180,

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

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

                                #[name = "commit_subject"]
                                gtk::Entry {
                                    set_placeholder_text: Some(&gettext("Commit Subject")),
                                    update_property: &[gtk::accessible::Property::Label(&gettext("Commit Subject"))],
                                    #[watch]
                                    set_max_length: model.max_line_length as i32,
                                    connect_changed[sender] => move |entry| {
                                        sender.input(WorkingCopyViewMsg::Commit(CommitAction::SubjectChanged(
                                            entry.text().to_string(),
                                        )));
                                    },
                                },

                                #[name = "commit_body"]
                                gtk::TextView {
                                    #[watch]
                                    set_visible: model.commit_body_visible,
                                    set_wrap_mode: gtk::WrapMode::Word,
                                    set_top_margin: 6,
                                    set_bottom_margin: 6,
                                    set_left_margin: 6,
                                    set_right_margin: 6,
                                    add_css_class: "card",
                                    set_height_request: 80,
                                    update_property: &[gtk::accessible::Property::Label(&gettext("Commit Details"))],
                                },

                                #[name = "amend_check"]
                                gtk::CheckButton {
                                    set_label: Some(&gettext("Amend commit")),
                                    #[watch]
                                    set_visible: model.commit_body_visible,
                                    #[watch]
                                    set_active: model.amend,
                                    connect_toggled[sender] => move |btn| {
                                        sender.input(WorkingCopyViewMsg::Commit(CommitAction::ToggleAmend(btn.is_active())));
                                    },
                                },

                                gtk::Box {
                                    set_orientation: gtk::Orientation::Horizontal,
                                    add_css_class: "gap-s",
                                    set_homogeneous: true,

                                    #[name = "stage_all_button"]
                                    gtk::Button {
                                        #[watch]
                                        set_label: &if model.all_staged {
                                            gettext("Unstage All")
                                        } else {
                                            gettext("Stage All")
                                        },
                                        connect_clicked => WorkingCopyViewMsg::Staging(StagingAction::ToggleStageAll),
                                    },

                                    #[name = "commit_button"]
                                    gtk::Button {
                                        #[watch]
                                        set_label: &if model.amend {
                                            pgettext("button", "Amend")
                                        } else {
                                            pgettext("button", "Commit")
                                        },
                                        #[watch]
                                        set_css_classes: if model.commit_button_active() {
                                            &["suggested-action"]
                                        } else {
                                            &["suggested-action", "dim-label"]
                                        },
                                        connect_clicked => WorkingCopyViewMsg::Commit(CommitAction::Commit),
                                    },
                                },
                            },

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

                            #[name = "search_bar"]
                            gtk::SearchBar {
                                set_search_mode: false,
                                set_show_close_button: false,

                                #[wrap(Some)]
                                #[name = "filter_entry"]
                                set_child = &gtk::SearchEntry {
                                    set_placeholder_text: Some(&gettext("Filter changed files")),
                                    update_property: &[gtk::accessible::Property::Label(&gettext("Filter changed files"))],
                                    set_hexpand: true,
                                    connect_search_changed[sender] => move |entry| {
                                        sender.input(WorkingCopyViewMsg::Filter(FilterAction::Changed(entry.text().to_string())));
                                    },
                                    connect_stop_search[sender] => move |_| {
                                        sender.input(WorkingCopyViewMsg::Filter(FilterAction::Hide));
                                    },
                                },
                            },

                            gtk::Stack {
                                set_vexpand: true,
                                set_hhomogeneous: true,
                                set_vhomogeneous: false,

                                add_named[Some("unified")] = &gtk::Box {
                                    set_orientation: gtk::Orientation::Vertical,

                                    #[local_ref]
                                    files_list -> gtk::ListBox {
                                        set_selection_mode: gtk::SelectionMode::Multiple,
                                        set_activate_on_single_click: false,
                                        add_css_class: "boxed-list",
                                        set_vexpand: true,
                                        set_placeholder: Some(&ui_helpers::empty_placeholder(&gettext("No changed files"))),
                                    },
                                },

                                add_named[Some("split")] = &gtk::Box {
                                    set_orientation: gtk::Orientation::Vertical,
                                    add_css_class: "gap-l",

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

                                    #[local_ref]
                                    unstaged_list -> gtk::ListBox {
                                        set_selection_mode: gtk::SelectionMode::Multiple,
                                        set_activate_on_single_click: false,
                                        add_css_class: "boxed-list",
                                        set_placeholder: Some(&ui_helpers::empty_placeholder(&gettext("No unstaged changes"))),
                                    },

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

                                    #[local_ref]
                                    staged_list -> gtk::ListBox {
                                        set_selection_mode: gtk::SelectionMode::Multiple,
                                        set_activate_on_single_click: false,
                                        add_css_class: "boxed-list",
                                        set_placeholder: Some(&ui_helpers::empty_placeholder(&gettext("No staged changes"))),
                                    },
                                },

                                #[watch]
                                set_visible_child_name: model.view_layout.stack_name(),
                            },
                    },
                },

                #[wrap(Some)]
                set_end_child = model.diff_view.widget(),
            },
        }
    }

    fn init(init: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let (path, repo_id) = init;
        let repo_settings = preferences::repo_settings(&repo_id);
        let app_settings = preferences::app_settings();

        let files_factory = FactoryVecDeque::builder()
            .launch(gtk::ListBox::default())
            .forward(sender.input_sender(), forward_row_output);

        let staged_factory = FactoryVecDeque::builder()
            .launch(gtk::ListBox::default())
            .forward(sender.input_sender(), forward_row_output);

        let unstaged_factory = FactoryVecDeque::builder()
            .launch(gtk::ListBox::default())
            .forward(sender.input_sender(), forward_row_output);

        let diff_view =
            DiffView::builder()
                .launch(path.clone())
                .forward(sender.input_sender(), |output| match output {
                    DiffViewOutput::RefreshFiles => WorkingCopyViewMsg::Refresh(RefreshAction::Refresh),
                    DiffViewOutput::ShowError(msg) => WorkingCopyViewMsg::ShowToast(msg),
                });

        let view_layout = ViewLayout::from_setting(app_settings.string("working-copy-view").as_str());

        let model = WorkingCopyView {
            path,
            files_factory,
            staged_factory,
            unstaged_factory,
            last_files: Vec::new(),
            diff_view,
            all_staged: false,
            has_staged_files: false,
            commit_subject_empty: true,
            commit_body_visible: false,
            amend: false,
            repo_state: git::RepoState::Clean,
            rebase_has_conflicts: false,
            confirm_dialog: None,
            text_prompt_dialog: None,
            max_line_length: repo_settings.int("commit-line-length").max(0) as u32,
            wrapping_body: false,
            filter_query: String::new(),
            view_layout,
            repo_settings,
            app_settings,
        };

        let files_list = model.files_factory.widget();
        let staged_list = model.staged_factory.widget();
        let unstaged_list = model.unstaged_factory.widget();

        let widgets = view_output!();

        widgets.search_bar.connect_entry(&widgets.filter_entry);

        preferences::persist_paned_position(&widgets.main_paned, "paned-files-position");

        {
            let s = sender.input_sender().clone();
            model
                .repo_settings
                .connect_changed(Some("commit-line-length"), move |settings: &gio::Settings, _| {
                    let len = settings.int("commit-line-length").max(0) as u32;
                    s.emit(WorkingCopyViewMsg::Commit(CommitAction::MaxLineLengthChanged(len)));
                });
        }

        {
            let s = sender.input_sender().clone();
            model
                .app_settings
                .connect_changed(Some("working-copy-view"), move |settings: &gio::Settings, _| {
                    let layout = ViewLayout::from_setting(settings.string("working-copy-view").as_str());
                    s.emit(WorkingCopyViewMsg::LayoutChanged(layout));
                });
        }

        {
            let s = sender.input_sender().clone();
            widgets.commit_body.buffer().connect_changed(move |_| {
                s.emit(WorkingCopyViewMsg::Commit(CommitAction::WrapBody));
            });
        }

        setup_commit_area_controllers(&widgets, &sender);

        let files_list_ref = model.files_factory.widget();
        let staged_list_ref = model.staged_factory.widget();
        let unstaged_list_ref = model.unstaged_factory.widget();

        attach_row_selection(files_list_ref, RowSection::Unified, None, sender.input_sender());

        attach_row_selection(
            staged_list_ref,
            RowSection::Staged,
            Some(unstaged_list_ref.clone()),
            sender.input_sender(),
        );

        attach_row_selection(
            unstaged_list_ref,
            RowSection::Unstaged,
            Some(staged_list_ref.clone()),
            sender.input_sender(),
        );

        attach_list_keys(files_list_ref, RowSection::Unified, sender.input_sender());
        attach_list_keys(staged_list_ref, RowSection::Staged, sender.input_sender());
        attach_list_keys(unstaged_list_ref, RowSection::Unstaged, sender.input_sender());

        // Load initial status async
        let p = model.path.clone();
        sender.spawn_oneshot_command(move || {
            WorkingCopyViewMsg::Refresh(RefreshAction::StatusLoaded(git::load_status_from_path(&p), Vec::new()))
        });

        ComponentParts { model, widgets }
    }

    fn update_with_view(
        &mut self,
        widgets: &mut Self::Widgets,
        msg: Self::Input,
        sender: ComponentSender<Self>,
        root: &Self::Root,
    ) {
        match msg {
            WorkingCopyViewMsg::Staging(action) => self.handle_staging_action(action, &sender),
            WorkingCopyViewMsg::Discard(action) => self.handle_discard_action(action, &sender, root),
            WorkingCopyViewMsg::Delete(action) => self.handle_delete_action(action, &sender, root),
            WorkingCopyViewMsg::Commit(action) => self.handle_commit_action(action, widgets, &sender),
            WorkingCopyViewMsg::Filter(action) => self.handle_filter_action(action, widgets, &sender),
            WorkingCopyViewMsg::Selection(action) => self.handle_selection_action(action, &sender),
            WorkingCopyViewMsg::Refresh(action) => self.handle_refresh_action(action, &sender),
            WorkingCopyViewMsg::Gitignore(action) => self.handle_gitignore_action(action, &sender, root),
            WorkingCopyViewMsg::Rebase(action) => self.handle_rebase_action(action, &sender, root),

            WorkingCopyViewMsg::LayoutChanged(layout) => {
                if self.view_layout != layout {
                    let prior_selected = self.selected_paths_all();
                    self.view_layout = layout;
                    self.repopulate_files_factory();
                    self.reselect_files(&prior_selected);
                }
            }

            WorkingCopyViewMsg::RepoStateChanged(state, has_conflicts) => {
                self.repo_state = state;
                self.rebase_has_conflicts = has_conflicts;
            }

            WorkingCopyViewMsg::ShowToast(msg) => {
                sender.output(WorkingCopyViewOutput::ShowToast(msg)).ok();
            }

            WorkingCopyViewMsg::ConfirmDialogDismissed => {
                self.confirm_dialog = None;
                self.text_prompt_dialog = None;
            }
        }

        self.update_view(widgets, sender);
    }

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