//! Deterministic, cross-platform filename and two-name helpers.

use crate::Error;
use libremetaverse_types::compat::Utf16CodeUnit;
use std::collections::HashSet;

pub struct FileHelper;

impl FileHelper {
    pub fn safe_file_name(original: String) -> Result<String, Error> {
        Ok(replace_utf16(
            &original,
            is_invalid_file_name,
            u16::from(b'_'),
        ))
    }

    pub fn safe_dir_name(original: String) -> Result<String, Error> {
        Ok(replace_utf16(&original, is_invalid_path, u16::from(b'_')))
    }

    pub fn sanitize(
        input: String,
        invalid_chars: Vec<Utf16CodeUnit>,
        replacement: Option<Utf16CodeUnit>,
    ) -> Result<String, Error> {
        let invalid: HashSet<u16> = invalid_chars.into_iter().map(|unit| unit.0).collect();
        Ok(replace_utf16(
            &input,
            |unit| invalid.contains(&unit),
            replacement.unwrap_or(Utf16CodeUnit(u16::from(b'_'))).0,
        ))
    }

    pub fn try_parse_two_names(
        input: String,
        first: &mut Option<String>,
        last: &mut Option<String>,
    ) -> bool {
        *first = None;
        *last = None;
        let mut names = input.split_whitespace();
        let Some(first_name) = names.next() else {
            return false;
        };
        let Some(last_name) = names.next() else {
            return false;
        };
        if names.next().is_some() {
            return false;
        }
        *first = Some(first_name.to_owned());
        *last = Some(last_name.to_owned());
        true
    }
}

fn replace_utf16(input: &str, invalid: impl Fn(u16) -> bool, replacement: u16) -> String {
    let units: Vec<u16> = input
        .encode_utf16()
        .map(|unit| if invalid(unit) { replacement } else { unit })
        .collect();
    String::from_utf16_lossy(&units)
}

fn is_invalid_file_name(unit: u16) -> bool {
    unit <= 31 || matches!(unit, 34 | 42 | 47 | 58 | 60 | 62 | 63 | 92 | 124)
}

fn is_invalid_path(unit: u16) -> bool {
    unit <= 31 || matches!(unit, 34 | 42 | 58 | 60 | 62 | 63 | 124)
}

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

    #[test]
    fn safe_names_use_the_portable_invalid_character_union() {
        assert_eq!(
            FileHelper::safe_file_name("a:b/c\\d?.txt".to_owned()).unwrap(),
            "a_b_c_d_.txt"
        );
        assert_eq!(
            FileHelper::safe_dir_name("root/child:name".to_owned()).unwrap(),
            "root/child_name"
        );
    }

    #[test]
    fn sanitize_operates_on_csharp_utf16_code_units() {
        let result = FileHelper::sanitize(
            "a:b🙂".to_owned(),
            vec![Utf16CodeUnit(u16::from(b':'))],
            None,
        )
        .unwrap();
        assert_eq!(result, "a_b🙂");
    }

    #[test]
    fn exactly_two_unicode_whitespace_delimited_names_are_accepted() {
        let mut first = Some("stale".to_owned());
        let mut last = Some("stale".to_owned());
        assert!(FileHelper::try_parse_two_names(
            "  Ada\u{2003}Lovelace  ".to_owned(),
            &mut first,
            &mut last,
        ));
        assert_eq!(first.as_deref(), Some("Ada"));
        assert_eq!(last.as_deref(), Some("Lovelace"));
        assert!(!FileHelper::try_parse_two_names(
            "Ada Byron Lovelace".to_owned(),
            &mut first,
            &mut last,
        ));
        assert_eq!(first, None);
        assert_eq!(last, None);
    }
}
