// License: see LICENSE file at root directory of `master` branch

//! # Hammer

#![warn(missing_docs)]

#[macro_use]
#[allow(unused_macros)]
mod __;
mod fmt;
mod hash;
#[allow(dead_code)]
mod tty;

use {
    std::{
        borrow::Cow,
        env,
        fs::File,
        io::{self, BufReader, Error, ErrorKind, Read},
        mem,
        path::PathBuf,
        process,
        str::FromStr,
        time::{UNIX_EPOCH, SystemTime},
    },

    dia_args::{
        Args,
        docs::{self, Cmd, Docs, Opt, Project},
    },
    zeros::{Hash, Keccak},

    self::fmt::Format,
};

type Result<T> = std::io::Result<T>;

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 ARG_TRIM: &[&str] = &["--trim"];
const ARG_TRIM_DOCS: Cow<str> = Cow::Borrowed(concat!(
    "If set to ", true, ", trim input string before calculating hash.\n",
    "This option is only available when calculating hash of an input string.\n",
));
const ARG_TRIM_DEFAULT: bool = false;

const ARG_FORMAT: &[&str] = &["--format"];
const ARG_FORMAT_DOCS: Cow<str> = Cow::Borrowed("Output format.");
const ARG_FORMAT_VALUES: &[Format] = &[Format::Hex, Format::HexArray];
const ARG_FORMAT_DEFAULT: Format = Format::Hex;

const ARG_LIMIT: &[&str] = &["--limit"];

const ARG_WITH_TIME: &[&str] = &["--with-time"];
const ARG_WITH_TIME_DOCS: Cow<str> = Cow::Borrowed(concat!(
    "If set to ", true, ", calculate duration from now since Unix Epoch, then feed it to hasher as \"salt\". For example, this argument",
    " can help you \"attach\" time while calculating hash from \"/dev/urandom\"\n",
));
const ARG_WITH_TIME_DEFAULT: bool = false;

const ARG_ITERATION: &[&str] = &["--iteration"];
const ARG_ITERATION_DOCS: Cow<str> = Cow::Borrowed(concat!(
    "Iteration.\n\n",
    "This option only works when calculating hash of a string.",
));
const ARG_ITERATION_DEFAULT: usize = 0;

const URANDOM_PATH: &str = "/dev/urandom";

/// # Main
fn main() {
    if let Err(err) = run() {
        match tty::is_stderr_tty() {
            true => eprintln!("{}", __w!("{}", err)),
            false => eprintln!("{}", 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.into_sub_cmd().1)?;
            print_help()
        },
        Some(CMD_VERSION) => {
            ensure_args_are_empty(args.into_sub_cmd().1)?;
            print_version()
        },
        Some(_) => run_main_job(args),
        None => Err(Error::new(ErrorKind::Other, "Missing command")),
    }
}

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

/// # Makes version string
fn make_version_string<'a>() -> Cow<'a, str> {
    format!("{} {} {:?}", dia_hammer::NAME, dia_hammer::VERSION, dia_hammer::RELEASE_DATE).into()
}

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

/// # Prints help
fn print_help() -> Result<()> {
    let docs = Cow::Owned(format!(
        concat!(
            "Supported algorithms:\n\n",
            "- {}, {}, {}, {}.\n",
            "- {}, {}.\n\n",
            "The program accepts case-insensitive algorithm names.\n",
        ),
        Hash::Sha3_224, Hash::Sha3_256, Hash::Sha3_384, Hash::Sha3_512,
        Hash::Shake128, Hash::Shake256,
    ));
    let options = Some(dia_args::make_opts![
        Opt::new(ARG_TRIM, false, None, Some(&false), ARG_TRIM_DOCS),
        Opt::new(ARG_FORMAT, false, Some(docs::make_cow_strings(ARG_FORMAT_VALUES)), Some(&ARG_FORMAT_DEFAULT), ARG_FORMAT_DOCS),
        Opt::new(ARG_LIMIT, false, None, None, Cow::Owned(format!(
            concat!(
                "Set limit of bytes when calculating hash(es) from stdin or file(s).\n",
                "It accepts a value in range [{}..{}] (which fits a Rust's u64). Note that 0 is a valid value.\n",
            ),
            u64::min_value(), u64::max_value(),
        ))),
        Opt::new(ARG_WITH_TIME, false, None, Some(&false), ARG_WITH_TIME_DOCS),
        Opt::new(ARG_ITERATION, false, None, Some(&ARG_ITERATION_DEFAULT), ARG_ITERATION_DOCS),
    ]);
    let commands = Some(dia_args::make_cmds![
        Cmd::new(CMD_HELP, CMD_HELP_DOCS, None),
        Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None),
    ]);
    let project = {
        let license = include_str!("../../../LICENSE");
        Some(Project::new("https://bitbucket.org/haibison/hammer", license.lines().next().unwrap(), Some(Cow::Borrowed(license))))
    };

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

    print_examples()
}

/// # Prints examples
fn print_examples() -> Result<()> {
    const SAMPLE_APP_NAME: &str = "hammer";

    let example_string = "haha";
    println!(
        concat!(
            "Examples:\n",
            "\n",
            "~> {} {} {} --format={}",
        ),
        SAMPLE_APP_NAME, Hash::Shake128.to_string().to_lowercase(), example_string, Format::HexArray,
    );
    run_main_job({
        let args: &[&dyn AsRef<str>] = &[&Hash::Shake128.to_string(), &example_string, &ARG_FORMAT[0], &Format::HexArray.to_string()];
        dia_args::parse_strings(args.iter())?
    })?;

    let exe = env::current_exe()?;
    println!(
        concat!(
            "\n",
            "~> {} {} {}=99 -- {}",
        ),
        SAMPLE_APP_NAME, Hash::Sha3_224.to_string().to_lowercase(), ARG_LIMIT[0], exe.display(),
    );
    run_main_job({
        let args: &[&dyn AsRef<str>] = &[&Hash::Sha3_224.to_string(), &ARG_LIMIT[0], &"99", &"--", &exe.display().to_string()];
        dia_args::parse_strings(args.iter())?
    })?;

    if cfg!(unix) {
        let urandom_path = PathBuf::from(URANDOM_PATH);
        if urandom_path.exists() && urandom_path.is_dir() == false && urandom_path.is_file() == false {
            println!(
                concat!(
                    "\n",
                    "~> {} {} {}=1024 -- {}",
                ),
                SAMPLE_APP_NAME, Hash::Sha3_256.to_string().to_lowercase(), ARG_LIMIT[0], URANDOM_PATH,
            );
            run_main_job({
                let args: &[&dyn AsRef<str>] = &[&Hash::Sha3_256.to_string(), &ARG_LIMIT[0], &"1024", &"--", &URANDOM_PATH];
                dia_args::parse_strings(args.iter())?
            })?;
        }
    }

    Ok(())
}

/// # Runs main job
fn run_main_job(args: Args) -> Result<()> {
    let (hash, use_stdin, fmt, salt, mut args) = parse_common_args(args)?;
    match use_stdin {
        true => {
            let limit = args.take::<u64>(ARG_LIMIT)?;
            ensure_args_are_empty(args)?;

            let mut keccak = new_keccak(hash.clone(), salt.as_ref())?;

            let stdin = io::stdin();
            let mut stdin = stdin.lock();
            match limit {
                Some(limit) => io::copy(&mut stdin.take(limit), &mut keccak)?,
                None => io::copy(&mut stdin, &mut keccak)?,
            };

            println!("{} ({})", fmt.fmt(keccak.finish()), hash);
        },
        false => {
            let input = args.take_args();
            let files = args.take_sub_args();
            match (input, files) {
                (Some(mut strings), None) => {
                    let trim = args.take(ARG_TRIM)?.unwrap_or(ARG_TRIM_DEFAULT);
                    let iteration = args.take(ARG_ITERATION)?.unwrap_or(ARG_ITERATION_DEFAULT);
                    ensure_args_are_empty(args)?;
                    match strings.len() {
                        1 => {
                            let data = strings.remove(0);
                            let data = match trim {
                                true => data.trim().as_bytes(),
                                false => data.as_bytes(),
                            };

                            let mut keccak = new_keccak(hash.clone(), salt.as_ref())?;
                            keccak.update(data);
                            let mut output = keccak.finish();

                            for _ in 0..iteration {
                                let mut keccak = new_keccak(hash.clone(), salt.as_ref())?;
                                keccak.update(output);
                                output = keccak.finish();
                            }

                            println!("{} ({})", fmt.fmt(output), hash);
                        },
                        _ => return Err(Error::new(ErrorKind::InvalidInput, "Only one input string is supported")),
                    };
                },
                (None, Some(files)) => {
                    let limit = args.take::<u64>(ARG_LIMIT)?;
                    ensure_args_are_empty(args)?;
                    for f in files {
                        let mut reader = BufReader::new(File::open(&f)?);
                        let mut keccak = new_keccak(hash.clone(), salt.as_ref())?;
                        match limit {
                            Some(limit) => io::copy(&mut reader.take(limit), &mut keccak)?,
                            None => io::copy(&mut reader, &mut keccak)?,
                        };
                        println!("{} [{}] {}", fmt.fmt(keccak.finish()), hash, f);
                    }
                },
                (Some(_), Some(_)) => return Err(Error::new(
                    ErrorKind::InvalidInput, "Either one input string or file(s) are supported at a time; not both",
                )),
                (None, None) => return Err(Error::new(ErrorKind::InvalidInput, "Missing input string or files")),
            };
        },
    };

    Ok(())
}

/// # Parses common arguments
fn parse_common_args(args: Args) -> Result<(Hash, bool, Format, Option<Vec<u8>>, Args)> {
    let (hash, mut args) = args.into_sub_cmd();

    let hash = match hash {
        Some(hash) => Hash::from(self::hash::Hash::from_str(&hash)?),
        None => return Err(Error::new(ErrorKind::InvalidInput, "Missing algorithm name")),
    };

    let use_stdin = tty::is_stdin_tty() == false || args.use_stdin();
    if use_stdin && args.args().is_some() {
        return Err(Error::new(ErrorKind::InvalidInput, "Either stdin or input is supported at a time; not both"));
    }

    let fmt = args.take(ARG_FORMAT)?.unwrap_or(ARG_FORMAT_DEFAULT);
    let with_time = args.take(ARG_WITH_TIME)?.unwrap_or(ARG_WITH_TIME_DEFAULT);
    let salt = match with_time {
        true => Some(make_salt_from_current_time()),
        false => None,
    };

    Ok((hash, use_stdin, fmt, salt, args))
}

/// # Makes salt from current time
fn make_salt_from_current_time() -> Vec<u8> {
    let duration = match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(d) => d,
        Err(e) => e.duration(),
    };

    let mut salt: Vec<u8> = Vec::with_capacity(mem::size_of::<u64>().saturating_add(mem::size_of::<u32>().saturating_mul(3)));

    salt.extend(duration.as_secs().to_be_bytes().iter());
    salt.extend(duration.subsec_millis().to_be_bytes().iter());
    salt.extend(duration.subsec_micros().to_be_bytes().iter());
    salt.extend(duration.subsec_nanos().to_be_bytes().iter());
    salt
}

/// # Makes new Keccak and optionally feeds it with a salt
fn new_keccak<B>(hash: Hash, salt: Option<B>) -> Result<Keccak> where B: AsRef<[u8]> {
    let mut keccak = hash.new_keccak();
    if let Some(salt) = salt {
        keccak.update(salt);
    }
    Ok(keccak)
}
