use core::fmt;
use std::collections::HashMap;
use std::sync::LazyLock;

use serde::Serialize;

use crate::layout::LayoutDirection;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct StringKey(&'static str);

impl StringKey {
    #[must_use]
    pub const fn new(id: &'static str) -> Self {
        Self(id)
    }

    #[must_use]
    pub const fn id(self) -> &'static str {
        self.0
    }
}

impl fmt::Display for StringKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.0)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub enum Locale {
    EnUs,
    ArXb,
}

impl Locale {
    pub const DEFAULT: Self = Self::EnUs;

    #[must_use]
    pub const fn as_bcp47(self) -> &'static str {
        match self {
            Self::EnUs => "en-US",
            Self::ArXb => "ar-XB",
        }
    }

    #[must_use]
    pub const fn direction(self) -> LayoutDirection {
        match self {
            Self::EnUs => LayoutDirection::Ltr,
            Self::ArXb => LayoutDirection::Rtl,
        }
    }

    #[must_use]
    pub const fn plural_category(self, n: u64) -> PluralCategory {
        match self {
            Self::EnUs => english_plural(n),
            Self::ArXb => arabic_plural(n),
        }
    }
}

impl Default for Locale {
    fn default() -> Self {
        Self::DEFAULT
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub enum PluralCategory {
    Zero,
    One,
    Two,
    Few,
    Many,
    Other,
}

const fn english_plural(n: u64) -> PluralCategory {
    match n {
        1 => PluralCategory::One,
        _ => PluralCategory::Other,
    }
}

const fn arabic_plural(n: u64) -> PluralCategory {
    match (n, n % 100) {
        (0, _) => PluralCategory::Zero,
        (1, _) => PluralCategory::One,
        (2, _) => PluralCategory::Two,
        (_, 3..=10) => PluralCategory::Few,
        (_, 11..=99) => PluralCategory::Many,
        _ => PluralCategory::Other,
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PluralEntry {
    pub other: String,
    pub zero: Option<String>,
    pub one: Option<String>,
    pub two: Option<String>,
    pub few: Option<String>,
    pub many: Option<String>,
}

impl PluralEntry {
    #[must_use]
    pub fn other(other: impl Into<String>) -> Self {
        Self::default().with(PluralCategory::Other, other)
    }

    #[must_use]
    pub fn new(one: impl Into<String>, other: impl Into<String>) -> Self {
        Self::other(other).with(PluralCategory::One, one)
    }

    #[must_use]
    pub fn with(mut self, category: PluralCategory, value: impl Into<String>) -> Self {
        let value = value.into();
        match category {
            PluralCategory::Zero => self.zero = Some(value),
            PluralCategory::One => self.one = Some(value),
            PluralCategory::Two => self.two = Some(value),
            PluralCategory::Few => self.few = Some(value),
            PluralCategory::Many => self.many = Some(value),
            PluralCategory::Other => self.other = value,
        }
        self
    }

    fn pick(&self, category: PluralCategory) -> &str {
        let variant = match category {
            PluralCategory::Zero => self.zero.as_deref(),
            PluralCategory::One => self.one.as_deref(),
            PluralCategory::Two => self.two.as_deref(),
            PluralCategory::Few => self.few.as_deref(),
            PluralCategory::Many => self.many.as_deref(),
            PluralCategory::Other => return &self.other,
        };
        variant.unwrap_or(&self.other)
    }
}

#[derive(Clone, Debug)]
pub struct StringTable {
    locale: Locale,
    entries: HashMap<StringKey, String>,
    plural_entries: HashMap<StringKey, PluralEntry>,
}

impl Default for StringTable {
    fn default() -> Self {
        Self::for_locale(Locale::DEFAULT)
    }
}

impl StringTable {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn for_locale(locale: Locale) -> Self {
        Self {
            locale,
            entries: HashMap::new(),
            plural_entries: HashMap::new(),
        }
    }

    #[must_use]
    pub fn empty() -> &'static Self {
        static EMPTY: LazyLock<StringTable> = LazyLock::new(StringTable::default);
        &EMPTY
    }

    #[must_use]
    pub fn from_entries<I>(iter: I) -> Self
    where
        I: IntoIterator<Item = (StringKey, String)>,
    {
        let mut table = Self::default();
        table.entries.extend(iter);
        table
    }

    #[must_use]
    pub const fn locale(&self) -> Locale {
        self.locale
    }

    #[must_use]
    pub const fn direction(&self) -> LayoutDirection {
        self.locale.direction()
    }

    pub fn insert(&mut self, key: StringKey, value: String) -> Option<String> {
        self.entries.insert(key, value)
    }

    pub fn insert_plural(&mut self, key: StringKey, plural: PluralEntry) -> Option<PluralEntry> {
        self.plural_entries.insert(key, plural)
    }

    #[must_use]
    pub fn resolve(&self, key: StringKey) -> &str {
        self.entries.get(&key).map_or(key.0, String::as_str)
    }

    #[must_use]
    pub fn resolve_plural(&self, key: StringKey, n: u64) -> &str {
        let category = self.locale.plural_category(n);
        self.plural_entries
            .get(&key)
            .map_or(key.0, |entry| entry.pick(category))
    }

    #[must_use]
    pub fn format_count(&self, n: u64) -> String {
        match self.locale {
            Locale::EnUs => group_thousands(n, ','),
            Locale::ArXb => group_thousands(n, '\u{066C}')
                .chars()
                .map(map_arabic_indic)
                .collect(),
        }
    }

    #[must_use]
    pub fn format_plural(&self, key: StringKey, n: u64) -> String {
        self.resolve_plural(key, n)
            .replace("{n}", &self.format_count(n))
    }

    #[must_use]
    pub fn contains(&self, key: StringKey) -> bool {
        self.entries.contains_key(&key) || self.plural_entries.contains_key(&key)
    }

    #[must_use]
    pub fn len(&self) -> usize {
        self.entries.len() + self.plural_entries.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty() && self.plural_entries.is_empty()
    }
}

fn group_thousands(n: u64, separator: char) -> String {
    let digits = n.to_string();
    let head_len = digits.len() % 3;
    let separator = separator.to_string();
    [&digits[..head_len]]
        .into_iter()
        .filter(|head| !head.is_empty())
        .chain(
            (head_len..digits.len())
                .step_by(3)
                .map(|start| &digits[start..start + 3]),
        )
        .collect::<Vec<_>>()
        .join(&separator)
}

fn map_arabic_indic(c: char) -> char {
    if c.is_ascii_digit() {
        let offset = u32::from(c) - u32::from('0');
        char::from_u32(0x0660 + offset).unwrap_or(c)
    } else {
        c
    }
}

#[cfg(test)]
mod tests {
    use super::{Locale, PluralCategory, PluralEntry, StringKey, StringTable};
    use crate::layout::LayoutDirection;

    const GREETING: StringKey = StringKey::new("dialog.greeting");
    const FAREWELL: StringKey = StringKey::new("dialog.farewell");
    const ITEMS: StringKey = StringKey::new("status.items");

    #[test]
    fn key_round_trips_id() {
        assert_eq!(GREETING.id(), "dialog.greeting");
    }

    #[test]
    fn key_displays_id() {
        assert_eq!(format!("{GREETING}"), "dialog.greeting");
    }

    #[test]
    fn distinct_keys_compare_distinct() {
        assert_ne!(GREETING, FAREWELL);
        assert_eq!(GREETING, StringKey::new("dialog.greeting"));
    }

    #[test]
    fn empty_table_falls_back_to_id() {
        let table = StringTable::new();
        assert_eq!(table.resolve(GREETING), "dialog.greeting");
        assert!(table.is_empty());
        assert_eq!(table.len(), 0);
    }

    #[test]
    fn populated_table_returns_entry() {
        let table = StringTable::from_entries([(GREETING, "Hello".to_owned())]);
        assert_eq!(table.resolve(GREETING), "Hello");
        assert_eq!(table.resolve(FAREWELL), "dialog.farewell");
        assert!(table.contains(GREETING));
        assert!(!table.contains(FAREWELL));
    }

    #[test]
    fn insert_replaces_existing() {
        let mut table = StringTable::new();
        assert!(table.insert(GREETING, "Hi".to_owned()).is_none());
        assert_eq!(
            table.insert(GREETING, "Hello".to_owned()),
            Some("Hi".to_owned()),
        );
        assert_eq!(table.resolve(GREETING), "Hello");
    }

    #[test]
    fn len_reflects_inserted_entries() {
        let table = StringTable::from_entries([
            (GREETING, "Hello".to_owned()),
            (FAREWELL, "Bye".to_owned()),
        ]);
        assert_eq!(table.len(), 2);
        assert!(!table.is_empty());
    }

    #[test]
    fn default_locale_is_en_us_ltr() {
        let table = StringTable::new();
        assert_eq!(table.locale(), Locale::EnUs);
        assert_eq!(table.direction(), LayoutDirection::Ltr);
    }

    #[test]
    fn ar_xb_pseudo_locale_is_rtl() {
        let table = StringTable::for_locale(Locale::ArXb);
        assert_eq!(table.direction(), LayoutDirection::Rtl);
        assert_eq!(table.locale().as_bcp47(), "ar-XB");
    }

    #[test]
    fn english_plural_picks_one_or_other() {
        assert_eq!(Locale::EnUs.plural_category(0), PluralCategory::Other);
        assert_eq!(Locale::EnUs.plural_category(1), PluralCategory::One);
        assert_eq!(Locale::EnUs.plural_category(2), PluralCategory::Other);
        assert_eq!(Locale::EnUs.plural_category(42), PluralCategory::Other);
    }

    #[test]
    fn arabic_plural_covers_every_category() {
        assert_eq!(Locale::ArXb.plural_category(0), PluralCategory::Zero);
        assert_eq!(Locale::ArXb.plural_category(1), PluralCategory::One);
        assert_eq!(Locale::ArXb.plural_category(2), PluralCategory::Two);
        assert_eq!(Locale::ArXb.plural_category(5), PluralCategory::Few);
        assert_eq!(Locale::ArXb.plural_category(20), PluralCategory::Many);
        assert_eq!(Locale::ArXb.plural_category(100), PluralCategory::Other);
        assert_eq!(Locale::ArXb.plural_category(102), PluralCategory::Other);
        assert_eq!(Locale::ArXb.plural_category(105), PluralCategory::Few);
    }

    #[test]
    fn plural_entry_picks_variant_per_category() {
        let mut table = StringTable::new();
        table.insert_plural(ITEMS, PluralEntry::new("{n} item", "{n} items"));
        assert_eq!(table.resolve_plural(ITEMS, 1), "{n} item");
        assert_eq!(table.resolve_plural(ITEMS, 0), "{n} items");
        assert_eq!(table.resolve_plural(ITEMS, 5), "{n} items");
    }

    #[test]
    fn plural_entry_falls_back_to_other_when_variant_absent() {
        let mut table = StringTable::for_locale(Locale::ArXb);
        table.insert_plural(ITEMS, PluralEntry::new("عنصر واحد", "{n} عناصر"));
        assert_eq!(table.resolve_plural(ITEMS, 0), "{n} عناصر");
        assert_eq!(table.resolve_plural(ITEMS, 1), "عنصر واحد");
        assert_eq!(table.resolve_plural(ITEMS, 5), "{n} عناصر");
    }

    #[test]
    fn plural_entry_uses_explicit_variant_when_present() {
        let mut table = StringTable::for_locale(Locale::ArXb);
        table.insert_plural(
            ITEMS,
            PluralEntry::new("one", "other")
                .with(PluralCategory::Zero, "zero")
                .with(PluralCategory::Two, "two")
                .with(PluralCategory::Few, "few")
                .with(PluralCategory::Many, "many"),
        );
        assert_eq!(table.resolve_plural(ITEMS, 0), "zero");
        assert_eq!(table.resolve_plural(ITEMS, 1), "one");
        assert_eq!(table.resolve_plural(ITEMS, 2), "two");
        assert_eq!(table.resolve_plural(ITEMS, 5), "few");
        assert_eq!(table.resolve_plural(ITEMS, 20), "many");
        assert_eq!(table.resolve_plural(ITEMS, 100), "other");
    }

    #[test]
    fn missing_plural_key_falls_back_to_id() {
        let table = StringTable::new();
        assert_eq!(table.resolve_plural(ITEMS, 1), "status.items");
    }

    #[test]
    fn english_number_formatting_groups_with_comma() {
        let table = StringTable::for_locale(Locale::EnUs);
        assert_eq!(table.format_count(0), "0");
        assert_eq!(table.format_count(42), "42");
        assert_eq!(table.format_count(1234), "1,234");
        assert_eq!(table.format_count(1_000_000), "1,000,000");
    }

    #[test]
    fn arabic_number_formatting_uses_indic_digits() {
        let table = StringTable::for_locale(Locale::ArXb);
        assert_eq!(table.format_count(42), "\u{0664}\u{0662}");
        assert_eq!(
            table.format_count(1234),
            "\u{0661}\u{066C}\u{0662}\u{0663}\u{0664}",
        );
    }

    #[test]
    fn format_plural_substitutes_count_into_template() {
        let mut table = StringTable::new();
        table.insert_plural(ITEMS, PluralEntry::new("{n} item", "{n} items"));
        assert_eq!(table.format_plural(ITEMS, 1), "1 item");
        assert_eq!(table.format_plural(ITEMS, 1234), "1,234 items");
    }

    #[test]
    fn format_plural_routes_count_through_locale_digits() {
        let mut table = StringTable::for_locale(Locale::ArXb);
        table.insert_plural(ITEMS, PluralEntry::other("{n}"));
        assert_eq!(table.format_plural(ITEMS, 42), "\u{0664}\u{0662}");
    }

    #[test]
    fn plural_entry_other_only_falls_back_to_other_for_every_category() {
        let mut table = StringTable::new();
        table.insert_plural(ITEMS, PluralEntry::other("{n} 件"));
        assert_eq!(table.resolve_plural(ITEMS, 0), "{n} 件");
        assert_eq!(table.resolve_plural(ITEMS, 1), "{n} 件");
        assert_eq!(table.resolve_plural(ITEMS, 5), "{n} 件");
    }
}
