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

//! Wrapper around sha3 lib and file IO

use std::{fmt, fs, io};
use crate::sha3::Digest;
use std::fs::{OpenOptions};
use std::io::{BufReader, BufRead, Read};
use data_encoding::{HEXLOWER};
use crate::Mode;

/// Performance can be improved by adjusting the number of bytes read each iteration
/// The environment variable `SHA3_BLOCK_SIZE` allow to modify the default
const DEFAULT_BLOCK_SIZE: u64 = 65536;

/*
 * Read env. var `SHA3_BLOCK_SIZE`
 */
fn get_block_size() -> u64  {
    match option_env!("SHA3_BLOCK_SIZE") {
        Some(v) => v.parse::<u64>().unwrap_or(DEFAULT_BLOCK_SIZE),
        None => DEFAULT_BLOCK_SIZE
    }
}

/// Define supported algorithm
#[derive(PartialEq, Clone, Copy)]
pub enum Sha3Mode {
    Sha3_224,
    Sha3_256,
    Sha3_384,
    Sha3_512,
    Keccak224,
    Keccak256,
    Keccak256Full,
    Keccak384,
    Keccak512,
    Shake128,
    Shake256,
}

/// Debug: Sha3Mode
impl fmt::Debug for Sha3Mode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Sha3Mode::Sha3_224 => write!(f, "SHA3_224"),
            Sha3Mode::Sha3_256 => write!(f, "SHA3_256"),
            Sha3Mode::Sha3_512 => write!(f, "SHA3_512"),
            Sha3Mode::Sha3_384 => write!(f, "SHA3_384"),
            Sha3Mode::Keccak224 => write!(f, "KECCAK224"),
            Sha3Mode::Keccak256 => write!(f, "KECCAK256"),
            Sha3Mode::Keccak256Full => write!(f, "KECCAK256FULL"),
            Sha3Mode::Keccak384 => write!(f, "KECCAK384"),
            Sha3Mode::Keccak512 => write!(f, "KECCAK512"),
            Sha3Mode::Shake128 => write!(f, "SHAKE128"),
            Sha3Mode::Shake256 => write!(f, "SHAKE256"),
        }
    }
}

impl fmt::Display for Sha3Mode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Sha3Mode::Sha3_224 => write!(f, "SHA3_224"),
            Sha3Mode::Sha3_256 => write!(f, "SHA3_256"),
            Sha3Mode::Sha3_512 => write!(f, "SHA3_512"),
            Sha3Mode::Sha3_384 => write!(f, "SHA3_384"),
            Sha3Mode::Keccak224 => write!(f, "KECCAK224"),
            Sha3Mode::Keccak256 => write!(f, "KECCAK256"),
            Sha3Mode::Keccak256Full => write!(f, "KECCAK256FULL"),
            Sha3Mode::Keccak384 => write!(f, "KECCAK384"),
            Sha3Mode::Keccak512 => write!(f, "KECCAK512"),
            Sha3Mode::Shake128 => write!(f, "SHAKE128"),
            Sha3Mode::Shake256 => write!(f, "SHAKE256"),
        }
    }
}

impl From<String> for Sha3Mode {
    fn from(mode: String) -> Self {
        match mode.to_uppercase().as_str() {
            "SHA3_224" => Sha3Mode::Sha3_224,
            "224" => Sha3Mode::Sha3_224,
            "SHA3_256" => Sha3Mode::Sha3_256,
            "256" => Sha3Mode::Sha3_256,
            "SHA3_384" => Sha3Mode::Sha3_384,
            "384" => Sha3Mode::Sha3_384,
            "SHA3_512" => Sha3Mode::Sha3_512,
            "512" => Sha3Mode::Sha3_512,
            "KECCAK224" => Sha3Mode::Keccak224,
            "KECCAK256" => Sha3Mode::Keccak256,
            "KECCAK256FULL" => Sha3Mode::Keccak256Full,
            "KECCAK384" => Sha3Mode::Keccak384,
            "KECCAK512" => Sha3Mode::Keccak512,
            "SHAKE128" => Sha3Mode::Shake128,
            "SHAKE256" => Sha3Mode::Shake256,
            v @ &_ => panic!("Could not convert {} to Sha3 algorithm.",v),
        }
    }
}

//impl Into<&str> for Sha3Mode {
impl From<Sha3Mode> for &str{
    fn from(sha3: Sha3Mode) -> &'static str {
        match sha3 {
            Sha3Mode::Sha3_224 => "SHA3_224",
            Sha3Mode::Sha3_256 => "SHA3_256",
            Sha3Mode::Sha3_512 => "SHA3_512",
            Sha3Mode::Sha3_384 => "SHA3_384",
            Sha3Mode::Keccak224 => "KECCAK224",
            Sha3Mode::Keccak256 => "KECCAK256",
            Sha3Mode::Keccak256Full =>  "KECCAK256FULL",
            Sha3Mode::Keccak384 => "KECCAK384",
            Sha3Mode::Keccak512 => "KECCAK512",
            Sha3Mode::Shake128 =>"SHAKE128",
            Sha3Mode::Shake256 => "SHAKE256",
        }
    }
}

/// Struct used by to check digest from file
#[derive(PartialEq, Clone, Debug)]
pub struct CheckFileInfo {
    pub algorithm: Sha3Mode,
    pub mode: Mode,
    pub is_bsd_format: bool,
    pub hash: String,
    pub file_name: String,
}

/// Read and create data to check each file
/// .tests/check-file.txt is a sample of format
pub fn read_check_file(file_name: &str,is_status:bool) -> Result<Vec <CheckFileInfo>,io::Error>{
    let result_file_handler = OpenOptions::new()
        .read(true)
        .write(false)
        .create(false)
        .open(file_name);

    if result_file_handler.is_err() {
        let message = format!("Could not open file {}.",file_name);
        if !is_status {
            eprintln!("{}",message);
        }
        let error = result_file_handler.err().unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, message));
        return Err(error);
    }

    let file_handler = result_file_handler.unwrap();
    let buffered = BufReader::new(file_handler);
    let mut result: Vec <CheckFileInfo> = Vec::new();
    for r_line in buffered.lines() {
        let line : String = r_line.unwrap();
        let tokens : Vec<&str> = line.rsplit(' ').collect();
        //println!("{:?}",tokens);
        match tokens.len() {
            2 => {
                let hash = tokens.get(1).unwrap().to_string();
                let mode = if tokens.first().unwrap().starts_with('*') {Mode::Binary} else { Mode::Text };
                let file = match mode {
                    Mode::Binary => tokens.first().unwrap().trim_start_matches('*'),
                    _=> tokens.get(1).unwrap(),
                };
                let algorithm  = match tokens.get(1).unwrap().len() {
                    56 => Ok(Sha3Mode::Sha3_224),
                    64 => Ok(Sha3Mode::Sha3_256),
                    96 => Ok(Sha3Mode::Sha3_384),
                    128 => Ok(Sha3Mode::Sha3_512),
                    _ => Err(io::Error::new(io::ErrorKind::Other, "Could not determine algorithm. Malformed line")),
                };
                if algorithm.is_err() {
                    return Err(algorithm.err().unwrap());
                }
                let s_line = CheckFileInfo {
                    algorithm: algorithm.unwrap(),
                    mode,
                    is_bsd_format: false,
                    hash,
                    file_name: file.to_string(),
                };
                //println!("{:?}",s_line);
                result.push(s_line);
            },
            4 => {
                let s_line = CheckFileInfo {
                    algorithm: Sha3Mode::from(tokens.get(3).unwrap().to_string()),
                    mode: Mode::Binary,
                    is_bsd_format: true,
                    hash: tokens.first().unwrap().to_string(),
                    file_name: tokens.get(2).unwrap().trim_start_matches('(').trim_end_matches(')').to_string(),
                };
                //println!("{:?}",s_line);
                result.push(s_line);
            },
            _ => {
                let message = format!("Could not tokenize the line. Malformed line {}",line);
                return Err(io::Error::new(io::ErrorKind::Other, message));
            },
        }

    }
    //println!("{:?}:",result);
    Ok(result)
}

/// Read data from file and create hash.
/// For text mode it read line by line
/// For binary mode it read block by block
pub fn hash_from_file <D: Digest>(file_name: &str,mode : Mode,is_status:bool) -> Result<String,io::Error> {
    let result_file_handler = OpenOptions::new()
        .read(true)
        .write(false)
        .create(false)
        .open(file_name);

    if result_file_handler.is_err() {
        let message = format!("Could not open file {}.",file_name);
        if !is_status {
            eprintln!("{}",message);
        }
        let error = result_file_handler.err().unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, message));
        return Err(error);
    }

    let file_handler = result_file_handler.unwrap();

    let size = fs::metadata(file_name).unwrap().len();

    let mut hasher  = D::new();
    let mut buffered = BufReader::new(file_handler);
    match mode {
        Mode::Text => {
            for line in buffered.lines() {
                let line = line.unwrap();
                hasher.update(line);
            }
        }
        _ => {
            let block_size = get_block_size();
            if size<=block_size {
                let mut data : Vec<u8> = Vec::with_capacity(size as usize);
                buffered.read_to_end(data.as_mut())?;
                hasher.update(data);
            } else {

                let mut position = 0;
                while size-position > 0 {
                    let data_size : usize = if size-position >= block_size { block_size as usize} else { (size-position) as usize };
                    let mut data : Vec<u8> = vec![0;data_size];
                    buffered.read_exact(data.as_mut())?;
                    position += data.len() as u64;
                    hasher.update(data);
                }

            }
        }
    }
    let hash = hasher.finalize();
    Ok(HEXLOWER.encode(&hash))
}


/// Read a stream in and create hash
#[allow(dead_code)]
pub fn hash_from_file_io <D: Digest>(file_name: &str) -> Result<String,io::Error>
    where D: std::io::Write
{
    let result_file_handler = OpenOptions::new()
        .read(true)
        .write(false)
        .create(false)
        .open(file_name);

    if result_file_handler.is_err() {
        let message = format!("Could not open file {}.",file_name);
        eprintln!("{}",message);
        let error = result_file_handler.err().unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, message));
        return Err(error);
    }

    let file_handler = result_file_handler.unwrap();

    let mut buffered = BufReader::new(file_handler);
    let mut hasher  = D::new();
    let _n = io::copy(&mut buffered, &mut hasher)?;
    let hash = hasher.finalize();
    Ok(HEXLOWER.encode(&hash))
}

pub fn hash_from_reader <D: Digest>(data_reader: Box<dyn Read>) -> Result<String,io::Error>
    where D: std::io::Write
{

    let mut buffered = BufReader::new(data_reader);
    let mut hasher  = D::new();
    let _n = io::copy(&mut buffered, &mut hasher)?;
    let hash = hasher.finalize();
    Ok(HEXLOWER.encode(&hash))
}

///
/// Tests
///
#[cfg(test)]
mod tests {
    use crate::Mode;
    use crate::wrapper::{hash_from_file, hash_from_file_io, read_check_file};
    use std::time::Instant;
    use crate::sha3::*;

    const CHECK_FILE : &str = "./tests/check-file.txt";
    const CHECK_FILE_NOK : &str = "./tests/check-file_nok.txt";
    const F5_FILE : (&str,&str) = ("./tests/data/f5.raw","62412944684d0ff87aaf88f67537f5a1e6df9a12de7d2274c7eaa219c2ac9cf6ae7fb3262cf47657d465683bab868d63b5d0c85c3dc087194bd4c205ad932708");
    const F1_FILE : (&str,&str) = ("./tests/data/f1.raw","42e4f1c5b2e098e641bbc4e83700dc7a5dd7be25e43d979ebf6c36f4f33d548c8ebe5b888516794aaa9a8de61ab07e84da86d95dcfd121c0312d13202f6c87e9");
    const F1_FILE_224 : (&str,&str) = ("./tests/data/f1.raw","5941ff906cf10dbfa8605512f2b75d3471ebb5844e554bf083f77d5d");
    const F1_FILE_256 : (&str,&str) = ("./tests/data/f1.raw","5fceef81eb395c5db97b344b5119a5a6089d6bebe686fd11cc364820a32b1c36");
    const F1_FILE_384 : (&str,&str) = ("./tests/data/f1.raw","b50e02127404b8f4a870d07b3fb1342838ac84dd07fc19c4080056bf007b8237040c8d90555dbd26fcfdd365001c5fd3");
    const F1_FILE_KECCAK224: (&str, &str) = ("./tests/data/f1.raw", "2b5539fae2c74329d11efc85f3a0273cf6f56d7525ab67b151b98616");
    const F1_FILE_KECCAK256: (&str, &str) = ("./tests/data/f1.raw", "2a9d36de8c15de4e17ac939e48e8c46af0cba879a61696474b53fd9e460203c6");
    const F1_FILE_KECCAK256FULL: (&str, &str) = ("./tests/data/f1.raw", "2a9d36de8c15de4e17ac939e48e8c46af0cba879a61696474b53fd9e460203c65e20e16d321cfd67b3674a769de85d76fbc5d5bd385efeafaa803b3d27758069ad94feb37c02d7b776bcc4a63e606dd1623da3775b4db3b08fed11e9ab7d27d162fd6e731b662dd8bc6caaf8db5f81c4bd41d808642db19bdd6aadc3e6906500439ad40fb0f91c7d09389dfee7d52934284522de3714965ab2e587e6071f3bd1b0e7000d2840a493dff48e6cc16f2c9446c6b236b9e95b3aabe3f1891e510f17406c158b157bb8f9");
    const F1_FILE_KECCAK384: (&str, &str) = ("./tests/data/f1.raw", "a950f25266bf58211f8c75112e2e5d0a506fbb2ac777cc28ea75f0262c9a9ad0aa44828ff5a7051efb1f1ef1a26f463d");
    const F1_FILE_KECCAK512: (&str, &str) = ("./tests/data/f1.raw", "7fc7ec2b472038298be81caa81a65fc2430779dd9fbddf46d9c95d380f86572b478a399e3254dcf3949557c40a6c8abb6c4bd0980ddea81ed7813567c89945b6");
    const ONE_LINE_FILE : (&str,&str) = ("./tests/data/one-line-text.txt","32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48");
    const UTF8_TEXT_POSIX : (&str,&str) = ("./tests/data/UTF8-Text-POSIX.txt","b6e9fe2e2f52d18950681868c10d95909e06ee50cec4029f5b585445acc325a19bd9b9750da6cbecbb0e7c84b52e40b7b00efd05689c4d466ab9f2b112ee4697");
    #[test]
    fn test_hash_from_file_f5() {
        let start = Instant::now();
        let result=hash_from_file::<Sha3_512>(F5_FILE.0,Mode::Binary,false).unwrap();
        let duration = start.elapsed();
        println!("elapsed time: {:?} hash: {}",duration,result);
        assert_eq!(result,F5_FILE.1);
    }

    #[test]
    fn test_hash_from_file_f5_io() {
        let start = Instant::now();
        let result=hash_from_file_io::<Sha3_512>(F5_FILE.0).unwrap();
        let duration = start.elapsed();
        println!("elapsed time fct io: {:?} hash: {}",duration,result);
        assert_eq!(result,F5_FILE.1);
    }

    #[test]
    fn test_hash_from_file_f1() {
        let result=hash_from_file::<Sha3_512>(F1_FILE.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE.1);
    }

    #[test]
    fn test_hash_from_file_f1_224() {
        let result=hash_from_file::<Sha3_224>(F1_FILE_224.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_224.1);
    }

    #[test]
    fn test_hash_from_file_f1_256() {
        let result=hash_from_file::<Sha3_256>(F1_FILE_256.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_256.1);
    }

    #[test]
    fn test_hash_from_file_f1_384() {
        let result=hash_from_file::<Sha3_384>(F1_FILE_384.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_384.1);
    }

    #[test]
    fn test_hash_from_file_f1_keccak256() {
        let result=hash_from_file::<Keccak256>(F1_FILE_KECCAK256.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_KECCAK256.1);
    }

    #[test]
    fn test_hash_from_file_f1_keccak256full() {
        let result=hash_from_file::<Keccak256Full>(F1_FILE_KECCAK256FULL.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_KECCAK256FULL.1);
    }

    #[test]
    fn test_hash_from_file_f1_keccak384() {
        let result=hash_from_file::<Keccak384>(F1_FILE_KECCAK384.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_KECCAK384.1);
    }

    #[test]
    fn test_hash_from_file_f1_keccak512() {
        let result=hash_from_file::<Keccak512>(F1_FILE_KECCAK512.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_KECCAK512.1);
    }

    #[test]
    fn test_hash_from_file_f1_keccak224() {
        let result=hash_from_file::<Keccak224>(F1_FILE_KECCAK224.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,F1_FILE_KECCAK224.1);
    }

    #[test]
    fn test_hash_from_file_one_line() {
        let result=hash_from_file::<sha3::Sha3_512>(ONE_LINE_FILE.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,ONE_LINE_FILE.1);
    }
    #[test]
    fn test_hash_from_file_one_line_mode_text() {
        let result=hash_from_file::<Sha3_512>(ONE_LINE_FILE.0,Mode::Text,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,ONE_LINE_FILE.1);
    }
    #[test]
    #[cfg_attr(target_os = "windows", ignore)]
    fn test_hash_from_file_utf8_txt_bin() {
        let result=hash_from_file::<Sha3_512>(UTF8_TEXT_POSIX.0,Mode::Binary,false).unwrap();
        println!("hash: {}",result);
        assert_eq!(result,UTF8_TEXT_POSIX.1);
    }
    #[test]
    fn test_hash_from_file_utf8_txt_txt() {
        let result=hash_from_file::<Sha3_512>(UTF8_TEXT_POSIX.0,Mode::Text,false).unwrap();
        println!("hash: {}",result);
        assert_ne!(result,UTF8_TEXT_POSIX.1);
    }

    #[test]
    fn test_read_check_file() {
        let result = read_check_file(CHECK_FILE,false);
        assert!(result.is_ok());
        assert_eq!(result.unwrap().len(),10);
    }

    #[test]
    fn test_read_check_file_nok() {
        let result = read_check_file(CHECK_FILE_NOK,false);
        assert!(result.is_err());
    }
}