rrn 0.1.4

batch rename files/dirs by regex
use crate::{
    fsio::list_current_dir,
    param::{ExecMode, Param},
};
use colored::Colorize;
use rust_i18n::t;
use std::{collections::HashMap, fs, path::PathBuf};
use unicode_display_width::width;

pub(crate) fn execute(param: &Param) {
    let paths = list_current_dir(param);
    match param.get_exec_mode() {
        ExecMode::DryRun => print_replace_result(paths, param),
        ExecMode::Exec => exec_replace(paths, param),
    }
}

fn get_filename(path: &PathBuf) -> Option<String> {
    path.file_name()
        .and_then(|name| name.to_str())
        .map(|s| s.to_string())
}

fn print_replace_result(paths: Vec<PathBuf>, param: &Param) {
    if paths.is_empty() {
        println!("{}", t!("result.no.found").red());
        return;
    }

    let str_from = t!("label.from");
    let str_to = t!("label.to");
    let str_status = t!("label.status");

    let str_from_len = width(&str_from) as usize;
    let str_to_len = width(&str_to) as usize;
    let str_status_len = width(&str_status) as usize;

    let mut output_vec = Vec::new();
    let from_regex = param.get_from_regex();
    let mut existed = HashMap::new();
    let mut conflect_count = 0;
    paths.iter().for_each(|x| {
        let from_target = x.file_name()
            .map(|name| name.to_string_lossy().to_string())
            .unwrap_or_default();
        let from_str = get_filename(x).unwrap_or_default();
        let to_result = from_regex
            .replace_all(from_str.as_str(), &param.get_to_pattern())
            .to_string();
        if existed.contains_key(&to_result) {
            output_vec.push((from_target, to_result, t!("status.dup").to_string(), false));
            conflect_count += 1;
        } else {
            existed.insert(to_result.clone(), 0);
            output_vec.push((from_target, to_result, t!("status.ok").to_string(), true));
        }
    });
    let max_left_len = output_vec
        .iter()
        .fold(0, |acc, x| acc.max(width(&x.0) as usize))
        .max(str_from_len);
    let max_right_len = output_vec
        .iter()
        .fold(0, |acc, x| acc.max(width(&x.1) as usize))
        .max(str_to_len);
    let max_status_len = output_vec
        .iter()
        .fold(0, |acc, x| acc.max(width(&x.2) as usize))
        .max(str_status_len);
    // 4 = 4 * "|", 6 = 3 * 2 * ' '
    let separator = format!("*{}*{}*{}*", 
        "-".repeat(max_left_len + 2),
        "-".repeat(max_right_len + 2),
        "-".repeat(max_status_len + 2));
    println!("{}", separator);
    println!("| {} | {} | {} |",
        fill(&str_from.to_string(), max_left_len).green(),
        fill(&str_to.to_string(), max_right_len).green(),
        fill(&str_status.to_string(), max_status_len).green(),
    );
    println!("{}", separator);
    output_vec.iter().for_each(|x| {
        let status = fill(&x.2, max_status_len);
        println!(
            "| {} | {} | {} |",
            fill(&x.0, max_left_len),
            fill(&x.1, max_right_len),
            if x.3 { status.green() } else { status.red() },
        )
    });
    println!("{}", separator);
    if conflect_count == 0 {
        println!("{}", t!("result.dryrun.suc").green());
    } else {
        println!(
            "{}",
            t!("result.dryrun.fail",conflect_count=> conflect_count).red()
        );
    }
}

fn fill(source: &String, max: usize) -> String {
    let display_width = width(source) as usize;
    let padding_needed = max - display_width;
    format!("{}{}", source, " ".repeat(padding_needed))
}

fn exec_replace(paths: Vec<PathBuf>, param: &Param) {
    if paths.is_empty() {
        println!("{}", t!("result.no.found").red());
        return;
    }
    let from_regex = param.get_from_regex();
    let mut existed = HashMap::new();
    let mut ready_vec = Vec::new();
    let mut conflect_count = 0;
    paths.iter().for_each(|x| {
        let from_str = get_filename(x).unwrap_or_default();
        let to_result = from_regex
            .replace_all(from_str.as_str(), &param.get_to_pattern())
            .to_string();
        let to_file = x.parent()
            .map(|p| p.join(&to_result))
            .unwrap_or_else(|| PathBuf::from(&to_result));
        if existed.contains_key(&to_result) {
            conflect_count += 1;
        } else {
            existed.insert(to_result.clone(), 0);
            ready_vec.push((x.clone(), to_file));
        }
    });
    if conflect_count == 0 {
        for (from_path, to_path) in ready_vec {
            let file1 = get_filename(&from_path).unwrap_or_default();
            let file2 = get_filename(&to_path).unwrap_or_default();
            println!("{}", t!("result.exec.log", file1 => file1, file2 => file2));
            if let Err(e) = fs::rename(from_path, to_path) {
                eprintln!("{}", t!("result.exec.fail", error => e.to_string()).red());
            }
        }
    } else {
        print_replace_result(paths, param);
    }
}