#![doc = include_str!("../README.md")]

use csv::StringRecord;
use icu_locale_core::subtags::Language;
use proc_macro::Span;
use proc_macro::TokenStream;
use std::fmt::Write;
use std::path::Path;
use std::path::PathBuf;
use syn::LitStr;
use syn::parse_macro_input;

static OUT_STR_PRELUDE: &str = r#"
pub fn set_global_language(language: Language) {
    *GLOBAL_LANGUAGE.write().unwrap() = language;
}

pub fn get_global_language() -> Language {
    *GLOBAL_LANGUAGE.read().unwrap()
}

#[derive(Debug, Clone)]
pub enum LanguageParseError {
    UnknownLanguage(String),
}
"#;
static OUT_STR_EPILOGUE: &str = r#"
impl std::fmt::Display for Language {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.as_str())
    }
}

impl std::fmt::Display for MultilangString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.as_str())
    }
}
"#;

struct LanguageEntry {
    enum_ident: String,
    ident: String,
}

impl From<Language> for LanguageEntry {
    fn from(value: Language) -> Self {
        let ident = value.to_string();

        let mut ident_iter = ident.chars();

        let first_char = ident_iter.next().unwrap();

        let enum_ident: String = first_char
            .to_uppercase()
            .to_string()
            .chars()
            .chain(ident_iter)
            .collect();

        Self { enum_ident, ident }
    }
}

#[proc_macro]
/// Generates structs and static MultilangStrings from the provided CSV
pub fn generate_from_csv(file_path: TokenStream) -> TokenStream {
    let in_literal_str = parse_macro_input!(file_path as LitStr);

    let file_path_str = in_literal_str.value();

    let mut file_path = Span::call_site()
        .local_file()
        .unwrap()
        .parent()
        .unwrap_or(Path::new(""))
        .to_path_buf();
    file_path.push(PathBuf::from(file_path_str.clone()));

    if !file_path.is_file() {
        panic!("Couldn't find csv file '{}'", file_path.to_str().unwrap());
    }

    let csv_reader = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_path(file_path)
        .unwrap();
    let mut record_iter = csv_reader.into_records();

    // First record includes the list of languages, and the first column is the
    // default language
    let first_record = record_iter.next().unwrap().unwrap();

    let mut first_record_iter = first_record.iter();

    let default_language: LanguageEntry = first_record_iter
        .next()
        .unwrap()
        .parse::<Language>()
        .unwrap()
        .into();

    let mut i_default_language = 0;

    let languages: Vec<LanguageEntry> = first_record_iter
        .enumerate()
        .map(|(i_x, x)| {
            let language: LanguageEntry = x.parse::<Language>().unwrap().into();

            if language.ident == default_language.enum_ident {
                i_default_language = i_x;
            }

            language
        })
        .collect();

    let mut out_string = String::with_capacity(1024);

    // Combats a bug where proc macros won't run if there's an update to the
    // in CSV
    writeln!(
        out_string,
        r####"const IN_CSV: &str = include_str!(r###"{}"###);"####,
        file_path_str
    )
    .unwrap();

    generate_global(default_language, &mut out_string);
    generate_languages(&languages, i_default_language, &mut out_string);
    generate_multilang_string_struct(&languages, &mut out_string);

    out_string.push_str(OUT_STR_PRELUDE);

    for record_result in record_iter {
        let record = record_result.unwrap();
        generate_multilang_string(record, &languages, &mut out_string);
    }

    out_string.push_str(OUT_STR_EPILOGUE);

    out_string.parse().unwrap()
}

fn generate_global(default_language: LanguageEntry, out_string: &mut String) {
    writeln!(out_string, "pub static GLOBAL_LANGUAGE: std::sync::RwLock<Language> = std::sync::RwLock::new(Language::{});", default_language.enum_ident).unwrap();
}

fn generate_languages(
    languages: &[LanguageEntry],
    i_default_language: usize,
    out_string: &mut String,
) {
    writeln!(
        out_string,
        "#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]"
    )
    .unwrap();
    writeln!(out_string, "pub enum Language {{").unwrap();
    for (i_language, language) in languages.iter().enumerate() {
        if i_language == i_default_language {
            writeln!(out_string, "    #[default]").unwrap();
        }
        writeln!(out_string, "    {},", language.enum_ident).unwrap();
    }
    writeln!(out_string, "}}").unwrap();

    writeln!(
        out_string,
        r#"impl Language {{
        pub fn as_str(&self) -> &'static str {{
            match self {{"#
    )
    .unwrap();

    for language in languages {
        writeln!(
            out_string,
            r#"    Language::{} => "{}","#,
            language.enum_ident, language.ident
        )
        .unwrap();
    }
    writeln!(out_string, "}}}}}}").unwrap();

    writeln!(
        out_string,
        r#"
        impl std::str::FromStr for Language {{
            type Err = LanguageParseError;

            fn from_str(s: &str) -> Result<Self, Self::Err> {{
                let s_processed = s.trim().to_lowercase();

                match s_processed.as_str() {{"#
    )
    .unwrap();

    for language in languages {
        writeln!(
            out_string,
            r#"    "{}" => Ok(Language::{}),"#,
            language.ident, language.enum_ident
        )
        .unwrap();
    }
    writeln!(
        out_string,
        "_ => Err(LanguageParseError::UnknownLanguage(s_processed)),"
    )
    .unwrap();
    writeln!(out_string, "}}}}}}").unwrap();
}

fn generate_multilang_string_struct(languages: &[LanguageEntry], out_string: &mut String) {
    writeln!(
        out_string,
        "#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]"
    )
    .unwrap();
    writeln!(out_string, "pub struct MultilangString {{").unwrap();

    for language in languages.iter() {
        writeln!(out_string, "    {}: &'static str,", language.ident).unwrap();
    }

    writeln!(out_string, "}}").unwrap();

    writeln!(
        out_string,
        r#"impl MultilangString {{
        pub fn as_str(&self) -> &'static str {{
            match *GLOBAL_LANGUAGE.read().unwrap() {{"#
    )
    .unwrap();

    for language in languages {
        writeln!(
            out_string,
            r#"    Language::{} => self.{},"#,
            language.enum_ident, language.ident
        )
        .unwrap();
    }

    writeln!(out_string, "}}}}}}").unwrap();
}

fn generate_multilang_string(
    record: StringRecord,
    languages: &[LanguageEntry],
    out_string: &mut String,
) {
    let mut record_iter = record.into_iter();

    // !TODO! add regex validation of variable names
    let var_name = record_iter.next().unwrap().trim().to_uppercase();

    writeln!(
        out_string,
        "pub static {}: MultilangString = MultilangString {{",
        var_name
    )
    .unwrap();

    for (i_record, record) in record_iter.enumerate() {
        writeln!(
            out_string,
            r####"   {}: r###"{}"###,"####,
            languages[i_record].ident, record
        )
        .unwrap();
    }

    writeln!(out_string, "}};").unwrap();
}
