/* Written By Angel Uniminin <uniminin@zoho.com> under the terms of MIT */

use std::{env, error::Error, fs::File, io::Read};

use colored::Colorize;

const ARGS_LIMIT: usize = 4;

/// The necessary configurations for initializing nanogrep.
///
/// - `file`: The file in which the search will be conducted
/// - `pattern`: The pattern to search for in the contents of the file
pub struct Config {
    pub file: String,
    pub pattern: String,
}

impl Config {
    /// Initializes a new Config.
    ///
    /// Returns error if incorrect option is passed.
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        if args.len() >= ARGS_LIMIT {
            return Err("Expected 2 arguments but more than 2 arguments were provided!");
        }

        /* Discard the first argument (args[0]), which is the filename along with path. */
        args.next();

        /* arg0[1] & arg0[2] are file & pattern respectively */
        let (file, pattern): (String, String) = (
            match args.next() {
                Some(arg) => arg,
                None => return Err("No file provided!"),
            },
            match args.next() {
                Some(arg) => arg.to_lowercase(),
                None => return Err("No pattern provided!"),
            },
        );

        /* var names are same as in the struct, so e dont need explicit key:val initialization style. */
        Ok(Config { file, pattern })
    }
}

/* The lifetime parameters says that: the search results will be valid as long as the contents are valid.
Can last even after pattern goes out of scope. */
/// Returns the line(s) where the pattern is found in the content.
fn search<'a>(pattern: &str, content: &'a str) -> Vec<&'a str> {
    content
        .lines()
        .filter(|line: &&str| line.to_lowercase().contains(&pattern.to_ascii_lowercase()))
        .collect()
}

/// Summarizes based on the total occurrence(s) & line count
fn summarize(occurrence: usize, line_count: usize) -> String {
    let summary: String;

    if occurrence == 0 {
        summary = format!(
            "{:1} {:1} {:1}",
            "Found".red(),
            occurrence.to_string().red(),
            "occurrence!".red()
        )
    } else {
        let (mut occurrence_word, mut line_word): (String, String) =
            ("occurrence".parse().unwrap(), "line".parse().unwrap());

        match (occurrence, line_count) {
            (0, _) | (1, _) => {
                line_word.push("s".parse().unwrap());
            }
            (_, 0) | (_, 1) => {
                occurrence_word.push("s".parse().unwrap());
            }
            _ => {
                line_word.push("s".parse().unwrap());
                occurrence_word.push("s".parse().unwrap());
            }
        }

        summary = format!(
            "{:1}\n{:1} {:1} {:1} {:1} {:1} {:1} {:1}{:1}",
            "--Summary--".yellow(),
            "*".green().bold(),
            "Found".green(),
            occurrence.to_string().trim().blue().bold(),
            occurrence_word.green(),
            "in".green(),
            line_count.to_string().blue().bold(),
            line_word.green(),
            "!".green()
        );
    }
    summary
}

/// Runs the library logic.
/// The functions expects a reference to a `Config` struct containing:
///
/// - `file`: The file in which the search will be conducted
/// - `pattern`: The pattern to search for in the contents of the file
///
/// If the execution runs without any problem, the function returns `()`. If not, a
/// `Box<std::error::Error>` struct is returned.
///
/// # Errors
///
/// The function returns errors on the following situations:
///
/// - `file` doesn't exist
/// - `file` could not be read (unauthorized read permission)
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    println!(
        "\n{:1} {:1} {:1} {:1}\n",
        "Search result for".green(),
        &config.pattern.bright_cyan().bold(),
        "in".green(),
        &config.file.bright_cyan().bold(),
    );

    /* `?` will return the error(ending fn execution)
     * if it encounters an `Err` in the `Result` it follows. */
    let (mut file, mut contents): (File, String) = (File::open(config.file)?, String::new());

    file.read_to_string(&mut contents)?;

    let results: Vec<&str> = search(&config.pattern, &contents);

    let (mut line_count, mut total_occurrence): (usize, usize) = (0usize, 0usize);

    for line in results {
        let occurrences: String = format!(
            "{:1}",
            line.to_lowercase()
                .matches(&config.pattern)
                .count()
                .to_string()
                .yellow()
                .bold()
        );
        line_count += 1;
        total_occurrence += line.to_lowercase().matches(&config.pattern).count();
        println!("{:1}  [ {:1} ]", line.to_string().magenta(), occurrences);
    }

    println!("\n{:1}", summarize(total_occurrence, line_count));

    /* `()` is a unit type. It means that we mostly do not care about return type if it goes well.
     * We do, however, care about the errors that might occur, and that's why the result type
     * exists with a dynamic error return type. */
    Ok(())
}

#[cfg(test)]
mod test {
    /* use every fn from above */
    use super::*;

    #[test]
    fn case_sensitive() {
        let pattern: &str = "is";
        let contents: &str = "\
Hello There!
This is just a random text.
There is no errors.";

        assert_eq!(
            vec!["This is just a random text.", "There is no errors."],
            search(pattern, contents)
        );
    }

    #[test]
    fn case_insensitive() {
        let pattern: &str = "tH";
        let contents: &str = "\
Hello There!
This is another random text.
There should be no errors as well.";

        assert_eq!(
            vec![
                "Hello There!",
                "This is another random text.",
                "There should be no errors as well."
            ],
            search(pattern, contents)
        );
    }
}
