use {
    super::*,
    crate::{
        command::*,
        display::*,
        errors::ProgramError,
        flag::Flag,
        help::HelpState,
        pattern::*,
        preview::*,
        print,
        stage::*,
        task_sync::Dam,
        tree::*,
        verb::*,
    },
    std::{
        path::{
            Path,
            PathBuf,
        },
        str::FromStr,
    },
};

/// a panel state, stackable to allow reverting
///  to a previous one
pub trait PanelState {
    fn get_type(&self) -> PanelStateType;

    fn set_mode(
        &mut self,
        mode: Mode,
    );
    fn get_mode(&self) -> Mode;

    /// called on start of `on_command`, remove the pending task
    fn clear_pending(&mut self) {}

    fn on_click(
        &mut self,
        _x: u16,
        _y: u16,
        _screen: Screen,
        _con: &AppContext,
    ) -> Result<CmdResult, ProgramError> {
        Ok(CmdResult::Keep)
    }

    fn on_double_click(
        &mut self,
        _x: u16,
        _y: u16,
        _screen: Screen,
        _con: &AppContext,
    ) -> Result<CmdResult, ProgramError> {
        Ok(CmdResult::Keep)
    }

    fn on_pattern(
        &mut self,
        _pat: InputPattern,
        _app_state: &AppState,
        _con: &AppContext,
    ) -> Result<CmdResult, ProgramError> {
        Ok(CmdResult::Keep)
    }

    fn on_mode_verb(
        &mut self,
        mode: Mode,
        con: &AppContext,
    ) -> CmdResult {
        if con.modal {
            self.set_mode(mode);
            CmdResult::Keep
        } else {
            CmdResult::error("modal mode not enabled in configuration")
        }
    }

    /// execute the internal with the optional given invocation.
    ///
    /// The invocation comes from the input and may be related
    /// to a different verb (the verb may have been triggered
    /// by a key shortcut)
    #[allow(clippy::too_many_arguments)]
    fn on_internal(
        &mut self,
        w: &mut W,
        invocation_parser: Option<&InvocationParser>,
        internal_exec: &InternalExecution,
        input_invocation: Option<&VerbInvocation>,
        trigger_type: TriggerType,
        app_state: &mut AppState,
        cc: &CmdContext,
    ) -> Result<CmdResult, ProgramError>;

    /// a generic implementation of `on_internal` which may be
    /// called by states when they don't have a specific
    /// behavior to execute
    #[allow(clippy::too_many_arguments)]
    fn on_internal_generic(
        &mut self,
        _w: &mut W,
        invocation_parser: Option<&InvocationParser>,
        internal_exec: &InternalExecution,
        input_invocation: Option<&VerbInvocation>,
        _trigger_type: TriggerType,
        app_state: &mut AppState,
        cc: &CmdContext,
    ) -> Result<CmdResult, ProgramError> {
        let con = &cc.app.con;
        let screen = cc.app.screen;
        let bang = input_invocation
            .map(|inv| inv.bang)
            .unwrap_or(internal_exec.bang);
        Ok(match internal_exec.internal {
            Internal::apply_flags => {
                debug!("applying flags input_invocation: {input_invocation:#?}");
                let flags = input_invocation.and_then(|inv| inv.args.as_ref());
                if let Some(flags) = flags {
                    self.with_new_options(
                        screen,
                        &|o| match o.apply_flags(flags) {
                            Ok(()) => "*flags applied*",
                            Err(e) => e,
                        },
                        bang,
                        con,
                    )
                } else {
                    CmdResult::error(":apply_flags needs flags as arguments")
                }
            }
            Internal::back => CmdResult::PopState,
            Internal::copy_line | Internal::copy_path => {
                #[cfg(not(feature = "clipboard"))]
                {
                    CmdResult::error("Clipboard feature not enabled at compilation")
                }
                #[cfg(feature = "clipboard")]
                {
                    if let Some(path) = self.selected_path() {
                        let path = path.to_string_lossy().to_string();
                        match terminal_clipboard::set_string(path) {
                            Ok(()) => CmdResult::Keep,
                            Err(_) => CmdResult::error("Clipboard error while copying path"),
                        }
                    } else {
                        CmdResult::error("Nothing to copy")
                    }
                }
            }
            Internal::close_panel_ok => CmdResult::ClosePanel {
                validate_purpose: true,
                panel_ref: PanelReference::Active,
                clear_cache: true,
            },
            Internal::close_panel_cancel => CmdResult::ClosePanel {
                validate_purpose: false,
                panel_ref: PanelReference::Active,
                clear_cache: false,
            },
            Internal::move_panel_divider => {
                let MoveDividerArgs { divider, dx } = get_arg(
                    input_invocation,
                    internal_exec,
                    MoveDividerArgs { divider: 0, dx: 1 },
                );
                CmdResult::ChangeLayout(LayoutInstruction::MoveDivider { divider, dx })
            }
            Internal::default_layout => CmdResult::ChangeLayout(LayoutInstruction::Clear),
            Internal::set_panel_width => {
                let SetPanelWidthArgs { panel, width } = get_arg(
                    input_invocation,
                    internal_exec,
                    SetPanelWidthArgs {
                        panel: 0,
                        width: 100,
                    },
                );
                CmdResult::ChangeLayout(LayoutInstruction::SetPanelWidth { panel, width })
            }
            #[cfg(any(
                target_os = "windows",
                all(
                    unix,
                    not(target_os = "macos"),
                    not(target_os = "ios"),
                    not(target_os = "android")
                )
            ))]
            Internal::purge_trash => {
                let res = trash::os_limited::list().and_then(trash::os_limited::purge_all);
                match res {
                    Ok(()) => CmdResult::RefreshState { clear_cache: false },
                    Err(e) => CmdResult::DisplayError(format!("{e}")),
                }
            }
            #[cfg(any(
                target_os = "windows",
                all(
                    unix,
                    not(target_os = "macos"),
                    not(target_os = "ios"),
                    not(target_os = "android")
                )
            ))]
            Internal::open_trash => {
                let trash_state = crate::trash::TrashState::new(self.tree_options(), con);
                match trash_state {
                    Ok(state) => {
                        let bang = input_invocation
                            .map(|inv| inv.bang)
                            .unwrap_or(internal_exec.bang);
                        if bang && cc.app.preview_panel.is_none() {
                            CmdResult::NewPanel {
                                state: Box::new(state),
                                purpose: PanelPurpose::None,
                                direction: HDir::Right,
                            }
                        } else {
                            CmdResult::new_state(Box::new(state))
                        }
                    }
                    Err(e) => CmdResult::DisplayError(format!("{e}")),
                }
            }
            #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
            Internal::filesystems => {
                let fs_state = crate::filesystems::FilesystemState::new(
                    self.selected_path(),
                    self.tree_options(),
                    con,
                );
                match fs_state {
                    Ok(state) => {
                        let bang = input_invocation
                            .map(|inv| inv.bang)
                            .unwrap_or(internal_exec.bang);
                        if bang && cc.app.preview_panel.is_none() {
                            CmdResult::NewPanel {
                                state: Box::new(state),
                                purpose: PanelPurpose::None,
                                direction: HDir::Right,
                            }
                        } else {
                            CmdResult::new_state(Box::new(state))
                        }
                    }
                    Err(e) => CmdResult::DisplayError(format!("{e}")),
                }
            }
            Internal::help => {
                let bang = input_invocation
                    .map(|inv| inv.bang)
                    .unwrap_or(internal_exec.bang);
                if bang && cc.app.preview_panel.is_none() {
                    CmdResult::NewPanel {
                        state: Box::new(HelpState::new(self.tree_options(), screen, con)),
                        purpose: PanelPurpose::None,
                        direction: HDir::Right,
                    }
                } else {
                    CmdResult::new_state(Box::new(HelpState::new(self.tree_options(), screen, con)))
                }
            }
            Internal::mode_input => self.on_mode_verb(Mode::Input, con),
            Internal::mode_command => self.on_mode_verb(Mode::Command, con),
            Internal::open_leave => {
                if let Some(selection) = self.selection() {
                    selection.to_opener(con)?
                } else {
                    CmdResult::error("no selection to open")
                }
            }
            Internal::open_preview => self.open_preview(None, false, cc),
            Internal::preview_image => self.open_preview(Some(PreviewMode::Image), false, cc),
            Internal::preview_text => self.open_preview(Some(PreviewMode::Text), false, cc),
            Internal::preview_tty => self.open_preview(Some(PreviewMode::Tty), false, cc),
            Internal::preview_binary => self.open_preview(Some(PreviewMode::Hex), false, cc),
            Internal::toggle_preview => self.open_preview(None, true, cc),
            Internal::sort_by_count => self.with_new_options(
                screen,
                &|o| {
                    if o.sort == Sort::Count {
                        o.sort = Sort::None;
                        o.show_counts = false;
                        "*not sorting anymore*"
                    } else {
                        o.sort = Sort::Count;
                        o.show_counts = true;
                        "*now sorting by file count*"
                    }
                },
                bang,
                con,
            ),
            Internal::sort_by_date => self.with_new_options(
                screen,
                &|o| {
                    if o.sort == Sort::Date {
                        o.sort = Sort::None;
                        o.show_dates = false;
                        "*not sorting anymore*"
                    } else {
                        o.sort = Sort::Date;
                        o.show_dates = true;
                        "*now sorting by last modified date*"
                    }
                },
                bang,
                con,
            ),
            Internal::sort_by_size => self.with_new_options(
                screen,
                &|o| {
                    if o.sort == Sort::Size {
                        o.sort = Sort::None;
                        o.show_sizes = false;
                        "*not sorting anymore*"
                    } else {
                        o.sort = Sort::Size;
                        o.show_sizes = true;
                        o.show_root_fs = true;
                        "*now sorting files and directories by total size*"
                    }
                },
                bang,
                con,
            ),
            Internal::sort_by_type => self.with_new_options(
                screen,
                &|o| match o.sort {
                    Sort::TypeDirsFirst => {
                        o.sort = Sort::TypeDirsLast;
                        "*sorting by type, directories last*"
                    }
                    Sort::TypeDirsLast => {
                        o.sort = Sort::None;
                        "*not sorting anymore*"
                    }
                    _ => {
                        o.sort = Sort::TypeDirsFirst;
                        "*sorting by type, directories first*"
                    }
                },
                bang,
                con,
            ),
            Internal::sort_by_type_dirs_first => self.with_new_options(
                screen,
                &|o| {
                    if o.sort == Sort::TypeDirsFirst {
                        o.sort = Sort::None;
                        "*not sorting anymore*"
                    } else {
                        o.sort = Sort::TypeDirsFirst;
                        "*now sorting by type, directories first*"
                    }
                },
                bang,
                con,
            ),
            Internal::sort_by_type_dirs_last => self.with_new_options(
                screen,
                &|o| {
                    if o.sort == Sort::TypeDirsLast {
                        o.sort = Sort::None;
                        "*not sorting anymore*"
                    } else {
                        o.sort = Sort::TypeDirsLast;
                        "*now sorting by type, directories last*"
                    }
                },
                bang,
                con,
            ),
            Internal::no_sort => self.with_new_options(
                screen,
                &|o| {
                    if o.sort == Sort::None {
                        "*still not searching*"
                    } else {
                        o.sort = Sort::None;
                        "*not sorting anymore*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_counts => self.with_new_options(
                screen,
                &|o| {
                    o.show_counts ^= true;
                    if o.show_counts {
                        "*displaying file counts*"
                    } else {
                        "*hiding file counts*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_tree => self.with_new_options(
                screen,
                &|o| {
                    o.show_tree ^= true;
                    if o.show_tree {
                        "*displaying tree structure (if possible)*"
                    } else {
                        "*displaying only current directory*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_dates => self.with_new_options(
                screen,
                &|o| {
                    o.show_dates ^= true;
                    if o.show_dates {
                        "*displaying last modified dates*"
                    } else {
                        "*hiding last modified dates*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_device_id => self.with_new_options(
                screen,
                &|o| {
                    o.show_device_id ^= true;
                    if o.show_device_id {
                        "*displaying device id*"
                    } else {
                        "*hiding device id*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_files => self.with_new_options(
                screen,
                &|o| {
                    o.only_folders ^= true;
                    if o.only_folders {
                        "*displaying only directories*"
                    } else {
                        "*displaying both files and directories*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_hidden => self.with_new_options(
                screen,
                &|o| {
                    o.show_hidden ^= true;
                    if o.show_hidden {
                        "h:**y** - *Hidden files displayed*"
                    } else {
                        "h:**n** - *Hidden files not displayed*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_root_fs => self.with_new_options(
                screen,
                &|o| {
                    o.show_root_fs ^= true;
                    if o.show_root_fs {
                        "*displaying filesystem info for the tree's root directory*"
                    } else {
                        "*removing filesystem info*"
                    }
                },
                bang,
                con,
            ),
            Internal::set_max_depth => {
                let args = input_invocation.and_then(|inv| inv.args.as_ref());

                if let Some(flags) = args {
                    self.with_new_options(
                        screen,
                        &|o| {
                            if let Ok(max_depth) = flags.parse::<u16>() {
                                o.max_depth = Some(max_depth);
                                "*max depth updated*"
                            } else {
                                "*depth must be an integer*"
                            }
                        },
                        bang,
                        con,
                    )
                } else {
                    CmdResult::error(":set_max_depth needs a depth as an argument")
                }
            }
            Internal::unset_max_depth => self.with_new_options(
                screen,
                &|o| {
                    o.max_depth = None;
                    "*cleared max depth*"
                },
                bang,
                con,
            ),
            Internal::toggle_git_ignore | Internal::toggle_ignore => self.with_new_options(
                screen,
                &|o| {
                    o.respect_git_ignore ^= true;
                    if o.respect_git_ignore {
                        "gi:**y** - *applying gitignore rules*"
                    } else {
                        "gi:**n** - *not applying gitignore rules*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_git_file_info => self.with_new_options(
                screen,
                &|o| {
                    o.show_git_file_info ^= true;
                    if o.show_git_file_info {
                        "*displaying git info next to files*"
                    } else {
                        "*removing git file info*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_git_status => self.with_new_options(
                screen,
                &|o| {
                    if o.filter_by_git_status {
                        o.filter_by_git_status = false;
                        "*not filtering according to git status anymore*"
                    } else {
                        o.filter_by_git_status = true;
                        o.show_hidden = true;
                        "*only displaying new or modified files*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_perm => self.with_new_options(
                screen,
                &|o| {
                    o.show_permissions ^= true;
                    if o.show_permissions {
                        "*displaying file permissions*"
                    } else {
                        "*removing file permissions*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_sizes => self.with_new_options(
                screen,
                &|o| {
                    if o.show_sizes {
                        o.show_sizes = false;
                        o.show_root_fs = false;
                        "*removing sizes of files and directories*"
                    } else {
                        o.show_sizes = true;
                        o.show_root_fs = true;
                        "*now displaying sizes of files and directories*"
                    }
                },
                bang,
                con,
            ),
            Internal::toggle_trim_root => self.with_new_options(
                screen,
                &|o| {
                    o.trim_root ^= true;
                    if o.trim_root {
                        "*now trimming root from excess files*"
                    } else {
                        "*not trimming root files anymore*"
                    }
                },
                bang,
                con,
            ),
            Internal::close_preview => {
                if let Some(id) = cc.app.preview_panel {
                    CmdResult::ClosePanel {
                        validate_purpose: false,
                        panel_ref: PanelReference::Id(id),
                        clear_cache: false,
                    }
                } else {
                    CmdResult::Keep
                }
            }
            Internal::escape => CmdResult::HandleInApp(Internal::escape),
            Internal::focus_staging_area_no_open => {
                CmdResult::HandleInApp(Internal::focus_staging_area_no_open)
            }
            // panel_left depends on the kind of panel and is usually handled
            // in a specific state, contrary to panel_left_no_open
            Internal::panel_left | Internal::panel_left_no_open => {
                CmdResult::HandleInApp(Internal::panel_left_no_open)
            }
            Internal::focus_panel_left => CmdResult::HandleInApp(Internal::focus_panel_left),
            Internal::focus_panel_right => CmdResult::HandleInApp(Internal::focus_panel_right),
            // panel_right depends on the kind of panel and is usually handled
            // in a specific state, contrary to panel_right_no_open
            Internal::panel_right | Internal::panel_right_no_open => {
                CmdResult::HandleInApp(Internal::panel_right_no_open)
            }
            Internal::toggle_second_tree => CmdResult::HandleInApp(Internal::toggle_second_tree),
            Internal::toggle_watch => CmdResult::HandleInApp(Internal::toggle_watch),
            Internal::clear_stage => {
                app_state.stage.clear();
                if let Some(panel_id) = cc.app.stage_panel {
                    CmdResult::ClosePanel {
                        validate_purpose: false,
                        panel_ref: PanelReference::Id(panel_id),
                        clear_cache: false,
                    }
                } else {
                    CmdResult::Keep
                }
            }
            Internal::stage => self.stage(app_state, cc, con),
            Internal::unstage => self.unstage(app_state, cc, con),
            Internal::toggle_stage => self.toggle_stage(app_state, cc, con),
            Internal::close_staging_area => {
                if let Some(id) = cc.app.stage_panel {
                    CmdResult::ClosePanel {
                        validate_purpose: false,
                        panel_ref: PanelReference::Id(id),
                        clear_cache: false,
                    }
                } else {
                    CmdResult::Keep
                }
            }
            Internal::open_staging_area => {
                if cc.app.stage_panel.is_none() {
                    CmdResult::NewPanel {
                        state: Box::new(StageState::new(app_state, self.tree_options(), con)),
                        purpose: PanelPurpose::None,
                        direction: HDir::Right,
                    }
                } else {
                    CmdResult::Keep
                }
            }
            Internal::toggle_staging_area => {
                if let Some(id) = cc.app.stage_panel {
                    CmdResult::ClosePanel {
                        validate_purpose: false,
                        panel_ref: PanelReference::Id(id),
                        clear_cache: false,
                    }
                } else {
                    CmdResult::NewPanel {
                        state: Box::new(StageState::new(app_state, self.tree_options(), con)),
                        purpose: PanelPurpose::None,
                        direction: HDir::Right,
                    }
                }
            }
            Internal::set_syntax_theme => CmdResult::HandleInApp(Internal::set_syntax_theme),
            Internal::print_path => print::print_paths(self.sel_info(app_state), con)?,
            Internal::print_relative_path => {
                print::print_relative_paths(self.sel_info(app_state), con)?
            }
            Internal::refresh => CmdResult::RefreshState { clear_cache: true },
            Internal::quit => CmdResult::Quit,
            Internal::clear_output => {
                verb_clear_output(con).unwrap_or_else(|e| CmdResult::DisplayError(format!("{e}")))
            }
            Internal::write_output => {
                let sel_info = self.sel_info(app_state);
                let mut exec_builder = match input_invocation {
                    Some(inv) => ExecutionBuilder::with_invocation(
                        invocation_parser,
                        sel_info,
                        app_state,
                        inv.args.as_ref(),
                    ),
                    None => ExecutionBuilder::without_invocation(sel_info, app_state),
                };
                let mut content = String::new();
                // There's normally exactly one string, except when the selection
                // is multiple (stage), there's an arg, and the coarity is
                // per_selection, in which case we have one line per selection
                let coarity = internal_exec.coarity();
                match (coarity, sel_info, internal_exec.arg.as_ref()) {
                    (CommandCoarity::PerSelection, SelInfo::More(stage), Some(pattern)) => {
                        let pattern = ExecPattern::from_string(pattern);
                        debug!("write_output executed once per selection");
                        // we execute once per selection (may be zero if the stage is empty)
                        let sels = stage.paths().iter().map(|path| Selection {
                            path,
                            line: 0,
                            stype: SelectionType::from(path),
                            is_exe: false,
                        });
                        for sel in sels {
                            content.push_str(&exec_builder.sel_shell_exec_string(&pattern, Some(sel), con));
                        }
                    }
                    (_, _, Some(pattern)) => {
                        debug!("write_output executed once, with pattern");
                        let pattern = ExecPattern::from_string(pattern);
                        // we execute only once, pattern is merging or ignoring the selection
                        content.push_str(&exec_builder.shell_exec_string(&pattern, con));
                    }
                    _ => {
                        // no pattern, the selection is not relevant
                        // (we're writing just what the input invocation contains)
                        debug!("write_output executed once, with no pattern");
                        let line = input_invocation
                            .and_then(|inv| inv.args.as_ref())
                            .map_or("", String::as_str);
                        content.push_str(line);
                    }
                }
                if !content.is_empty() {
                    verb_write(con, &content)?;
                }
                CmdResult::Keep
            }
            internal if internal.is_input_related() => CmdResult::HandleInApp(internal),
            _ => CmdResult::Keep,
        })
    }

    fn stage(
        &self,
        app_state: &mut AppState,
        cc: &CmdContext,
        con: &AppContext,
    ) -> CmdResult {
        if let Some(path) = self.selected_path() {
            let path = path.to_path_buf();
            app_state.stage.add(path);
            if cc.app.con.auto_open_staging_area && cc.app.stage_panel.is_none() {
                return CmdResult::NewPanel {
                    state: Box::new(StageState::new(app_state, self.tree_options(), con)),
                    purpose: PanelPurpose::None,
                    direction: HDir::Right,
                };
            }
        } else {
            // TODO display error ?
            warn!("no path in state");
        }
        CmdResult::Keep
    }

    fn unstage(
        &self,
        app_state: &mut AppState,
        cc: &CmdContext,
        _con: &AppContext,
    ) -> CmdResult {
        if let Some(path) = self.selected_path() {
            if app_state.stage.remove(path) && app_state.stage.is_empty() {
                if let Some(panel_id) = cc.app.stage_panel {
                    return CmdResult::ClosePanel {
                        validate_purpose: false,
                        panel_ref: PanelReference::Id(panel_id),
                        clear_cache: false,
                    };
                }
            }
        }
        CmdResult::Keep
    }

    fn toggle_stage(
        &self,
        app_state: &mut AppState,
        cc: &CmdContext,
        con: &AppContext,
    ) -> CmdResult {
        if let Some(path) = self.selected_path() {
            if app_state.stage.contains(path) {
                self.unstage(app_state, cc, con)
            } else {
                self.stage(app_state, cc, con)
            }
        } else {
            CmdResult::error("no selection")
        }
    }

    fn execute_verb(
        &mut self,
        w: &mut W, // needed because we may want to switch from alternate in some externals
        verb: &Verb,
        invocation: Option<&VerbInvocation>,
        trigger_type: TriggerType,
        app_state: &mut AppState,
        cc: &CmdContext,
    ) -> Result<CmdResult, ProgramError> {
        if verb.needs_selection && !self.has_at_least_one_selection(app_state) {
            return Ok(CmdResult::error("This verb needs a selection"));
        }
        if verb.needs_another_panel && app_state.other_panel_path.is_none() {
            return Ok(CmdResult::error("This verb needs another panel"));
        }
        let res = match &verb.execution {
            VerbExecution::Internal(internal_exec) => self.on_internal(
                w,
                verb.invocation_parser.as_ref(),
                internal_exec,
                invocation,
                trigger_type,
                app_state,
                cc,
            ),
            VerbExecution::External(external) => {
                self.execute_external(w, verb, external, invocation, app_state, cc)
            }
            VerbExecution::Sequence(seq_ex) => {
                self.execute_sequence(w, verb, seq_ex, invocation, app_state, cc)
            }
        };
        if res.is_ok() {
            // if the stage has been emptied by the operation (eg a "rm"), we
            // close it
            app_state.stage.refresh();
            if app_state.stage.is_empty() {
                if let Some(id) = cc.app.stage_panel {
                    return Ok(CmdResult::ClosePanel {
                        validate_purpose: false,
                        panel_ref: PanelReference::Id(id),
                        clear_cache: true,
                    });
                }
            }
        }
        res
    }

    fn execute_external(
        &mut self,
        w: &mut W,
        verb: &Verb,
        external_execution: &ExternalExecution,
        invocation: Option<&VerbInvocation>,
        app_state: &mut AppState,
        cc: &CmdContext,
    ) -> Result<CmdResult, ProgramError> {
        let sel_info = self.sel_info(app_state);
        if let Some(invocation) = &invocation {
            if let Some(error) = verb.check_args(sel_info, invocation, &app_state.other_panel_path)
            {
                debug!("verb.check_args prevented execution: {:?}", &error);
                return Ok(CmdResult::error(error));
            }
        }
        let exec_builder = ExecutionBuilder::with_invocation(
            verb.invocation_parser.as_ref(),
            sel_info,
            app_state,
            if let Some(inv) = invocation {
                inv.args.as_ref()
            } else {
                None
            },
        );
        external_execution.to_cmd_result(w, exec_builder, cc.app.con)
    }

    fn execute_sequence(
        &mut self,
        _w: &mut W,
        verb: &Verb,
        seq_ex: &SequenceExecution,
        invocation: Option<&VerbInvocation>,
        app_state: &mut AppState,
        cc: &CmdContext,
    ) -> Result<CmdResult, ProgramError> {
        let sel_info = self.sel_info(app_state);
        if matches!(sel_info, SelInfo::More(_)) {
            // sequences would be hard to execute as the execution on a file can change the
            // state in too many ways (changing selection, focused panel, parent, unstage or
            // stage files, removing the staged paths, etc.)
            return Ok(CmdResult::error(
                "sequences can't be executed on multiple selections",
            ));
        }
        let mut exec_builder = ExecutionBuilder::with_invocation(
            verb.invocation_parser.as_ref(),
            sel_info,
            app_state,
            if let Some(inv) = invocation {
                inv.args.as_ref()
            } else {
                None
            },
        );
        let sequence = exec_builder.sequence(
            &seq_ex.sequence,
            &cc.app.con.verb_store,
            cc.app.con,
            Some(self.get_type()),
        );
        Ok(CmdResult::ExecuteSequence { sequence })
    }

    /// change the state, does no rendering
    fn on_command(
        &mut self,
        w: &mut W,
        app_state: &mut AppState,
        cc: &CmdContext,
    ) -> Result<CmdResult, ProgramError> {
        self.clear_pending();
        let con = &cc.app.con;
        let screen = cc.app.screen;
        match &cc.cmd {
            Command::Click(x, y) => self.on_click(*x, *y, screen, con),
            Command::DoubleClick(x, y) => self.on_double_click(*x, *y, screen, con),
            Command::PatternEdit { raw, expr } => match InputPattern::new(raw.clone(), expr, con) {
                Ok(pattern) => self.on_pattern(pattern, app_state, con),
                Err(e) => Ok(CmdResult::DisplayError(format!("{e}"))),
            },
            Command::VerbTrigger {
                verb_id,
                input_invocation,
            } => self.execute_verb(
                w,
                con.verb_store.verb(*verb_id),
                input_invocation.as_ref(),
                TriggerType::Other,
                app_state,
                cc,
            ),
            Command::Internal {
                internal,
                input_invocation,
            } => self.on_internal(
                w,
                None,
                &InternalExecution::from_internal(*internal),
                input_invocation.as_ref(),
                TriggerType::Other,
                app_state,
                cc,
            ),
            Command::VerbInvocate(invocation) => {
                let sel_info = self.sel_info(app_state);
                match con.verb_store.search_sel_info(
                    &invocation.name,
                    sel_info,
                    Some(self.get_type()),
                ) {
                    PrefixSearchResult::Match(_, verb) => self.execute_verb(
                        w,
                        verb,
                        Some(invocation),
                        TriggerType::Input(verb),
                        app_state,
                        cc,
                    ),
                    _ => Ok(CmdResult::verb_not_found(&invocation.name)),
                }
            }
            Command::None | Command::VerbEdit(_) => {
                // we do nothing here, the real job is done in get_status
                Ok(CmdResult::Keep)
            }
        }
    }

    /// return a cmdresult asking for the opening of a preview
    fn open_preview(
        &mut self,
        preferred_mode: Option<PreviewMode>,
        close_if_open: bool,
        cc: &CmdContext,
    ) -> CmdResult {
        if let Some(id) = cc.app.preview_panel {
            if close_if_open {
                CmdResult::ClosePanel {
                    validate_purpose: false,
                    panel_ref: PanelReference::Id(id),
                    clear_cache: false,
                }
            } else if preferred_mode.is_some() {
                // we'll make the preview mode change be
                // applied on the preview panel
                CmdResult::ApplyOnPanel { id }
            } else {
                CmdResult::Keep
            }
        } else if let Some(path) = self.selected_path() {
            CmdResult::NewPanel {
                state: Box::new(PreviewState::new(
                    path.to_path_buf(),
                    InputPattern::none(),
                    preferred_mode,
                    self.tree_options(),
                    cc.app.con,
                )),
                purpose: PanelPurpose::Preview,
                direction: HDir::Right,
            }
        } else {
            CmdResult::error("no selected file")
        }
    }

    /// must return None if the state doesn't display a file tree
    fn tree_root(&self) -> Option<&Path> {
        None
    }

    fn watchable_paths(&self) -> Vec<PathBuf> {
        vec![]
    }

    fn selected_path(&self) -> Option<&Path>;

    fn selection(&self) -> Option<Selection<'_>>;

    fn sel_info<'c>(
        &'c self,
        _app_state: &'c AppState,
    ) -> SelInfo<'c> {
        // overloaded in stage_state
        match self.selection() {
            None => SelInfo::None,
            Some(selection) => SelInfo::One(selection),
        }
    }

    fn has_at_least_one_selection(
        &self,
        _app_state: &AppState,
    ) -> bool {
        true // overloaded in stage_state
    }

    fn refresh(
        &mut self,
        screen: Screen,
        con: &AppContext,
    ) -> Command; // FIXME this command is never used

    fn tree_options(&self) -> TreeOptions;

    /// Build a cmdResult in response to a command being a change of
    /// tree options. This may or not be a new state.
    ///
    /// The provided `change_options` function returns a status message
    /// explaining the change
    fn with_new_options(
        &mut self,
        screen: Screen,
        change_options: &dyn Fn(&mut TreeOptions) -> &'static str,
        in_new_panel: bool,
        con: &AppContext,
    ) -> CmdResult;

    fn do_pending_task(
        &mut self,
        _app_state: &mut AppState,
        _screen: Screen,
        _con: &AppContext,
        _dam: &mut Dam,
    ) -> Result<(), ProgramError> {
        // no pending task in default impl
        unreachable!();
    }

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

    fn display(
        &mut self,
        w: &mut W,
        disc: &DisplayContext,
    ) -> Result<(), ProgramError>;

    /// return the flags to display
    fn get_flags(&self) -> Vec<Flag> {
        vec![]
    }

    fn get_starting_input(&self) -> String {
        String::new()
    }

    fn set_selected_path(
        &mut self,
        _path: PathBuf,
        _con: &AppContext,
    ) {
        // this function is useful for preview states
    }

    /// return the status which should be used when there's no verb edited
    fn no_verb_status(
        &self,
        _has_previous_state: bool,
        _con: &AppContext,
        _width: usize, // available width
    ) -> Status {
        Status::from_message("Hit *esc* to get back, or a space to start a verb")
    }

    fn get_status(
        &self,
        app_state: &AppState,
        cc: &CmdContext,
        has_previous_state: bool,
        width: usize,
    ) -> Status {
        match &cc.cmd {
            Command::PatternEdit { .. } => {
                self.no_verb_status(has_previous_state, cc.app.con, width)
            }
            Command::VerbEdit(invocation)
            | Command::VerbTrigger {
                input_invocation: Some(invocation),
                ..
            } => {
                if invocation.name.is_empty() {
                    Status::new(
                        "Type a verb then *enter* to execute it (*?* for the list of verbs)",
                        false,
                    )
                } else {
                    let sel_info = self.sel_info(app_state);
                    match cc.app.con.verb_store.search_sel_info(
                        &invocation.name,
                        sel_info,
                        Some(self.get_type()),
                    ) {
                        PrefixSearchResult::NoMatch => {
                            Status::new("No matching verb (*?* for the list of verbs)", true)
                        }
                        PrefixSearchResult::Match(_, verb) => {
                            self.get_verb_status(verb, invocation, sel_info, cc, app_state)
                        }
                        PrefixSearchResult::Matches(completions) => Status::new(
                            format!(
                                "Possible verbs: {}",
                                completions
                                    .iter()
                                    .map(|c| format!("*{c}*"))
                                    .collect::<Vec<String>>()
                                    .join(", "),
                            ),
                            false,
                        ),
                    }
                }
            }
            _ => self.no_verb_status(has_previous_state, cc.app.con, width),
        }
    }

    fn get_verb_status(
        &self,
        verb: &Verb,
        invocation: &VerbInvocation,
        sel_info: SelInfo<'_>,
        cc: &CmdContext,
        app_state: &AppState,
    ) -> Status {
        if sel_info.count_paths() > 1 {
            if let VerbExecution::External(external) = &verb.execution {
                if external.exec_mode != ExternalExecutionMode::StayInBroot {
                    let coarity = external.exec_pattern.coarity();
                    info!("coarity of the command is {coarity:?}");
                    if coarity == CommandCoarity::PerSelection {
                        return Status::new(MULTI_SELECTION_ERROR.to_owned(), true);
                    }
                }
            }
            // right now there's no check for sequences but they're inherently dangerous
        }
        if let Some(err) = verb.check_args(sel_info, invocation, &app_state.other_panel_path) {
            Status::new(err, true)
        } else {
            Status::new(
                verb.get_status_markdown(sel_info, app_state, invocation, cc.app.con),
                false,
            )
        }
    }
}

pub fn get_arg<T: Copy + FromStr>(
    verb_invocation: Option<&VerbInvocation>,
    internal_exec: &InternalExecution,
    default: T,
) -> T {
    verb_invocation
        .and_then(|vi| vi.args.as_ref())
        .or(internal_exec.arg.as_ref())
        .and_then(|s| s.parse::<T>().ok())
        .unwrap_or(default)
}
