use clap::ArgMatches;
use colored::*;
use dialoguer::Input;
use lazy_regex::{regex, Lazy, Regex};
use std::{
    cmp::Ordering,
    fs,
    path::{Path, PathBuf},
    process,
};

// Logging conventions:
//      Level:
//          Error = red
//          Info = green
//          Verbose = bright_black
//      Text:
//          Error = bright_red
//          Info = white
//          Verbose = blue
//      Data (paths, numbers, etc):
//          yellow

pub struct Arguments {
    pub path: PathBuf,
    pub logfile: bool,
    pub yes: bool,
    pub nro: usize,
    pub zeroes: usize,
    pub prefix: String,
    pub mode: String,
    pub ordering: String,
    pub ignore: Vec<usize>,
    pub file_extension: &'static Lazy<Regex>,
    pub file_numbers: Regex,
}

impl From<ArgMatches> for Arguments {
    fn from(a: ArgMatches) -> Self {
        let path: PathBuf = {
            let folder: &String = a.get_one("folder").unwrap();
            Path::new(folder).to_owned()
        };

        let logfile = a.get_flag("logfile");
        let yes = a.get_flag("yes");
        let nro: usize = *a.get_one("nro").unwrap_or(&1);
        let zeroes: usize = *a.get_one("zeroes").unwrap_or(&7);
        let prefix: String = a.get_one("prefix").unwrap_or(&"".to_string()).to_owned();
        let mode: String = a.get_one("mode").unwrap_or(&"n".to_string()).to_owned();
        let ordering: String = a.get_one("ordering").unwrap_or(&"a".to_string()).to_owned();

        let ignore: Vec<usize> = if let Some(i) = a.try_get_many("ignore").unwrap() {
            i.copied().collect()
        } else {
            vec![]
        };

        let file_extension = regex!(r"(?i)\.[0-9A-Z]+$");
        let file_numbers = Regex::new(
            format!(r"(?i)(^{p}\d{{{z}}})\.[0-9A-Z]+$", p = prefix, z = zeroes).as_str(),
        )
        .expect("Unable to create 'file_numbers' regex");

        Self {
            path,
            logfile,
            yes,
            nro,
            zeroes,
            prefix,
            mode,
            ordering,
            ignore,
            file_extension,
            file_numbers,
        }
    }
}

#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
pub enum LogLevel {
    Verbose,
    Standard,
    Quiet,
}

impl LogLevel {
    pub fn is_verbose(&self) -> bool {
        matches!(self, LogLevel::Verbose)
    }

    pub fn is_standard(&self) -> bool {
        matches!(self, LogLevel::Standard)
    }

    pub fn is_quiet(&self) -> bool {
        matches!(self, LogLevel::Quiet)
    }
}

pub fn rename(args: Arguments, log_level: LogLevel) {
    if !args.path.exists() || !args.path.is_dir() {
        eprintln!(
            "{} {}",
            "[Error]:".red(),
            "Folder doesn't exists or is not a folder".bright_red()
        );
        process::exit(1);
    }

    // Get files
    if log_level.is_verbose() {
        println!("{} {}", "[Verbose]".bright_black(), "Checking files".blue());
    }
    let files: Vec<String> = {
        let mut ready: Vec<String> = Vec::new();
        let mut i: Vec<(String, u128)> = Vec::new();
        for file in args
            .path
            .read_dir()
            .expect("Unable to read path contents")
            .map(|x| x.unwrap())
        {
            if file.file_type().unwrap().is_dir()
                || file.file_name().to_str().unwrap().starts_with('.')
            {
                // Ignore folders and hidden files
                continue;
            }

            let file_name = file
                .file_name()
                .into_string()
                .expect("Unable to read file name");
            let file_date: u128 =
                fs::metadata(format!("{:?}/{}", args.path, file_name).replace('\"', ""))
                    .unwrap()
                    .modified()
                    .unwrap()
                    .elapsed()
                    .unwrap()
                    .as_micros();
            i.push((file_name, file_date));
        }
        if args.ordering == "n" {
            // newest to oldest
            ready = order_by_date(i);
        } else if args.ordering == "o" {
            // oldest to newest
            ready = order_by_date(i).into_iter().rev().collect();
        } else if args.ordering == "z" {
            // Z-A
            for item in i {
                ready.push(item.0);
            }
            ready.sort();
            ready.reverse();
        } else {
            // A-Z
            for item in i {
                ready.push(item.0);
            }
            ready.sort();
        }
        ready
    };

    let total_nro: usize = files.len();

    // Get files to rename
    if log_level.is_verbose() {
        println!(
            "{} {}",
            "[Verbose]".bright_black(),
            "Generating file names".blue()
        );
    }
    let files_to_rename: Vec<(String, String)> = match args.mode.as_str() {
        "n" => normal_rename(files, total_nro, &args),
        "a" => add_to_the_end(files, total_nro, &args),
        "f" => number_fixing(files, &args, log_level),
        _ => panic!("Mode not found"),
    };

    if !files_to_rename.is_empty() {
        rename_files(files_to_rename, total_nro, &args, log_level);
    } else if !log_level.is_quiet() {
        println!(
            "{} {}",
            "[Info]:".green(),
            "Nothing to rename. Exiting...".white()
        )
    }
}

fn order_by_date(list: Vec<(String, u128)>) -> Vec<String> {
    let mut i: Vec<String> = Vec::new();
    let mut numbers: Vec<u128> = Vec::new();
    for item in &list {
        numbers.push(item.1);
    }
    numbers.sort_unstable();
    for number in numbers {
        i.push(
            list[list.iter().position(|x| x.1 == number).unwrap()]
                .0
                .to_owned(),
        );
    }
    i
}

fn normal_rename(files: Vec<String>, total_nro: usize, args: &Arguments) -> Vec<(String, String)> {
    // Aka mode 0
    // Rename only new files not following correct naming

    let mut files_to_rename: Vec<(String, String)> = Vec::new();
    let mut nro_to_skip: Vec<usize> = Vec::new();

    // Set numbers to skip by nro variable
    if args.nro != 1 {
        for nro in 1..args.nro {
            nro_to_skip.push(nro);
        }
    }

    // Ignore specific numbers
    if !args.ignore.is_empty() {
        nro_to_skip.extend(args.ignore.iter());
    }

    // Find what numbers to skip
    let mut sorted_files = files.clone();
    sorted_files.sort();
    for file in &sorted_files {
        match args.file_numbers.captures(file) {
            Some(i) => {
                let i_str = i.get(1).unwrap().as_str();
                let i_nro: usize = i_str.parse().unwrap();

                if (!i_nro > total_nro + (args.nro - 1)
                    && i_nro >= args.nro
                    && i_str.len() == args.zeroes)
                    && (i_nro == 1 || !nro_to_skip.is_empty() && nro_to_skip.contains(&(i_nro - 1)))
                {
                    nro_to_skip.push(i_nro);
                }
            }
            None => {
                continue;
            }
        };
    }
    // Find what to rename to what
    for (index, file) in files.iter().enumerate() {
        let mut index = index + args.nro;
        while nro_to_skip.contains(&index) {
            index += 1;
        }
        let ext = match args.file_extension.find(file) {
            Some(i) => i.as_str().to_string(),
            None => {
                eprintln!(
                    "{} {}{}{}",
                    "[Error]:".red(),
                    "Unable to get file extension of \"".bright_red(),
                    &file.yellow(),
                    "\" with regex".bright_red()
                );
                process::exit(1);
            }
        };
        if !args.file_numbers.is_match(file) {
            nro_to_skip.push(index);
            files_to_rename.push((
                file.to_owned(),
                format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
            ));
            continue;
        }

        let ii = match args.file_numbers.captures(file) {
            Some(i) => i.get(1).unwrap().as_str(),
            None => {
                nro_to_skip.push(index);
                files_to_rename.push((
                    file.to_owned(),
                    format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
                ));
                continue;
            }
        };

        if ii
            .parse::<usize>()
            .expect("Unable to turn regex string into usize")
            > total_nro + (args.nro - 1)
            || ii
                .parse::<usize>()
                .expect("Unable to turn regex string into usize")
                < args.nro
            || !ii.len() == args.zeroes
        {
            // the range needs to be the range of numbers to use in renaming
            nro_to_skip.push(index);
            files_to_rename.push((
                file.to_owned(),
                format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
            ));
        } else if !nro_to_skip.contains(
            &ii.parse::<usize>()
                .expect("Unable to turn regex string into usize"),
        ) {
            // it's named correclty, but it is not correct number
            nro_to_skip.push(index);
            files_to_rename.push((
                file.to_owned(),
                format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
            ));
            continue;
        } else {
            continue;
        }
    }

    files_to_rename
}

fn add_to_the_end(files: Vec<String>, total_nro: usize, args: &Arguments) -> Vec<(String, String)> {
    // Aka mode 2
    // Rename only new files, skip missing numbers and add new files to the end

    let mut files_to_rename: Vec<(String, String)> = Vec::new();
    let mut files_to_edit: Vec<String> = Vec::new();
    let mut files_numbered: Vec<String> = Vec::new();

    // Remove files that are already named correctly
    for file in &files {
        if !args.file_numbers.is_match(file) {
            files_to_edit.push(file.to_owned());
        } else {
            files_numbered.push(file.to_owned());
        }
    }

    // Get biggest number
    files_numbered.sort();
    let biggest_number: usize = match args.file_numbers.captures(files_numbered.last().unwrap()) {
        Some(i) => i.get(1).unwrap().as_str().parse().unwrap(),
        None => {
            eprintln!(
                "{} {}{}{}",
                "[Error]:".red(),
                "Unable to get numbers from \"".bright_red(),
                &files_numbered.last().unwrap().yellow(),
                "\" with regex".bright_red()
            );
            process::exit(1);
        }
    };

    // Find what to rename to what
    for (index, file) in files_to_edit.iter().enumerate() {
        let index = index + biggest_number + 1;

        let ext = match args.file_extension.find(file) {
            Some(i) => i.as_str().to_string(),
            None => {
                eprintln!(
                    "{} {}{}{}",
                    "[Error]:".red(),
                    "Unable to get file extension of \"".bright_red(),
                    &file.yellow(),
                    "\" with regex".bright_red()
                );
                process::exit(1);
            }
        };
        if !args.file_numbers.is_match(file) {
            files_to_rename.push((
                file.to_owned(),
                format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
            ));
            continue;
        }

        let ii = match args.file_numbers.captures(file) {
            Some(i) => i.get(1).unwrap().as_str(),
            None => {
                files_to_rename.push((
                    file.to_owned(),
                    format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
                ));
                continue;
            }
        };

        if ii
            .parse::<usize>()
            .expect("Unable to turn regex string into usize")
            > biggest_number
            || ii
                .parse::<usize>()
                .expect("Unable to turn regex string into usize")
                < args.nro
            || !ii.len() == args.zeroes
        {
            // the range needs to be the range of numbers to use in renaming
            files_to_rename.push((
                file.to_owned(),
                format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
            ));
        } else if !total_nro
            == ii
                .parse::<usize>()
                .expect("Unable to turn regex string into usize")
        {
            // it's named correclty, but it is not correct number
            files_to_rename.push((
                file.to_owned(),
                format!("{}{}", generate_name(index, args.zeroes, &args.prefix), ext),
            ));
            continue;
        } else {
            continue;
        }
    }

    files_to_rename
}

fn number_fixing(
    files: Vec<String>,
    args: &Arguments,
    log_level: LogLevel,
) -> Vec<(String, String)> {
    // Aka mode 1
    // Fix missing numbers, by moving numbers backwards.
    let mut files_to_rename: Vec<(String, String)> = Vec::new();
    let mut nros_missing: Vec<usize> = Vec::new();

    // Find all numbers
    let mut sorted_files = files.clone();
    sorted_files.sort();
    let nros: Vec<usize> = {
        let mut i: Vec<usize> = Vec::new();
        for file in &sorted_files {
            // Skip if no numbers found
            if !args.file_numbers.is_match(file) {
                continue;
            }
            match args.file_numbers.captures(file) {
                Some(j) => {
                    if j.get(1).unwrap().as_str().len() == args.zeroes {
                        i.push(j.get(1).unwrap().as_str().parse().unwrap());
                    }
                }
                None => {
                    continue;
                }
            };
        }
        // Ignore specific numbers
        if !args.ignore.is_empty() {
            i.extend(args.ignore.iter());
            i.sort_unstable();
        }
        i
    };

    // Findout what numbers are missing
    let mut offset: usize = 0;
    for (index, number) in nros.iter().enumerate() {
        let index = index + args.nro - offset;
        if number < &args.nro {
            offset += 1;
        } else if index != *number {
            nros_missing.push(index);
        }
    }
    // Create list what to rename to what
    if !nros_missing.is_empty() {
        // Add only already correclty named to the list
        let mut count = 0;
        for file in &files {
            if count >= nros_missing.len() {
                break;
            }
            if !args.file_numbers.is_match(file) {
                continue;
            }

            let ii = match args.file_numbers.captures(file) {
                Some(i) => i.get(1).unwrap().as_str().to_string(),
                None => {
                    eprintln!(
                        "{} {}{}{}",
                        "[Error]".red(),
                        "Unable to get numbers from \"".bright_red(),
                        &file.yellow(),
                        "\" with regex".bright_red()
                    );
                    process::exit(1);
                }
            };

            match ii.parse::<usize>().unwrap().cmp(&nros_missing[count]) {
                Ordering::Equal => {
                    count += 1;
                }
                Ordering::Less => {
                    continue;
                }
                _ => {
                    if ii.parse::<usize>().unwrap() < args.nro {
                        continue;
                    }
                    let ext = match args.file_extension.find(file) {
                        Some(i) => i.as_str().to_string(),
                        None => {
                            eprintln!(
                                "{} {}{}{}",
                                "[Error]:".red(),
                                "Unable to get file extension from \"".bright_red(),
                                &file.yellow(),
                                "\" with regex".bright_red()
                            );
                            process::exit(1);
                        }
                    };
                    files_to_rename.push((
                        file.to_owned(),
                        format!(
                            "{}{}",
                            generate_name(nros_missing[count], args.zeroes, &args.prefix),
                            ext
                        ),
                    ));
                    count += 1;
                }
            }
        }
        // Add missing files to the end
        // Get last file number
        let mut count: usize = match args
            .file_numbers
            .captures(&files_to_rename.last().unwrap().1)
        {
            Some(i) => {
                let i: usize = i.get(1).unwrap().as_str().parse().unwrap();
                i + 1
            }
            None => {
                eprintln!(
                    "{} {}{}{}",
                    "[Error]:".red(),
                    "Unable to get numbers from \"".bright_red(),
                    &files_to_rename.last().unwrap().1.yellow(),
                    "\" with regex".bright_red()
                );
                if !log_level.is_quiet() {
                    println!(
                        "{} {}",
                        "[Info]:".green(),
                        "Did you run normal mode first?".white()
                    );
                }
                process::exit(1);
            }
        };
        // Find files missing in files_to_rename list and add to end
        for file in &files {
            if !files_to_rename.iter().any(|i| &i.0 == file) {
                let nro: usize = match args.file_numbers.captures(file) {
                    Some(i) => i.get(1).unwrap().as_str().parse().unwrap(),
                    None => {
                        eprintln!(
                            "{} {}{}{}",
                            "[Error]:".red(),
                            "Unable to get numbers from \"".bright_red(),
                            &file.yellow(),
                            "\" with regex".bright_red()
                        );
                        if !log_level.is_quiet() {
                            println!(
                                "{} {}",
                                "[Info]:".green(),
                                "Did you run normal mode first?".white()
                            );
                        }
                        process::exit(1);
                    }
                };
                if nro >= count || nro >= args.nro {
                    continue;
                }
                let ext = match args.file_extension.find(file) {
                    Some(i) => i.as_str().to_string(),
                    None => {
                        eprintln!(
                            "{} {}{}{}",
                            "[Error]:".red(),
                            "Unable to get file extension from \"".bright_red(),
                            &file.yellow(),
                            "\" with regex".bright_red()
                        );
                        process::exit(1);
                    }
                };
                files_to_rename.push((
                    file.to_owned(),
                    format!("{}{}", generate_name(count, args.zeroes, &args.prefix), ext),
                ));
                count += 1;
            }
        }
    } else {
        if !log_level.is_quiet() {
            println!(
                "{} {}",
                "[Info]:".green(),
                "Nothing to rename. Exiting...".white()
            )
        }
        process::exit(0);
    }

    files_to_rename
}

fn rename_files(
    files_to_rename: Vec<(String, String)>,
    total_nro: usize,
    args: &Arguments,
    log_level: LogLevel,
) {
    // Check/Ask stuff
    if !args.yes {
        if !log_level.is_quiet() {
            println!("{} {}", "[Info]:".green(), "These will be renamed:".white());
            for file in &files_to_rename {
                println!("{}{}{}", file.0.yellow(), " -> ".white(), file.1.yellow());
            }
            println!("{:_<20}", "");
        }
        println!(
            "{}{}{}{}",
            "Renaming ".white(),
            format!("{}", files_to_rename.len()).yellow(),
            " of ".white(),
            format!("{}", total_nro).yellow(),
        );

        let y = Input::<String>::new()
            .with_prompt("Are you sure you want to rename [y/N]")
            .default("n".to_string())
            .show_default(false)
            .interact()
            .unwrap();

        if y.to_lowercase() != "y" {
            println!("{}", "Aborting".bright_red());
            process::exit(0);
        } else if !args.logfile {
            let y = Input::<String>::new()
                .with_prompt("Do you want to save a log file [y/N]")
                .default("n".to_string())
                .show_default(false)
                .interact()
                .unwrap();
            if y.to_lowercase() == "y" {
                create_log(&files_to_rename, &args.path);
            }
        } else {
            create_log(&files_to_rename, &args.path);
        }
    } else if args.logfile {
        create_log(&files_to_rename, &args.path);
    }

    if args.yes && log_level.is_verbose() {
        println!("{} {}", "[Verbose]:".bright_black(), "Renaming".blue());

        for file in &files_to_rename {
            println!("{}{}{}", file.0.yellow(), " -> ".blue(), file.1.yellow());
        }

        println!("{:_<20}", "");
        println!(
            "{}{}{}{}",
            "Renaming ".white(),
            format!("{}", files_to_rename.len()).yellow(),
            " of ".white(),
            format!("{}", total_nro).yellow(),
        );
    }

    // Rename files with prefix
    if log_level.is_verbose() {
        println!("{} {}", "[Verbose]".bright_black(), "Renaming files".blue());
    }
    for (index, file) in files_to_rename.iter().enumerate() {
        if let Err(e) = fs::rename(
            format!("{}/{}", args.path.display(), file.0),
            format!("{}/{}{}", args.path.display(), index, file.1),
        ) {
            eprintln!("{} {}", "[Error]:".red(), e.to_string().bright_red());
        }
    }
    // Rename files for real
    for (index, file) in files_to_rename.iter().enumerate() {
        if let Err(e) = fs::rename(
            format!("{}/{}{}", args.path.display(), index, file.1),
            format!("{}/{}", args.path.display(), file.1),
        ) {
            eprintln!("{} {}", "[Error]:".red(), e.to_string().bright_red());
        }
    }
}

fn generate_name(number: usize, zeroes: usize, prefix: &str) -> String {
    format!(
        "{pre}{:0<1$}{nu}",
        "",
        zeroes - number.to_string().len(),
        nu = number,
        pre = prefix
    )
}

fn create_log(files: &[(String, String)], path: &Path) {
    let text = {
        let mut i: String = String::new();
        for file in files {
            i += format!("{} -> {}\n", file.0, file.1).as_str();
        }
        i
    };

    let mut file_name: String = "rename.log".to_string();
    let mut count = 0;

    while Path::new(&format!("{}/{}", path.display(), file_name)).exists() {
        file_name = format!("rename.{}.log", count);
        count += 1;
    }
    let file_name = format!("{}/{}", path.display(), file_name);

    fs::write(file_name, text).expect("Unable to create a logfile");
}

#[cfg(test)]
mod tests;
