/// Describes what the caller should do after an ex command runs.
///
/// Variants that mutate editor state (substitute, goto-line, clear-highlight)
/// are applied in-place inside the dispatcher; everything else is returned so
/// the host loop can act on it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExEffect {
    /// The `:s/pat/rep/c` (or `/gc`) flag triggered interactive confirm-mode.
    ///
    /// The caller should enter a per-match prompt loop: display each match,
    /// ask `y/n/a/q/l`, then apply the accepted subset via
    /// [`hjkl_engine::apply_collected_matches`].
    SubstituteConfirm {
        /// All candidate matches in document order (low row first).
        matches: Vec<hjkl_engine::SubstituteMatch>,
    },
    /// Nothing happened (empty input or no-op effect).
    None,
    /// Save the current buffer to the current filename.
    Save,
    /// Save to a specific path (`:w <path>`). The caller updates its
    /// `filename` field so future `:w` writes there.
    SaveAs(String),
    /// Quit (`:q`, `:q!`, `:wq`, `:x`).
    Quit { force: bool, save: bool },
    /// Unknown command — caller should surface as an error toast.
    Unknown(String),
    /// Substitution finished — report replacement count and lines changed.
    Substituted { count: usize, lines_changed: usize },
    /// A no-op response for successful commands that don't need a side
    /// effect but should not be reported as unknown (e.g. `:noh`).
    Ok,
    /// Surface an informational message (single-line or unclassified multi-line).
    Info(String),
    /// Surface a titled multi-line listing (`:reg`, `:marks`, `:jumps`, `:changes`).
    ///
    /// The `title` is a static label used by the host to open a named info
    /// popup without brittle header-prefix string matching.
    InfoTitled {
        /// Short human-readable label for the popup window (e.g. `"registers"`).
        title: &'static str,
        /// Full body text, newline-separated.
        content: String,
    },
    /// Surface an error message (syntax error, bad pattern, …).
    Error(String),
    /// `:e <path>` / `:edit <path>` — open a different file in the current
    /// window. An empty `path` means reload the current buffer.
    EditFile { path: String, force: bool },
    /// `:bd[!]` / `:bw[!]` — close the current buffer.
    /// `wipe = true` for `:bwipeout`; `force = true` when `!` was given.
    BufferDelete { force: bool, wipe: bool },
    /// `:[range]put[!] [{reg}]` / `:[range]pu[!] [{reg}]` — paste register
    /// contents linewise at the addressed line (or the cursor line when no
    /// range was given). `above = true` for `:put!` (paste before the
    /// addressed line instead of after). `target_line` is the range's
    /// resolved 1-based line address — `Some(0)` is vim's special "before
    /// line 1" address for `:0put`; `None` means "no range given, use the
    /// cursor's line".
    PutRegister {
        reg: char,
        above: bool,
        target_line: Option<usize>,
    },
    /// `:saveas {path}` / `:sav {path}` — write buffer to `path` AND rename
    /// the buffer identity so future `:w` writes there.
    /// Distinct from `SaveAs` (`:w <path>`) which writes elsewhere but keeps
    /// the buffer's own filename unchanged.
    SaveAndRename { path: String },
    /// `:file {name}` — rename the current buffer in-memory without writing.
    RenameBuffer { name: String },
    /// `:cd [{path}]` — change the working directory. An empty path means
    /// `$HOME`. The directory change is applied inside the handler; the new
    /// path is surfaced so the host can update its status-line / title.
    Cwd(String),
    /// `:redraw[!]` — signal the host to repaint on the next frame.
    /// When `clear` is `true` (`:redraw!`) the host should clear the terminal
    /// before repainting. `:redraw` (no `!`) requests a plain repaint without
    /// clearing.
    Redraw { clear: bool },
    /// `:preserve` — force-write the swap file for the active buffer
    /// immediately, regardless of the `updatetime` idle timer.
    Preserve,
    /// `:recover [file]` — explicitly trigger swap-file recovery.
    ///
    /// An empty `path` means recover the current buffer's swap.
    /// A non-empty `path` means open that file and force recovery on it.
    Recover(String),
    /// Quickfix-list command (`:copen`, `:cnext`, `:grep`, … — #184). The host
    /// owns the quickfix list state, popup, and file-open; this just names the
    /// requested action.
    Quickfix(QfCommand),
    /// Location-list command (`:lopen`, `:lnext`, `:lgrep`, … — #184 phase 3).
    /// Same actions as [`ExEffect::Quickfix`] but against the window-local
    /// location list rather than the global quickfix list.
    Location(QfCommand),
}

/// A quickfix-list action requested by a `:c*` / `:grep` command (#184).
/// The host interprets these against its `QfList` + popup state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QfCommand {
    /// `:copen` — show the quickfix popup.
    Open,
    /// `:cclose` — hide the quickfix popup.
    Close,
    /// `:cwindow` / `:cw` — show the popup only when the list is non-empty;
    /// when the list is empty, close the popup if it is open (vim `:h :cwindow`).
    Window,
    /// `:cnext` — jump to the next entry.
    Next,
    /// `:cprev` — jump to the previous entry.
    Prev,
    /// `:cfirst` — jump to the first entry.
    First,
    /// `:clast` — jump to the last entry.
    Last,
    /// `:cc [N]` — jump to the 1-based entry `N`; `0` means "current".
    Nth(usize),
    /// `:grep <pattern>` — run ripgrep, populate the quickfix list, open popup.
    Grep(String),
    /// `:make [args]` — run `makeprg` (appending `args`), parse the output via
    /// the errorformat, populate the quickfix list, open popup.
    Make(String),
    /// `:cexpr` / `:cgetexpr` / `:caddexpr` (and `l*` variants) — parse `text`
    /// via the current `&errorformat` and populate the quickfix / location list.
    ///
    /// `append = false` replaces the list (`:cexpr`, `:cgetexpr`);
    /// `append = true` appends to it (`:caddexpr`).
    /// `jump = true` jumps to the first entry after population (`:cexpr`, `:lexpr`).
    Expr {
        /// The text to parse (may be quoted or raw).
        text: String,
        /// Whether to append to the existing list (instead of replacing it).
        append: bool,
        /// Whether to jump to the first entry after population.
        jump: bool,
    },
    /// `:cbuffer` / `:cgetbuffer` / `:caddbuffer` (and `l*` variants) — parse
    /// the CURRENT buffer's text via `&errorformat` and populate the list.
    ///
    /// `append = false` replaces the list; `append = true` appends.
    /// `jump = true` jumps to the first entry after population.
    FromBuffer {
        /// Whether to append to the existing list (instead of replacing it).
        append: bool,
        /// Whether to jump to the first entry after population.
        jump: bool,
    },
    /// `:cfile` / `:cgetfile` / `:caddfile` (and `l*` variants) — read `path`
    /// from disk, parse via `&errorformat`, and populate the list.
    ///
    /// An empty `path` means use the default errorfile name `"errors.err"`
    /// (vim's default `'errorfile'`).
    /// `append = false` replaces the list; `append = true` appends.
    /// `jump = true` jumps to the first entry after population.
    FromFile {
        /// Path to the error file. Empty string → `"errors.err"`.
        path: String,
        /// Whether to append to the existing list (instead of replacing it).
        append: bool,
        /// Whether to jump to the first entry after population.
        jump: bool,
    },
    /// `:colder [N]` / `:lolder [N]` — activate an older error list.
    ///
    /// The current list is pushed onto the newer stack and the N-th previous
    /// list (default 1) is popped from the older stack and made current.
    /// Vim keeps up to 10 quickfix lists. Saturates when the older stack is
    /// exhausted (no error in vim; we just stop moving).
    Older(usize),
    /// `:cnewer [N]` / `:lnewer [N]` — activate a newer error list.
    ///
    /// Mirror of [`QfCommand::Older`]: pops from the newer stack.
    Newer(usize),
    /// `:cdo {cmd}` / `:ldo {cmd}` — run `cmd` once per valid entry
    /// (jumping to each entry first).
    ///
    /// `:cfdo {cmd}` / `:lfdo {cmd}` — run `cmd` once per DISTINCT FILE
    /// (the first entry of each file in the list, in order).
    ///
    /// `per_file = false` → visit every entry (`:cdo` / `:ldo`).
    /// `per_file = true`  → visit only the first entry per file (`:cfdo` / `:lfdo`).
    Do {
        /// The ex command to run at each visited entry.
        cmd: String,
        /// `false` = per-entry (`:cdo`), `true` = per-file (`:cfdo`).
        per_file: bool,
    },
    /// `:diagnostics` — populate the quickfix / location list from LSP diagnostics.
    ///
    /// For [`ExEffect::Quickfix`]: collects diagnostics from ALL non-explorer
    /// buffer slots, sorted by `(path, row, col)`.
    ///
    /// For [`ExEffect::Location`]: collects diagnostics from the CURRENT buffer
    /// slot only.
    Diagnostics,
}
