use colored::Colorize;
use include_dir::{include_dir, Dir};
use is_url::is_url;
use lingua::{LanguageDetectorBuilder};
use lingua::Language::{English, Portuguese, Spanish};
use reqwest;
use std::collections::HashSet;
use std::{env};
use std::fs;
use std::fs::File;
use std::path::{PathBuf};
use std::io::Write;

const ACCEPTED_FILE_TYPES: [&str; 2] = ["md", "txt"];

static DICTIONARIES_FOLDER: Dir<'_> = include_dir!("dictionaries");

pub struct Dictionary {
    data: HashSet<String>
}

impl Dictionary {
    pub fn new() -> Self {
        let mut dictionary: HashSet<String> = HashSet::new();

        for file in DICTIONARIES_FOLDER.find("*").unwrap() {
            let file_unwrapped = DICTIONARIES_FOLDER.get_file(file.path()).unwrap();
            let words = file_unwrapped.contents_utf8().unwrap();
            let lowercase_words = words.to_lowercase();

            for word in lowercase_words.split('\n') {
                dictionary.insert(word.to_string());
            }
        }

        Self {
            data: dictionary
        }
    }

    pub fn get(&self, key: &str) -> Option<&String> {
        self.data.get(key)
    }
}

trait CharExt {
    fn accept(self) -> bool;
}

impl CharExt for char {
    fn accept(self) -> bool {
        match self {
            'a'..='z' | 'A'..='Z' | '\'' => true,
            _ => false,
        }
    }
}

fn check_path(folder: &str, cb: fn(PathBuf, dictionary: &Dictionary), dictionary: &Dictionary) {
    for entry in fs::read_dir(folder).expect("Could not read directory") {
        let entry = entry.expect("Could not read entry");
        let path = entry.path();

        if path.is_dir() {
            check_path(path.to_str().unwrap(), cb, &dictionary);
        } else {
            cb(path, &dictionary);
        } 
    }
}

fn check_words(file_path: &str, words: &Dictionary) -> i32 {
    let args: Vec<String> = env::args().collect();
    let mut typos_count = 0;
    let mut line_number = 0;
    let output = if args.len() > 2 { args[2].clone() } else { "".to_string() };
    let source;
    let path_is_url = is_url(file_path);

    if path_is_url {
        let response = match reqwest::blocking::get(file_path) {
            // Unwraps the response value from the Result
            Ok(r) => r,
        
            // Unwraps the error from the Result and terminates main()
            Err(err) => {
                println!("Request failed: {}", err.to_string());
                return -1;
            },
        };
    
        source = response.text().unwrap();
    } else {
        source = fs::read_to_string(file_path).unwrap();
    }

    if !is_english(source.as_str()) {
        return -1;
    }

    for line in source.lines() {
        line_number += 1;

        for word in line.split_whitespace() {
            let target = word.to_lowercase();
            let column = target.len() + 1;
            let target = sanitize(target);
            let has_typo = word.chars().all(char::accept) && words.get(&&*target).is_none();

            if has_typo && typos_count == 0 {
                if path_is_url {
                    if !output.is_empty() {
                        write_to_file(&output, &format!("🌐 {}\n", file_path));
                    } else {
                        println!("🌐 {}", file_path.bold().underline());
                    }
                } else {
                    if !output.is_empty() {
                        write_to_file(&output, &format!("📄 {}\n", file_path));
                    } else {
                        println!("📄 {}", file_path.bold().underline());
                    }
                } 
            }

            if has_typo {
                if path_is_url {
                    if !output.is_empty() {
                        write_to_file(&output, &format!("> {}\n", word));
                    } else {
                        println!("> {}", word.red().bold());
                    }
                } else {
                    if !output.is_empty() {
                        write_to_file(&output, &format!("> {} @ {}:{}\n", word, line_number, column));
                    } else {
                        println!("> {} @ {}:{}", word.red().bold(), line_number, column);
                    }
                }
                
                typos_count += 1;
            }
        }
    }

    return typos_count;
}

fn clear_file(path: &str) {
    let path_on_folder = std::env::current_dir().unwrap().join(path);
    let file = File::create(path_on_folder).unwrap();

    file.sync_all().unwrap();
}

fn is_english(source: &str) -> bool {
    let detected_language = LanguageDetectorBuilder::from_languages(&[English, Portuguese, Spanish]).build()
        .detect_language_of(source);

    detected_language == Some(English)
}

fn read_file(path: PathBuf, dictionary: &Dictionary) {
    let path_on_folder = std::env::current_dir().unwrap().join(path);
    let file = path_on_folder.display().to_string();
    
    let file_type = file.split(".").last().unwrap();
    let file_type_is_accepted = ACCEPTED_FILE_TYPES.contains(&file_type);

    if !file_type_is_accepted {
        return;
    }

    let typos_count = check_words(&file, &dictionary);

    if typos_count > 0 {
        println!("");
    }
}

pub fn sanitize(word: String) -> String {
    word.replace(|c:char| "`[<>-#(){}\"'!?“”,;=]".contains(c), "")
}

fn write_to_file(
    output: &str, 
    content: &str,
) {
    let path_on_folder = std::env::current_dir().unwrap().join(output);
    let file_path = path_on_folder.display().to_string();

    let mut file = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .append(true)
        .open(file_path)
        .unwrap();

    file.write_all(content.as_bytes()).unwrap();
}

fn main() {
    let now = std::time::Instant::now();
    let dictionary = Dictionary::new();
    let args: Vec<String> = env::args().collect();
    let folder = if args.len() > 1 { args[1].clone() } else { "./".to_string() };
    let output = if args.len() > 2 { args[2].clone() } else { "".to_string() };
    let path_on_folder = std::env::current_dir().unwrap().join(&output);

    println!("🔎 Checking for typos in {}...", path_on_folder.display().to_string().bold().underline());

    if !output.is_empty() {
        clear_file(&output);
    }

    if is_url(folder.as_str()) {
        println!("");
        println!("⏳ Please wait while url is being checked...");
        println!("");

        check_words(&folder, &dictionary);
    } else {
        println!("");
        println!("⏳ Please wait while files are being checked...");
        println!("");

        check_path(&folder, read_file, &dictionary);
    }

    if !output.is_empty() {
        write_to_file(&output, "\nMade with <3 by AE Studio");
    }

    let elapsed = now.elapsed().as_millis();

    println!("Done! Took {}ms", elapsed);
}