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

touch-als

Copyright (C) 2021, 2024  Anonymous



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/>.

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

//! # `touch-als`

#![warn(missing_docs)]

#[macro_use]
#[allow(unused_macros)]
mod __;
mod cmd_option;
mod version_info;

use {
    core::borrow::Borrow,
    std::{
        borrow::Cow,
        io::{Error, ErrorKind},
        os::{
            linux::net::SocketAddrExt,
            unix::net::{SocketAddr, UnixStream},
        },
        process::{self, Command, Stdio},
    },
    self::cmd_option::CmdOption,
    dia_args::{
        Args,
        docs::Project,
    },
};

// ╔═════════════════╗
// ║   IDENTIFIERS   ║
// ╚═════════════════╝

macro_rules! code_name  { () => { "touch-als" }}
macro_rules! version    { () => { "0.4.0" }}

/// # Crate name
pub const NAME: &str = "touch-als";

/// # Crate code name
pub const CODE_NAME: &str = code_name!();

/// # ID of this crate
pub const ID: &str = concat!(
    "c9e74de3-b0b0d8c4-9b84ee53-f1aae85f-26a06f64-37924a21-baee6dfd-e6bbcfbc-",
    "868ccd21-a21be567-d078cf0b-5df23b4a-e32e6e2e-78c5dd39-eb564cce-aa606fe7",
);

/// # Crate version
pub const VERSION: &str = version!();

/// # Crate release date (year/month/day)
pub const RELEASE_DATE: (u16, u8, u8) = (2024, 6, 18);

/// # Tag, which can be used for logging...
pub const TAG: &str = concat!(code_name!(), "::c9e74de3::", version!());

// ╔════════════════════╗
// ║   IMPLEMENTATION   ║
// ╚════════════════════╝

/// # Result type used in this crate
pub (crate) type Result<T> = core::result::Result<T, std::io::Error>;

#[test]
fn test_crate_version() {
    assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
}

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_LICENSES: &str = "licenses";
const CMD_LICENSES_DOCS: Cow<str> = Cow::Borrowed("Prints licenses and exits.");

const OPTION_CMD: &[&str] = &["--cmd"];
const OPTION_CMD_DOCS: Cow<str> = Cow::Borrowed(concat!(
    "Command option.\n\n",
    "Notes:\n\n",
    "- This option is supported for only one single address.\n",
    "- The command will be spawned and ignored.",
));
const OPTION_CMD_VALUES: &[CmdOption] = &[CmdOption::OnSuccess, CmdOption::OnFailure];

/// # Main
fn main() {
    if let Err(err) = run() {
        eprintln!("{}", __w!("{}", err));
        process::exit(1);
    }
}

/// # Runs the program
fn run() -> Result<()> {
    let args = dia_args::parse()?;
    match args.cmd() {
        Some(CMD_HELP) => {
            ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
            print_help()
        },
        Some(CMD_VERSION) => {
            ensure_args_are_empty(args.try_into_sub_cmd()?.1)?;
            print_version()
        },
        Some(CMD_LICENSES) => print_licenses(args.try_into_sub_cmd()?.1),
        Some(_) => run_main_job(args),
        None => Err(Error::new(ErrorKind::Other, "Missing address(es)")),
    }
}

/// # Ensures arguments are empty
fn ensure_args_are_empty<A>(args: A) -> Result<()> where A: Borrow<Args> {
    let args = args.borrow();
    if args.is_empty() {
        Ok(())
    } else {
        Err(Error::new(ErrorKind::InvalidInput, format!("Unknown arguments: {:?}", args)))
    }
}

/// # Makes version string
fn make_version_string<'a>() -> Cow<'a, str> {
    Cow::Owned(format!(
        "{name} {version} {release_date:?}",
        name=NAME, version=VERSION, release_date=RELEASE_DATE,
    ))
}

/// # Prints version
fn print_version() -> Result<()> {
    println!("{}", make_version_string());
    Ok(())
}

/// # Parses command documentation from README
fn parse_cmd_docs_from_readme<'a>() -> Cow<'a, str> {
    const START: &str = "<!-- PROGRAM-DOCS:START -->";
    const END: &str = "<!-- PROGRAM-DOCS:END -->";

    let readme = include_str!("../README.md");
    Cow::Borrowed(readme[readme.find(START).unwrap() + START.len() .. readme.find(END).unwrap()].trim())
}

#[test]
fn test_parse_cmd_docs_from_readme() {
    parse_cmd_docs_from_readme();
}

/// # Makes project info
fn make_project_info<'a>() -> Option<Project<'a>> {
    Some(Project::new(
        None,
        "GNU Lesser General Public License, either version 3, or (at your option) any later version",
        None,
    ))
}

#[test]
fn test_make_project_info() {
    make_project_info().unwrap();
}

/// # Prints help
fn print_help() -> Result<()> {
    use dia_args::docs::{Cmd, Docs, Option};

    let options = Some(dia_args::make_options![
        Option::new(OPTION_CMD, false, OPTION_CMD_VALUES, None, OPTION_CMD_DOCS),
    ]);

    let commands = Some(dia_args::make_cmds![
        Cmd::new(CMD_HELP, CMD_HELP_DOCS, None),
        Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None),
        Cmd::new(CMD_LICENSES, CMD_LICENSES_DOCS, None),
    ]);

    let mut docs = Docs::new(make_version_string(), parse_cmd_docs_from_readme());
    docs.options = options;
    docs.commands = commands;
    docs.project = make_project_info();
    docs.print()?;

    println!(
        concat!(
            "EXAMPLES\n\n",
            "# Touch an address and echo some message on success\n",
            "$ {code_name} 24047f96d377a477098106f6a474bd89 {arg_cmd}={on_success} -- echo ok",
        ),
        code_name=CODE_NAME,
        arg_cmd=OPTION_CMD[0],
        on_success=CmdOption::OnSuccess,
    );

    Ok(())
}

/// # Prints licenses
fn print_licenses(args: Args) -> Result<()> {
    ensure_args_are_empty(args)?;

    dia_args::lock_write_out(format!(
        "{copying}\n\n================================================================\n\n{copying_lesser}\n",
        copying=include_str!("../COPYING"),
        copying_lesser=include_str!("../COPYING.LESSER"),
    ));

    Ok(())
}

/// # Runs main job
fn run_main_job(mut args: Args) -> Result<()> {
    let addresses = args.take_args()?;
    if addresses.is_empty() {
        return Err(Error::new(ErrorKind::InvalidInput, "Missing address(es)"));
    }

    let cmd_option = args.take::<CmdOption>(OPTION_CMD)?;

    let mut cmd = match cmd_option {
        Some(_) => {
            if addresses.len() > 1 {
                return Err(Error::new(ErrorKind::InvalidInput, format!("{OPTION_CMD:?} only supports one single address.")));
            }
            let mut sub_args = args.take_sub_args();
            if sub_args.is_empty() {
                return Err(Error::new(ErrorKind::InvalidInput, format!("{OPTION_CMD:?} requires a command.")));
            }
            let mut cmd = Command::new(sub_args.remove(0));
            cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
            for s in sub_args {
                cmd.arg(s);
            }
            Some(cmd)
        },
        None => None,
    };

    ensure_args_are_empty(args)?;

    for addr in addresses {
        match UnixStream::connect_addr(&SocketAddr::from_abstract_name(&hex_to_address(&addr)?)?) {
            Ok(_) => {
                println!("Ok: {}", addr);
                if let (Some(CmdOption::OnSuccess), Some(mut cmd)) = (cmd_option.as_ref(), cmd.take()) {
                    cmd.spawn()?;
                }
            },
            Err(err) => {
                eprintln!("{}", __w!("Failed: {addr} -> {err}", addr=addr, err=err));
                if let (Some(CmdOption::OnFailure), Some(mut cmd)) = (cmd_option.as_ref(), cmd.take()) {
                    cmd.spawn()?;
                }
            },
        };
    }

    Ok(())
}

/// # Converts a hex string to address
fn hex_to_address<S>(hex: S) -> Result<Vec<u8>> where S: AsRef<str> {
    const RAW: &str = "raw:";

    let hex = hex.as_ref();

    if hex.starts_with(RAW) {
        return Ok(hex[RAW.len()..].as_bytes().to_vec());
    }

    let len = hex.len();
    let result_len = len / 2;

    // 512 is only for protection against flood attacks
    if len % 2 != 0 || result_len >= 512 {
        return Err(Error::new(ErrorKind::InvalidInput, format!("Invalid address: {:?}", hex)));
    }

    let mut result = Vec::with_capacity(result_len);
    result.push(0);

    for i in (0..len).step_by(2) {
        match hex.get(i .. i + 2).map(|s| u8::from_str_radix(s, 16)) {
            Some(Ok(b)) => result.push(b),
            _ => return Err(Error::new(ErrorKind::InvalidInput, format!("Invalid address: {:?}", hex))),
        };
    }

    Ok(result)
}