use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

#[derive(Debug, Default)]
struct PoEntry {
    msgid: String,
    msgid_plural: Option<String>,
    msgstr: String,
    msgstr_plural: BTreeMap<usize, String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PoField {
    Id,
    PluralId,
    Translation,
    PluralTranslation(usize),
}

fn main() {
    generate_localization_catalog().expect("failed to generate localization catalog");

    #[cfg(windows)]
    {
        let mut res = winres::WindowsResource::new();
        res.set_resource_file("app.rc");
        res.compile().expect("failed to compile Windows resources");
    }
}

fn generate_localization_catalog() -> io::Result<()> {
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"));
    let catalog_path = out_dir.join("localization_catalog.rs");
    let po_dir = Path::new("po");
    let mut catalogs = Vec::new();

    println!("cargo:rerun-if-changed=po");
    if po_dir.exists() {
        for entry in fs::read_dir(po_dir)? {
            let path = entry?.path();
            if path.extension().and_then(|extension| extension.to_str()) != Some("po") {
                continue;
            }
            println!("cargo:rerun-if-changed={}", path.display());
            let language_code = path
                .file_stem()
                .and_then(|stem| stem.to_str())
                .expect("PO filenames are valid UTF-8")
                .to_string();
            catalogs.push((language_code, parse_po_file(&path)?));
        }
    }
    catalogs.sort_by(|left, right| left.0.cmp(&right.0));

    let mut generated = String::from(
        "// Generated by build.rs from po/*.po.\n\
         pub(crate) fn gettext(language: &str, msgid: &'static str) -> &'static str {\n\
         \x20   match language {\n",
    );

    for (language_code, entries) in &catalogs {
        generated.push_str(&format!("        {:?} => match msgid {{\n", language_code));
        for entry in entries
            .iter()
            .filter(|entry| !entry.msgid.is_empty() && entry.msgid_plural.is_none())
        {
            generated.push_str(&format!(
                "            {:?} => {:?},\n",
                entry.msgid, entry.msgstr
            ));
        }
        generated.push_str("            _ => msgid,\n        },\n");
    }
    generated.push_str("        _ => msgid,\n    }\n}\n\n");

    generated.push_str(
        "pub(crate) fn ngettext(\n\
         \x20   language: &str,\n\
         \x20   singular: &'static str,\n\
         \x20   plural: &'static str,\n\
         \x20   count: usize,\n\
         ) -> &'static str {\n\
         \x20   let fallback = if count == 1 { singular } else { plural };\n\
         \x20   match language {\n",
    );

    for (language_code, entries) in &catalogs {
        generated.push_str(&format!(
            "        {:?} => match (singular, count == 1) {{\n",
            language_code
        ));
        for entry in entries
            .iter()
            .filter(|entry| !entry.msgid.is_empty() && entry.msgid_plural.is_some())
        {
            if let Some(singular_translation) = entry.msgstr_plural.get(&0) {
                generated.push_str(&format!(
                    "            ({:?}, true) => {:?},\n",
                    entry.msgid, singular_translation
                ));
            }
            if let Some(plural_translation) = entry.msgstr_plural.get(&1) {
                generated.push_str(&format!(
                    "            ({:?}, false) => {:?},\n",
                    entry.msgid, plural_translation
                ));
            }
        }
        generated.push_str("            _ => fallback,\n        },\n");
    }
    generated.push_str("        _ => fallback,\n    }\n}\n");

    fs::write(catalog_path, generated)
}

fn parse_po_file(path: &Path) -> io::Result<Vec<PoEntry>> {
    let text = fs::read_to_string(path)?;
    let mut entries = Vec::new();
    let mut current = PoEntry::default();
    let mut current_field = None;

    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            finish_po_entry(&mut entries, &mut current);
            current_field = None;
            continue;
        }
        if trimmed.starts_with('#') {
            continue;
        }

        if let Some(value) = trimmed.strip_prefix("msgid_plural ") {
            current.msgid_plural = Some(parse_po_string(value));
            current_field = Some(PoField::PluralId);
        } else if let Some(value) = trimmed.strip_prefix("msgid ") {
            current.msgid = parse_po_string(value);
            current_field = Some(PoField::Id);
        } else if let Some(rest) = trimmed.strip_prefix("msgstr[") {
            let Some((index, value)) = rest.split_once("] ") else {
                continue;
            };
            let Ok(index) = index.parse::<usize>() else {
                continue;
            };
            current.msgstr_plural.insert(index, parse_po_string(value));
            current_field = Some(PoField::PluralTranslation(index));
        } else if let Some(value) = trimmed.strip_prefix("msgstr ") {
            current.msgstr = parse_po_string(value);
            current_field = Some(PoField::Translation);
        } else if trimmed.starts_with('"')
            && let Some(field) = current_field
        {
            let value = parse_po_string(trimmed);
            match field {
                PoField::Id => current.msgid.push_str(&value),
                PoField::PluralId => {
                    current
                        .msgid_plural
                        .get_or_insert_with(String::new)
                        .push_str(&value);
                }
                PoField::Translation => current.msgstr.push_str(&value),
                PoField::PluralTranslation(index) => {
                    current
                        .msgstr_plural
                        .entry(index)
                        .or_default()
                        .push_str(&value);
                }
            }
        }
    }
    finish_po_entry(&mut entries, &mut current);

    Ok(entries)
}

fn finish_po_entry(entries: &mut Vec<PoEntry>, current: &mut PoEntry) {
    if !current.msgid.is_empty() {
        entries.push(std::mem::take(current));
    } else {
        *current = PoEntry::default();
    }
}

fn parse_po_string(input: &str) -> String {
    let quoted = input.trim();
    let mut chars = quoted
        .strip_prefix('"')
        .and_then(|value| value.strip_suffix('"'))
        .unwrap_or(quoted)
        .chars();
    let mut output = String::new();

    while let Some(ch) = chars.next() {
        if ch != '\\' {
            output.push(ch);
            continue;
        }
        match chars.next() {
            Some('n') => output.push('\n'),
            Some('t') => output.push('\t'),
            Some('r') => output.push('\r'),
            Some('"') => output.push('"'),
            Some('\\') => output.push('\\'),
            Some(other) => {
                output.push('\\');
                output.push(other);
            }
            None => output.push('\\'),
        }
    }

    output
}
