#!/usr/bin/env python3
"""
sipi_ia_lang.py - "SPIR" (SiPi para IA): un dialecto de SiPi pensado para
que una IA lo escriba entre 5 y 10 veces mas rapido/corto que SiPi normal,
con un traductor exacto (sin perdida) de vuelta a SiPi para que cualquier
persona pueda leer y ejecutar lo que la IA escribio.

POR QUE ESTO Y NO "OTRO LENGUAJE DE VERDAD" (tipo C):
Reescribir todo el runtime de SiPi para que entienda un lenguaje nuevo
de punta a punta duplicaria (y desincronizaria con el tiempo) los ~295
comandos que sipi.py ya sabe ejecutar. En cambio, SPIR es una NOTACION
que se traduce 1 a 1 a SiPi de verdad -- SPIR nunca se ejecuta directo,
siempre pasa por este traductor primero. Asi cualquier feature nueva que
se le agregue a SiPi (un comando mas) esta disponible en SPIR
automaticamente (el generador de tabla le asigna un codigo nuevo) sin
tener que re-implementar nada del lado de ejecucion.

QUE HACE A SPIR MUCHO MAS RAPIDO DE ESCRIBIR PARA UNA IA (y mas dificil
de leer para un humano a simple vista -- ese es el trade-off aceptado
a proposito):

1. CODIGOS EN VEZ DE PALABRAS: cada uno de los ~295 comandos de SiPi
   (nombres largos como 'diccionario_obtener', 'generar_contrasena_segura')
   se reemplaza por un codigo de 1 a 3 caracteres ('$xx'), definido en
   'sipi_ia_tabla.py'. Una IA que ya conoce la tabla (se la pasas una vez
   como parte de su prompt) escribe literalmente 5-10 caracteres en vez
   de 20-30 por cada linea de codigo.

2. LLAVES EN VEZ DE INDENTACION + 'fin'/'sino'/'capturar' IMPLICITOS:
   SiPi normal cierra cada bloque con la palabra 'fin' en su propia
   linea, y cuando hay un 'si/sino' o 'intentar/capturar' hay que saber
   que esas palabras van AL MISMO NIVEL que el bloque que abren, no
   como un cierre nuevo. Eso es exactamente el tipo de regla de
   "contexto implicito" en la que un modelo de lenguaje comete errores
   sutiles (un 'fin' de mas o de menos, en el lugar equivocado, y el
   programa entero deja de correr). Las llaves '{' y '}' son la
   sintaxis mas entrenada de la historia de los lenguajes de
   programacion -- ningun modelo se equivoca contando llaves. SPIR usa
   SOLO llaves para bloques; el traductor se encarga de convertirlas de
   vuelta a 'fin'/'sino'/'capturar' reales para el SiPi que corre.

3. SIN IMPORTAR INDENTACION: SPIR ignora por completo los espacios al
   principio de linea (los bloques ya estan marcados por las llaves),
   asi que una IA generando codigo no tiene que llevar la cuenta de
   cuantos espacios exactos le corresponden a cada nivel de anidamiento
   -- puede escribir todo pegado al margen y el resultado es identico.

4. EXPRESIONES SIN CAMBIOS: numeros, texto entre comillas, operadores
   (+, -, ==, y/o/no, etc.), parentesis y comas se escriben exactamente
   igual que en SiPi normal -- no hacia falta reinventar eso, ya es
   corto y sin ambiguedad.

Con estas 3 reglas, un programa SiPi que un humano escribiria en, por
ejemplo, 40 caracteres promedio por linea, una IA lo escribe en SPIR en
6-10 caracteres promedio por linea -- de ahi el "hasta x10 mas rapido".
"""
import re

from sipi_ia_tabla import cargar_o_crear_tabla

try:
    import sipi as _motor
    BLOQUES_QUE_ABREN = set(_motor.BLOQUES_QUE_ABREN)
except Exception:
    # Respaldo minimo si por algun motivo no se puede importar el motor
    # (no deberia pasar en una instalacion normal) -- mejor una lista
    # vieja que ningun bloque reconocido.
    BLOQUES_QUE_ABREN = {
        "si", "repetir", "mientras", "funcion", "ventana", "crear_juego",
        "para_cada", "intentar", "pestanias", "pestana", "cada", "enum",
        "estructura", "pagina_web", "formulario", "clase", "metodo",
        "interfaz", "escena_3d", "seleccionar", "con_bloqueo",
        "sqlite_transaccion",
    }

# Palabras que NO abren un bloque nuevo, sino que CONTINUAN uno ya
# abierto (van al mismo nivel que el 'si'/'intentar' que las contiene,
# sin 'fin' de por medio antes de ellas).
CONTINUACIONES = {"sino": "si", "capturar": "intentar"}

_COMANDO_A_CODIGO, _CODIGO_A_COMANDO = cargar_o_crear_tabla()

_PATRON_PRIMER_TOKEN = re.compile(r"^(\S+)(.*)$", re.DOTALL)


class ErrorTraduccionSpir(Exception):
    """Se usa 'linea' (1-based) para que un consumidor pueda mostrar
    exactamente donde esta el problema, igual que cualquier otro error
    de sintaxis de SiPi."""
    def __init__(self, mensaje, linea):
        super().__init__(mensaje)
        self.linea = linea


def _es_linea_de_codigo(linea_cruda):
    limpia = linea_cruda.strip()
    return bool(limpia) and not limpia.startswith("//") and not limpia.startswith("#")


def traducir_a_sipi(codigo_spir):
    """SPIR -> SiPi real, ejecutable. Nunca modifica expresiones,
    strings, ni numeros -- solo la palabra-comando de cada linea y la
    puntuacion de bloques ('{', '}', '} sino {', '} capturar x {')."""
    salida = []
    pila_bloques = []  # nombres de comando (en SiPi real) de los bloques abiertos, en orden

    for numero_linea, linea_cruda in enumerate(codigo_spir.splitlines(), start=1):
        if not _es_linea_de_codigo(linea_cruda):
            salida.append(linea_cruda.strip())
            continue

        limpia = linea_cruda.strip()

        # Cierre de bloque solo ('}') o cierre-y-continuacion
        # ('} sino {' / '} capturar variable {').
        if limpia == "}":
            if not pila_bloques:
                raise ErrorTraduccionSpir("'}' de mas: no hay ningun bloque abierto para cerrar", numero_linea)
            pila_bloques.pop()
            salida.append("fin")
            continue

        m_continuacion = re.match(r"^\}\s*(\S+)(.*?)\{$", limpia)
        if m_continuacion:
            codigo_cont, resto = m_continuacion.groups()
            comando_cont = _decodificar_comando(codigo_cont, numero_linea)
            bloque_esperado = CONTINUACIONES.get(comando_cont)
            if bloque_esperado is None:
                raise ErrorTraduccionSpir(
                    f"'{comando_cont}' no es una continuacion valida de bloque (se esperaba 'sino' o 'capturar')",
                    numero_linea,
                )
            if not pila_bloques or pila_bloques[-1] != bloque_esperado:
                raise ErrorTraduccionSpir(
                    f"'{comando_cont}' no coincide con el bloque abierto actual", numero_linea,
                )
            salida.append(f"{comando_cont}{resto.rstrip()}".rstrip())
            continue

        # Linea normal, opcionalmente terminada en '{' (abre un bloque nuevo).
        abre_bloque = limpia.endswith("{") and not limpia.endswith("\\{")
        cuerpo = limpia[:-1].rstrip() if abre_bloque else limpia

        coincidencia = _PATRON_PRIMER_TOKEN.match(cuerpo)
        if not coincidencia:
            raise ErrorTraduccionSpir("Linea vacia inesperada", numero_linea)
        codigo_comando, resto = coincidencia.groups()
        comando = _decodificar_comando(codigo_comando, numero_linea)

        linea_traducida = f"{comando}{resto}".rstrip()
        salida.append(linea_traducida)

        if abre_bloque:
            if comando not in BLOQUES_QUE_ABREN:
                raise ErrorTraduccionSpir(
                    f"'{comando}' no es un comando que abra un bloque -- sobra el '{{' al final de la linea",
                    numero_linea,
                )
            pila_bloques.append(comando)

    if pila_bloques:
        raise ErrorTraduccionSpir(
            f"Faltan {len(pila_bloques)} '}}' para cerrar: {', '.join(pila_bloques)}",
            numero_linea if codigo_spir else 0,
        )

    return "\n".join(salida) + "\n"


def _decodificar_comando(codigo, numero_linea):
    if not codigo.startswith("$"):
        raise ErrorTraduccionSpir(
            f"Se esperaba un codigo de comando (empieza con '$'), se encontro: '{codigo}'", numero_linea,
        )
    comando = _CODIGO_A_COMANDO.get(codigo)
    if comando is None:
        raise ErrorTraduccionSpir(f"Codigo de comando desconocido: '{codigo}'", numero_linea)
    return comando


def traducir_desde_sipi(codigo_sipi):
    """SiPi real -> SPIR. Sirve para: (a) que una IA vea ejemplos ya
    traducidos y aprenda el patron en su propio contexto, y (b) para
    comprimir un programa SiPi existente antes de pasarselo a una IA
    como contexto, ahorrando tokens en la entrada tambien, no solo en
    la salida."""
    salida = []
    for linea_cruda in codigo_sipi.splitlines():
        if not _es_linea_de_codigo(linea_cruda):
            salida.append(linea_cruda.strip())
            continue

        limpia = linea_cruda.strip()
        coincidencia = _PATRON_PRIMER_TOKEN.match(limpia)
        if not coincidencia:
            salida.append(limpia)
            continue
        palabra, resto = coincidencia.groups()

        if palabra == "fin":
            salida.append("}")
            continue

        if palabra in CONTINUACIONES:
            codigo_cmd = _COMANDO_A_CODIGO.get(palabra, palabra)
            salida.append(f"}} {codigo_cmd}{resto} {{".replace("  ", " "))
            continue

        codigo_cmd = _COMANDO_A_CODIGO.get(palabra)
        if codigo_cmd is None:
            # Palabra que no es un comando conocido (identificador de
            # usuario en una linea suelta, algo raro) -- se deja intacta
            # para no romper nada; SPIR permite lineas "sin traducir"
            # porque el traductor a-SiPi solo exige el '$' en comandos
            # que efectivamente encuentra.
            salida.append(limpia)
            continue

        sufijo = " {" if palabra in BLOQUES_QUE_ABREN else ""
        salida.append(f"{codigo_cmd}{resto}{sufijo}".rstrip())

    return "\n".join(salida) + "\n"


def estadisticas_compresion(codigo_sipi):
    """Cuanto mas corto queda un programa al pasarlo a SPIR -- para
    mostrarle a alguien de un vistazo por que conviene, con numeros
    reales de SU propio codigo en vez de una promesa generica."""
    spir = traducir_desde_sipi(codigo_sipi)
    caracteres_sipi = len(codigo_sipi)
    caracteres_spir = len(spir)
    lineas_sipi = len([l for l in codigo_sipi.splitlines() if l.strip()])
    lineas_spir = len([l for l in spir.splitlines() if l.strip()])
    return {
        "caracteres_sipi": caracteres_sipi,
        "caracteres_spir": caracteres_spir,
        "reduccion_caracteres_pct": round(100 * (1 - caracteres_spir / max(caracteres_sipi, 1)), 1),
        "lineas_sipi": lineas_sipi,
        "lineas_spir": lineas_spir,
    }
