//! # lhash
//!
//! Tokenización determinista — texto a `Togen`s con detección de dominio
//! y detección de idioma.
//!
//! `lhash` convierte texto plano en una secuencia de tokens hash (Togens),
//! clasificando cada texto en uno de tres dominios:
//!
//! - **Ring 0** (`RING_WORDS`): lenguaje natural (texto humano).
//! - **Ring 1** (`RING_CODE`): código fuente (keywords como `fn`, `def`, `class`).
//! - **Ring 2** (`RING_DATA`): datos numéricos (mayoría dígitos, símbolos de datos).
//!
//! También detecta idioma mediante perfiles de trigramas, soportando:
//! español (0), inglés (1), francés (2), portugués (3), y genérico (4).
//!
//! ## Togen
//!
//! Un `Togen` es un token con:
//! - `id`: hash determinista del token (siempre el mismo para la misma palabra).
//! - `ring`: dominio al que pertenece (0, 1 o 2).
//!
//! ## Hash
//!
//! El hash combina caracteres con bigramas y posiciones, dando resultados
//! consistentes (misma palabra → mismo hash) pero distintos entre palabras
//! diferentes. También expone `hash_similarity()` que compara dos hashes
//! por distancia de Hamming.

use std::collections::HashMap;

/// Anillo 0: lenguaje natural / texto humano.
pub const RING_WORDS: u8 = 0;
/// Anillo 1: código fuente / keywords de programación.
pub const RING_CODE: u8 = 1;
/// Anillo 2: datos numéricos.
pub const RING_DATA: u8 = 2;

/// Un token hash con detección de dominio.
pub struct Togen {
    pub id: u32,
    pub ring: u8,
}

// Perfiles de trigramas por idioma: (lang_id, &[trigramas_frecuentes])
const TRIGRAM_PROFILES: [(u8, &[&str]); 5] = [
    (0, &[
        "que", "los", "las", "ent", "aci", "ión", "nte", "por", "par", "con",
        "del", "com", "des", "est", "pro", "tra", "ien", "res", "men", "tal",
        "cio", "has", "sie", "sob", "tod", "cad", "cua", "esp", "sin", "sob",
    ]),
    (1, &[
        "the", "and", "ing", "ent", "ion", "tio", "for", "all", "thi", "ght",
        "ter", "est", "her", "hat", "his", "ith", "ome", "are", "rea", "ver",
        "ear", "hou", "ith", "men", "not", "ont", "out", "ove", "res", "thi",
    ]),
    (2, &[
        "les", "des", "dan", "ent", "ons", "que", "pas", "our", "une", "par",
        "com", "sur", "dans", "ave", "cet", "cett", "tou", "son", "ses",
    ]),
    (3, &[
        "que", "com", "dos", "das", "por", "par", "ent", "os ", "as ", "res",
        "men", "con", "sen", "sem", "cim", "des", "est", "pro", "tra", "çã",
    ]),
    (4, &[
        "and", "the", "ent", "ion", "ing",
    ]),
];

/// Keywords de programación para detección de dominio código.
const CODE_KEYWORDS: &[&str] = &[
    "fn", "def", "class", "struct", "enum", "impl", "trait", "pub", "let", "mut",
    "const", "static", "return", "if", "else", "for", "while", "loop", "match",
    "import", "from", "as", "where", "type", "mod", "use", "break", "continue",
    "true", "false", "null", "none", "nil", "void", "int", "float", "bool",
    "string", "array", "list", "map", "set", "object", "function", "var",
    "async", "await", "yield", "throw", "try", "catch", "finally",
];

/// Caracteres típicos de código fuente.
const CODE_CHARS: &[char] = &['{', '}', '(', ')', '[', ']', ';', ':', '|', '&', '<', '>'];

/// Detecta el dominio de un texto: palabras (0), código (1), o datos (2).
pub fn detect_domain(text: &str) -> u8 {
    if text.is_empty() { return RING_WORDS; }
    let tokens: Vec<&str> = text.split_whitespace().collect();
    if tokens.is_empty() { return RING_WORDS; }

    let mut code_keywords = 0;
    let mut code_chars = 0;
    let mut digits = 0;
    let mut total_chars = 0;

    for token in &tokens {
        let lower = token.to_lowercase();
        if CODE_KEYWORDS.contains(&lower.as_str()) { code_keywords += 1; }
        for c in token.chars() {
            total_chars += 1;
            if CODE_CHARS.contains(&c) { code_chars += 1; }
            if c.is_ascii_digit() || c == '%' || c == '$' || c == '=' { digits += 1; }
        }
    }

    let code_ratio = (code_keywords + code_chars) as f32 / tokens.len().max(1) as f32;
    let digit_ratio = digits as f32 / total_chars.max(1) as f32;

    if code_ratio > 0.15 { RING_CODE }
    else if digit_ratio > 0.35 { RING_DATA }
    else { RING_WORDS }
}

/// Detecta el idioma de un texto mediante perfiles de trigramas.
///
/// Returns: `0`=español, `1`=inglés, `2`=francés, `3`=portugués, `4`=genérico.
pub fn detect_lang(text: &str) -> u8 {
    let clean: String = text.chars()
        .filter(|c| c.is_ascii_alphanumeric() || c.is_whitespace()).collect();
    let chars: Vec<char> = clean.chars().collect();
    let mut trigrams = Vec::new();
    for triple in chars.windows(3) {
        let t: String = triple.iter().map(|c| c.to_ascii_lowercase()).collect();
        if t.contains(char::is_whitespace) || t.len() < 3 { continue; }
        trigrams.push(t);
    }
    if trigrams.is_empty() { return 1; }
    let mut scores: Vec<(u8, usize)> = TRIGRAM_PROFILES.iter()
        .map(|(lang, grams)| (*lang, grams.iter().filter(|g| trigrams.contains(&g.to_string())).count()))
        .collect();
    scores.sort_by(|a, b| b.1.cmp(&a.1));
    scores[0].0
}

/// Hash determinista de una palabra.
///
/// Combina caracteres, bigramas y posiciones para producir hashes
/// consistentes: misma palabra → mismo hash siempre.
pub fn hash_word(word: &str) -> u32 {
    let lower = word.to_lowercase();
    let chars: Vec<char> = lower.chars().collect();
    if chars.is_empty() { return 0; }
    let mut h: u32 = 0;
    for (i, &c) in chars.iter().enumerate() {
        h = h.wrapping_mul(17).wrapping_add(c as u32).wrapping_add(i as u32);
    }
    for pair in chars.windows(2) {
        h = h.wrapping_mul(13).wrapping_add((pair[0] as u32) << 8 | (pair[1] as u32));
    }
    h
}

/// Similitud entre dos hashes por distancia de Hamming (bits en común).
///
/// 1.0 = idénticos, 0.0 = completamente diferentes.
pub fn hash_similarity(a: u32, b: u32) -> f32 {
    let bits = (a ^ b).count_ones();
    1.0 - (bits as f32 / 32.0)
}

/// Convierte texto plano en una secuencia de Togens.
///
/// Filtra palabras cortas (≤2 chars) y tokens numéricos puros.
pub fn text_to_togens(text: &str) -> Vec<Togen> {
    let ring = detect_domain(text);
    let mut togens = Vec::new();
    for word in text.split_whitespace() {
        let clean: String = word.trim_matches(|c: char| !c.is_alphanumeric()).to_string();
        let w = clean.to_lowercase();
        if w.len() <= 2 { continue; }
        if w.chars().all(|c| c.is_ascii_digit()) { continue; }
        togens.push(Togen { id: hash_word(&clean), ring });
    }
    togens
}

/// Frecuencias de Togens (id → cuenta).
pub fn togen_frequencies(togens: &[Togen]) -> HashMap<u32, usize> {
    let mut freq = HashMap::new();
    for t in togens { *freq.entry(t.id).or_insert(0) += 1; }
    freq
}

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

    #[test] fn domain_lang_text() { assert_eq!(detect_domain("El perro come pan"), RING_WORDS); }
    #[test] fn domain_code_fn() { assert_eq!(detect_domain("fn main() { let x = 42; }"), RING_CODE); }
    #[test] fn domain_code_def() { assert_eq!(detect_domain("def hello(): print('x')"), RING_CODE); }
    #[test] fn domain_data() { assert_eq!(detect_domain("42 100 200 300 400 500"), RING_DATA); }
    #[test] fn domain_empty() { assert_eq!(detect_domain(""), RING_WORDS); }

    #[test] fn hash_word_consistency() { assert_eq!(hash_word("hola"), hash_word("hola")); }
    #[test] fn hash_word_different() { assert_ne!(hash_word("hola"), hash_word("mundo")); }
    #[test] fn text_to_togens_filters_short_words() {
        assert!(text_to_togens("a an the de el").iter().all(|t| t.id != hash_word("a")));
    }
    #[test] fn text_to_togens_filters_digits() { assert_eq!(text_to_togens("hello 123 world 456").len(), 2); }
    #[test] fn text_to_togens_empty() { assert!(text_to_togens("").is_empty()); }
    #[test] fn text_to_togens_ring_assignment() {
        assert!(text_to_togens("fn main() { return 42; }").iter().all(|t| t.ring == RING_CODE));
    }
    #[test] fn togen_frequencies_counts() {
        assert_eq!(togen_frequencies(&text_to_togens("hola mundo hola")).get(&hash_word("hola")), Some(&2));
    }
}
