use crate::misc::{compress_writer, decompress_reader};
use indicatif::ParallelProgressIterator;
use indicatif::ProgressIterator;
use rayon::prelude::*;
use smol_str::SmolStr;
use std::fs::File;
use std::hash::Hasher;
use std::io;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::PathBuf;
use tlsh::prelude::*;

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

const H_BUF_SIZE: usize = 1 << 15;

unsafe fn to_io(hash: (GHash, Option<THash>), writer: &mut impl Write) -> Result<(), io::Error> {
    let mut buf = [0u8; 15];
    writer.write_all(&(hash.0).to_le_bytes())?;
    if let Some(h) = hash.1 {
        writer.write_all(&[0x00])?;
        h.store_into_bytes(&mut buf).unwrap_unchecked();
        writer.write_all(&buf)?;
    } else {
        writer.write_all(&[0x01])?;
    }
    Ok(())
}

unsafe fn from_io(reader: &mut impl Read) -> Result<(GHash, Option<THash>), io::Error> {
    let mut buf = [0u8; 8];
    reader.read_exact(&mut buf)?;
    let id = u64::from_le_bytes(buf);

    let mut cmd = [0u8; 1];
    reader.read_exact(&mut cmd)?;

    if cmd[0] == 0 {
        let mut tlsh_buf = [0u8; 15];
        reader.read_exact(&mut tlsh_buf)?;
        let hash = THash::try_from(&tlsh_buf).unwrap();
        Ok((id, Some(hash)))
    } else {
        Ok((id, None))
    }
}

pub fn hash_stream(reader: &mut impl Read) -> Result<(GHash, Option<THash>), io::Error> {
    let mut t_hasher = tlsh::TlshGeneratorFor::<THash>::new();
    let mut g_hasher = gxhash::GxHasher::with_seed(0);
    let mut buf = [0u8; H_BUF_SIZE];
    let mut pos = 0;

    loop {
        match reader.read(&mut buf[pos..]) {
            Ok(0) => {
                if pos > 0 {
                    t_hasher.update(&buf[..pos]);
                    g_hasher.write(&buf[..pos]);
                }
                break;
            }
            Ok(n) => {
                pos += n;
                if pos == H_BUF_SIZE {
                    t_hasher.update(&buf);
                    g_hasher.write(&buf);
                    pos = 0;
                }
            }
            Err(e) => return Err(e),
        }
    }

    if let Ok(h) = t_hasher.finalize() {
        Ok((g_hasher.finish(), Some(h)))
    } else {
        Ok((g_hasher.finish(), None))
    }
}

pub fn hash_file(file: &str, is_compressed: bool) -> Result<(GHash, Option<THash>), io::Error> {
    if is_compressed {
        Ok(hash_stream(&mut decompress_reader(file)?)?)
    } else {
        Ok(hash_stream(&mut BufReader::new(File::open(file)?))?)
    }
}

pub fn hash_bytes(bytes: Vec<u8>) -> (GHash, Option<THash>) {
    hash_stream(&mut io::Cursor::new(bytes)).unwrap()
}

pub fn get_distance(old_hash: (GHash, Option<THash>), new_hash: (GHash, Option<THash>)) -> u32 {
    if old_hash.0 == new_hash.0 {
        0u32
    } else {
        match old_hash.1 {
            Some(a) => match new_hash.1 {
                Some(b) => {
                    let distance = a.compare(&b);
                    if distance == 0 {
                        1u32 // tlsh thinks same, but gx not
                    } else {
                        distance
                    }
                }
                None => u32::MAX,
            },
            None => u32::MAX, // we already know gx isn't equal
        }
    }
}

use crate::misc::{get_spinner, get_style};
pub fn read_hashes(file: &String) -> Result<Vec<(GHash, Option<THash>)>, io::Error> {
    let mut hash_file = decompress_reader(file)?;
    let mut hashes: Vec<(GHash, Option<THash>)> = vec![];
    unsafe {
        let style = get_spinner("Reading hashes");
        loop {
            match from_io(&mut hash_file) {
                Ok(e) => {
                    hashes.push(e);
                    style.inc(1);
                }
                Err(e) => {
                    if e.kind() == io::ErrorKind::UnexpectedEof {
                        break;
                    } else {
                        return Err(e);
                    }
                }
            }
        }
        style.finish_and_clear();
    }
    Ok(hashes)
}

pub fn write_hashes(
    file: &String,
    hashes: &mut Vec<(SmolStr, (GHash, Option<THash>))>,
) -> Result<(), io::Error> {
    let mut hash_file = compress_writer(file)?;
    hashes.sort_unstable_by_key(|h| h.0.clone());
    unsafe {
        if !hashes.is_empty() {
            let style = get_style("Writing hashes", hashes.len());
            hashes
                .into_iter()
                .progress_with(style)
                .map(|hash| -> Result<(), io::Error> { to_io(hash.1, &mut hash_file) })
                .collect::<Result<(), io::Error>>()?;
        }
    }
    hash_file.flush()?;
    Ok(())
}

pub fn create_hashes(
    files: &Vec<PathBuf>,
    repo_base: &String,
    is_compressed: bool,
) -> Result<Vec<(SmolStr, (GHash, Option<THash>))>, io::Error> {
    if !files.is_empty() {
        let style = get_style("Computing hashes", files.len());
        let hashes = files
            .into_par_iter()
            .progress_with(style)
            .map(
                |f| -> Result<(SmolStr, (GHash, Option<THash>)), io::Error> {
                    Ok((
                        SmolStr::from(f.strip_prefix(repo_base).unwrap().to_str().unwrap()),
                        hash_file(f.to_str().unwrap(), is_compressed)?,
                    ))
                },
            )
            .collect::<Result<Vec<_>, io::Error>>()?;

        Ok(hashes)
    } else {
        Ok(vec![])
    }
}
