/*
 *  This file is part of sha3sum
 *
 *  sha3sum is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *
 *   sha3sum 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with  sha3sum .  If not, see <http://www.gnu.org/licenses/>
 */

//! Command line that wraps sha3 lib from [RustCrypto/hashes](https://github.com/RustCrypto/hashes).
//! It includes only Sha3 and Keccak to be a mirror of shaXXXsum from GNU Linux command.
//! It uses few dependencies to keep it small and fast.
//! Main contains command line management (using clap) and the calls to functions
//! The number of threads depends on the numbers CPUs.

extern crate clap;
extern crate sha3;
extern crate data_encoding;
extern crate num_cpus;

mod wrapper;

use clap::{Arg, App, AppSettings};
use std::process::exit;
use std::{fmt, fs, thread, io};
use std::path::Path;
use std::io::{Error, ErrorKind};
use wrapper::Sha3Mode;
use crate::wrapper::{hash_from_file, read_check_file, hash_from_reader};
use crate::sha3::*;
use std::sync::mpsc;

// Constants
pub const LICENSE: &str = "GPL-3.0-or-later";
pub const NO_BREAK_SPACE : char = '\u{00a0}';
pub const EXIT_CODE_OK: i32 = 0;
pub const EXIT_CODE_NOK: i32 = 1;
pub const EXIT_CODE_WRONG_PARAMETERS: i32 = 2;
pub const EXIT_CODE_FILE_ERROR: i32 = 64;
pub const EXIT_CODE_HASH_NOT_EQUAL: i32 = 65;

/// State of parameter how the file is read
#[derive(PartialEq, Clone, Copy)]
pub enum Mode {
    Binary,
    Text,
}

/// Debug: Sha3Mode
impl fmt::Debug for Mode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Mode::Binary => write!(f, "Binary"),
            Mode::Text => write!(f, "Text"),
        }
    }
}

/// Define a task, it include display parameters
#[derive(Clone)]
pub struct HashTask {
    mode : Mode,
    file_name : String,
    hash_algorithm : Sha3Mode,
    is_bsd_display: bool,
    ref_hash : Option<String>,
    is_quiet : bool,
    is_status : bool,
}

/// Define a worker
struct Worker {
    sender: Option<mpsc::Sender<HashTask>>
}

/// Entry point. It use lib clap
fn main() {
    let mut exit_code = EXIT_CODE_WRONG_PARAMETERS;
    //let cpus = num_cpus::get();
    let matches = App::new(env!("CARGO_PKG_NAME"))
        .version(env!("CARGO_PKG_VERSION"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .about(env!("CARGO_PKG_DESCRIPTION"))
        .setting(AppSettings::ArgRequiredElseHelp)
        .arg(Arg::with_name("algorithm")
            .short("a")
            .long("algorithm")
            .help("sha3 algorithm {224 256 384 512 Keccak224 Keccak256 Keccak256Full Keccak384 Keccak512}")
            .takes_value(true)
            .required(false))
        .arg(Arg::with_name("Binary")
            .short("b")
            .long("binary")
            .help("read in Binary mode (default)")
            .required(false))
        .arg(Arg::with_name("check")
            .short("c")
            .long("check")
            .takes_value(true)
            .help("read SHA3 sums from the FILEs and check them")
            .required(false))
        .arg(Arg::with_name("Text")
            .short("t")
            .long("text")
            .help("read in Text mode")
            .required(false))
        .arg(Arg::with_name("Tag")
            .long("tag")
            .help("create a BSD-style checksum")
            .required(false))
        .arg(Arg::with_name("Quiet")
            .long("quiet")
            .help("don't print OK for each successfully verified file")
            .required(false))
        .arg(Arg::with_name("Status")
            .long("status")
            .help("don't output anything, status code shows success")
            .required(false))
        .arg(Arg::with_name("FILE(S)")
            .multiple(true)
            .help("Print check sums from File(s)"))
        .arg(Arg::with_name("license")
            .short("l")
            .long("license")
            .help("Prints license information")
            .takes_value(false))
        .get_matches();

    if matches.is_present("license") {
        println!("The license is {}. The gpl.txt file contains the full Text.", LICENSE);
        exit_code = EXIT_CODE_OK;
    }

    if matches.is_present("license") {
        println!("The license is {}. The gpl.txt file contains the full Text.", LICENSE);
        exit_code = EXIT_CODE_OK;
    }


    let mut algorithm = None;
    if matches.is_present("algorithm") {
        let value = matches.value_of("algorithm").unwrap_or("512");
        algorithm = match value.to_lowercase().as_str() {
            "224" => Some(Sha3Mode::Sha3_224),
            "256" => Some(Sha3Mode::Sha3_256),
            "384" => Some(Sha3Mode::Sha3_384),
            "512" => Some(Sha3Mode::Sha3_512),
            "sha3_224" => Some(Sha3Mode::Sha3_224),
            "sha3_256" => Some(Sha3Mode::Sha3_256),
            "sha3_384" => Some(Sha3Mode::Sha3_384),
            "sha3_512" => Some(Sha3Mode::Sha3_512),
            "keccak224" =>  Some(Sha3Mode::Keccak224),
            "keccak256" =>  Some(Sha3Mode::Keccak256),
            "keccak256full" =>  Some(Sha3Mode::Keccak256Full),
            "keccak384" =>  Some(Sha3Mode::Keccak384),
            "keccak512" =>  Some(Sha3Mode::Keccak512),
            "shake128" =>  unimplemented!(),
            "shake256" =>  unimplemented!(),
            v=> {
                eprintln!("Invalid value for algorithm. {}",v);
                None
            },
        }
    }

    if algorithm.is_none() {
        exit_code = EXIT_CODE_WRONG_PARAMETERS;
    }

    let mut mode = Mode::Binary;
    if matches.is_present("Text") { mode = Mode::Text };

    // Core section
    // 1) Test if we need the check file using digest file
    // 2) else if check if we need to generate digest from file(s) parameter
    // 3) else read data from input stream
    let is_quiet = matches.is_present("Quiet");
    let is_status = matches.is_present("Status");
    if matches.is_present("check") {
        let file_name = Some(matches.value_of("check").expect("Expect a file name")).unwrap();
        // Read file and return file and reference hash to check
        let result_list = read_check_file(file_name,is_status);
        if let Ok(list)=result_list.as_ref(){
            let mut tasks:Vec<HashTask> = Vec::new();
            //Build tasks
            for  item in list.iter() {
                let task = HashTask {
                    mode : item.mode,
                    file_name: item.file_name.clone(),
                    hash_algorithm:  item.algorithm,
                    is_bsd_display: false,
                    ref_hash: Some(item.hash.clone()),
                    is_quiet,
                    is_status,
                };
                tasks.push(task);
            }
            exit_code = do_hashes(tasks);
        } else {
            if !is_status {
                eprintln!("{}",result_list.unwrap_err());
            }
            exit_code = EXIT_CODE_FILE_ERROR;
        }
    } else if matches.is_present("FILE(S)") {
        let mut all_file: Vec<String> = Vec::new();
        for param in matches.values_of("FILE(S)").unwrap() {
            let candidate = Path::new(param);
            if candidate.is_file() {
                all_file.push(param.to_string());
            } else if candidate.is_dir() {
                if let Ok(files) = fs::read_dir(param) {
                    files.filter_map(Result::ok)
                        .filter(|d| d.metadata().unwrap().is_file())
                        .filter(|d| !d.file_name().into_string().unwrap().starts_with('.'))
                        .for_each(|f| {
                            let file_name = f.path().to_str().unwrap().to_string();
                            all_file.push(file_name);
                        });
                }
            } else {
                eprintln!("Error file: {} has been rejected.", param);
            }
        }
        let mut tasks:Vec<HashTask> = Vec::new();
        for file in all_file {
            let task = HashTask {
                mode,
                file_name: file,
                hash_algorithm: algorithm.unwrap(),
                is_bsd_display: matches.is_present("Tag"),
                ref_hash : None,
                is_quiet : false,
                is_status : false,
            };
            tasks.push(task);
        }
        exit_code = do_hashes(tasks);
    } else if let Some(algo_mode)=algorithm {
        let result = match algo_mode {
            Sha3Mode::Sha3_224 => hash_from_reader::<Sha3_224>(Box::new(io::stdin())),
            Sha3Mode::Sha3_256 => hash_from_reader::<Sha3_256>(Box::new(io::stdin())),
            Sha3Mode::Sha3_384 => hash_from_reader::<Sha3_384>(Box::new(io::stdin())),
            Sha3Mode::Sha3_512 => hash_from_reader::<Sha3_512>(Box::new(io::stdin())),
            Sha3Mode::Keccak224 => hash_from_reader::<Keccak224>(Box::new(io::stdin())),
            Sha3Mode::Keccak256 => hash_from_reader::<Keccak256>(Box::new(io::stdin())),
            Sha3Mode::Keccak384 => hash_from_reader::<Keccak384>(Box::new(io::stdin())),
            Sha3Mode::Keccak256Full => hash_from_reader::<Keccak256Full>(Box::new(io::stdin())),
            Sha3Mode::Keccak512 => hash_from_reader::<Keccak512>(Box::new(io::stdin())),
            _ => Err(Error::new(ErrorKind::Other, "Could not determine algorithm.")),
        };

        if let Ok(hash)=result.as_ref() {
            display_result("-" ,Some(Mode::Binary),hash.as_str(),None,algorithm,false,false);
        } else {
            eprintln!("{}",result.unwrap_err());
            exit_code = EXIT_CODE_FILE_ERROR;
        }
    }
    // Exit with code
    exit(exit_code)
}

/*
 * If number of task = 1 it doesn't create thread's
 * otherwise it create workers and then dispatch files to workers
 */
fn do_hashes(tasks:Vec<HashTask>) -> i32 {
    let mut result = EXIT_CODE_NOK;
    if tasks.is_empty() {
        result = EXIT_CODE_FILE_ERROR;
    } else if tasks.len() == 1 {
        let task = tasks.first();
        result = execute_task(task.unwrap().clone());
    } else {
        let result_chn: (mpsc::Sender<i32>, mpsc::Receiver<i32>) = mpsc::channel();
        let mut workers : Vec<Worker> = Vec::new();
        create_workers(workers.as_mut(), result_chn.0);
        let nb_worker = workers.len();
        let nb_task = tasks.len();
        for i in 0..tasks.len() {
            let worker_id = i % nb_worker;
            let worker = workers.get(worker_id);
            let sender = worker.unwrap().sender.as_ref().unwrap();
            let task = tasks.get(i);
            sender.send(task.unwrap().clone()).expect("Expect send a task.");
        }

        for (task_done_cnt, x) in result_chn.1.iter().enumerate() {
            if x != EXIT_CODE_OK {
                result = x;
            }
            if task_done_cnt >= nb_task -1 {
                break;
            }
        }
    }
    result
}

/*
 * Create workers and channels to compute digest
 */
fn create_workers(workers: &mut Vec<Worker>, rlt_sender : mpsc::Sender<i32>) {
    let cpus = num_cpus::get();
    for i in 0..cpus {
        let task_chn: (mpsc::Sender<HashTask>, mpsc::Receiver<HashTask>) = mpsc::channel();
        let task_sender = task_chn.0.clone();
        let result_sender = rlt_sender.clone();
        // Create builder
        let builder = thread::Builder::new().name(format!("{}", i));
        builder
            .spawn(move || {
                let receiver = task_chn.1;
                for task in receiver.iter() {
                    let code = execute_task(task);
                    result_sender.send(code).unwrap();
                }
            })
            .unwrap_or_else(|_| panic!("Expect no error from thread {}", i));
        let worker = Worker {
            sender: Some(task_sender),
        };
        workers.push(worker);
    }
}

/*
 * Execute the task and display result
 */
fn execute_task (task : HashTask) -> i32 {
    let result_code : i32;
    let result = match task.hash_algorithm {
        Sha3Mode::Sha3_224 => hash_from_file::<Sha3_224>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Sha3_256 => hash_from_file::<Sha3_256>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Sha3_384 => hash_from_file::<Sha3_384>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Sha3_512 => hash_from_file::<Sha3_512>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Keccak224 => hash_from_file::<Keccak224>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Keccak256 => hash_from_file::<Keccak256>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Keccak384 => hash_from_file::<Keccak384>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Keccak256Full => hash_from_file::<Keccak256Full>(task.file_name.as_str(), task.mode,task.is_status),
        Sha3Mode::Keccak512 => hash_from_file::<Keccak512>(task.file_name.as_str(), task.mode,task.is_status),
        _ => Err(Error::new(ErrorKind::Other, "Could not determine algorithm.")),
    };
    if let Ok(hash) = result.as_ref() {
        let display_mode= if task.is_bsd_display { None } else { Some(task.mode) };
        display_result(task.file_name.as_str(),display_mode,hash.as_str(),task.ref_hash,Some(task.hash_algorithm),task.is_quiet,task.is_status);
        result_code = EXIT_CODE_OK;
    } else {
        if !task.is_status {
            eprintln!("{}",result.unwrap_err());
        }
        result_code = EXIT_CODE_FILE_ERROR;
    };
    result_code
}

/*
 * Format output of result
 * 4 output format:
 * When parameter --tag is present mode = None:
 *      Algorithm (file_name) = hash
 *      SHA256 (./tests/data/f1.raw) = 099399daeebaac85a3eeab033990b7b1efc2a41ef2cf0f99445b8a1d2480cd58
 * Without parameter --tag
 *      if mode is binary:
 *          hash *file_name
 *          e407db77b2e64fc30468c767afd6e2410f4e36b1e39ff8ce40eee7583f6ae88e *./tests/data/UTF8-Text-WIN.txt
 *      if text mode:
 *          hash file_name
 *          e407db77b2e64fc30468c767afd6e2410f4e36b1e39ff8ce40eee7583f6ae88e ./tests/data/UTF8-Text-WIN.txt
 * Check hash output
 *      file_name OK/NOK
 *      ./tests/data/one-line-text.txt: Ok
 *      ./tests/data/one-line-text.txt: NOK
 */
fn display_result (file_name:&str, mode : Option<Mode>, hash: &str, ref_hash: Option<String>, algorithm : Option<Sha3Mode>, is_quiet : bool, is_status:bool) {
    let mut text : Option<String> = None;
    if let Some(the_ref)=ref_hash {
        let is_hash_equal = the_ref.eq_ignore_ascii_case(hash);
        let hash_match = if is_hash_equal { "Ok" } else { "NOk" };
        if ((is_quiet && !is_hash_equal) || (!is_quiet && is_hash_equal)) && !is_status {
            text = Some(format!("{}{}{}",file_name,NO_BREAK_SPACE,hash_match));
        }
    } else if let Some(the_mode)=mode {
        text = match the_mode {
            Mode::Binary => Some(format!("{}{}*{}",hash,NO_BREAK_SPACE,file_name)),
            _ => Some(format!("{}{}{}",hash,NO_BREAK_SPACE,file_name)),
        }
    } else {
        text = Some(format!("{}{}({}){}={}{}",algorithm.unwrap(),NO_BREAK_SPACE,file_name,NO_BREAK_SPACE,NO_BREAK_SPACE,hash));
    }
    if text.is_some() {
        println!("{}",text.unwrap());
    }
}
