// SPDX-FileCopyrightText: 2024 Andrey Kutejko <andy128k@gmail.com>
// SPDX-FileCopyrightText: 2026 Wladimir Palant https://palant.info/
//
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::{
    advanced_rename::advanced_rename_dialog::advanced_rename_dialog_show,
    config::{PACKAGE_BUGREPORT, PACKAGE_NAME, PACKAGE_URL, PACKAGE_VERSION},
    connection::{
        ConnectionExt, ConnectionInterface,
        bookmark::BookmarkGoToVariant,
        home::ConnectionHome,
        list::ConnectionList,
        remote::{ConnectionRemote, ConnectionRemoteExt},
    },
    dialogs::{
        chmod_dialog::show_chmod_dialog,
        chown_dialog::show_chown_dialog,
        connect_dialog::ConnectDialog,
        create_symlink_dialog::show_create_symlink_dialog,
        file_properties_dialog::FilePropertiesDialog,
        make_copy_dialog::make_copy_dialog,
        manage_bookmarks_dialog::{BookmarksDialog, bookmark_directory},
        mkdir_dialog::show_mkdir_dialog,
        new_text_file::show_new_textfile_dialog,
        options::options_dialog::show_options_dialog,
        pattern_selection_dialog::select_by_pattern,
        prepare_copy_dialog::prepare_copy_dialog_show,
        prepare_move_dialog::prepare_move_dialog_show,
        remote_dialog::RemoteDialog,
        shortcuts_dialog::ShortcutsDialog,
    },
    dir::Directory,
    file::{File, FileOps},
    file_list::{
        list::{ColumnID, FileList},
        quick_search::QuickSearchMode,
    },
    file_selector::TabOptions,
    libgcmd::file_actions::{FileActions, FileActionsExt},
    main_win::{ExecutionTarget, MainWindow},
    options::{ConfirmOptions, GeneralOptions, NetworkOptions, ProgramsOptions, SearchConfig},
    plugin_manager::{PluginActionVariant, show_plugin_manager},
    search::search_dialog::SearchDialog,
    shortcuts::Area,
    spawn::{SpawnError, spawn_async, spawn_async_command},
    types::FileSelectorID,
    utils::{ErrorMessage, display_help, get_modifiers_state},
};
use gettextrs::{gettext, ngettext};
use gtk::{gdk, gio, glib, prelude::*};
use std::{
    borrow::Cow,
    cmp::Ordering,
    collections::{HashMap, HashSet},
    ffi::OsString,
    iter::Iterator,
    path::{Path, PathBuf},
};

async fn file_copy(main_win: MainWindow) {
    let src_fs = main_win.file_selector(FileSelectorID::Active);
    let dst_fs = main_win.file_selector(FileSelectorID::Inactive);
    let options = ConfirmOptions::new();
    prepare_copy_dialog_show(&main_win, &src_fs, &dst_fs, &options).await;
}

async fn file_copy_as(main_win: MainWindow) {
    let file_list = main_win.file_selector(FileSelectorID::Active).file_list();

    let Some(file) = file_list.selected_file() else {
        return;
    };
    let Some(dir) = file_list.directory() else {
        return;
    };

    make_copy_dialog(&file, &dir, &main_win).await;
}

async fn file_move(main_win: MainWindow) {
    let src_fs = main_win.file_selector(FileSelectorID::Active);
    let dst_fs = main_win.file_selector(FileSelectorID::Inactive);
    let options = ConfirmOptions::new();
    prepare_move_dialog_show(&main_win, &src_fs, &dst_fs, &options).await;
}

async fn file_delete(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .show_delete_dialog(false)
        .await;
}

async fn file_delete_permanently(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .show_delete_dialog(true)
        .await;
}

async fn file_view(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if let Err(error) = file_list.activate_action("fl.file-view", Some(&None::<bool>.to_variant()))
    {
        eprintln!("Cannot activate action `file-view`: {}", error);
    }
}

async fn file_internal_view(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if let Err(error) = file_list.activate_action("fl.file-view", Some(&true.to_variant())) {
        eprintln!("Cannot activate action `file-view`: {}", error);
    }
}

async fn file_external_view(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if let Err(error) = file_list.activate_action("fl.file-view", Some(&false.to_variant())) {
        eprintln!("Cannot activate action `file-view`: {}", error);
    }
}

async fn file_edit(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    let mask = get_modifiers_state(main_win.upcast_ref());

    if mask.is_some_and(|m| m.contains(gdk::ModifierType::SHIFT_MASK)) {
        show_new_textfile_dialog(main_win.upcast_ref(), &file_list).await;
    } else if let Err(error) = file_list.activate_action("fl.file-edit", None) {
        eprintln!("Cannot activate action `file-edit`: {}", error);
    }
}

async fn file_edit_new_doc(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    show_new_textfile_dialog(main_win.upcast_ref(), &file_list).await;
}

async fn file_search(main_win: MainWindow) {
    let options = ProgramsOptions::new();

    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if options.use_internal_search.get() {
        let start_dir = file_list.directory();
        let dlg = main_win.get_or_create_dialog("search", || {
            let search_config = SearchConfig::get();

            SearchDialog::new(search_config, &main_win.file_metadata_service(), &main_win)
        });
        let options = GeneralOptions::new();
        dlg.show_and_set_focus(
            start_dir.as_ref(),
            options
                .search_window_is_transient
                .get()
                .then_some(&main_win),
        );
    } else {
        fn no_search_command_error() -> String {
            format!(
                "{}\n{}",
                gettext("No search command given."),
                gettext("You can set a command for a search tool in the program options.")
            )
        }

        let files = file_list.selected_files().into_iter().collect::<Vec<_>>();
        let error_message = match spawn_async(None, &files, &options.search_cmd.get()) {
            Ok(_) => return,
            Err(SpawnError::InvalidTemplate) => ErrorMessage::brief(no_search_command_error()),
            Err(SpawnError::InvalidCommand(e)) => {
                ErrorMessage::with_error(no_search_command_error(), &e)
            }
            Err(SpawnError::Failure(e)) => {
                ErrorMessage::with_error(gettext("Unable to execute command."), &e)
            }
        };
        if let Some(window) = main_win.root().and_downcast::<gtk::Window>() {
            error_message.show(&window).await;
        } else {
            eprintln!(
                "No window. Search failed: {}\n{}",
                error_message.message,
                error_message.secondary_text.unwrap_or_default()
            );
        }
    }
}

pub async fn file_quick_search(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .show_quick_search(None, Some(QuickSearchMode::Search));
}

async fn file_quick_filter(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .show_quick_search(None, Some(QuickSearchMode::Filter));
}

async fn file_chmod(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    let files = file_list.selected_files();
    if !files.is_empty() {
        show_chmod_dialog(
            main_win.upcast_ref(),
            file_list.connection().as_ref(),
            &files,
        )
        .await;
    }
}

async fn file_chown(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    let files = file_list.selected_files();
    if !files.is_empty() {
        show_chown_dialog(
            main_win.upcast_ref(),
            file_list.connection().as_ref(),
            &files,
        )
        .await;
    }
}

async fn file_mkdir(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if let Some(dir) = file_list.directory() {
        let selected_file = file_list.selected_file();
        let dir_file = dir.file().clone();
        let new_dir =
            show_mkdir_dialog(main_win.upcast_ref(), &dir_file, selected_file.as_ref()).await;

        if let Some(new_dir) = new_dir {
            // focus the created directory (if possible)
            if new_dir.parent().is_some_and(|p| p.equal(&*dir.file())) {
                dir.file_created(&new_dir.uri());
                file_list.focus_file(&new_dir.basename().unwrap(), true);
            }
        }
    }
}

async fn file_properties(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if let Some(file) = file_list.selected_file() {
        let changed = FilePropertiesDialog::show(
            &main_win,
            &main_win.file_metadata_service(),
            &file,
            file_list.connection(),
        )
        .await;

        if changed {
            file_list.focus_file(&file.path_name(), true);
        }
    }
}

fn ensure_file_list_is_local(file_list: &FileList) -> Result<(), ErrorMessage> {
    if file_list.connection().is_some_and(|c| c.is_local()) {
        Ok(())
    } else {
        Err(ErrorMessage::brief(gettext(
            "Operation not supported on remote file systems",
        )))
    }
}

async fn do_file_diff(
    main_win: &MainWindow,
    options: &ProgramsOptions,
) -> Result<(), ErrorMessage> {
    let active_fl = main_win.file_selector(FileSelectorID::Active).file_list();
    let inactive_fl = main_win.file_selector(FileSelectorID::Inactive).file_list();

    ensure_file_list_is_local(&active_fl)?;

    let selected_files = active_fl.selected_files().into_iter().collect::<Vec<_>>();
    match selected_files.len() {
        0 => Ok(()),
        1 => {
            ensure_file_list_is_local(&inactive_fl)?;

            let active_file = selected_files.first().unwrap().clone();

            let inactive_file = inactive_fl
                .selected_files()
                .first()
                .ok_or_else(|| ErrorMessage::new(gettext("No file selected"), None::<String>))?
                .clone();

            spawn_async(
                None,
                &[active_file, inactive_file],
                &options.differ_cmd.get(),
            )
            .map_err(SpawnError::into_message)
        }
        2 | 3 => spawn_async(None, &selected_files, &options.differ_cmd.get())
            .map_err(SpawnError::into_message),

        _ => Err(ErrorMessage::brief(gettext("Too many selected files"))),
    }
}

async fn file_diff(main_win: MainWindow) {
    let options = ProgramsOptions::new();
    if let Err(error) = do_file_diff(&main_win, &options).await {
        error.show(main_win.upcast_ref()).await;
    }
}

async fn do_file_sync_dirs(
    main_win: &MainWindow,
    options: &ProgramsOptions,
) -> Result<(), ErrorMessage> {
    let active_fl = main_win.file_selector(FileSelectorID::Active).file_list();
    let inactive_fl = main_win.file_selector(FileSelectorID::Inactive).file_list();

    ensure_file_list_is_local(&active_fl)?;
    ensure_file_list_is_local(&inactive_fl)?;

    let (active_dir, inactive_dir) = active_fl
        .directory()
        .zip(inactive_fl.directory())
        .ok_or_else(|| ErrorMessage::brief(gettext("Nothing to compare")))?;

    spawn_async(None, &[active_dir, inactive_dir], &options.differ_cmd.get())
        .map_err(SpawnError::into_message)
}

async fn file_sync_dirs(main_win: MainWindow) {
    let options = ProgramsOptions::new();
    if let Err(error) = do_file_sync_dirs(&main_win, &options).await {
        error.show(main_win.upcast_ref()).await;
    }
}

pub async fn file_rename(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    file_list.show_rename_dialog().await;
}

fn symlink_name(file_name: &str, options: &GeneralOptions) -> String {
    let mut format = options.symlink_format.get();
    if format.is_empty() {
        format = gettext("link to %s");
    }
    if !format.contains("%s") {
        format.push_str(" %s");
    }
    format.replace("%s", file_name)
}

async fn create_symlinks(
    parent_window: &gtk::Window,
    files: &[File],
    directory: &Directory,
    options: &GeneralOptions,
) {
    let mut skip_all = false;

    for file in files {
        let target_absolute_name = file.file().parse_name();

        let symlink_file =
            directory.get_child_gfile(Path::new(&symlink_name(&file.name(), options)));

        loop {
            match symlink_file.make_symbolic_link(&target_absolute_name, gio::Cancellable::NONE) {
                Ok(_) => {
                    directory.file_created(&symlink_file.uri());
                    break;
                }
                Err(_) if skip_all => {
                    // do nothing
                }
                Err(error) => {
                    let choice = gtk::AlertDialog::builder()
                        .modal(true)
                        .message(error.message())
                        .buttons([
                            gettext("Skip"),
                            gettext("Skip all"),
                            gettext("Cancel"),
                            gettext("Retry"),
                        ])
                        .cancel_button(0)
                        .default_button(3)
                        .build()
                        .choose_future(Some(parent_window))
                        .await;
                    match choice {
                        Ok(0) /* Skip */ => { break; },
                        Ok(1) /* Skip all */ => { skip_all = true; break; },
                        Ok(3) /* Retry */ => { continue; },
                        _ /* Cancel */ => { return },
                    }
                }
            }
        }
    }
}

async fn file_create_symlink(main_win: MainWindow) {
    let options = GeneralOptions::new();

    let active_fs = main_win.file_selector(FileSelectorID::Active);
    let inactive_fs = main_win.file_selector(FileSelectorID::Inactive);

    let Some(dest_directory) = inactive_fs.file_list().directory() else {
        eprintln!("Cannot create symlinks: No destination directory.");
        return;
    };

    let active_fl = active_fs.file_list();
    let selected_files = active_fl.selected_files();
    let selected_files_len = selected_files.len();

    if selected_files_len > 1 {
        let message = ngettext(
            "Create symbolic links of {count} file in {dir}?",
            "Create symbolic links of {count} files in {dir}?",
            selected_files_len as u32,
        )
        .replace("{count}", &selected_files_len.to_string())
        .replace("{dir}", &dest_directory.display_path());

        let choice = gtk::AlertDialog::builder()
            .modal(true)
            .message(message)
            .buttons([gettext("Cancel"), gettext("Create")])
            .cancel_button(0)
            .default_button(1)
            .build()
            .choose_future(Some(&main_win))
            .await;
        if choice == Ok(1) {
            create_symlinks(
                main_win.upcast_ref(),
                &selected_files,
                &dest_directory,
                &options,
            )
            .await;
        }
    } else if let Some(focused_file) = active_fl.focused_file() {
        show_create_symlink_dialog(
            main_win.upcast_ref(),
            &focused_file,
            &dest_directory,
            &symlink_name(&focused_file.name(), &options),
        )
        .await;
    }
}

async fn file_advrename(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    let file_metadata_service = main_win.file_metadata_service();
    advanced_rename_dialog_show(&main_win, &file_list, &file_metadata_service);
}

async fn file_sendto(main_win: MainWindow) {
    let options = ProgramsOptions::new();

    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    let files = file_list.selected_files().into_iter().collect::<Vec<_>>();

    let command_template = options.sendto_cmd.get();

    if command_template == "xdg-email --attach %s" && files.len() > 1 {
        ErrorMessage::new(gettext("Warning"), Some(gettext("The default send-to command only supports one selected file at a time. You can change the command in the program options."))).show(main_win.upcast_ref()).await;
    }

    if let Err(error) = spawn_async(None, &files, &command_template) {
        error.into_message().show(main_win.upcast_ref()).await;
    }
}

async fn file_exit(main_win: MainWindow) {
    main_win.close();
}

/************** Mark Menu **************/

async fn mark_toggle(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .toggle()
}

async fn mark_toggle_and_step(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .toggle_and_step()
}

async fn mark_select_all(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .select_all()
}

async fn mark_unselect_all(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .unselect_all()
}

async fn mark_select_all_files(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .select_all_files()
}

async fn mark_unselect_all_files(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .unselect_all_files()
}

async fn mark_select_with_pattern(main_win: MainWindow) {
    let file_list = main_win.file_selector(FileSelectorID::Active).file_list();
    select_by_pattern(&file_list, true).await;
}

async fn mark_unselect_with_pattern(main_win: MainWindow) {
    let file_list = main_win.file_selector(FileSelectorID::Active).file_list();
    select_by_pattern(&file_list, false).await;
}

async fn mark_invert_selection(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .invert_selection();
}

async fn mark_select_all_with_same_extension(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .toggle_files_with_same_extension(true);
}

async fn mark_unselect_all_with_same_extension(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .toggle_files_with_same_extension(false);
}

async fn mark_compare_directories(main_win: MainWindow) {
    let fl1 = main_win.file_selector(FileSelectorID::Active).file_list();
    let fl2 = main_win.file_selector(FileSelectorID::Inactive).file_list();

    let mut files2: HashMap<String, File> = fl2
        .visible_files()
        .into_iter()
        .filter(|f| !f.is_directory())
        .map(|f| (f.name(), f))
        .collect();

    let mut selection1 = HashSet::new();
    for f1 in fl1.visible_files() {
        if f1.is_directory() {
            continue;
        }

        let name = f1.name();

        if let Some(f2) = files2.get(&name) {
            let date1 = f1.modification_date();
            let date2 = f2.modification_date();

            // select the younger one
            match date1.cmp(&date2) {
                Ordering::Less => {
                    // f2 stays selected
                }
                Ordering::Equal => {
                    if f1.size() == f2.size() {
                        // no selection
                        files2.remove(&name);
                    } else {
                        // select both
                        selection1.insert(f1);
                    }
                }
                Ordering::Greater => {
                    // f1 selected
                    selection1.insert(f1);
                    files2.remove(&name);
                }
            }
        } else {
            selection1.insert(f1);
        }
    }

    fl1.set_selected_files(&selection1);
    fl2.set_selected_files(&files2.into_values().collect());
}

/************** Edit Menu **************/

async fn edit_cap_cut(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .cut_files();
}

async fn edit_cap_copy(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .copy_files();
}

async fn edit_cap_paste(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .paste_files()
        .await;
}

async fn edit_copy_fnames(main_win: MainWindow) {
    let mask = get_modifiers_state(main_win.upcast_ref());

    let files = main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .selected_files();

    let names: Vec<String> = match mask {
        Some(gdk::ModifierType::SHIFT_MASK) => files
            .into_iter()
            .filter_map(|f| f.local_path())
            .map(|p| p.to_string_lossy().to_string())
            .collect(),
        Some(gdk::ModifierType::ALT_MASK) => files.into_iter().map(|f| f.uri()).collect(),
        _ => files.into_iter().map(|f| f.name()).collect(),
    };

    main_win.clipboard().set_text(&names.join(" "));
}

/************** Command Menu **************/

async fn command_execute(main_win: MainWindow, command_template: String) {
    let fs = main_win.file_selector(FileSelectorID::Active);
    let fl = fs.file_list();

    let sfl = fl.selected_files().into_iter().collect::<Vec<_>>();

    let grouped = sfl
        .iter()
        .map(|f| f.file().parent().and_then(|p| p.path()))
        .collect::<HashSet<Option<PathBuf>>>();
    let dir_path = if grouped.len() == 1 {
        grouped.into_iter().next().flatten()
    } else {
        None
    };
    // TODO: gnome_cmd_dir_is_local (dir) ? dir_path.c_str() : nullptr

    if let Err(error_message) =
        spawn_async(dir_path.as_deref(), &sfl, &command_template).map_err(SpawnError::into_message)
    {
        error_message.show(main_win.upcast_ref()).await;
    }
}

fn open_terminal(main_win: &MainWindow, options: &ProgramsOptions) -> Result<(), ErrorMessage> {
    let dpath = main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .directory()
        .and_then(|d| d.file().path());

    let command = OsString::from(options.terminal_cmd.get());
    if command.is_empty() {
        return Err(ErrorMessage {
            message: gettext("Terminal command is not configured properly."),
            secondary_text: None,
        });
    }

    spawn_async_command(dpath.as_deref(), &command).map_err(SpawnError::into_message)
}

/// Executes the command stored in `terminal-cmd` in the active directory.
async fn command_open_terminal(main_win: MainWindow) {
    let options = ProgramsOptions::new();
    if let Err(error_message) = open_terminal(&main_win, &options) {
        error_message.show(main_win.upcast_ref()).await;
    }
}

/* ***************************** View Menu ****************************** */

async fn view_dir_history(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .show_history();
}

async fn view_cmdline_history(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.show_history();
    }
}

async fn view_up(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if file_selector.is_tab_locked(&file_list) {
        if let Some(directory) = file_list.directory().and_then(|d| d.parent()) {
            file_selector.new_tab(Some(&directory), TabOptions::from(&file_list));
        }
    } else {
        file_list.goto_directory(Path::new(".."));
    }
}

async fn view_first(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).first();
}

async fn view_back(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).back();
}

async fn view_forward(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).forward();
}

async fn view_last(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).last();
}

async fn view_refresh(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .reload()
        .await;
}

async fn view_equal_panes(main_win: MainWindow) {
    main_win.ensure_slide_position(50).await
}

async fn view_maximize_pane(main_win: MainWindow) {
    main_win
        .ensure_slide_position(if main_win.current_panel() == 0 {
            100
        } else {
            0
        })
        .await
}

async fn view_in_left_pane(main_win: MainWindow) {
    main_win.set_directory_to_opposite(FileSelectorID::Left);
}

async fn view_in_right_pane(main_win: MainWindow) {
    main_win.set_directory_to_opposite(FileSelectorID::Right);
}

async fn view_in_active_pane(main_win: MainWindow) {
    main_win.set_directory_to_opposite(FileSelectorID::Active);
}

async fn view_in_inactive_pane(main_win: MainWindow) {
    main_win.set_directory_to_opposite(FileSelectorID::Inactive);
}

async fn view_directory(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    if let Some(file) = file_list.selected_file()
        && file.is_directory()
    {
        file_selector.do_file_specific_action(&file_list, &file);
    }
}

async fn view_home(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    let home = ConnectionList::get().home();
    if !file_selector.is_tab_locked(&file_list) {
        file_list.set_connection(&home, None);
        file_list.goto_directory(Path::new("~"));
    } else {
        let directory = Directory::new(&home, &home.create_uri(&glib::home_dir()));
        file_selector.new_tab(Some(&directory), TabOptions::from(&file_list));
    }
}

async fn view_root(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();

    if file_selector.is_tab_locked(&file_list) {
        if let Some(connection) = file_list.connection() {
            let directory = Directory::new(&connection, &connection.create_uri(Path::new("/")));
            file_selector.new_tab(Some(&directory), TabOptions::from(&file_list));
        }
    } else {
        file_list.goto_directory(Path::new("/"));
    }
}

async fn view_new_tab(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    if let Some(directory) = file_list.directory() {
        let mut options = TabOptions::from(&file_list);
        if let Some(selected_file) = file_list.selected_file() {
            options = options.selected_file(selected_file.path_name());
        }
        file_selector.new_tab(Some(&directory), options);
    }
}

async fn ask_close_locked_tab(parent_window: &gtk::Window) -> bool {
    gtk::AlertDialog::builder()
        .modal(true)
        .message(gettext("The tab is locked, close anyway?"))
        .buttons([gettext("_Cancel"), gettext("C_lose Tab")])
        .cancel_button(0)
        .default_button(1)
        .build()
        .choose_future(Some(parent_window))
        .await
        == Ok(1)
}

async fn view_close_tab(main_win: MainWindow) {
    let fs = main_win.file_selector(FileSelectorID::Active);
    if fs.tab_count() > 1
        && (!fs.is_current_tab_locked() || ask_close_locked_tab(main_win.upcast_ref()).await)
    {
        fs.close_tab();
    }
}

async fn view_close_all_tabs(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .close_all_tabs();
}

async fn view_close_duplicate_tabs(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .close_duplicate_tabs();
}

async fn view_prev_tab(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).prev_tab();
}

async fn view_next_tab(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).next_tab();
}

async fn view_in_new_tab(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    if let Some(dir) = file_list
        .selected_file()
        .filter(|file| file.is_directory())
        .and_then(|file| {
            file_list
                .directory()
                .map(|parent| parent.child(file.name()))
        })
        .or_else(|| file_list.directory())
    {
        file_selector.new_tab(Some(&dir), TabOptions::from(&file_list).activate(false));
    }
}

async fn view_in_inactive_tab(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    if let Some(dir) = file_list
        .selected_file()
        .filter(|file| file.is_directory())
        .and_then(|file| {
            file_list
                .directory()
                .map(|parent| parent.child(file.name()))
        })
        .or_else(|| file_list.directory())
    {
        let file_selector = main_win.file_selector(FileSelectorID::Inactive);
        file_selector.new_tab(
            Some(&dir),
            TabOptions::from(&file_selector.file_list()).activate(false),
        );
    }
}

async fn view_toggle_tab_lock(main_win: MainWindow) {
    let file_selector = main_win.file_selector(FileSelectorID::Active);
    let file_list = file_selector.file_list();
    file_selector.toggle_tab_lock(&file_list);
}

async fn view_step_up(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .focus_prev();
}

async fn view_step_down(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .focus_next();
}

async fn switch_panels(main_win: MainWindow) {
    main_win.switch_to_opposite();
}

async fn swap_panels(main_win: MainWindow) {
    main_win.swap_panels();
}

async fn show_slide_popup(main_win: MainWindow) {
    main_win.show_slide_popup();
}

async fn view_slide(main_win: MainWindow, percentage: i32) {
    main_win.set_slide(percentage);
}

async fn calculate_directory_sizes(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .show_visible_tree_sizes();
}

fn sort_file_list(main_win: MainWindow, column: ColumnID) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .sort_by(column);
}

async fn sort_by_name(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Name);
}

async fn sort_by_ext(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Ext);
}

async fn sort_by_date(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Date);
}

async fn sort_by_size(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Size);
}

async fn sort_by_perm(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Perm);
}

async fn sort_by_owner(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Owner);
}

async fn sort_by_group(main_win: MainWindow) {
    sort_file_list(main_win, ColumnID::Group);
}

/************** Bookmarks Menu **************/

async fn bookmarks_add_current(main_win: MainWindow) {
    let fs = main_win.file_selector(FileSelectorID::Active);
    let Some(dir) = fs.file_list().directory() else {
        eprintln!("No directory. Nothing to bookmark.");
        return;
    };
    let options = GeneralOptions::new();
    bookmark_directory(main_win.upcast_ref(), &dir, &options).await;
}

async fn bookmarks_edit(main_win: MainWindow) {
    let connection_list = ConnectionList::get();
    let shortcuts = main_win.shortcuts();
    let result = BookmarksDialog::show(&main_win, connection_list, shortcuts).await;
    if let Some(bookmark) = result {
        let fs = main_win.file_selector(FileSelectorID::Active);
        fs.goto_directory(&bookmark.connection, Path::new(&bookmark.bookmark.path()));
    }
}

async fn bookmarks_goto(main_win: MainWindow, goto: BookmarkGoToVariant) {
    let Some(connection) = ConnectionList::get().find_by_uuid(&goto.connection_uuid) else {
        eprintln!(
            "[{}] Unsupported bookmark group: '{}' - ignored",
            goto.bookmark_name, goto.connection_uuid
        );
        return;
    };

    let bookmarks = connection.bookmarks();
    let Some(path) = bookmarks.iter().find_map(|bookmark| {
        if bookmark.name() == goto.bookmark_name {
            Some(Path::new(bookmark.path()))
        } else {
            None
        }
    }) else {
        eprintln!(
            "[{}] Unknown bookmark name: '{}' - ignored",
            connection.alias().unwrap_or_default(),
            goto.bookmark_name
        );
        return;
    };

    let fs = main_win.file_selector(FileSelectorID::Active);
    fs.goto_directory(&connection, path);
}

async fn bookmarks_view(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .show_bookmarks();
}

/************** Options Menu **************/

async fn options_edit(main_win: MainWindow) {
    if show_options_dialog(&main_win).await {
        main_win.update_view();
    }
}

async fn options_edit_shortcuts(main_win: MainWindow) {
    let shortcuts = main_win.shortcuts();
    ShortcutsDialog::run(&main_win, shortcuts).await;
}

/************** Connections Menu **************/

async fn connections_open(main_win: MainWindow) {
    let dialog = main_win.get_or_create_dialog("connections", || RemoteDialog::new(&main_win));
    dialog.present();
}

async fn connections_new(main_win: MainWindow) {
    let options = NetworkOptions::new();
    let uri = glib::Uri::parse(&options.quick_connect_uri.get(), glib::UriFlags::NONE).ok();
    if let Some(connection) = ConnectDialog::new_connection(&main_win, uri).await {
        let fs = main_win.file_selector(FileSelectorID::Active);
        let mut file_list = fs.file_list();
        if fs.is_current_tab_locked() {
            file_list = fs.new_tab(None, TabOptions::from(&file_list));
        }
        file_list.set_connection(&connection, None);
        if let Some(quick_connect_uri) = connection.uri().map(|uri| uri.to_str())
            && let Err(error) = options.quick_connect_uri.set(quick_connect_uri)
        {
            eprintln!("Failed to save quick connect parameters: {error}");
        }
    }
}

async fn connections_connect_first(main_win: MainWindow) {
    if let Some(remote_con) = ConnectionList::get()
        .iter()
        .find_map(|c| c.downcast::<ConnectionRemote>().ok())
    {
        main_win
            .file_selector(FileSelectorID::Active)
            .file_list()
            .set_connection(&remote_con, None);
    }
}

async fn connections_change_left(main_win: MainWindow) {
    main_win.change_connection(FileSelectorID::Left);
}

async fn connections_change_right(main_win: MainWindow) {
    main_win.change_connection(FileSelectorID::Right);
}

async fn connections_set_current(main_win: MainWindow, uuid: String) {
    let con_list = ConnectionList::get();
    let Some(con) = con_list.find_by_uuid(&uuid) else {
        eprintln!("No connection corresponds to {uuid}");
        return;
    };

    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .set_connection(&con, None);
}

async fn connections_close(main_win: MainWindow, uuid: String) {
    let con_list = ConnectionList::get();
    let Some(con) = con_list.find_by_uuid(&uuid) else {
        eprintln!("No connection corresponds to {uuid}");
        return;
    };

    con.close(Some(main_win.upcast_ref())).await;
}

async fn connections_close_current(main_win: MainWindow) {
    if let Some(con) = main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .connection()
        .filter(|c| c.downcast_ref::<ConnectionHome>().is_none())
    {
        con.close(Some(main_win.upcast_ref())).await;
    }
}

/************** Plugins Menu ***********/

async fn plugins_configure(main_win: MainWindow) {
    let dialog = main_win.get_or_create_dialog("plugins", || {
        let plugin_manager = main_win.plugin_manager();
        show_plugin_manager(&plugin_manager, main_win.upcast_ref())
    });
    dialog.present();
}

async fn plugin_action(main_win: MainWindow, plugin_action: PluginActionVariant) {
    if let Some(plugin) = main_win
        .plugin_manager()
        .active_plugins()
        .into_iter()
        .find_map(|(name, plugin)| (name == plugin_action.plugin).then_some(plugin))
        && let Some(file_actions) = plugin.downcast_ref::<FileActions>()
    {
        file_actions.execute(
            &plugin_action.action,
            Some(&plugin_action.parameter),
            main_win.upcast_ref(),
            &main_win.state(),
        );
    }
}

/************** Help Menu **************/

async fn help_help(main_win: MainWindow) {
    display_help(main_win.upcast_ref(), None).await;
}

async fn help_keyboard(main_win: MainWindow) {
    display_help(main_win.upcast_ref(), Some("gnome-commander-keyboard")).await;
}

async fn help_web(main_win: MainWindow) {
    if let Err(error) = gtk::UriLauncher::new(PACKAGE_URL)
        .launch_future(Some(&main_win))
        .await
    {
        ErrorMessage::with_error(gettext("There was an error opening home page."), &error)
            .show(main_win.upcast_ref())
            .await;
    }
}

async fn help_problem(main_win: MainWindow) {
    if let Err(error) = gtk::UriLauncher::new(PACKAGE_BUGREPORT)
        .launch_future(Some(&main_win))
        .await
    {
        ErrorMessage::with_error(gettext("There was an error reporting problem."), &error)
            .show(main_win.upcast_ref())
            .await;
    }
}

async fn help_about(main_win: MainWindow) {
    let authors = [
        "Marcus Bjurman <marbj499@student.liu.se>",
        "Piotr Eljasiak <epiotr@use.pl>",
        "Assaf Gordon <agordon88@gmail.com>",
        "Uwe Scholz <u.scholz83@gmx.de>",
        "Andrey Kutejko <andy128k@gmail.com>",
        "Wladimir Palant https://palant.info/",
    ];

    let documenters = [
        "Marcus Bjurman <marbj499@student.liu.se>",
        "Piotr Eljasiak <epiotr@use.pl>",
        "Laurent Coudeur <laurentc@eircom.net>",
        "Uwe Scholz <u.scholz83@gmx.de>",
    ];

    let copyright = "Copyright \u{00A9} 2001-2006 Marcus Bjurman
Copyright \u{00A9} 2007-2012 Piotr Eljasiak
Copyright \u{00A9} 2013-2026 Uwe Scholz
Copyright \u{00A9} 2024-2026 Andrey Kutejko
Copyright \u{00A9} 2026 Wladimir Palant";

    gtk::AboutDialog::builder()
        .transient_for(&main_win)
        .modal(true)
        .name("Gnome Commander")
        .version(PACKAGE_VERSION)
        .comments(gettext("A fast and powerful file manager"))
        .copyright(copyright)
        .license_type(gtk::License::Gpl30)
        .authors(authors)
        .documenters(documenters)
        .logo_icon_name(PACKAGE_NAME)
        .translator_credits(gettext("translator-credits"))
        .website("https://gnome.pages.gitlab.gnome.org/gnome-commander/")
        .website_label("Gnome Commander Website")
        .build()
        .present();
}

async fn go_to_panels(main_win: MainWindow) {
    main_win.file_selector(FileSelectorID::Active).grab_focus();
}

async fn go_to_cmdline(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.grab_focus();
    }
}

async fn go_to_terminal(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.terminal().grab_focus();
    }
}

async fn clear_cmdline(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.set_text("");
        if RootExt::focus(&main_win).is_some_and(|focus| focus.is_ancestor(&cmdline)) {
            main_win.focus_file_lists();
        }
    }
}

async fn cmdline_run_embedded(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.process_command(ExecutionTarget::EmbeddedTerminal);
    }
}

async fn cmdline_run_external(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.process_command(ExecutionTarget::ExternalTerminal);
    }
}

async fn cmdline_run_nocapture(main_win: MainWindow) {
    let cmdline = main_win.cmdline();
    if cmdline.is_visible() {
        cmdline.process_command(ExecutionTarget::Background);
    }
}

async fn open_file(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .open_file();
}

async fn add_cwd_to_cmdline(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .add_cwd_to_cmdline();
}

async fn add_file_name_to_cmdline(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .add_file_to_cmdline(false);
}

async fn add_file_path_to_cmdline(main_win: MainWindow) {
    main_win
        .file_selector(FileSelectorID::Active)
        .file_list()
        .add_file_to_cmdline(true);
}

trait Activatable {
    fn activate(self, action: &UserAction, mw: MainWindow, parameter: Option<&glib::Variant>);
    fn parameter_type(self) -> Option<Cow<'static, glib::VariantTy>>;
    fn bound_property(self) -> Option<&'static str>;
}

// It would have been easier to implement Activatable directly for the actual handler function.
// However, this results in "unbound type" compilation errors. In order to make parameter type
// "bound" we need ActionHandler as intermediate type as well as marker types.
struct NoParameter {}
struct BoundProperty {}

struct ActionHandler<Handler, T> {
    handler: Handler,
    _data: std::marker::PhantomData<T>,
}

// User actions without parameter, handler is a function taking MainWindow parameter only.

impl<Handler: AsyncFnOnce(MainWindow)> From<Handler> for ActionHandler<Handler, NoParameter> {
    fn from(handler: Handler) -> Self {
        Self {
            handler,
            _data: std::marker::PhantomData,
        }
    }
}

impl<Handler: AsyncFnOnce(MainWindow) + 'static> Activatable
    for ActionHandler<Handler, NoParameter>
{
    fn activate(self, action: &UserAction, mw: MainWindow, parameter: Option<&glib::Variant>) {
        if let Some(parameter) = parameter {
            eprintln!(
                "Unexpected parameter {parameter} for action {}",
                action.name()
            );
        }
        glib::spawn_future_local((self.handler)(mw));
    }

    fn parameter_type(self) -> Option<Cow<'static, glib::VariantTy>> {
        None
    }

    fn bound_property(self) -> Option<&'static str> {
        None
    }
}

// User actions with parameter, handler is a function taking MainWindow and a generic parameter.

impl<T: FromVariant, Handler: AsyncFnOnce(MainWindow, T)> From<Handler>
    for ActionHandler<Handler, T>
{
    fn from(handler: Handler) -> Self {
        Self {
            handler,
            _data: std::marker::PhantomData,
        }
    }
}

impl<T: FromVariant + 'static, Handler: AsyncFnOnce(MainWindow, T) + 'static> Activatable
    for ActionHandler<Handler, T>
{
    fn activate(self, action: &UserAction, mw: MainWindow, parameter: Option<&glib::Variant>) {
        if let Some(parameter) = parameter.and_then(|v| v.get::<T>()) {
            glib::spawn_future_local((self.handler)(mw, parameter));
        } else if let Some(parameter) = parameter {
            eprintln!("Invalid parameter {parameter} for action {}", action.name());
        } else {
            eprintln!("Missing parameter for action {}", action.name());
        }
    }

    fn parameter_type(self) -> Option<Cow<'static, glib::VariantTy>> {
        Some(T::static_variant_type())
    }

    fn bound_property(self) -> Option<&'static str> {
        None
    }
}

// User actions bound to a property, handler is a string (property name).

impl From<&'static str> for ActionHandler<&'static str, BoundProperty> {
    fn from(handler: &'static str) -> Self {
        Self {
            handler,
            _data: std::marker::PhantomData,
        }
    }
}

impl Activatable for ActionHandler<&'static str, BoundProperty> {
    fn activate(self, _action: &UserAction, _mw: MainWindow, _parameter: Option<&glib::Variant>) {}

    fn parameter_type(self) -> Option<Cow<'static, glib::VariantTy>> {
        None
    }

    fn bound_property(self) -> Option<&'static str> {
        Some(self.handler)
    }
}

macro_rules! user_actions {
    (
        $(
            $(#[$meta:meta])*
            $id:ident in $area:ident => ($name:literal $(| $($legacy_name:literal)|+)?, $description:expr, $handler:expr$(,)?),
        )+
    ) => {
        #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
        pub enum UserAction {
            $($(#[$meta])* $id,)*
        }

        impl UserAction {
            pub fn from_name(name: &str) -> Option<Self> {
                match name {
                    $($name $(| $($legacy_name)|+)? => Some(Self::$id),)*
                    _ => None,
                }
            }

            pub fn all() -> impl Iterator<Item=Self> {
                $(
                    let _max_index = Self::$id as usize;
                )*
                (0..=_max_index).map(|n| Self::try_from(n).unwrap_or_default())
            }

            pub fn name(&self) -> &'static str {
                match self {
                    $(Self::$id => $name,)*
                }
            }

            pub fn menu_description(&self) -> String {
                match self {
                    $(Self::$id => $description,)*
                }
            }

            pub fn activate(&self, mw: MainWindow, parameter: Option<&glib::Variant>) {
                match self {
                    $(Self::$id => {
                        ActionHandler::from($handler).activate(self, mw, parameter);
                    })*
                }
            }

            pub fn parameter_type(&self) -> Option<Cow<'static, glib::VariantTy>> {
                match self {
                    $(Self::$id => {
                        ActionHandler::from($handler).parameter_type()
                    })*
                }
            }

            pub fn bound_property(&self) -> Option<&'static str> {
                match self {
                    $(Self::$id => {
                        ActionHandler::from($handler).bound_property()
                    })*
                }
            }

            pub fn area(&self) -> Area {
                match self {
                    $(Self::$id => Area::$area,)*
                }
            }
        }

        impl TryFrom<usize> for UserAction {
            type Error = ();

            fn try_from(value: usize) -> Result<Self, Self::Error> {
                match value {
                    $(value if value == Self::$id as usize => Ok(Self::$id),)*
                    _ => Err(()),
                }
            }
        }
    }
}

impl UserAction {
    pub fn description(&self) -> String {
        let mut description = self.menu_description();
        description.retain(|c| c != '_' && c != '…');
        description
    }

    pub fn has_parameter(&self) -> bool {
        self.parameter_type().is_some()
    }
}

user_actions! {
    // File actions
    #[default]
    FileCopy in Panel => (
        "file-copy" | "file.copy",
        gettext("Copy Files"),
        file_copy,
    ),

    FileCopyAs in Panel => (
        "file-copy-as" | "file.copy_as",
        gettext("Copy files with rename"),
        file_copy_as,
    ),

    FileMove in Panel => (
        "file-move" | "file.move",
        gettext("Move Files"),
        file_move,
    ),

    FileDelete in Panel => (
        "file-delete" | "file.delete",
        gettext("_Delete"),
        file_delete,
    ),

    FileDeletePermanently in Panel => (
        "file-delete-permanently",
        gettext("Delete Permanently"),
        file_delete_permanently,
    ),

    FileView in MainWindow => (
        "file-view" | "file.view",
        gettext("View File"),
        file_view,
    ),

    FileInternalView in MainWindow => (
        "file-internal-view" | "file.internal_view",
        gettext("View with internal viewer"),
        file_internal_view,
    ),

    FileExternalView in MainWindow => (
        "file-external-view" | "file.external_view",
        gettext("View with external viewer"),
        file_external_view,
    ),

    FileEdit in MainWindow => (
        "file-edit" | "file.edit",
        gettext("Edit (SHIFT for new document)"),
        file_edit,
    ),

    FileEditNewDoc in MainWindow => (
        "file-edit-new-doc" | "file.edit_new_doc",
        gettext("New _Text File"),
        file_edit_new_doc,
    ),

    FileSearch in MainWindow => (
        "file-search" | "file.search",
        gettext("_Search…"),
        file_search,
    ),

    FileQuickSearch in Panel => (
        "file-quick-search" | "file.quick_search",
        gettext("_Quick Search…"),
        file_quick_search,
    ),

    FileQuickFilter in Panel => (
        "edit-filter" | "edit.filter",
        gettext("Quick _Filter…"),
        file_quick_filter,
    ),

    FileChmod in Panel => (
        "file-chmod" | "file.chmod",
        gettext("Change Per_missions"),
        file_chmod,
    ),

    FileChown in Panel => (
        "file-chown" | "file.chown",
        gettext("Change _Owner/Group"),
        file_chown,
    ),

    FileMkdir in Panel => (
        "file-mkdir" | "file.mkdir",
        gettext("New _Directory"),
        file_mkdir,
    ),

    FileProperties in Panel => (
        "file-properties" | "file.properties",
        gettext("_Properties…"),
        file_properties,
    ),

    FileDiff in Panel => (
        "file-diff" | "file.diff",
        gettext("_Diff"),
        file_diff,
    ),

    FileSyncDirs in Panel => (
        "file-sync-dirs" | "file.synchronize_directories",
        gettext("S_ynchronize Directories"),
        file_sync_dirs,
    ),

    FileRename in Panel => (
        "file-rename" | "file.rename",
        gettext("_Rename"),
        file_rename,
    ),

    FileCreateSymlink in Panel => (
        "file-create-symlink" | "file.create_symlink",
        gettext("Create _Symbolic Link"),
        file_create_symlink,
    ),

    FileAdvrename in Panel => (
        "file-advrename" | "file.advrename",
        gettext("Advanced _Rename Tool"),
        file_advrename,
    ),

    FileSendto in Panel => (
        "file-sendto" | "file.sendto",
        gettext("_Send Files"),
        file_sendto,
    ),

    FileExit in MainWindow => (
        "file-exit" | "file.exit",
        gettext("Quit"),
        file_exit,
    ),

    // Mark actions
    MarkToggle in Panel => (
        "mark-toggle" | "mark.toggle",
        gettext("Toggle selection"),
        mark_toggle,
    ),

    MarkToggleAndStep in Panel => (
        "mark-toggle-and-step" | "mark.toggle_and_step",
        gettext("Toggle selection and move cursor downward"),
        mark_toggle_and_step,
    ),

    MarkSelectAll in Panel => (
        "mark-select-all" | "mark.select_all",
        gettext("_Select All"),
        mark_select_all,
    ),

    MarkUnselectAll in Panel => (
        "mark-unselect-all" | "mark.unselect_all",
        gettext("_Unselect All"),
        mark_unselect_all,
    ),

    MarkSelectAllFiles in Panel => (
        "mark-select-all-files" | "mark.select_all_files",
        gettext("Select all _Files"),
        mark_select_all_files,
    ),

    MarkUnselectAllFiles in Panel => (
        "mark-unselect-all-files" | "mark.unselect_all_files",
        gettext("Unselect all Fi_les"),
        mark_unselect_all_files,
    ),

    MarkSelectWithPattern in Panel => (
        "mark-select-with-pattern" | "mark.select_with_pattern",
        gettext("Select with _Pattern"),
        mark_select_with_pattern,
    ),

    MarkUnselectWithPattern in Panel => (
        "mark-unselect-with-pattern" | "mark.unselect_with_pattern",
        gettext("Unselect with P_attern"),
        mark_unselect_with_pattern,
    ),

    MarkInvertSelection in Panel => (
        "mark-invert-selection" | "mark.invert",
        gettext("_Invert Selection"),
        mark_invert_selection,
    ),

    MarkSelectAllWithSameExtension in Panel => (
        "mark-select-all-with-same-extension" | "mark.select_all_with_same_extension",
        gettext("Select with same _Extension"),
        mark_select_all_with_same_extension,
    ),

    MarkUnselectAllWithSameExtension in Panel => (
        "mark-unselect-all-with-same-extension" | "mark.unselect_all_with_same_extension",
        gettext("Unselect with same E_xtension"),
        mark_unselect_all_with_same_extension,
    ),

    MarkCompareDirectories in Panel => (
        "mark-compare-directories" | "mark.compare_directories",
        gettext("_Compare Directories"),
        mark_compare_directories,
    ),

    // Edit actions
    EditCapCut in Panel => (
        "edit-cap-cut" | "edit.cut",
        gettext("Cu_t"),
        edit_cap_cut,
    ),

    EditCapCopy in Panel => (
        "edit-cap-copy" | "edit.copy",
        gettext("_Copy"),
        edit_cap_copy,
    ),

    EditCapPaste in Panel => (
        "edit-cap-paste" | "edit.paste",
        gettext("_Paste"),
        edit_cap_paste,
    ),

    EditCopyNames in Panel => (
        "edit-copy-fnames" | "edit.copy_filenames",
        gettext("Copy _File Names (SHIFT for full paths, ALT for URIs)"),
        edit_copy_fnames,
    ),

    // Command actions
    CommandExecute in MainWindow => (
        "command-execute" | "command.execute",
        gettext("Execute command"),
        command_execute,
    ),

    CommandOpenTerminal in MainWindow => (
        "command-open-terminal" | "command.open_terminal",
        gettext("Open _Terminal"),
        command_open_terminal,
    ),

    // View actions
    ViewConbuttons in MainWindow => (
        "view-conbuttons" | "view.conbuttons",
        gettext("Show Device Buttons"),
        "connection-buttons-visible",
    ),

    ViewDevlist in MainWindow => (
        "view-devlist" | "view.devlist",
        gettext("Show Device List"),
        "connection-list-visible",
    ),

    ViewToolbar in MainWindow => (
        "view-toolbar" | "view.toolbar",
        gettext("Show Toolbar"),
        "toolbar-visible",
    ),

    ViewButtonbar in MainWindow => (
        "view-buttonbar" | "view.buttonbar",
        gettext("Show Buttonbar"),
        "buttonbar-visible",
    ),

    ViewCmdline in MainWindow => (
        "view-cmdline" | "view.cmdline",
        gettext("Show Command Line"),
        "command-line-visible",
    ),

    ViewDirHistory in Panel => (
        "view-dir-history" | "view.dir_history",
        gettext("Show Directory History"),
        view_dir_history,
    ),

    ViewCmdlineHistory in MainWindow => (
        "view-cmdline-hisotry",
        gettext("Show Command Line History"),
        view_cmdline_history,
    ),

    ViewHiddenFiles in MainWindow => (
        "view-hidden-files" | "view.hidden_files",
        gettext("Show Hidden Files"),
        "view-hidden-files",
    ),

    ViewBackupFiles in MainWindow => (
        "view-backup-files" | "view.backup_files",
        gettext("Show Backup Files"),
        "view-backup-files",
    ),

    CmdlineAutohideOutput in MainWindow => (
        "cmdline-autohide-output",
        gettext("Hide Command Line Output When Inactive"),
        "cmdline-autohide-output",
    ),

    ViewUp in Panel => (
        "view-up" | "view.up",
        gettext("Up one directory"),
        view_up,
    ),

    ViewFirst in Panel => (
        "view-first" | "view.first",
        gettext("Back to oldest"),
        view_first,
    ),

    ViewBack in Panel => (
        "view-back" | "view.back",
        gettext("_Back"),
        view_back,
    ),

    ViewForward in Panel => (
        "view-forward" | "view.forward",
        gettext("_Forward"),
        view_forward,
    ),

    ViewLast in Panel => (
        "view-last" | "view.last",
        gettext("Forward to latest"),
        view_last,
    ),

    ViewRefresh in Panel => (
        "view-refresh" | "view.refresh",
        gettext("_Refresh"),
        view_refresh,
    ),

    ViewEqualPanes in Panel => (
        "view-equal-panes" | "view.equal_panes",
        gettext("_Equal Panel Size"),
        view_equal_panes,
    ),

    ViewMaximizePane in Panel => (
        "view-maximize-pane" | "view.maximize_pane",
        gettext("Maximize Panel Size"),
        view_maximize_pane,
    ),

    ViewInLeftPane in Panel => (
        "view-in-left-pane" | "view.in_left_pane",
        gettext("Open directory in the left panel"),
        view_in_left_pane,
    ),

    ViewInRightPane in Panel => (
        "view-in-right-pane" | "view.in_right_pane",
        gettext("Open directory in the right panel"),
        view_in_right_pane,
    ),

    ViewInActivePane in Panel => (
        "view-in-active-pane" | "view.in_active_pane",
        gettext("Open directory in the active panel"),
        view_in_active_pane,
    ),

    ViewInInactivePane in Panel => (
        "view-in-inactive-pane" | "view.in_inactive_pane",
        gettext("Open directory in the inactive panel"),
        view_in_inactive_pane,
    ),

    ViewDirectory in Panel => (
        "view-directory" | "view.directory",
        gettext("Change directory"),
        view_directory,
    ),

    ViewHome in Panel => (
        "view-home" | "view.home",
        gettext("Home directory"),
        view_home,
    ),

    ViewRoot in Panel => (
        "view-root" | "view.root",
        gettext("Root directory"),
        view_root,
    ),

    ViewNewTab in Panel => (
        "view-new-tab" | "view.new_tab",
        gettext("Open in New _Tab"),
        view_new_tab,
    ),

    ViewCloseTab in Panel => (
        "view-close-tab" | "view.close_tab",
        gettext("_Close Tab"),
        view_close_tab,
    ),

    ViewCloseAllTabs in Panel => (
        "view-close-all-tabs" | "view.close_all_tabs",
        gettext("Close _All Tabs"),
        view_close_all_tabs,
    ),

    ViewCloseDuplicateTabs in Panel => (
        "view-close-duplicate-tabs" | "view.close_duplicate_tabs",
        gettext("Close duplicate tabs"),
        view_close_duplicate_tabs,
    ),

    ViewPrevTab in Panel => (
        "view-prev-tab" | "view.prev_tab",
        gettext("Previous tab"),
        view_prev_tab,
    ),

    ViewNextTab in Panel => (
        "view-next-tab" | "view.next_tab",
        gettext("Next tab"),
        view_next_tab,
    ),

    ViewInNewTab in Panel => (
        "view-in-new-tab" | "view.in_new_tab",
        gettext("Open directory in the new tab"),
        view_in_new_tab,
    ),

    ViewInInactiveTab in Panel => (
        "view-in-inactive-tab" | "view.in_inactive_tab",
        gettext("Open directory in the new tab (inactive panel)"),
        view_in_inactive_tab,
    ),

    ViewToggleTabLock in Panel => (
        "view-toggle-tab-lock" | "view.toggle_lock_tab",
        gettext("Lock/unlock tab"),
        view_toggle_tab_lock,
    ),

    ViewHorizontalOrientation in Panel => (
        "view-horizontal-orientation" | "view.horizontal-orientation",
        gettext("Horizontal Orientation"),
        "horizontal-orientation",
    ),

    ViewMainMenu in MainWindow => (
        "view-main-menu" | "view.main_menu",
        gettext("Display main menu"),
        "menu-visible",
    ),

    ViewStepUp in Panel => (
        "view-step-up" | "view.step_up",
        gettext("Move cursor one step up"),
        view_step_up,
    ),

    ViewStepDown in Panel => (
        "view-step-down" | "view.step_down",
        gettext("Move cursor one step down"),
        view_step_down,
    ),

    SwitchPanels in Panel => (
        "switch-panels",
        gettext("Switch to Opposite Panel"),
        switch_panels,
    ),

    SwapPanes in Panel => (
        "swap-panes",
        gettext("Swap panels"),
        swap_panels,
    ),

    ShowSlidePopup in Panel => (
        "show-slide-popup",
        gettext("Show panel size selector"),
        show_slide_popup,
    ),

    ViewSlide in Panel => (
        "view-slide",
        String::new(), // invisible to users
        view_slide,
    ),

    CalculateDirectorySizes in Panel => (
        "calculate-directory-sizes",
        gettext("Calculate Size for All Directories"),
        calculate_directory_sizes,
    ),

    SortByName in Panel => (
        "sort-by-name",
        gettext("Sort by Name"),
        sort_by_name,
    ),

    SortByExt in Panel => (
        "sort-by-ext",
        gettext("Sort by Extension"),
        sort_by_ext,
    ),

    SortByDate in Panel => (
        "sort-by-date",
        gettext("Sort by Date"),
        sort_by_date,
    ),

    SortBySize in Panel => (
        "sort-by-size",
        gettext("Sort by Size"),
        sort_by_size,
    ),

    SortByPerm in Panel => (
        "sort-by-perm",
        gettext("Sort by Permissions"),
        sort_by_perm,
    ),

    SortByOwner in Panel => (
        "sort-by-owner",
        gettext("Sort by Owner"),
        sort_by_owner,
    ),

    SortByGroup in Panel => (
        "sort-by-group",
        gettext("Sort by Group"),
        sort_by_group,
    ),

    // Bookmark actions
    BookmarksAddCurrent in Panel => (
        "bookmarks-add-current" | "bookmarks.add_current",
        gettext("_Bookmark this Directory…"),
        bookmarks_add_current,
    ),

    BookmarksEdit in Panel => (
        "bookmarks-edit" | "bookmarks.edit",
        gettext("_Manage Bookmarks…"),
        bookmarks_edit,
    ),

    BookmarksGoto in Panel => (
        "bookmarks-goto" | "bookmarks.goto",
        gettext("Go to bookmarked location"),
        bookmarks_goto,
    ),

    BookmarksView in Panel => (
        "bookmarks-view" | "bookmarks.view",
        gettext("Show bookmarks of current device"),
        bookmarks_view,
    ),

    // Option actions
    OptionsEdit in MainWindow => (
        "options-edit" | "options.edit",
        gettext("_Options…"),
        options_edit,
    ),

    OptionsEditShortcuts in MainWindow => (
        "options-edit-shortcuts" | "options.shortcuts",
        gettext("_Keyboard Shortcuts…"),
        options_edit_shortcuts,
    ),

    // Connections actions
    ConnectionsOpen in Panel => (
        "connections-open" | "connections.open",
        gettext("_Remote Server…"),
        connections_open,
    ),

    ConnectionsNew in Panel => (
        "connections-new" | "connections.new",
        gettext("New Connection…"),
        connections_new,
    ),

    ConnectionsSetCurrent in Panel => (
        "connections-set-current" | "connections.set-uuid",
        gettext("Set Connection"),
        connections_set_current,
    ),

    // This should really be restricted to Panel but its default shortcut is Alt+Shift+F and this
    // one will be handled by the menu unless we register this for the entire window.
    ConnectionsConnectFirst in MainWindow => (
        "connections-connect-first",
        gettext("Connect to First Remote Server"),
        connections_connect_first,
    ),

    ConnectionsChangeLeft in Panel => (
        "connections-change-left" | "connections.change_left",
        gettext("Change Left Connection"),
        connections_change_left,
    ),

    ConnectionsChangeRight in Panel => (
        "connections-change-right" | "connections.change_right",
        gettext("Change Right Connection"),
        connections_change_right,
    ),

    ConnectionsClose in Panel => (
        "connections-close" | "connections.close-uuid",
        gettext("Close Connection"),
        connections_close,
    ),

    ConnectionsCloseCurrent in Panel => (
        "connections-close-current" | "connections.close",
        gettext("Drop Connection"),
        connections_close_current,
    ),

    // Plugin actions
    PluginsConfigure in MainWindow => (
        "plugins-configure" | "plugins.configure",
        gettext("_Configure Plugins…"),
        plugins_configure,
    ),

    PluginAction in MainWindow => (
        "plugin-action" | "plugin.action",
        String::new(), // invisible to users
        plugin_action,
    ),

    // Help actions
    HelpHelp in MainWindow => (
        "help-help" | "help.help",
        gettext("_Documentation"),
        help_help,
    ),

    HelpKeyboard in MainWindow => (
        "help-keyboard" | "help.keyboard",
        gettext("_Keyboard Shortcuts"),
        help_keyboard,
    ),

    HelpWeb in MainWindow => (
        "help-web" | "help.web",
        gettext("Gnome Commander on the _Web"),
        help_web,
    ),

    HelpProblem in MainWindow => (
        "help-problem" | "help.problem",
        gettext("Report a _Problem"),
        help_problem,
    ),

    HelpAbout in MainWindow => (
        "help-about" | "help.about",
        gettext("_About"),
        help_about,
    ),

    // Other actions
    GoToPanels in MainWindow => (
        "go-to-panels",
        gettext("Jump to Panels"),
        go_to_panels,
    ),

    GoToCmdline in MainWindow => (
        "go-to-cmdline",
        gettext("Jump to Command Line"),
        go_to_cmdline,
    ),

    GoToTerminal in MainWindow => (
        "go-to-terminal",
        gettext("Jump to Command Output"),
        go_to_terminal,
    ),

    ClearCmdline in CommandLine => (
        "clear-cmdline",
        gettext("Clear Command Line"),
        clear_cmdline,
    ),

    RunEmbedded in CommandLine => (
        "run-embedded",
        gettext("_Run"),
        cmdline_run_embedded,
    ),

    RunExternal in CommandLine => (
        "run-external",
        gettext("Run in _terminal"),
        cmdline_run_external,
    ),

    RunNoCapture in CommandLine => (
        "cmdline-run-nocapture",
        gettext("Run _ignoring output"),
        cmdline_run_nocapture,
    ),

    OpenFile in Panel => (
        "open-file",
        gettext("Open Current File"),
        open_file,
    ),

    AddCwdToCmdLine in MainWindow => (
        "add-cwd-to-cmdline",
        gettext("Add Current Directory to Command Line"),
        add_cwd_to_cmdline,
    ),

    AddFileNameToCmdLine in MainWindow => (
        "add-file-name-to-cmdline",
        gettext("Add Current File Name to Command Line"),
        add_file_name_to_cmdline,
    ),

    AddFilePathToCmdLine in MainWindow => (
        "add-file-path-to-cmdline",
        gettext("Add Current File Path to Command Line"),
        add_file_path_to_cmdline,
    ),
}
