// SPDX-FileCopyrightText: 2024 Christoph Sax <c_sax@mailbox.org>
//
// SPDX-License-Identifier: EUPL-1.2

//! [Note] Command Line Argument Parser
//! ===================================
//!
//! The command line parser of Linux shells is automatically taking care of
//! quoted arguments. This command line argument
//!
//! ~~~~ text
//! --t32SystemDir="C:\Program Files\T32"
//! ~~~~
//! becomes this input argument:
//!
//! ~~~~ text
//! --t32SystemDir=C:\\Program Files\\T32
//! ~~~~
//!
//! Double quotes are automatically removed and escape sequences are converted.
//! The same mechanism is applied for single quotes.
//!
//! On the other hand, the Node.js command line parser is giving us this:
//!
//! ~~~~ text
//! --t32SystemDir=\"C:\\Program Files\\T32\"
//! ~~~~
//!

use std::{
    env,
    io::{self, Write},
    path::{self, Path, PathBuf},
    str::FromStr,
    time::Duration,
};

use serde::Serialize;

use crate::{
    ReturnCode,
    protocol::{
        FoldingRangeKind, PositionEncodingKind, SemanticTokensLegend, TraceValue, Uri,
        WorkspaceFolder,
    },
};

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ChannelKind {
    Pipe,
    Socket,
    Stdio,
}

#[derive(Debug, PartialEq)]
pub enum OperationMode {
    Server,
    StdioTransport,
}

#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Workspace {
    Root(Option<Uri>),
    Folders(Option<Vec<WorkspaceFolder>>),
}

#[derive(Clone, Debug)]
pub struct CodeFoldingEncoding {
    pub line_folds_only: bool,
    pub collapsed_text_supported: bool,
}

#[derive(Clone, Debug)]
pub struct CodeFoldingSupport {
    pub encoding: CodeFoldingEncoding,
    pub folding_range_kinds: Vec<FoldingRangeKind>,
}

#[derive(Debug)]
pub struct Config {
    pub channel: ChannelKind,
    pub code_folding: CodeFoldingSupport,
    pub did_rename_files_supported: bool,
    pub location_links: LocationLinkSupport,
    pub mode: OperationMode,
    pub parent_pid: Option<u32>,
    pub pid_check_interval: Duration,
    pub position_encoding: PositionEncodingKind,
    pub semantic_tokens: SemanticTokenSupport,
    pub server_progress_supported: bool,
    pub t32_dirs: T32DefaultDirs,
    pub trace_level: TraceValue,
    pub workspace: Workspace,
    pub workspace_folders_supported: bool,
}

#[derive(Debug)]
pub struct LocationLinkSupport {
    pub definitions_supported: bool,
}

#[derive(Clone, Debug)]
pub struct SemanticTokenEncoding {
    pub overlapping_tokens: bool,
    pub multiline_tokens: bool,
}

#[derive(Clone, Debug)]
pub struct SemanticTokenSupport {
    pub encoding: SemanticTokenEncoding,
    pub legend: SemanticTokensLegend,
}

#[derive(Clone, Debug)]
pub struct T32DefaultDirs {
    pub system_dir: Option<PathBuf>,
    pub temp_dir: Option<PathBuf>,
    pub home_dir: Option<PathBuf>,
}
const PKG_VERSION: &'static str = env!("CARGO_PKG_VERSION");
const GIT_HEADREF: Option<&'static str> = option_env!("GIT_HEAD_REF");

impl Config {
    pub fn build(args: &[String]) -> Result<Self, ReturnCode> {
        let mut ppid: Option<u32> = None;
        let mut show_help: bool = false;
        let mut show_version: bool = false;
        let mut trace_level: TraceValue = TraceValue::Off;
        let mut mode: OperationMode = OperationMode::Server;
        let mut t32_system_dir: Option<PathBuf> = None;
        let mut t32_temp_dir: Option<PathBuf> = None;

        debug_assert!(args.len() > 0);
        let len = args[1..].len();
        for (ii, arg) in args[1..].iter().enumerate() {
            let next = if ii < len - 1 {
                Some(args[1..][ii + 1].as_str())
            } else {
                None
            };

            match Self::parse_flag_value::<u32>("--clientProcessId=", Some("-c"), arg, next) {
                Err(err) => return Err(err),
                Ok(Some(num)) => {
                    ppid = Some(num);
                    continue;
                }
                Ok(None) => (),
            }

            match Self::parse_flag_value::<TraceValue>("--trace=", Some("-t"), arg, next) {
                Err(err) => return Err(err),
                Ok(Some(level)) => {
                    trace_level = level;
                    continue;
                }
                Ok(None) => (),
            }

            match Self::parse_flag_value::<OperationMode>("--mode=", None, arg, next) {
                Err(err) => return Err(err),
                Ok(Some(opmode)) => {
                    mode = opmode;
                    continue;
                }
                Ok(None) => (),
            }

            // Check [Note: Command Line Argument Parser] for details about shape of input arguments.
            match Self::parse_flag_value::<PathBuf>("--t32SystemDir=", Some("-s"), arg, next) {
                Err(err) => return Err(err),
                Ok(None) => (),
                Ok(Some(p)) => {
                    let path = match path::absolute(&p) {
                        Ok(p) => p,
                        Err(_) => {
                            warn_invalid_abs_path(&mut io::stderr(), &"--t32SystemDir", &p);
                            continue;
                        }
                    };

                    if path.is_dir() {
                        t32_system_dir = Some(path);
                    } else {
                        warn_dir_not_exist(&mut io::stderr(), &"--t32SystemDir", &path);
                    }
                    continue;
                }
            }

            // Check [Note: Command Line Argument Parser] for details about shape of input arguments.
            match Self::parse_flag_value::<PathBuf>("--t32TempDir=", Some("-T"), arg, next) {
                Err(err) => return Err(err),
                Ok(None) => (),
                Ok(Some(p)) => {
                    let path = match path::absolute(&p) {
                        Ok(p) => p,
                        Err(_) => {
                            warn_invalid_abs_path(&mut io::stderr(), &"--t32TempDir", &p);
                            continue;
                        }
                    };

                    if path.is_dir() {
                        t32_temp_dir = Some(path);
                    } else {
                        warn_dir_not_exist(&mut io::stderr(), &"--t32TempDir", &path);
                    }
                    continue;
                }
            }

            if Self::parse_flag("--help", "-h", arg) {
                show_help = true;
            }

            if Self::parse_flag("--version", "-V", arg) {
                show_version = true;
            }
        }

        if show_help {
            usage(&mut io::stdout());
            return Err(ReturnCode::OkExit);
        }

        if show_version {
            version(&mut io::stdout());
            return Err(ReturnCode::OkExit);
        }

        #[cfg(all(target_os = "wasi", target_env = "p1"))]
        if mode == OperationMode::StdioTransport {
            error_wasm(&mut io::stderr(), "--mode=StdioTransport");
        }

        if ppid.is_none() {
            warn_missing_ppid(&mut io::stderr(), "--clientProcessId=PID");
        }

        Ok(Config {
            code_folding: CodeFoldingSupport::default(),
            channel: ChannelKind::Stdio,
            location_links: LocationLinkSupport {
                definitions_supported: false,
            },
            did_rename_files_supported: false,
            mode,
            parent_pid: ppid,
            pid_check_interval: Duration::from_secs(5),
            position_encoding: PositionEncodingKind::Utf16,
            semantic_tokens: SemanticTokenSupport::default(),
            server_progress_supported: false,
            t32_dirs: T32DefaultDirs::build(t32_system_dir, t32_temp_dir),
            trace_level,
            workspace: Workspace::Root(None),
            workspace_folders_supported: false,
        })
    }

    fn parse_flag_value<'a, T: std::str::FromStr>(
        long: &str,
        short: Option<&str>,
        arg: &'a str,
        next: Option<&str>,
    ) -> Result<Option<T>, ReturnCode> {
        if let Some(sh) = short
            && sh == arg
        {
            if let None = next {
                error_format_value(&mut io::stderr(), sh);
                return Err(ReturnCode::UsageErr);
            }

            let val = next
                .expect("The flag must have a value.")
                .trim_matches(&['"', '\'']);

            match val.parse::<T>() {
                Ok(v) => return Ok(Some(v)),
                Err(_) => {
                    error_format(&mut io::stderr(), long);
                    return Err(ReturnCode::UsageErr);
                }
            }
        }

        if !arg.starts_with(long) {
            return Ok(None);
        }

        let trim_parens = |s: &'a str| -> &'a str { s.trim_matches(&['"', '\'']) };

        let val: Vec<&str> = arg
            .split(long)
            .map(str::trim)
            .map(trim_parens)
            .filter(|s| !s.is_empty())
            .collect();

        if val.len() != 1 {
            error_format(&mut io::stderr(), long);
            return Err(ReturnCode::UsageErr);
        }

        match val[0].parse::<T>() {
            Ok(v) => Ok(Some(v)),
            Err(_) => {
                error_format(&mut io::stderr(), long);
                Err(ReturnCode::UsageErr)
            }
        }
    }

    fn parse_flag(long: &str, short: &str, arg: &str) -> bool {
        arg == short || arg == long
    }
}

impl T32DefaultDirs {
    pub fn build(t32_sys_dir: Option<PathBuf>, t32_temp_dir: Option<PathBuf>) -> Self {
        let home_dir = env::home_dir();
        if home_dir.is_none() {
            warn_missing_user_home(&mut io::stderr());
        }

        Self {
            system_dir: t32_sys_dir,
            temp_dir: t32_temp_dir,
            home_dir,
        }
    }
}

impl Default for CodeFoldingSupport {
    fn default() -> Self {
        Self {
            encoding: CodeFoldingEncoding {
                line_folds_only: true,
                collapsed_text_supported: false,
            },
            folding_range_kinds: vec![
                FoldingRangeKind::Comment,
                FoldingRangeKind::Imports,
                FoldingRangeKind::Region,
            ],
        }
    }
}

impl Default for SemanticTokenSupport {
    fn default() -> Self {
        Self {
            encoding: SemanticTokenEncoding {
                overlapping_tokens: false,
                multiline_tokens: false,
            },
            legend: SemanticTokensLegend::new(),
        }
    }
}

impl Default for T32DefaultDirs {
    fn default() -> Self {
        Self {
            system_dir: None,
            temp_dir: None,
            home_dir: None,
        }
    }
}

impl FromStr for OperationMode {
    type Err = ();

    fn from_str(val: &str) -> Result<Self, Self::Err> {
        match val {
            "server" => Ok(OperationMode::Server),
            "stdio-transport" => Ok(OperationMode::StdioTransport),
            _ => Err(()),
        }
    }
}

pub fn version_str() -> String {
    format!(
        "{}{}",
        PKG_VERSION,
        if let Some(hash) = GIT_HEADREF {
            format!("+{}", hash)
        } else {
            "".to_string()
        }
    )
}

fn error_format(writer: &mut impl Write, param: &str) {
    let _ = writeln!(writer, "ERROR: Invalid format for argument \"{param}\"");
}

fn error_format_value(writer: &mut impl Write, param: &str) {
    let _ = writeln!(
        writer,
        "ERROR: Invalid format for argument value \"{param}\""
    );
}

#[allow(dead_code)]
fn error_missing(writer: &mut impl Write, param: &str) {
    let _ = writeln!(writer, "ERROR: Missing argument \"{param}\"");
}

fn warn_missing_ppid(writer: &mut impl Write, param: &str) {
    let _ = writeln!(
        writer,
        r#"WARNING: Missing argument \"{param}\"
Process ID of parent process is not available. The editor process cannot be
monitored for shutdown."#
    );
}
fn warn_invalid_abs_path(writer: &mut impl Write, flag: &str, path: &Path) {
    let _ = writeln!(
        writer,
        r#"WARNING: Invalid path \"{}\"
The path value of the flag \"{}\" cannot be converted into an absolute path."#,
        path.to_string_lossy(),
        flag
    );
}

fn warn_dir_not_exist(writer: &mut impl Write, flag: &str, path: &Path) {
    let _ = writeln!(
        writer,
        r#"WARNING: Directory \"{}\" does not exist.
The path value of the flag \"{}\" does not specify a valid directory."#,
        path.to_string_lossy(),
        flag
    );
}

fn warn_missing_user_home(writer: &mut impl Write) {
    let _ = writeln!(
        writer,
        r#"WARNING: Missing home directory
The path to the user home directory cannot be retrieved."#
    );
}

#[cfg(all(target_os = "wasi", target_env = "p1"))]
fn error_wasm(writer: &mut impl Write, param: &str) {
    let _ = writeln!(
        writer,
        "ERROR: Operation mode \"{param}\" not supported for WebAssembly runtime environments"
    );
}

fn usage(writer: &mut impl Write) {
    let _ = writeln!(
        writer,
        r#"Usage: t32ls [OPTIONS]

Language server for the Lauterbach TRACE32® script language.


General options:
  -h, --help
    Show this help message and exit.

  -c PID, --clientProcessId=PID
    Process ID of the client that started the server. The server can use the
    PID to monitor the client process and shut itself down if the client
    process dies. If the client process ID is not provided, set the value to 0
    to suppress the warning.

  -s DIR, --t32SystemDir=DIR
    System directory of your TRACE32 installation. The server will use this
    path to resolve the path prefix "~~" in script files. This prefix expands
    to the absolute path of the TRACE32 system directory, e.g. "C:\T32". It is
    commonly used to reference files from the subdirectory "demo" in the TRACE32
    system directory.

  -t LEVEL, --trace=LEVEL
    Set the initial logging level of the server's execution trace. LEVEL must
    be one of 'off,messages,verbose'.

  -T DIR, --t32TempDir=DIR
    Selected temporary directory of TRACE32. The server will use this path to
    resolve the path prefix "~~~" in script files. This prefix expands
    to the absolute path of the TRACE32 temporary directory, e.g. "/tmp".

  -V, --version
    Print version info and exit."#
    )
    .expect("Writer must be configured correctly.");
}

fn version(writer: &mut impl Write) {
    // REUSE-IgnoreStart
    let _ = writeln!(
        writer,
        r#"t32ls (t32-language-server), version {}
SPDX-FileCopyrightText: 2024 Christoph Sax <c_sax@mailbox.org>
SPDX-License-Identifier: EUPL-1.2"#,
        version_str(),
    )
    .expect("Writer must be configured correctly.");
    // REUSE-IgnoreEnd
}

mod tests {
    #[allow(unused)]
    use super::*;

    #[test]
    fn can_parse_t32_system_directory() {
        let cfg = Config::build(&["t32ls", "--t32SystemDir=tests/samples"].map(String::from))
            .expect("Must complete without error.");

        assert_eq!(
            cfg.t32_dirs.system_dir,
            Some(path::absolute(PathBuf::from("tests/samples")).expect("Must not fail."))
        );
    }

    #[test]
    fn can_parse_t32_temporary_directory() {
        let cfg = Config::build(&["t32ls", "--t32TempDir=tests/samples/a"].map(String::from))
            .expect("Must complete without error.");

        assert_eq!(
            cfg.t32_dirs.temp_dir,
            Some(path::absolute(PathBuf::from("tests/samples/a")).expect("Must not fail."))
        );
    }
}
