use rayon::prelude::*;
use std::fs::File;
use std::hash::Hasher;
use std::io;
use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
use tlsh::prelude::*;

pub type THash = tlsh::hashes::Short;

fn to_io(hash: Option<THash>, writer: &mut impl Write) -> Result<(), io::Error> {
    let mut buf = [0u8; 15];
    match hash {
        Some(v) => {
            v.store_into_bytes(&mut buf).unwrap();
        }
        _ => {}
    }
    writer.write_all(&buf)?;
    Ok(())
}

fn from_io(reader: &mut impl Read) -> Result<Option<THash>, io::Error> {
    let mut buf = [0u8; 15];
    reader.read_exact(&mut buf)?;
    Ok(if buf == [0u8; 15] {
        None
    } else {
        Some(THash::try_from(&buf).unwrap())
    })
}

pub fn hash_file(file: String) -> Result<Option<THash>, io::Error> {
    let mut f = BufReader::new(File::open(file.clone())?);
    let hash = tlsh::hash_stream_for::<THash, BufReader<File>>(&mut f);
    Ok(match hash {
        Ok(h) => Some(h),
        Err(_) => {
            // too small
            None
        }
    })
}

pub fn get_distance(
    old_hash: Option<THash>,
    new_hash: Option<THash>,
    old: String,
    new: String,
) -> Result<u32, io::Error> {
    Ok(match old_hash {
        Some(a) => match new_hash {
            Some(b) => {
                let distance = a.compare(&b);
                if distance == 0 {
                    // if actually the same file
                    if comp_gx(old, new)? {
                        0u32
                    } else {
                        1u32 // tlsh thinks but gx not, very similar
                    }
                } else {
                    distance
                }
            }
            None => u32::MAX,
        },
        None => match new_hash {
            Some(_) => u32::MAX,
            None => {
                if comp_gx(old, new)? {
                    0u32
                } else {
                    u32::MAX
                }
            }
        },
    })
}

fn comp_gx(old: String, new: String) -> Result<bool, io::Error> {
    let mut old_builder = gxhash::GxHasher::with_seed(0);
    let mut new_builder = gxhash::GxHasher::with_seed(0);
    let mut old_file = BufReader::new(File::open(old)?);
    let mut new_file = BufReader::new(File::open(new)?);
    loop {
        let mut buf = [0u8; 1];
        match old_file.read_exact(&mut buf) {
            Ok(_) => {
                old_builder.write_u8(buf[0]);
            }
            Err(e) => {
                if e.kind() == io::ErrorKind::UnexpectedEof {
                    break;
                } else {
                    return Err(e);
                }
            }
        }
    }
    loop {
        let mut buf = [0u8; 1];
        match new_file.read_exact(&mut buf) {
            Ok(_) => {
                new_builder.write_u8(buf[0]);
            }
            Err(e) => {
                if e.kind() == io::ErrorKind::UnexpectedEof {
                    break;
                } else {
                    return Err(e);
                }
            }
        }
    }
    Ok(old_builder.finish_u128() == new_builder.finish_u128())
}

pub fn read_hashes(file: String) -> Result<Vec<Option<THash>>, io::Error> {
    let mut patch_file = BufReader::new(File::open(file)?);
    let mut patch_entries: Vec<Option<THash>> = vec![];
    loop {
        match from_io(&mut patch_file) {
            Ok(e) => {
                patch_entries.push(e);
            }
            Err(e) => {
                if e.kind() == io::ErrorKind::UnexpectedEof {
                    break;
                } else {
                    return Err(e);
                }
            }
        }
    }
    Ok(patch_entries)
}

pub fn write_hashes(file: String, hashes: Vec<Option<THash>>) -> Result<(), io::Error> {
    let mut patch_file = BufWriter::new(File::create(file)?);
    for hash in hashes {
        to_io(hash, &mut patch_file)?;
    }
    patch_file.flush()?;
    Ok(())
}

pub fn create_hashes(files: Vec<String>) -> Result<Vec<Option<THash>>, io::Error> {
    let hashes = files
        .into_par_iter()
        .map(|f| -> Result<Option<THash>, io::Error> { hash_file(f) })
        .collect::<Result<Vec<_>, io::Error>>()?;
    Ok(hashes)
}
