// 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 super::{actions::Script, list::ColumnID, list::FileList};
use crate::{
    app::{App, AppTarget, RegularApp, UserDefinedApp, load_favorite_apps},
    config::PACKAGE,
    debug::debug,
    file::{File, FileOps},
    filter::fnmatch,
    libgcmd::file_actions::{FileActions, FileActionsExt},
    main_win::MainWindow,
    options::GeneralOptions,
    plugin_manager::wrap_plugin_menu,
    user_actions::UserAction,
    utils::MenuBuilderExt,
};
use gettextrs::gettext;
use gtk::{gio, glib, prelude::*};
use std::{
    fs,
    io::{self, BufRead},
    path::Path,
};

const MAX_OPEN_WITH_APPS: usize = 20;

fn fav_app_menu_item(app: &App) -> gio::MenuItem {
    let item = gio::MenuItem::new(Some(&app.name()), None);
    item.set_action_and_target_value(Some("fl.open-with"), Some(&app.to_variant()));
    item
}

fn fav_app_matches_files(app: &UserDefinedApp, files: &[File]) -> bool {
    match app.target {
        AppTarget::AllFiles => files.iter().all(|file| file.is_regular()),
        AppTarget::AllDirs => files.iter().all(|file| file.is_directory()),
        AppTarget::AllDirsAndFiles => files
            .iter()
            .all(|file| file.is_regular() || file.is_directory()),
        AppTarget::SomeFiles => {
            files.iter().all(|file| {
                if !file.is_regular() {
                    return false;
                }
                // Check that the file matches at least one pattern
                let name = file.name();
                app.pattern_list()
                    .iter()
                    .any(|pattern| fnmatch(pattern, &name, false))
            })
        }
    }
}

/// Try to get the script info out of the script
fn extract_script_info(script_path: &Path) -> Option<(String, bool)> {
    let file = fs::File::open(script_path).ok()?;

    let mut script_name = None;
    let mut in_terminal = false;
    for line in io::BufReader::new(file).lines().map_while(Result::ok) {
        if let Some(name) = line.strip_prefix("#name:") {
            script_name = Some(name.trim().to_string());
        }
        if let Some(term) = line.trim().strip_prefix("#term:") {
            in_terminal = term.trim() == "true";
        }
    }
    Some((script_name?, in_terminal))
}

fn create_action_script_menu() -> Option<gio::Menu> {
    let scripts_dir = glib::user_config_dir().join(PACKAGE).join("scripts");

    let menu = gio::Menu::new();

    for entry in fs::read_dir(scripts_dir).ok()?.flatten() {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }

        let mut script_info = Script {
            path,
            in_terminal: false,
        };
        let script_name;

        // Try to get the scriptname out of the script, otherwise take the filename
        if let Some((name, in_terminal)) = extract_script_info(&script_info.path) {
            script_info.in_terminal = in_terminal;
            script_name = name;
        } else {
            script_name = entry.file_name().to_string_lossy().to_string();
        }

        let item = gio::MenuItem::new(Some(&script_name), None);
        item.set_action_and_target_value(
            Some("fl.execute-script"),
            Some(&script_info.to_variant()),
        );

        menu.append_item(&item);
    }

    Some(menu)
}

/// This method adds all "open" popup entries
/// (for opening the selected files or folders with dedicated programs).
fn add_open_with_entries(menu: &gio::Menu, file_list: &FileList) {
    let files = file_list.selected_files();
    let first_file = files.first();

    let content_type = first_file.and_then(|f| f.content_type());

    // Only try to find a default application for the first file in the list of selected files
    let default_app_info = first_file.and_then(|f| f.app_info_for_content_type());
    if let Some(ref app_info) = default_app_info {
        // Add the default "Open" menu entry at the top of the popup
        let label = gettext("_Open with “{}”").replace("{}", &app_info.name().replace("_", "__"));
        menu.append(Some(&label), Some("fl.open-with-default"));
    } else {
        debug!(
            'u',
            "No default application found for the MIME type {:?} of {:?}",
            content_type,
            first_file.map(|f| f.name())
        );
        menu.append(Some(&gettext("Open Wit_h…")), Some("fl.open-with-other"));
    }

    // Add menu items in the "Open with" submenu
    let submenu = gio::Menu::new();

    let app_infos = content_type
        .map(|ct| gio::AppInfo::all_for_type(&ct))
        .unwrap_or_default()
        .into_iter()
        .filter(|app_info| {
            // Only add the entry to the submenu if its name different from the default app
            match default_app_info {
                Some(ref d) => !d.equal(app_info),
                None => true,
            }
        })
        .take(MAX_OPEN_WITH_APPS)
        .map(|app_info| App::Regular(RegularApp { app_info }));

    for app in app_infos {
        let item = gio::MenuItem::new(Some(&app.name()), None);
        item.set_action_and_target_value(Some("fl.open-with"), Some(&app.to_variant()));
        submenu.append_item(&item);
    }

    // We haven't added an "Open with..." entry further above, so adding it here.
    if default_app_info.is_some() {
        submenu.append(Some(&gettext("Open Wit_h…")), Some("fl.open-with-other"));
    }

    if submenu.n_items() > 0 {
        menu.append_submenu(Some(&gettext("Open Wit_h")), &submenu);
    }
}

pub fn file_popup_menu(main_win: &MainWindow, file_list: &FileList) -> Option<gio::Menu> {
    let files = file_list.selected_files();

    let menu = gio::Menu::new();

    // Add execute menu entry
    let first_file = files.first()?;
    if first_file.is_executable() && files.len() == 1 {
        let section = gio::Menu::new();
        section.append_item(&gio::MenuItem::new(
            Some(&gettext("E_xecute")),
            Some("fl.execute"),
        ));

        menu.append_section(None, &section);
    }

    // Add "Open With" popup entries
    add_open_with_entries(&menu, file_list);

    // Add plugin popup entries
    for (action_group_name, plugin) in main_win.plugin_manager().active_plugins() {
        if let Some(file_actions) = plugin.downcast_ref::<FileActions>()
            && let Some(plugin_menu) = file_actions.create_popup_menu_items(&main_win.state())
        {
            let plugin_menu = wrap_plugin_menu(&action_group_name, &plugin_menu);
            menu.append_section(None, &plugin_menu);
        }
    }

    // Add favorite applications menu entries
    let options = GeneralOptions::new();
    let fav_menu = gio::Menu::new();
    for app in load_favorite_apps(&options) {
        if fav_app_matches_files(&app, &files) {
            fav_menu.append_item(&fav_app_menu_item(&App::UserDefined(app)));
        }
    }
    menu.append_section(None, &fav_menu);

    if let Some(section) = create_action_script_menu() {
        menu.append_section(None, &section);
    }

    menu.append_section(None, &{
        gio::Menu::new()
            .action(UserAction::EditCapCut)
            .action(UserAction::EditCapCopy)
            .action(UserAction::EditCopyNames)
            .action(UserAction::FileDelete)
            .action(UserAction::FileRename)
            .action(UserAction::FileSendto)
            .action(UserAction::CommandOpenTerminal)
    });

    let menu = menu.action(UserAction::FileProperties);

    Some(menu)
}

pub fn list_popup_menu() -> gio::Menu {
    let mut columns_menu = gio::Menu::new();
    for column in ColumnID::all() {
        if column != ColumnID::Dir
            && let Some(title) = column.title()
        {
            columns_menu = columns_menu.item(title, format!("fl.toggle-column-{}", column.name()));
        }
    }

    gio::Menu::new().section(columns_menu).section(
        gio::Menu::new()
            .submenu(gettext("New"), {
                gio::Menu::new()
                    .action(UserAction::FileMkdir)
                    .action(UserAction::FileEditNewDoc)
            })
            .action(UserAction::EditCapPaste)
            .action(UserAction::CommandOpenTerminal)
            .action(UserAction::ViewRefresh),
    )
}
