//! Korean-like Hangul pseudo-word generator.
//!
//! Mirrors `@readable-code/korean`. The TypeScript package composes jamo with
//! `es-hangul`, which has no Rust equivalent we depend on. Instead this crate
//! composes precomposed Hangul syllables directly via the Unicode algorithm:
//!
//! ```text
//! syllable = 0xAC00 + (choseong * 21 + jungseong) * 28 + jongseong
//! ```
//!
//! Index tables below select the same readable jamo subset as the TypeScript
//! package, so both ecosystems produce conceptually aligned output. Depends
//! only on `readable-code-core`.

use readable_code_core::{code, CodeBuilder, OsRandom, RandomSource};

const SYLLABLE_BASE: u32 = 0xAC00;
const JUNGSEONG_COUNT: u32 = 21;
const JONGSEONG_COUNT: u32 = 28;

// Jamo index tables, weighted by reading feel to match the TypeScript package.
// Common, soft jamo are repeated so they are picked more often; harsh initials
// and busy finals stay rare. Index 0 of each table is the stable anchor used by
// deterministic tests (ㄱ / ㅏ / no final).

/// Choseong (initial) indices, weighted.
/// Order: ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅎ | ㄴㄹㅁㅇㅅㅈㄱㅂ | ㅊㅋㅌㅍ.
const CHOSEONG: [u32; 22] = [
    0, 2, 3, 5, 6, 7, 9, 11, 12, 18, // common
    2, 5, 6, 11, 9, 12, 0, 7, // soft/common boost
    14, 15, 16, 17, // harsh, rare
];
/// Jungseong (medial) indices, weighted.
/// Order: ㅏㅓㅗㅜㅡㅣㅐㅔ | ㅏㅓㅗㅜㅣㅏㅗ.
const JUNGSEONG: [u32; 15] = [
    0, 4, 8, 13, 18, 20, 1, 5, // base
    0, 4, 8, 13, 20, 0, 8, // clear open-vowel boost
];
/// Jongseong (final) indices: mostly none, then soft sonorant finals ㄴㄹㅁㅇ.
const JONGSEONG: [u32; 11] = [0, 0, 0, 0, 0, 0, 0, 4, 8, 16, 21];

/// 자음동화 (consonant assimilation) guard. We only emit sonorant finals, so the
/// only clusters that would assimilate across the syllable boundary are ㄴ받침
/// (coda 4) + ㄹ초성 (onset 5) — 신라→실라 — and ㄹ받침 (coda 8) + ㄴ초성
/// (onset 2) — 설날→설랄. Return the onset index to avoid after the given coda.
fn blocked_onset(previous_coda: u32) -> Option<u32> {
    match previous_coda {
        4 => Some(5), // ㄴ받침 -> avoid ㄹ초성
        8 => Some(2), // ㄹ받침 -> avoid ㄴ초성
        _ => None,
    }
}

fn compose(choseong: u32, jungseong: u32, jongseong: u32) -> char {
    let code_point =
        SYLLABLE_BASE + (choseong * JUNGSEONG_COUNT + jungseong) * JONGSEONG_COUNT + jongseong;
    char::from_u32(code_point).expect("composed Hangul code point must be valid")
}

/// Generate a Hangul pseudo-word of `length` syllables.
///
/// When `final_consonants` is `false`, no jongseong (final consonant) is used.
///
/// Phonotactic smoothing, grounded in Korean phonology:
/// - A final consonant (받침) is never placed two syllables in a row, so output
///   reads as alternating open/closed syllables rather than stacked 받침.
/// - The onset after a 받침 is constrained so it does not trigger 자음동화
///   (consonant assimilation) across the boundary.
pub fn ko_word<R: RandomSource>(length: usize, final_consonants: bool, rng: &mut R) -> String {
    let mut out = String::with_capacity(length * 3);
    let mut previous_coda = 0u32;
    for _ in 0..length {
        let mut choseong = CHOSEONG[rng.gen_below(CHOSEONG.len() as u32) as usize];
        if let Some(blocked) = blocked_onset(previous_coda) {
            while choseong == blocked {
                choseong = CHOSEONG[rng.gen_below(CHOSEONG.len() as u32) as usize];
            }
        }
        let jungseong = JUNGSEONG[rng.gen_below(JUNGSEONG.len() as u32) as usize];
        let jongseong = if final_consonants && previous_coda == 0 {
            JONGSEONG[rng.gen_below(JONGSEONG.len() as u32) as usize]
        } else {
            0
        };
        previous_coda = jongseong;
        out.push(compose(choseong, jungseong, jongseong));
    }
    out
}

/// Fluent builder that can append more Korean pseudo-words mid-chain.
///
/// Wraps the core [`CodeBuilder`] and adds [`KoreanBuilder::hangul`]. Chaining
/// stays limited to string fragment composition.
pub struct KoreanBuilder<R: RandomSource> {
    inner: CodeBuilder<R>,
    final_consonants: bool,
}

impl<R: RandomSource> KoreanBuilder<R> {
    /// Append another Hangul pseudo-word.
    pub fn hangul(self, length: usize) -> Self {
        let final_consonants = self.final_consonants;
        KoreanBuilder {
            inner: self.inner.add_with(|rng| ko_word(length, final_consonants, rng)),
            final_consonants,
        }
    }

    /// Append a `-` separator.
    pub fn dash(self) -> Self {
        KoreanBuilder {
            inner: self.inner.dash(),
            final_consonants: self.final_consonants,
        }
    }

    /// Append `length` random decimal digits.
    pub fn digits(self, length: usize) -> Self {
        KoreanBuilder {
            inner: self.inner.digits(length),
            final_consonants: self.final_consonants,
        }
    }

    /// Alias for [`KoreanBuilder::digits`].
    pub fn nums(self, length: usize) -> Self {
        self.digits(length)
    }

    /// Append an arbitrary string fragment.
    pub fn add(self, value: impl Into<String>) -> Self {
        KoreanBuilder {
            inner: self.inner.add(value),
            final_consonants: self.final_consonants,
        }
    }

    /// Finalize and return the joined code string.
    pub fn build(self) -> String {
        self.inner.build()
    }
}

/// Primary Korean fluent API with a custom random source and final-consonant flag.
pub fn hangul_with<R: RandomSource>(
    length: usize,
    final_consonants: bool,
    rng: R,
) -> KoreanBuilder<R> {
    KoreanBuilder {
        inner: code(rng),
        final_consonants,
    }
    .hangul(length)
}

/// Primary Korean fluent API using the secure default random source (with finals).
pub fn hangul(length: usize) -> KoreanBuilder<OsRandom> {
    hangul_with(length, true, OsRandom::new())
}

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

    struct Fixed(u32);
    impl RandomSource for Fixed {
        fn gen_below(&mut self, max_exclusive: u32) -> u32 {
            self.0 % max_exclusive
        }
    }

    struct Seq {
        values: Vec<u32>,
        index: usize,
    }
    impl RandomSource for Seq {
        fn gen_below(&mut self, max_exclusive: u32) -> u32 {
            let value = self.values[self.index % self.values.len()];
            self.index += 1;
            value % max_exclusive
        }
    }

    #[test]
    fn no_two_finals_in_a_row() {
        // JONGSEONG[7] -> ㄴ. s0: ㄱ+ㅏ+ㄴ -> "간"; s1: final forced off -> "가".
        let mut rng = Seq {
            values: vec![0, 0, 7, 0, 0],
            index: 0,
        };
        assert_eq!(ko_word(2, true, &mut rng), "간가");
    }

    #[test]
    fn avoids_jaeum_donghwa_onset_after_coda() {
        // s0: ㄱ+ㅏ+ㄴ -> "간" (prev coda ㄴ). s1 onset draw idx3 = ㄹ -> blocked
        // -> resample idx2 = ㄷ; +ㅏ -> "다". Without the guard it would be "간라".
        let mut rng = Seq {
            values: vec![0, 0, 7, 3, 2, 0],
            index: 0,
        };
        assert_eq!(ko_word(2, true, &mut rng), "간다");
    }

    #[test]
    fn ko_word_composes_syllables() {
        // Fixed(0): choseong ㄱ, jungseong ㅏ, jongseong none -> "가".
        assert_eq!(ko_word(3, true, &mut Fixed(0)), "가가가");
    }

    #[test]
    fn ko_word_without_finals() {
        // Fixed(3): choseong ㄹ, jungseong ㅜ -> "루".
        assert_eq!(ko_word(2, false, &mut Fixed(3)), "루루");
    }

    /// Records the `max_exclusive` of every draw to assert pool sizing.
    struct Probe {
        maxes: Vec<u32>,
    }
    impl RandomSource for Probe {
        fn gen_below(&mut self, max_exclusive: u32) -> u32 {
            self.maxes.push(max_exclusive);
            0
        }
    }

    #[test]
    fn ko_word_empty_for_zero_length() {
        assert_eq!(ko_word(0, true, &mut Fixed(0)), "");
        assert_eq!(ko_word(0, false, &mut Fixed(0)), "");
    }

    #[test]
    fn ko_word_single_closed_syllable() {
        // JONGSEONG[7]=4 (ㄴ): ㄱ+ㅏ+ㄴ -> "간".
        let mut rng = Seq {
            values: vec![0, 0, 7],
            index: 0,
        };
        assert_eq!(ko_word(1, true, &mut rng), "간");
    }

    #[test]
    fn blocks_n_onset_after_r_coda() {
        // 설날→설랄 direction. s0: ㄱ+ㅏ+ㄹ -> "갈" (JONGSEONG[8]=8, coda ㄹ).
        // s1 onset draw idx1 = ㄴ (CHOSEONG[1]=2) -> blocked after ㄹ coda ->
        // resample idx2 = ㄷ (CHOSEONG[2]=3); +ㅏ -> "다". Guarded result "갈다".
        let mut rng = Seq {
            values: vec![0, 0, 8, 1, 2, 0],
            index: 0,
        };
        assert_eq!(ko_word(2, true, &mut rng), "갈다");
    }

    #[test]
    fn ko_word_draws_from_correctly_sized_pools() {
        let mut probe = Probe { maxes: Vec::new() };
        ko_word(2, true, &mut probe);
        // Per open syllable: 22 choseong, 15 jungseong, 11 jongseong.
        assert_eq!(probe.maxes, vec![22, 15, 11, 22, 15, 11]);
    }

    #[test]
    fn ko_word_never_draws_jongseong_when_finals_disabled() {
        let mut probe = Probe { maxes: Vec::new() };
        ko_word(3, false, &mut probe);
        // Only choseong (22) and jungseong (15) — never jongseong (11).
        assert_eq!(probe.maxes, vec![22, 15, 22, 15, 22, 15]);
    }

    #[test]
    fn ko_word_emits_only_hangul_with_os_random() {
        let word = hangul(40).build();
        assert_eq!(word.chars().count(), 40);
        assert!(word.chars().all(|c| ('가'..='힣').contains(&c)));
    }

    #[test]
    fn hangul_chain_with_digits() {
        assert_eq!(hangul_with(3, true, Fixed(0)).dash().digits(4).build(), "가가가-0000");
    }

    #[test]
    fn hangul_chain_with_second_word() {
        assert_eq!(
            hangul_with(3, true, Fixed(0)).dash().hangul(4).dash().digits(4).build(),
            "가가가-가가가가-0000"
        );
    }

    #[test]
    fn hangul_starts_with_one_word_added() {
        assert_eq!(hangul_with(2, true, Fixed(0)).build(), "가가");
    }

    #[test]
    fn hangul_propagates_finals_disabled_through_chain() {
        // Fixed(3): ㄹ+ㅜ -> "루"; finals stay off through the chained word too.
        assert_eq!(
            hangul_with(2, false, Fixed(3)).dash().hangul(2).build(),
            "루루-루루"
        );
    }

    #[test]
    fn hangul_nums_is_alias_for_digits() {
        assert_eq!(
            hangul_with(2, true, Fixed(0)).nums(3).build(),
            hangul_with(2, true, Fixed(0)).digits(3).build()
        );
    }

    #[test]
    fn hangul_add_appends_literal_fragment() {
        assert_eq!(hangul_with(2, true, Fixed(0)).add("-x").digits(2).build(), "가가-x00");
    }
}
