use crate::config::{Config, ReplaceMode, RunMode};
use crate::dumpfile;
use crate::error::*;
use crate::fileutils::{cleanup_paths, create_backup, get_paths};
use crate::solver;
use crate::solver::{Operation, Operations, RenameMap};
use any_ascii::any_ascii;
use rayon::prelude::*;
use regex::Replacer;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

pub struct Renamer {
    config: Arc<Config>,
}

impl Renamer {
    pub fn new(config: &Arc<Config>) -> Result<Renamer> {
        Ok(Renamer {
            config: config.clone(),
        })
    }

    /// Process path batch
    pub fn process(&self) -> Result<Operations> {
        let operations = match self.config.run_mode {
            RunMode::Simple(_) | RunMode::Recursive { .. } => {
                // Get paths
                let input_paths = get_paths(&self.config.run_mode);

                // Remove directories and on existing paths from the list
                let clean_paths = cleanup_paths(input_paths, self.config.dirs);

                // Relate original names with their targets
                let rename_map = self.get_rename_map(&clean_paths)?;

                // Solve renaming operation ordering to avoid conflicts
                solver::solve_rename_order(&rename_map)?
            }
            RunMode::FromFile { ref path, undo } => {
                // Read operations from file
                let operations = dumpfile::read_from_file(&PathBuf::from(path))?;
                if undo {
                    solver::revert_operations(&operations)?
                } else {
                    operations
                }
            }
        };

        // Dump operations into a file if required
        if self.config.dump {
            dumpfile::dump_to_file(self.config.dump_prefix.clone(), &operations)?;
        }

        Ok(operations)
    }

    /// Rename an operation batch
    pub fn batch_rename(&self, operations: Operations) -> Result<()> {
        for operation in operations {
            self.rename(&operation)?;
        }
        Ok(())
    }

    /// Replace file name matches in the given path using stored config.
    fn replace_match(&self, path: &Path) -> PathBuf {
        let file_name = path.file_name().unwrap().to_str().unwrap();
        let parent = path.parent();

        let target_name = match &self.config.replace_mode {
            ReplaceMode::RegExp {
                expression,
                replacement,
                limit,
                transform,
            } => {
                let replacer = TransformReplacer {
                    replacement,
                    transform: *transform,
                };
                expression
                    .replacen(file_name, *limit, &replacer)
                    .to_string()
            }
            ReplaceMode::ToASCII => to_ascii(file_name),
            ReplaceMode::None => file_name.to_string(),
        };

        match parent {
            None => PathBuf::from(target_name),
            Some(path) => path.join(Path::new(&target_name)),
        }
    }

    /// Get hash map containing all replacements to be done
    fn get_rename_map(&self, paths: &[PathBuf]) -> Result<RenameMap> {
        let printer = &self.config.printer;
        let colors = &printer.colors;

        let mut rename_map = RenameMap::new();
        let mut error_string = String::new();

        let targets: Vec<(PathBuf, PathBuf)> = paths
            .into_par_iter()
            .filter_map(|p| {
                let target = self.replace_match(p);
                // Discard paths with no changes
                if *p != target {
                    Some((p.clone(), target))
                } else {
                    None
                }
            })
            .collect();

        for (source, target) in targets {
            // Targets cannot be duplicated by any reason
            if let Some(previous_source) = rename_map.get(&target) {
                error_string.push_str(
                    &colors
                        .error
                        .paint(format!(
                            "\n{0}->{2}\n{1}->{2}\n",
                            previous_source.display(),
                            source.display(),
                            target.display()
                        ))
                        .to_string(),
                );
            } else {
                rename_map.insert(target, source);
            }
        }

        if !error_string.is_empty() {
            return Err(Error {
                kind: ErrorKind::SameFilename,
                value: Some(error_string),
            });
        }

        Ok(rename_map)
    }

    /// Rename path in the filesystem or simply print renaming information. Checks if target
    /// filename exists before renaming.
    fn rename(&self, operation: &Operation) -> Result<()> {
        let printer = &self.config.printer;
        let colors = &printer.colors;

        if self.config.force {
            // Create a backup before actual renaming
            if self.config.backup && !&operation.source.is_dir() {
                match create_backup(&operation.source) {
                    Ok(backup) => printer.print(&format!(
                        "{} Backup created - {}",
                        colors.info.paint("Info: "),
                        colors.source.paint(format!(
                            "{} -> {}",
                            operation.source.display(),
                            backup.display()
                        ))
                    )),
                    Err(err) => {
                        return Err(err);
                    }
                }
            }

            // Rename paths in the filesystem
            if let Err(err) = fs::rename(&operation.source, &operation.target) {
                return Err(Error {
                    kind: ErrorKind::Rename,
                    value: Some(format!(
                        "{} -> {}\n{}",
                        operation.source.display(),
                        operation.target.display(),
                        err
                    )),
                });
            } else {
                printer.print_operation(&operation.source, &operation.target);
            }
        } else {
            // Just print info in dry-run mode
            printer.print_operation(&operation.source, &operation.target);
        }

        Ok(())
    }
}

/// Text tranformation type.
#[derive(Debug, Copy, Clone)]
pub enum TextTransformation {
    /// To uppercase.
    Upper,
    /// To lowercase.
    Lower,
    /// To ASCII representation.
    Ascii,
    /// Leave text as it is.
    None,
}

impl TextTransformation {
    pub fn transform(&self, text: String) -> String {
        match self {
            TextTransformation::Upper => text.to_uppercase(),
            TextTransformation::Lower => text.to_lowercase(),
            TextTransformation::Ascii => to_ascii(&text),
            TextTransformation::None => text,
        }
    }
}

/// Replacer for Regex usage that is able to transform the replacement.
struct TransformReplacer<'h> {
    replacement: &'h str,
    transform: TextTransformation,
}

/// Replace with ASCII characters using `anyascii` table. It handles characters that conflict with
/// path routes (p.e. `╱` -> `/`).
fn to_ascii(text: &str) -> String {
    any_ascii(text).replace("/", "_")
}

impl Replacer for &TransformReplacer<'_> {
    fn replace_append(&mut self, caps: &regex::Captures<'_>, dst: &mut String) {
        let mut replaced = String::default();
        caps.expand(self.replacement, &mut replaced);
        replaced = self.transform.transform(replaced);
        dst.push_str(&replaced);
    }
}

#[cfg(test)]
mod test {
    extern crate tempfile;
    use super::*;
    use crate::config::RunMode;
    use crate::output::Printer;
    use regex::Regex;
    use std::fs;
    use std::path::Path;
    use std::sync::Arc;
    use tempfile::TempDir;

    impl Default for Config {
        fn default() -> Self {
            Config {
                force: true,
                backup: false,
                dirs: false,
                dump: false,
                dump_prefix: "rnr-".to_string(),
                run_mode: RunMode::Simple(vec![]),
                replace_mode: ReplaceMode::None,
                printer: Printer::color(),
            }
        }
    }

    /// Run renamer batch with provided config.
    fn run_with_config(mock_config: Arc<Config>) {
        let renamer = match Renamer::new(&mock_config) {
            Ok(renamer) => renamer,
            Err(err) => {
                mock_config.printer.print_error(&err);
                panic!("Error initializing renamer");
            }
        };
        let operations = match renamer.process() {
            Ok(operations) => operations,
            Err(err) => {
                mock_config.printer.print_error(&err);
                panic!("Error processing");
            }
        };
        if let Err(err) = renamer.batch_rename(operations) {
            mock_config.printer.print_error(&err);
            panic!("Error renaming");
        }
    }

    /// Generate a mock directory tree and files.
    /// ```
    /// - temp_path
    ///     |
    ///     - test_file_1.txt
    ///     |
    ///     - test_file_2.txt
    ///     |
    ///     - test_dir
    ///         |
    ///         - test_file_1.txt
    ///         |
    ///         - test_file_2.txt
    /// ```
    fn generate_file_tree() -> (TempDir, String, Vec<String>) {
        let temp_dir = tempfile::tempdir().expect("Error creating temp directory");
        let temp_path = temp_dir.path().to_str().unwrap().to_string();

        let mock_dir = format!("{}/test_dir", temp_path);
        let mock_files: Vec<String> = vec![
            format!("{}/test_file_1.txt", temp_path),
            format!("{}/test_file_2.txt", temp_path),
            format!("{}/test_file_1.txt", mock_dir),
            format!("{}/test_file_2.txt", mock_dir),
        ];

        // Create directory tree and files in the filesystem
        fs::create_dir(&mock_dir).expect("Error creating mock directory...");
        for file in &mock_files {
            fs::File::create(file).expect("Error creating mock file...");
        }

        (temp_dir, temp_path, mock_files)
    }

    #[test]
    fn rename_files_with_backup() {
        let (_temp_dir, temp_path, mock_files) = generate_file_tree();
        println!("Running test in '{}'", temp_path);

        // Create config
        let mock_config = Arc::new(Config {
            backup: true,
            run_mode: RunMode::Simple(mock_files),
            replace_mode: ReplaceMode::RegExp {
                expression: Regex::new("test").unwrap(),
                replacement: "passed".to_string(),
                limit: 1,
                transform: TextTransformation::None,
            },
            ..Config::default()
        });

        run_with_config(mock_config);

        // Check renamed files
        assert!(Path::new(&format!("{}/passed_file_1.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/passed_file_2.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/test_dir/passed_file_1.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/test_dir/passed_file_2.txt", temp_path)).exists());

        // Check backup files
        assert!(Path::new(&format!("{}/test_file_1.txt.bk", temp_path)).exists());
        assert!(Path::new(&format!("{}/test_file_2.txt.bk", temp_path)).exists());
        assert!(Path::new(&format!("{}/test_dir/test_file_1.txt.bk", temp_path)).exists());
        assert!(Path::new(&format!("{}/test_dir/test_file_2.txt.bk", temp_path)).exists());
    }

    #[test]
    fn rename_files_and_directories_recursively_with_backup() {
        let (_temp_dir, temp_path, _) = generate_file_tree();
        println!("Running test in '{}'", temp_path);

        // Create config
        let mock_config = Arc::new(Config {
            dirs: true,
            backup: true,
            run_mode: RunMode::Recursive {
                paths: vec![temp_path.clone()],
                max_depth: None,
                hidden: false,
            },
            replace_mode: ReplaceMode::RegExp {
                expression: Regex::new("test").unwrap(),
                replacement: "passed".to_string(),
                limit: 1,
                transform: TextTransformation::None,
            },
            ..Config::default()
        });

        run_with_config(mock_config);

        // Check renamed files
        assert!(Path::new(&format!("{}/passed_file_1.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/passed_file_2.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/passed_dir/passed_file_1.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/passed_dir/passed_file_2.txt", temp_path)).exists());

        // Check backup files
        assert!(Path::new(&format!("{}/test_file_1.txt.bk", temp_path)).exists());
        assert!(Path::new(&format!("{}/test_file_2.txt.bk", temp_path)).exists());
        assert!(Path::new(&format!("{}/passed_dir/test_file_1.txt.bk", temp_path)).exists());
        assert!(Path::new(&format!("{}/passed_dir/test_file_2.txt.bk", temp_path)).exists());

        // Directory backup must not be created.
        let directory_backup = &format!("{}/test_dir.bk", temp_path);
        assert!(!Path::new(directory_backup).exists());
    }

    #[test]
    fn replace_limit() {
        let tempdir = tempfile::tempdir().expect("Error creating temp directory");
        println!("Running test in '{:?}'", tempdir);
        let temp_path = tempdir.path().to_str().unwrap();

        let mock_files: Vec<String> = vec![format!("{}/replace_all_aaaaa.txt", temp_path)];
        for file in &mock_files {
            fs::File::create(file).expect("Error creating mock file...");
        }

        let mock_config = Arc::new(Config {
            run_mode: RunMode::Simple(mock_files),
            replace_mode: ReplaceMode::RegExp {
                expression: Regex::new("a").unwrap(),
                replacement: "b".to_string(),
                limit: 0,
                transform: TextTransformation::None,
            },
            ..Config::default()
        });

        run_with_config(mock_config);

        // Check renamed files
        assert!(Path::new(&format!("{}/replbce_bll_bbbbb.txt", temp_path)).exists());
    }

    #[test]
    fn to_ascii() {
        let tempdir = tempfile::tempdir().expect("Error creating temp directory");
        println!("Running test in '{:?}'", tempdir);
        let temp_path = tempdir.path().to_str().unwrap();

        let mock_files: Vec<String> = vec![
            format!("{}/ǹön-âścîı-lower.txt", temp_path),
            format!("{}/ǸÖN-ÂŚCÎI-UPPER.txt", temp_path),
            format!("{}/with-slashes-╱.txt", temp_path),
        ];
        for file in &mock_files {
            fs::File::create(file).expect("Error creating mock file...");
        }

        let mock_config = Arc::new(Config {
            run_mode: RunMode::Simple(mock_files),
            replace_mode: ReplaceMode::ToASCII,
            ..Config::default()
        });

        run_with_config(mock_config);

        // Check renamed files
        assert!(Path::new(&format!("{}/non-ascii-lower.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/NON-ASCII-UPPER.txt", temp_path)).exists());
        assert!(Path::new(&format!("{}/with-slashes-_.txt", temp_path)).exists());
    }

    #[test]
    fn captures_transform() {
        let hay = "Thïs-Îs-my-fîle.txt";
        let expression = Regex::new(r"(\w+)-(\w+)-my-fîle").unwrap();
        let replacement = "${1}.${2}-a-Fïle";

        let mut replacer = TransformReplacer {
            replacement,
            transform: TextTransformation::None,
        };

        // Without any transformation.
        let result = expression.replace(hay, &replacer);
        assert_eq!(result, "Thïs.Îs-a-Fïle.txt");
        // To uppercase.
        replacer.transform = TextTransformation::Upper;
        let result = expression.replace(hay, &replacer);
        assert_eq!(result, "THÏS.ÎS-A-FÏLE.txt");
        // To lowercase.
        replacer.transform = TextTransformation::Lower;
        let result = expression.replace(hay, &replacer);
        assert_eq!(result, "thïs.îs-a-fïle.txt");
        // To ASCII.
        replacer.transform = TextTransformation::Ascii;
        let result = expression.replace(hay, &replacer);
        assert_eq!(result, "This.Is-a-File.txt");
    }
}
