/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use super::language_info::LanguageInfo;
use super::zip::{read_archive_file_as_string, read_zip, Archive};
use crate::config::installation_resource_path;
use crate::prefs_parser::find_string_pref;
use crate::std::path::{Path, PathBuf};
use anyhow::Context;

const LOCALE_PREF_KEY: &str = "intl.locale.requested";

/// Use the profile language preferences to determine the localization to use.
pub fn read(
    profile_dir: &Path,
    base_locales: &[String],
    useragent_locale: Option<&str>,
) -> anyhow::Result<Option<LanguageInfo>> {
    let Some((identifier, langpack)) =
        select_langpack(profile_dir, base_locales, useragent_locale)?
    else {
        return Ok(None);
    };

    let mut zip = read_zip(&langpack)?;

    let ftl_definitions = read_archive_ftl_definitions(&mut zip, &identifier)?;
    let ftl_branding = read_archive_ftl_branding(&mut zip, &identifier).unwrap_or_else(|e| {
        log::warn!(
            "failed to read branding for {identifier} from {}: {e:#}",
            langpack.display()
        );
        log::info!("using fallback branding info");
        LanguageInfo::default().ftl_branding
    });

    Ok(Some(LanguageInfo {
        identifier,
        ftl_definitions,
        ftl_branding,
    }))
}

/// Select the langpack to use based on profile settings, the useragent locale, and the existence
/// of langpack files.
/// Returns `Ok(None)` if no locales are found in pref and from useragent locale. Also return
/// `Ok(None)` if the locale specified is the base installation locale, as there is no langpack for
/// the base installation locale.
fn select_langpack(
    profile_dir: &Path,
    base_locales: &[String],
    useragent_locale: Option<&str>,
) -> anyhow::Result<Option<(String, PathBuf)>> {
    let mut locales = locales_from_prefs(profile_dir)?
        .into_iter()
        .flatten()
        .chain(useragent_locale.map(str::to_string));

    if locales.clone().count() == 0 {
        return Ok(None);
    }

    // Use the first language that is either the base locale or we can find a langpack extension for
    locales
        .find_map(|lang| {
            if base_locales.contains(&lang) {
                Some(None)
            } else {
                find_langpack_extension(profile_dir, &lang).map(|path| Some((lang, path)))
            }
        })
        .with_context(|| format!("couldn't locate langpack for requested locales ({locales:?})"))
}

fn locales_from_prefs(profile_dir: &Path) -> anyhow::Result<Option<Vec<String>>> {
    let prefs = profile_dir.join("prefs.js");
    if !prefs.exists() {
        log::debug!(
            "no prefs.js exists in {}; not reading localization info",
            profile_dir.display()
        );
        return Ok(None);
    }

    let prefs_contents = std::fs::read_to_string(&prefs)
        .with_context(|| format!("while reading {}", prefs.display()))?;

    // Logic dictated by https://firefox-source-docs.mozilla.org/intl/locale.html#requested-locales

    let Some(langs) = parse_requested_locales(&prefs_contents) else {
        // If the pref is unset, the installation locale should be used.
        log::debug!(
            "no locale pref set in {}; using installation locale",
            prefs.display()
        );
        return Ok(None);
    };

    Ok(Some(if langs.is_empty() {
        // If the pref is an empty string, use the OS locale settings.
        let os_locales: Vec<String> = sys_locale::get_locales().collect();
        if os_locales.is_empty() {
            log::debug!("no OS locales found");
            return Ok(None);
        }
        log::debug!(
            "locale pref blank in {}; using OS locale settings ({os_locales:?})",
            prefs.display()
        );
        os_locales
    } else {
        langs.into_iter().map(|s| s.to_owned()).collect()
    }))
}

/// Parse the language pref (if any) from the prefs file.
///
/// For example:
/// ```rust
/// let input = r#"user_pref("intl.locale.requested", "foo , bar,,");"#;
/// let expected_output = Some(vec!["foo","bar"]);
/// assert_eq!(parse_requested_locales(input), output);
/// ```
///
/// This will parse the locales out of the user prefs file contents, which looks like
/// `user_pref("intl.locale.requested", "<LOCALE LIST>")`.
fn parse_requested_locales(prefs_content: &str) -> Option<Vec<&str>> {
    let v = find_string_pref(prefs_content, LOCALE_PREF_KEY)?;
    Some(
        v.split(",")
            .map(|s| s.trim())
            .filter(|s| !s.is_empty())
            .collect(),
    )
}

/// Find the langpack extension for the given locale.
fn find_langpack_extension(profile_dir: &Path, locale: &str) -> Option<PathBuf> {
    let path_tail = format!("extensions/langpack-{locale}@firefox.mozilla.org.xpi");

    // Check profile extensions
    let profile_path = profile_dir.join(&path_tail);
    if profile_path.exists() {
        return Some(profile_path);
    }

    // Check installation extensions
    let install_path = installation_resource_path().join(format!("browser/{}", &path_tail));
    if install_path.exists() {
        return Some(install_path);
    }

    None
}

fn read_archive_ftl_definitions(
    langpack: &mut Archive,
    language_identifier: &str,
) -> anyhow::Result<String> {
    let path = format!("localization/{language_identifier}/crashreporter/crashreporter.ftl");
    read_archive_file_as_string(langpack, &path)
}

fn read_archive_ftl_branding(
    langpack: &mut Archive,
    language_identifier: &str,
) -> anyhow::Result<String> {
    let path = format!("browser/localization/{language_identifier}/branding/brand.ftl");
    read_archive_file_as_string(langpack, &path)
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn parse_locales_none() {
        assert_eq!(parse_requested_locales(""), None);
    }

    #[test]
    fn parse_locales_empty() {
        assert_eq!(
            parse_requested_locales(r#"user_pref("intl.locale.requested","");"#),
            Some(vec![])
        );
    }

    #[test]
    fn parse_locales() {
        assert_eq!(
            parse_requested_locales(r#"user_pref("intl.locale.requested", "fr,en-US");"#),
            Some(vec!["fr", "en-US"])
        );
    }
}
