/*
 *  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::{Parser,ArgGroup};
use std::process::exit;
use std::{fmt, fs, thread, io};
//use std::{fmt, thread};
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::wrapper::{hash_from_file};
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>>
}

/// Define cli option
#[derive(Parser)]
#[command(author=env!("CARGO_PKG_AUTHORS"), version=env!("CARGO_PKG_VERSION"), about=env!("CARGO_PKG_DESCRIPTION"), long_about = None)]
#[command(next_line_help = true)]
#[command(group(
    ArgGroup::new("sum")
        .args(["algorithm","tag"])
        .conflicts_with("check"),
))]
#[command(group(
    ArgGroup::new("hash")
        .args(["check"])
        .conflicts_with("algorithm"),
))]
#[command(group(
    ArgGroup::new("read_mode")
        .args(["text"])
        .conflicts_with("binary"),
))]
#[command(group(
    ArgGroup::new("show_help")
        .args(["license"])
        .conflicts_with_all(["binary","text","quiet","status","tag","check"]),
))]
struct Cli {

    // Option
    #[arg(short, long,help = "sha3 algorithm {224 256 384 512 Keccak224 Keccak256 Keccak256Full Keccak384 Keccak512}")]
    algorithm: Option<String>,

    #[arg(short, long, help = "read SHA3 sums and file path from a file and check them")]
    check: Option<String>,

    // Flag
    #[arg(short, long, default_value_t =  true ,help = "read using Binary mode (default)")]
    binary: bool,

    #[arg(long, help = "don't print OK for each successfully verified file")]
    quiet : bool,

    #[arg(long, help = "don't output anything, status code shows success")]
    status : bool,

    #[arg(long, help = "create a BSD-style checksum")]
    tag : bool,

    #[arg(short, long, help = "read using Text mode")]
    text: bool,

    #[arg(short, long, help = "Prints license information")]
    license: bool,

    // Arguments
    #[arg(help = "Displays the check sum for the files")]
    files: Option<Vec<String>>,
}

/// Entry point. It use lib clap
fn main() {
    let mut exit_code :i32 = EXIT_CODE_WRONG_PARAMETERS;
    //let cpus = num_cpus::get();
    let cli = Cli::parse();

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

    let mut mode = Mode::Binary;
    if cli.text{
        mode = Mode::Text
    }

    // Either one or more hashes are validated or one or more hashes are generated.
    if let Some(check) = cli.check.as_deref() {
        // get file name with the list with pair of hash anf file to check
        let file_name = check;
        // Read file and return file and reference hash to check
        let result_list = read_check_file(file_name, cli.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: cli.quiet,
                    is_status: cli.status,
                };
                tasks.push(task);
            }
            exit_code = do_hashes(tasks);
        } else {
            if !cli.status {
                eprintln!("{}",result_list.unwrap_err());
            }
            exit_code = EXIT_CODE_FILE_ERROR;
        }
    } else if let Some(algorithm) = cli.algorithm.as_deref() {
        // create
        // Convert to Sha3Mode
        let selected_sha3_mode: Option<Sha3Mode> = match algorithm.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 let Some(selected_mode) = selected_sha3_mode{
            // Check if files have been specified
            if let Some(files) = cli.files.as_deref() {
                let mut all_file: Vec<String> = Vec::new();
                for param in files {
                    let candidate = Path::new(param.as_str());
                    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: selected_mode,
                        is_bsd_display: cli.tag,
                        ref_hash: None,
                        is_quiet: false,
                        is_status: false,
                    };
                    tasks.push(task);
                }
                exit_code = do_hashes(tasks);
            } else {
                // Take data from input stream
                let result = match selected_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, selected_sha3_mode, false, false);
                    exit_code = EXIT_CODE_OK;
                } else {
                    eprintln!("{}", result.unwrap_err());
                    exit_code = EXIT_CODE_FILE_ERROR;
                }
            }
        } else {
            exit_code = EXIT_CODE_WRONG_PARAMETERS;
        }
    }
    // 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_OK;
    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 mut result_code : i32 = EXIT_CODE_OK;
    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.as_ref(),Some(task.hash_algorithm),task.is_quiet,task.is_status);
        if let Some(the_ref)=task.ref_hash.as_ref() {
            if !the_ref.eq_ignore_ascii_case(hash) {
                result_code = EXIT_CODE_HASH_NOT_EQUAL;
            }
        }
    } 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_status || is_quiet && is_hash_equal) {
            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());
    }
}