//! Clap argument model for the public RMUX command surface.

use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

use clap::{ArgAction, Args, CommandFactory, FromArgMatches, Parser};
use rmux_core::command_parser::{CommandEntry, ParsedCommands, COMMAND_TABLE};

#[cfg(test)]
use rmux_core::command_parser::CommandParser as TmuxCommandParser;

#[path = "cli_args/buffer.rs"]
mod buffer;
pub(crate) use buffer::{
    DeleteBufferArgs, ListBuffersArgs, LoadBufferArgs, PasteBufferArgs, SaveBufferArgs,
    SetBufferArgs, ShowBufferArgs,
};
#[path = "cli_args/client.rs"]
mod client;
pub(crate) use client::{
    DetachClientArgs, ListClientsArgs, RefreshClientArgs, SuspendClientArgs, SwitchClientArgs,
};
#[path = "cli_args/config.rs"]
mod config;
pub(crate) use config::{
    build_scope, SetEnvironmentArgs, SetHookArgs, SetOptionArgs, SetOptionCommandKind,
    ShowEnvironmentArgs, ShowHooksArgs, ShowOptionsArgs, ShowOptionsCommandKind,
};
#[path = "cli_args/keys.rs"]
mod keys;
pub(crate) use keys::{BindKeyArgs, ListKeysArgs, SendKeysArgs, SendPrefixArgs, UnbindKeyArgs};
#[path = "cli_args/history.rs"]
mod history;
pub(crate) use history::{CapturePaneArgs, ClearHistoryArgs};
#[path = "cli_args/inventory.rs"]
mod inventory;
pub(crate) use inventory::ListCommandsArgs;
#[path = "cli_args/mode_tree.rs"]
mod mode_tree;
pub(crate) use mode_tree::{ChooseBufferArgs, ChooseClientArgs, ChooseTreeArgs, CustomizeModeArgs};
#[path = "cli_args/message.rs"]
mod message;
pub(crate) use message::DisplayMessageArgs;
#[path = "cli_args/prompt.rs"]
mod prompt;
pub(crate) use prompt::{ConfirmBeforeArgs, PromptArgs, PromptHistoryArgs};
#[path = "cli_args/queue.rs"]
mod queue;
use queue::{command_from_parsed, parse_command_queue};
#[path = "cli_args/script.rs"]
mod script;
pub(crate) use script::{IfShellArgs, RunShellArgs, SourceFileArgs, WaitForArgs};
#[path = "cli_args/overlay.rs"]
mod overlay;
pub(crate) use overlay::{DisplayMenuArgs, DisplayPopupArgs};
#[path = "cli_args/targets.rs"]
mod targets;
use targets::{parse_session_name, parse_target};
pub(crate) use targets::{parse_target_spec, TargetSpec};
#[path = "cli_args/pane.rs"]
mod pane;
use pane::{parse_resize_pane_args, parse_select_pane_args};
pub(crate) use pane::{
    BreakPaneArgs, ClockModeArgs, CopyModeArgs, DisplayPanesArgs, JoinPaneArgs, ListPanesArgs,
    PaneTargetArgs, PipePaneArgs, ResizePaneArgs, RespawnPaneArgs, SelectLayoutArgs,
    SelectPaneArgs, SplitWindowArgs, SwapPaneArgs,
};
#[path = "cli_args/session.rs"]
mod session;
pub(crate) use session::{
    AlertSessionTargetArgs, AttachSessionArgs, ClientTargetArgs, KillSessionArgs, ListSessionsArgs,
    NewSessionArgs, RenameSessionArgs, ServerAccessArgs, SessionTargetArgs, ShowMessagesArgs,
};
#[path = "cli_args/window.rs"]
mod window;
pub(crate) use window::{
    FindWindowArgs, KillWindowArgs, LinkWindowArgs, ListWindowsArgs, MoveWindowArgs, NewWindowArgs,
    RenameWindowArgs, ResizeWindowArgs, RespawnWindowArgs, RotateWindowArgs, SwapWindowArgs,
    UnlinkWindowArgs, WindowTargetArgs,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DocumentedCliAlias {
    pub(crate) alias: &'static str,
    pub(crate) expansion: &'static str,
}

const DOCUMENTED_CLI_ALIASES: &[DocumentedCliAlias] = &[
    DocumentedCliAlias {
        alias: "choose-session",
        expansion: "choose-tree -s",
    },
    DocumentedCliAlias {
        alias: "choose-window",
        expansion: "choose-tree -w",
    },
];

static IMPLEMENTED_COMMAND_SURFACE: OnceLock<Vec<&'static CommandEntry>> = OnceLock::new();

static IMPLEMENTED_COMMAND_HELP: OnceLock<String> = OnceLock::new();

pub(crate) fn parse<I, T>(args: I) -> Result<Cli, clap::Error>
where
    I: IntoIterator<Item = T>,
    T: Into<OsString> + Clone,
{
    let mut command = RawCli::command();
    command = command.after_help(implemented_command_help());
    let matches = command.try_get_matches_from(args)?;
    let raw = RawCli::from_arg_matches(&matches)?;
    let parsed_commands = parse_command_queue(&raw.command)?;
    Cli::from_raw(raw, parsed_commands)
}

fn build_implemented_command_help() -> String {
    let mut help = String::from("Commands:\n");
    for entry in implemented_command_surface() {
        help.push_str("  ");
        help.push_str(entry.name);
        if let Some(alias) = entry.alias {
            help.push_str(" (");
            help.push_str(alias);
            help.push(')');
        }
        help.push('\n');
    }

    help.push_str("\nBuilt-in command aliases:\n");
    for alias in documented_cli_aliases() {
        help.push_str("  ");
        help.push_str(alias.alias);
        help.push_str(" => ");
        help.push_str(alias.expansion);
        help.push('\n');
    }

    help.trim_end().to_owned()
}

pub(crate) fn implemented_command_surface() -> &'static [&'static CommandEntry] {
    IMPLEMENTED_COMMAND_SURFACE
        .get_or_init(|| COMMAND_TABLE.iter().collect())
        .as_slice()
}

fn implemented_command_help() -> &'static str {
    IMPLEMENTED_COMMAND_HELP
        .get_or_init(build_implemented_command_help)
        .as_str()
}

pub(crate) fn documented_cli_aliases() -> &'static [DocumentedCliAlias] {
    DOCUMENTED_CLI_ALIASES
}

#[derive(Debug)]
pub(crate) struct Cli {
    pub(crate) assume_256_colors: bool,
    pub(crate) control_mode: u8,
    pub(crate) no_fork: bool,
    pub(crate) shell_command: Option<String>,
    config_files: Vec<PathBuf>,
    pub(crate) login_shell: bool,
    socket_name: Option<OsString>,
    pub(crate) no_start_server: bool,
    socket_path: Option<PathBuf>,
    terminal_features: Vec<String>,
    pub(crate) utf8: bool,
    pub(crate) verbose: u8,
    pub(crate) command: Option<Command>,
    command_queue: Vec<Command>,
    control_command_lines: Vec<String>,
}

#[derive(Debug, Parser)]
#[command(disable_help_subcommand = true, version)]
struct RawCli {
    #[arg(short = '2', action = ArgAction::SetTrue)]
    assume_256_colors: bool,
    #[arg(short = 'C', action = ArgAction::Count)]
    control_mode: u8,
    #[arg(short = 'D', action = ArgAction::SetTrue)]
    no_fork: bool,
    #[arg(short = 'c', value_name = "shell-command")]
    shell_command: Option<String>,
    #[arg(short = 'f', value_name = "file")]
    config_files: Vec<PathBuf>,
    #[arg(short = 'l', action = ArgAction::SetTrue)]
    login_shell: bool,
    #[arg(short = 'L', value_name = "socket-name", allow_hyphen_values = true)]
    socket_name: Option<OsString>,
    #[arg(short = 'N', action = ArgAction::SetTrue)]
    no_start_server: bool,
    #[arg(short = 'S', value_name = "socket-path", allow_hyphen_values = true)]
    socket_path: Option<PathBuf>,
    #[arg(short = 'T', value_name = "features", allow_hyphen_values = true)]
    terminal_features: Vec<String>,
    #[arg(short = 'u', action = ArgAction::SetTrue)]
    utf8: bool,
    #[arg(short = 'v', action = ArgAction::Count)]
    verbose: u8,
    #[arg(
        value_name = "command",
        allow_hyphen_values = true,
        trailing_var_arg = true
    )]
    command: Vec<OsString>,
}

impl Cli {
    fn from_raw(raw: RawCli, parsed_commands: ParsedCommands) -> Result<Self, clap::Error> {
        let control_command_lines = if parsed_commands.is_empty() {
            Vec::new()
        } else {
            vec![parsed_commands.to_tmux_string()]
        };
        let command_queue = parsed_commands
            .into_commands()
            .into_iter()
            .map(command_from_parsed)
            .collect::<Result<Vec<_>, _>>()?;
        Ok(Self {
            assume_256_colors: raw.assume_256_colors,
            control_mode: raw.control_mode,
            no_fork: raw.no_fork,
            shell_command: raw.shell_command,
            config_files: raw.config_files,
            login_shell: raw.login_shell,
            socket_name: raw.socket_name,
            no_start_server: raw.no_start_server,
            socket_path: raw.socket_path,
            terminal_features: raw.terminal_features,
            utf8: raw.utf8,
            verbose: raw.verbose,
            command: command_queue.first().cloned(),
            command_queue,
            control_command_lines,
        })
    }

    pub(crate) fn socket_name(&self) -> Option<&std::ffi::OsStr> {
        self.socket_name.as_deref()
    }

    pub(crate) fn socket_path(&self) -> Option<&Path> {
        self.socket_path.as_deref()
    }

    pub(crate) fn config_file_selection(&self) -> ConfigFileSelection<'_> {
        match self.config_files.as_slice() {
            [] => ConfigFileSelection::Default,
            files => ConfigFileSelection::Custom(files),
        }
    }

    pub(crate) fn terminal_features(&self) -> &[String] {
        &self.terminal_features
    }

    pub(crate) fn into_command_queue(self) -> Vec<Command> {
        self.command_queue
    }

    pub(crate) fn control_command_lines(&self) -> &[String] {
        &self.control_command_lines
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConfigFileSelection<'a> {
    Default,
    Custom(&'a [PathBuf]),
}

fn parse_command_args<T>(
    command_name: &'static str,
    arguments: Vec<String>,
) -> Result<T, clap::Error>
where
    T: Args + FromArgMatches,
{
    let command = T::augment_args(
        clap::Command::new(command_name)
            .no_binary_name(true)
            .disable_help_flag(true),
    )
    .disable_help_subcommand(true)
    .arg(
        clap::Arg::new("help")
            .long("help")
            .action(ArgAction::Help)
            .help("Print help"),
    );
    let matches = command.try_get_matches_from(arguments)?;
    T::from_arg_matches(&matches)
}

#[derive(Debug, Clone)]
pub(crate) enum Command {
    NewSession(NewSessionArgs),
    StartServer,
    KillServer,
    HasSession(SessionTargetArgs),
    KillSession(KillSessionArgs),
    RenameSession(RenameSessionArgs),
    ServerAccess(ServerAccessArgs),
    LockServer,
    LockSession(SessionTargetArgs),
    LockClient(ClientTargetArgs),
    NewWindow(NewWindowArgs),
    KillWindow(KillWindowArgs),
    SelectWindow(WindowTargetArgs),
    RenameWindow(RenameWindowArgs),
    NextWindow(AlertSessionTargetArgs),
    PreviousWindow(AlertSessionTargetArgs),
    LastWindow(SessionTargetArgs),
    ListSessions(ListSessionsArgs),
    ListWindows(ListWindowsArgs),
    MoveWindow(MoveWindowArgs),
    SwapWindow(SwapWindowArgs),
    RotateWindow(RotateWindowArgs),
    ResizeWindow(ResizeWindowArgs),
    RespawnWindow(RespawnWindowArgs),
    SplitWindow(SplitWindowArgs),
    SwapPane(SwapPaneArgs),
    LastPane(WindowTargetArgs),
    JoinPane(JoinPaneArgs),
    MovePane(JoinPaneArgs),
    BreakPane(BreakPaneArgs),
    PipePane(PipePaneArgs),
    RespawnPane(RespawnPaneArgs),
    KillPane(PaneTargetArgs),
    SelectLayout(SelectLayoutArgs),
    NextLayout(WindowTargetArgs),
    PreviousLayout(WindowTargetArgs),
    ResizePane(ResizePaneArgs),
    DisplayPanes(DisplayPanesArgs),
    ListPanes(ListPanesArgs),
    SelectPane(SelectPaneArgs),
    CopyMode(CopyModeArgs),
    ClockMode(ClockModeArgs),
    SendKeys(SendKeysArgs),
    BindKey(BindKeyArgs),
    UnbindKey(UnbindKeyArgs),
    ListCommands(ListCommandsArgs),
    ListKeys(ListKeysArgs),
    SendPrefix(SendPrefixArgs),
    Prompt(PromptArgs),
    ConfirmBefore(ConfirmBeforeArgs),
    FindWindow(FindWindowArgs),
    LinkWindow(LinkWindowArgs),
    UnlinkWindow(UnlinkWindowArgs),
    ChooseTree(ChooseTreeArgs),
    ChooseBuffer(ChooseBufferArgs),
    ChooseClient(ChooseClientArgs),
    CustomizeMode(CustomizeModeArgs),
    AttachSession(AttachSessionArgs),
    RefreshClient(RefreshClientArgs),
    ListClients(ListClientsArgs),
    SwitchClient(SwitchClientArgs),
    DetachClient(DetachClientArgs),
    SuspendClient(SuspendClientArgs),
    SetOption(SetOptionArgs),
    SetWindowOption(SetOptionArgs),
    SetEnvironment(SetEnvironmentArgs),
    ShowOptions(ShowOptionsArgs),
    ShowWindowOptions(ShowOptionsArgs),
    ShowEnvironment(ShowEnvironmentArgs),
    SetHook(SetHookArgs),
    ShowHooks(ShowHooksArgs),
    SetBuffer(SetBufferArgs),
    ShowBuffer(ShowBufferArgs),
    PasteBuffer(PasteBufferArgs),
    ListBuffers(ListBuffersArgs),
    DeleteBuffer(DeleteBufferArgs),
    LoadBuffer(LoadBufferArgs),
    SaveBuffer(SaveBufferArgs),
    CapturePane(CapturePaneArgs),
    ClearHistory(ClearHistoryArgs),
    DisplayMessage(DisplayMessageArgs),
    ShowMessages(ShowMessagesArgs),
    RunShell(RunShellArgs),
    SourceFile(SourceFileArgs),
    IfShell(IfShellArgs),
    WaitFor(WaitForArgs),
    DisplayMenu(DisplayMenuArgs),
    DisplayPopup(DisplayPopupArgs),
    ClearPromptHistory(PromptHistoryArgs),
    ShowPromptHistory(PromptHistoryArgs),
    Unsupported(UnsupportedCommandArgs),
}

#[derive(Debug, Clone)]
pub(crate) struct UnsupportedCommandArgs {
    pub(crate) name: String,
    pub(crate) arguments: Vec<String>,
}

trait QueuedCommand {
    fn set_queue_command(&mut self, queue_command: String);
}

#[cfg(test)]
#[path = "cli_args_tests.rs"]
mod tests;

#[cfg(test)]
#[path = "cli_args_config_tests.rs"]
mod config_tests;
#[cfg(test)]
#[path = "cli_args_layout_tests.rs"]
mod layout_tests;
#[cfg(test)]
#[path = "cli_args_zoom_tests.rs"]
mod zoom_tests;
