use crate::misc::get_style;
use crate::{diff, fs::util::*};
use humantime::Timestamp;
use indicatif::ParallelProgressIterator;
use rayon::prelude::*;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::time::UNIX_EPOCH;

pub fn pull_patch(
    repo_dir: &PathBuf,
    out_dir: &PathBuf,
    target_patch: Timestamp,
    rebase: bool,
) -> Result<(), io::Error> {
    let dirs = setup_dirs(repo_dir, Some(out_dir), true, rebase);
    init_lock(repo_dir)?;

    let base_paths = get_walk(&dirs.repo_base, &dirs.ignore_path)?;

    if !rebase {
        fs::create_dir(dirs.data_dir.clone().unwrap())?;
    };

    let mut patches_paths: Vec<String> = vec![];

    let target = target_patch.duration_since(UNIX_EPOCH).unwrap().as_secs();
    for patch_path in get_patches(dirs.patches_path).iter().rev() {
        let patch_id = patch_path
            .file_name()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string()
            .parse::<u64>()
            .unwrap();

        if patch_id < target {
            break;
        }

        patches_paths.push(patch_path.to_str().unwrap().to_string());
    }

    // will create dirs + write files
    let (rm_files, processed) = diff::apply_entries(
        dirs.repo_base.clone(),
        dirs.data_dir.clone().unwrap(),
        patches_paths.clone(),
        rebase,
    )?;

    let del_files = &rm_files
        .clone()
        .into_iter()
        .map(|f| PathBuf::from(format!("{}/{}", dirs.data_dir.clone().unwrap(), f)))
        .collect();

    let all_files: Vec<PathBuf> = get_diffs_file(
        &base_paths,
        &del_files,
        &dirs.repo_base,
        &dirs.data_dir.as_ref().unwrap(),
        true,
    )
    .0
    .into_iter()
    .map(|f| PathBuf::from(format!("{}/{}", dirs.repo_base, f.1)))
    .collect();

    let full_dirs = get_full_dirs(&all_files, &dirs.repo_base);
    let all_paths = &full_dirs.iter().chain(&all_files).collect();

    if !rebase {
        // copy any files that aren't in entries
        // some files in all_files may already have been written
        copy_dir_all(
            &dirs.repo_base,
            &all_paths,
            &dirs.data_dir.clone().unwrap(),
            false,
            false,
            false,
        )?;
    } else {
        // delete old dirs + files
        rm_old(
            &dirs.repo_base.clone(),
            rm_files
                .into_iter()
                .chain(get_empty_dirs(&base_paths, &full_dirs, &dirs.repo_base))
                .collect(),
        )?;

        let processed_files = processed
            .iter()
            .map(|(h, _)| PathBuf::from(format!("{}/{}", dirs.repo_base.clone(), h.clone())))
            .collect();

        // we want to compute hashes only for files not in processed files
        let files_to_hash = get_diffs_file(
            &all_files,
            &processed_files,
            &dirs.repo_base,
            &dirs.repo_base,
            true,
        )
        .0
        .iter()
        .map(|s| PathBuf::from(format!("{}/{}", dirs.repo_base.clone(), s.1)))
        .collect();

        // processed may include new files that didnt exist in all_files
        let mut hashes: Vec<_> = diff::hash::create_hashes(&files_to_hash, &dirs.repo_base, true)?
            .into_iter()
            .chain(processed)
            .collect();

        diff::hash::write_hashes(&dirs.hash_path, &mut hashes)?;

        // remove all the patches used to get back to timestamp
        if !patches_paths.is_empty() {
            let style = get_style("Removing patches", patches_paths.len());
            patches_paths
                .par_iter()
                .progress_with(style)
                .map(|patch| -> Result<(), io::Error> { fs::remove_dir_all(patch) })
                .collect::<Result<(), io::Error>>()?;
        }
    }

    Ok(())
}

pub fn revert(repo_dir: &PathBuf, target_patch: Timestamp) -> Result<(), io::Error> {
    let dirs = setup_dirs(repo_dir, None, true, false);

    pull_patch(
        &PathBuf::from(dirs.repo_dir.clone()),
        &PathBuf::from(dirs.repo_base.clone()),
        target_patch.clone(),
        true,
    )?;

    Ok(())
}
