use crate::diff::hash;
use ignore::{Walk, WalkBuilder};
use std::collections::HashSet;
use std::fs;
use std::fs::File;
use std::io;
use std::path;
use std::path::PathBuf;

pub struct Dirs {
    pub repo_dir: String,
    pub data_dir: Option<String>,
    pub patches_path: String,
    pub repo_base: String,
    pub ignore_path: String,
    pub lock_path: String,
    pub hash_path: String,
}
pub fn compare_vectors<T, U>(
    first: &Vec<(T, U)>,
    second: &Vec<(T, U)>,
) -> (Vec<(T, U)>, Vec<(T, U)>, Vec<(T, U)>)
where
    U: std::hash::Hash + Eq + Clone,
    T: Clone,
{
    let first_set: HashSet<_> = first.iter().map(|(_, u)| u).collect();
    let second_set: HashSet<_> = second.iter().map(|(_, u)| u).collect();

    let only_in_first: Vec<(T, U)> = first
        .iter()
        .filter(|(_, u)| !second_set.contains(u))
        .cloned()
        .collect();

    let only_in_second: Vec<(T, U)> = second
        .iter()
        .filter(|(_, u)| !first_set.contains(u))
        .cloned()
        .collect();

    let in_both: Vec<(T, U)> = first
        .iter()
        .filter(|(_, u)| second_set.contains(u))
        .cloned()
        .collect();

    (only_in_first, only_in_second, in_both)
}

pub fn get_diffs_dir(
    new_paths: &Vec<PathBuf>,
    repo_paths: &Vec<PathBuf>,
    dir1: String,
    dir2: String,
) -> (Vec<(u32, String)>, Vec<(u32, String)>, Vec<(u32, String)>) {
    compare_vectors(
        &new_paths
            .iter()
            .filter(|p| p.is_dir())
            .map(|p| {
                p.strip_prefix(dir1.clone())
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
            })
            .enumerate()
            .map(|(u, p)| (u as u32, p))
            .collect(),
        &repo_paths
            .iter()
            .filter(|p| p.is_dir())
            .map(|p| {
                p.strip_prefix(dir2.clone())
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
            })
            .enumerate()
            .map(|(u, p)| (u as u32, p))
            .collect(),
    )
}

 
pub fn get_diffs_file(
    new_paths: &Vec<PathBuf>,
    repo_paths: &Vec<PathBuf>,
    dir1: String,
    dir2: String,
) -> (Vec<(u32, String)>, Vec<(u32, String)>, Vec<(u32, String)>) {
    compare_vectors(
        &new_paths
            .iter()
            .filter(|p| p.is_file())
            .map(|p| {
                p.strip_prefix(dir1.clone())
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
            })
            .enumerate()
            .map(|(u, p)| (u as u32, p))
            .collect(),
        &repo_paths
            .iter()
            .filter(|p| p.is_file())
            .map(|p| {
                p.strip_prefix(dir2.clone())
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_string()
            })
            .enumerate()
            .map(|(u, p)| (u as u32, p))
            .collect(),
    )
}                                

pub fn rm_old(base: &String, paths: Vec<String>) -> Result<(), io::Error> {
    for path in paths {
        let path = PathBuf::from(format!("{}/{}", base, path));
        if path.is_dir() {
            fs::remove_dir_all(path.clone())?;
        }
        if path.is_file() {
            match fs::remove_file(path.clone()) {
                Ok(_) => {}
                Err(e) => {
                    if e.kind() == io::ErrorKind::NotFound {
                        continue;
                    } else {
                        return Err(e);
                    }
                }
            }
        }
    }
    Ok(())
}

pub fn copy_dir_all(
    src: &String,
    paths: &Vec<PathBuf>,
    dst: &String,
    overwrite: bool,
    only_dirs: bool,
) -> io::Result<()> {
    for path in paths {
        let file = path.strip_prefix(src).unwrap().to_str().unwrap();
        let dst_file = PathBuf::from(format!("{}/{}", dst, file));
        let n_exists = !dst_file.exists();

        if path.is_file() && (n_exists || overwrite) && !only_dirs {
            let src_file = format!("{}/{}", src, file);
            fs::copy(src_file, dst_file.clone())?;
        }

        if path.is_dir() && n_exists {
            fs::create_dir(dst_file)?;
        }
    }

    Ok(())
}

pub fn setup_dirs(repo_dir: &PathBuf, data_dir: Option<&PathBuf>) -> Dirs {
    let repo_dir = path::absolute(repo_dir).unwrap();
    let data_dir = match data_dir {
        Some(dir) => {
            let dir = path::absolute(dir).unwrap();
            let dir = dir.to_str().unwrap().trim_end_matches('/').to_string();
            Some(dir)
        }
        _ => None,
    };

    let repo_dir = repo_dir.to_str().unwrap().trim_end_matches('/').to_string();
    let patches_path = format!("{}/patches", repo_dir);
    let repo_base = format!("{}/base", repo_dir);
    let ignore_path = format!("{}/.ignore", repo_dir);
    let lock_path = format!("{}/.lock", repo_dir);
    let hash_path = format!("{}/hashes", repo_dir);
    Dirs {
        repo_dir,
        data_dir,
        patches_path,
        repo_base,
        ignore_path,
        lock_path,
        hash_path,
    }
}

pub fn get_walk(dir: String, ignore: String) -> Result<Vec<PathBuf>, io::Error> {
    let mut base_walker = WalkBuilder::new(dir.clone());
    if fs::exists(ignore.clone())? {
        base_walker.add_ignore(ignore);
    }
    let base_paths: Vec<PathBuf> = base_walker
        .standard_filters(false)
        .build()
        .filter_map(|r| r.ok())
        .map(|d| d.path().to_path_buf())
        .filter(|d| *d != PathBuf::from(dir.clone()))
        .map(|p| is_empty_dir(p))
        .collect::<Result<Vec<_>, io::Error>>()?
        .into_iter()
        .filter(|p| !p.0)
        .map(|p| p.1)
        .collect();
    Ok(base_paths)
}

pub fn get_patches(dir: String) -> Vec<PathBuf> {
    Walk::new(dir)
        .filter_map(|r| r.ok())
        .map(|d| d.path().to_path_buf())
        .filter(|p| p.is_dir())
        .collect::<Vec<PathBuf>>()[1..]
        .to_vec()
}

pub fn init_lock(repo_dir: &PathBuf) -> Result<(), io::Error> {
    let lock_path = setup_dirs(repo_dir, None).lock_path;
    File::create_new(lock_path)?;
    Ok(())
}

pub fn del_lock(repo_dir: &PathBuf) -> Result<(), io::Error> {
    let lock_path = setup_dirs(repo_dir, None).lock_path;
    fs::remove_file(lock_path)
}

pub fn get_hashes(data_paths: Vec<PathBuf>) -> Result<Vec<Option<hash::THash>>, io::Error> {
    hash::create_hashes(
        data_paths
            .iter()
            .filter(|p| p.is_file())
            .map(|p| p.to_str().unwrap().to_string())
            .collect(),
    )
}

fn is_empty_dir(p: PathBuf) -> Result<(bool, PathBuf), io::Error> {
    Ok((
        if p.is_dir() {
            p.read_dir()?.count() == 0
        } else {
            false
        },
        p,
    ))
}

pub fn get_full_dirs(paths: Vec<PathBuf>, repo_base: String) -> Vec<PathBuf> {
    let files: Vec<PathBuf> = paths.into_iter().filter(|p| p.is_file()).collect();
    let mut full_dirs: Vec<PathBuf> = files
        .iter()
        .map(|f| PathBuf::from(f.parent().unwrap()))
        .filter(|f| *f != PathBuf::from(repo_base.clone()))
        .collect();
    full_dirs.sort();
    full_dirs.dedup();

    full_dirs
}

pub fn get_empty_dirs(
    paths: &Vec<PathBuf>,
    full_dirs: &Vec<PathBuf>,
    repo_base: String,
) -> Vec<String> {
    get_diffs_dir(&full_dirs, &paths, repo_base.clone(), repo_base.clone())
        .1
        .into_iter()
        .map(|(_, f)| f)
        .collect()
}
