//! lexer.rs - Replica deliberada de `lexer_sipi.py`: misma gramatica de
//! tokens (numeros, texto entre comillas dobles con escapes, identificadores,
//! operadores de uno y dos caracteres, parentesis, coma), mismos alias en
//! espanol para los operadores logicos (`y`/`o`/`no` -> `and`/`or`/`not`).

#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    Numero(f64),
    Texto(String),
    Identificador(String),
    Operador(String),
    ParentesisAbre,
    ParentesisCierra,
    Coma,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ErrorLexico {
    pub mensaje: String,
    pub columna: usize,
}

impl std::fmt::Display for ErrorLexico {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} (columna {})", self.mensaje, self.columna)
    }
}

fn alias_logico(palabra: &str) -> Option<&'static str> {
    match palabra {
        "y" => Some("and"),
        "o" => Some("or"),
        "no" => Some("not"),
        "and" => Some("and"),
        "or" => Some("or"),
        "not" => Some("not"),
        _ => None,
    }
}

pub fn tokenizar(entrada: &str) -> Result<Vec<Token>, ErrorLexico> {
    let caracteres: Vec<char> = entrada.chars().collect();
    let mut tokens = Vec::new();
    let mut i = 0usize;

    while i < caracteres.len() {
        let c = caracteres[i];

        if c.is_whitespace() {
            i += 1;
            continue;
        }

        if c.is_ascii_digit() {
            let inicio = i;
            while i < caracteres.len() && (caracteres[i].is_ascii_digit() || caracteres[i] == '.') {
                i += 1;
            }
            let texto_numero: String = caracteres[inicio..i].iter().collect();
            match texto_numero.parse::<f64>() {
                Ok(valor) => tokens.push(Token::Numero(valor)),
                Err(_) => {
                    return Err(ErrorLexico {
                        mensaje: format!("Numero mal formado: '{}'", texto_numero),
                        columna: inicio,
                    })
                }
            }
            continue;
        }

        if c == '"' {
            let inicio = i;
            i += 1;
            let mut resultado = String::new();
            let mut cerrado = false;
            while i < caracteres.len() {
                let ch = caracteres[i];
                if ch == '"' {
                    cerrado = true;
                    i += 1;
                    break;
                }
                if ch == '\\' && i + 1 < caracteres.len() {
                    let siguiente = caracteres[i + 1];
                    let traducido = match siguiente {
                        'n' => '\n',
                        't' => '\t',
                        '"' => '"',
                        '\\' => '\\',
                        otro => otro,
                    };
                    resultado.push(traducido);
                    i += 2;
                    continue;
                }
                resultado.push(ch);
                i += 1;
            }
            if !cerrado {
                return Err(ErrorLexico {
                    mensaje: "Texto sin cerrar (falta la comilla doble de cierre)".to_string(),
                    columna: inicio,
                });
            }
            tokens.push(Token::Texto(resultado));
            continue;
        }

        if c.is_alphabetic() || c == '_' {
            let inicio = i;
            while i < caracteres.len() && (caracteres[i].is_alphanumeric() || caracteres[i] == '_') {
                i += 1;
            }
            let palabra: String = caracteres[inicio..i].iter().collect();
            if let Some(alias) = alias_logico(&palabra) {
                tokens.push(Token::Operador(alias.to_string()));
            } else {
                tokens.push(Token::Identificador(palabra));
            }
            continue;
        }

        if c == '(' {
            tokens.push(Token::ParentesisAbre);
            i += 1;
            continue;
        }
        if c == ')' {
            tokens.push(Token::ParentesisCierra);
            i += 1;
            continue;
        }
        if c == ',' {
            tokens.push(Token::Coma);
            i += 1;
            continue;
        }

        // Operadores de dos caracteres antes que uno solo.
        if i + 1 < caracteres.len() {
            let dos: String = caracteres[i..i + 2].iter().collect();
            if ["==", "!=", "<=", ">="].contains(&dos.as_str()) {
                tokens.push(Token::Operador(dos));
                i += 2;
                continue;
            }
        }

        if ['+', '-', '*', '/', '%', '<', '>'].contains(&c) {
            tokens.push(Token::Operador(c.to_string()));
            i += 1;
            continue;
        }

        return Err(ErrorLexico {
            mensaje: format!("Caracter desconocido: '{}'", c),
            columna: i,
        });
    }

    Ok(tokens)
}

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

    #[test]
    fn numero_entero() {
        let tokens = tokenizar("42").unwrap();
        assert_eq!(tokens, vec![Token::Numero(42.0)]);
    }

    #[test]
    fn numero_decimal() {
        let tokens = tokenizar("3.14").unwrap();
        assert_eq!(tokens, vec![Token::Numero(3.14)]);
    }

    #[test]
    fn texto_con_escapes() {
        let tokens = tokenizar(r#""hola\nmundo""#).unwrap();
        assert_eq!(tokens, vec![Token::Texto("hola\nmundo".to_string())]);
    }

    #[test]
    fn texto_sin_cerrar_da_error() {
        let err = tokenizar(r#""sin cerrar"#).unwrap_err();
        assert_eq!(err.columna, 0);
    }

    #[test]
    fn alias_logicos_en_espanol() {
        let tokens = tokenizar("verdadero y falso o no verdadero").unwrap();
        let operadores: Vec<&str> = tokens
            .iter()
            .filter_map(|t| match t {
                Token::Operador(op) => Some(op.as_str()),
                _ => None,
            })
            .collect();
        assert_eq!(operadores, vec!["and", "or", "not"]);
    }

    #[test]
    fn operadores_de_dos_caracteres() {
        let tokens = tokenizar("a == b != c <= d >= e").unwrap();
        let operadores: Vec<&str> = tokens
            .iter()
            .filter_map(|t| match t {
                Token::Operador(op) => Some(op.as_str()),
                _ => None,
            })
            .collect();
        assert_eq!(operadores, vec!["==", "!=", "<=", ">="]);
    }

    #[test]
    fn caracter_desconocido_da_error_con_columna() {
        let err = tokenizar("2 @ 3").unwrap_err();
        assert_eq!(err.columna, 2);
    }
}
