Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

Dia-Args

Copyright (C) 2018-2019, 2021-2025  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2018-2019".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Kit for documentation
//!
//! ## Examples
//!
//! ```
//! use std::borrow::Cow;
//! use dia_args::docs::{self, Cfg, Cmd, Docs, I18n, Option, Project};
//!
//! const CMD_HELP: &str = "help";
//! const CMD_HELP_DOCS: Cow<str> = Cow::Borrowed("Prints help and exits.");
//!
//! const CMD_VERSION: &str = "version";
//! const CMD_VERSION_DOCS: Cow<str> = Cow::Borrowed("Prints version and exits.");
//!
//! const CMD_DO_SOMETHING: &str = "do-something";
//! const CMD_DO_SOMETHING_DOCS: Cow<str> = Cow::Borrowed(concat!(
//!     "This command does something.\n",
//!     "\n",
//!     "It might NOT do that thing. If you encounter any problems,",
//!     " please contact developers.\n",
//! ));
//!
//! const OPTION_THIS: &[&str] = &["-t", "--this"];
//! const OPTION_THIS_DEFAULT: bool = true;
//! const OPTION_THIS_DOCS: Cow<str> = Cow::Borrowed("This argument has 2 names.");
//!
//! const OPTION_THAT: &[&str] = &["--that"];
//! const OPTION_THAT_VALUES: &[u8] = &[99, 100];
//! const OPTION_THAT_DEFAULT: u8 = OPTION_THAT_VALUES[0];
//! const OPTION_THAT_DOCS: Cow<str> = Cow::Borrowed("This argument has 1 single name.");
//!
//! let help_cmd = Cmd::new(CMD_HELP, CMD_HELP_DOCS, None);
//! let version_cmd = Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None);
//! let do_something_cmd = Cmd::new(
//!     CMD_DO_SOMETHING, CMD_DO_SOMETHING_DOCS,
//!     Some(dia_args::make_options![
//!         Option::new(OPTION_THIS, false, &[], Some(OPTION_THIS_DEFAULT), OPTION_THIS_DOCS),
//!         Option::new(OPTION_THAT, true, OPTION_THAT_VALUES, Some(OPTION_THAT_DEFAULT), OPTION_THAT_DOCS),
//!     ]),
//! );
//!
//! let mut docs = Docs::new(
//!     // Name
//!     "The-Program".into(),
//!     // Docs
//!     "This is the Program".into(),
//! );
//! docs.commands = Some(dia_args::make_cmds![help_cmd, version_cmd, do_something_cmd,]);
//! docs.project = Some(Project::new(Some("https://project-home"), "Nice License", None));
//! docs.print()?;
//!
//! // This does the same as above command:
//! // println!("{}", docs);
//!
//! # Ok::<_, std::io::Error>(())
//! ```

mod cfg;
mod cmd;
mod i18n;
mod option;

use {
    core::{
        fmt::{self, Display},
        option::Option as RustOption,
    },
    std::borrow::Cow,
};

pub use self::{
    cfg::*,
    cmd::*,
    i18n::*,
    option::*,
};

#[cfg(unix)]
const LINE_BREAK: &str = "\n";

#[cfg(not(unix))]
const LINE_BREAK: &str = "\r\n";

/// # Makes a vector of [`Cow<'_, Cmd<'_>>`][struct:Cmd] from a list of either `Cmd` or `&Cmd`
///
/// [struct:Cmd]: docs/struct.Cmd.html
#[macro_export]
macro_rules! make_cmds {
    ($($e: expr),+ $(,)?) => {{
        vec!($(std::borrow::Cow::<dia_args::docs::Cmd>::from($e),)+)
    }};
}

/// # Makes a vector of [`Cow<'_, Option<'_>>`][struct:Option] from a list of either `Option` or `&Option`
///
/// [struct:Option]: docs/struct.Option.html
#[macro_export]
macro_rules! make_options {
    ($($e: expr),+ $(,)?) => {{
        vec!($(std::borrow::Cow::<dia_args::docs::Option>::from($e),)+)
    }};
}

/// # No values
///
/// This can be used conveniently with [`Option`][struct:Option].
///
/// ## Examples
///
/// ```
/// use std::borrow::Cow;
/// use dia_args::docs::{NO_VALUES, Option};
///
/// const OPTION_PORT: &[&str] = &["-p", "--port"];
/// const OPTION_PORT_DOCS: Cow<str> = Cow::Borrowed("Port for server.");
///
/// let _option = Option::new(OPTION_PORT, false, NO_VALUES, None, OPTION_PORT_DOCS);
/// ```
///
/// [struct:Option]: struct.Option.html
pub const NO_VALUES: &[&str] = &[];

/// # Documentation
pub struct Docs<'a> {

    /// # Name
    pub name: Cow<'a, str>,

    /// # Documentation
    pub docs: Cow<'a, str>,

    /// # Configuration
    pub cfg: Cfg,

    /// # Internatinonalization
    pub i18n: I18n<'a>,

    /// # Options
    pub options: RustOption<Vec<Cow<'a, Option<'a>>>>,

    /// # Commands
    pub commands: RustOption<Vec<Cow<'a, Cmd<'a>>>>,

    /// # Project
    pub project: RustOption<Project<'a>>,

}

impl<'a> Docs<'a> {

    /// # Makes new instance with default configurations
    pub fn new(name: Cow<'a, str>, docs: Cow<'a, str>) -> Self {
        Self {
            name,
            docs,
            cfg: Cfg::default(),
            i18n: I18n::default(),
            options: None,
            commands: None,
            project: None,
        }
    }

    /// # Prints this documentation to stdout
    pub fn print(self) -> crate::Result<()> {
        crate::lock_write_out(self.to_string().trim());
        crate::lock_write_out([b'\n']);
        Ok(())
    }

}

impl Display for Docs<'_> {

    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        let tab = self.cfg.tab_len().saturating_mul(self.cfg.tab_level().into());
        let next_tab = self.cfg.tab_len().saturating_mul(self.cfg.tab_level().saturating_add(1).into());

        // Name
        f.write_str(&format(&self.name, tab, self.cfg.columns()))?;
        f.write_str(LINE_BREAK)?;

        // Docs
        f.write_str(&format(&self.docs, next_tab, self.cfg.columns()))?;
        f.write_str(LINE_BREAK)?;

        // Options
        f.write_str(&format(self.i18n.options.to_uppercase(), tab, self.cfg.columns()))?;
        f.write_str(LINE_BREAK)?;
        match self.options.as_ref() {
            Some(options) => {
                let cfg = self.cfg.increment_level();
                for option in options {
                    option.format(&cfg, &self.i18n, f)?;
                }
            },
            None => {
                f.write_str(&format(&self.i18n.no_options, next_tab, self.cfg.columns()))?;
                f.write_str(LINE_BREAK)?;
            },
        };

        // Commands
        f.write_str(&format(self.i18n.commands.to_uppercase(), tab, self.cfg.columns()))?;
        f.write_str(LINE_BREAK)?;
        match self.commands.as_ref() {
            Some(commands) => {
                let cfg = self.cfg.increment_level();
                for command in commands {
                    command.format(&cfg, &self.i18n, f)?;
                }
            },
            None => {
                f.write_str(&format(&self.i18n.no_commands, next_tab, self.cfg.columns()))?;
                f.write_str(LINE_BREAK)?;
            },
        };

        // Project
        if let Some(project) = self.project.as_ref() {
            f.write_str(&format(self.i18n.project.to_uppercase(), tab, self.cfg.columns()))?;
            f.write_str(LINE_BREAK)?;
            if let Some(home) = project.home {
                f.write_str(&format(format!("- {}: {}", self.i18n.home, home), next_tab, self.cfg.columns()))?;
            }
            f.write_str(&format(format!("- {}: {}", self.i18n.license, project.license_name), next_tab, self.cfg.columns()))?;
            if let Some(license) = project.license.as_ref() {
                f.write_str(LINE_BREAK)?;
                f.write_str(&format(license, next_tab, self.cfg.columns()))?;
            }
        }

        Ok(())
    }

}

/// # Project information
#[derive(Debug)]
pub struct Project<'a> {
    home: RustOption<&'a str>,
    license_name: &'a str,
    license: RustOption<Cow<'a, str>>,
}

impl<'a> Project<'a> {

    /// # Makes new instance
    pub const fn new(home: RustOption<&'a str>, license_name: &'a str, license: RustOption<Cow<'a, str>>) -> Self {
        Self {
            home,
            license_name,
            license,
        }
    }

}

/// # Formats a string
fn format<S>(s: S, size_of_indentation: usize, columns: usize) -> String where S: AsRef<str> {
    let s = s.as_ref();

    if s.is_empty() || size_of_indentation >= columns {
        return String::new();
    }

    let mut result = String::with_capacity(s.len().saturating_add(s.len() / 10));
    let tab = concat!(' ').repeat(size_of_indentation);

    for line in s.lines() {
        let line_indentation = match line.split_whitespace().next() {
            Some(word) => match word.chars().next() {
                Some('-') | Some('+') | Some('*') | Some('~') | Some('$') | Some('#') =>
                    Some(concat!(' ').repeat(word.chars().count().saturating_add(1))),
                _ => None,
            },
            None => None,
        };

        let mut col = 0;
        for (idx, word) in line.split_whitespace().enumerate() {
            if idx == 0 {
                result += &tab;
                col = size_of_indentation;
            }

            let chars: Vec<_> = word.chars().collect();
            if col + if col == size_of_indentation { 0 } else { 1 } + chars.len() <= columns {
                if col > size_of_indentation {
                    result.push(' ');
                    col += 1;
                }
                col += chars.len();
                result.extend(chars.into_iter());
            } else {
                for (i, c) in chars.into_iter().enumerate() {
                    if i == 0 || col >= columns {
                        result += LINE_BREAK;
                        result += &tab;
                        col = size_of_indentation;
                        if let Some(line_indentation) = line_indentation.as_ref() {
                            result += line_indentation;
                            col += line_indentation.len();
                        }
                    }
                    result.push(c);
                    col += 1;
                }
            };
        }

        result += LINE_BREAK;
    }

    result
}