# 
# Tomar archivo C# generardo por Swagger con modelo de objeto json y lo
# simplifica con uso de record type. 
#
import os
import re

# --- Formato de la función de valida_xxx
# Entrada: tupla con 
#   es_ajustado : True si ha sido ajustado el texto, con lo cual el resto de funciones no tienen que procesarlo.
#   texto : texto a validar
#   flags : lista de strings con flags usados por las funciones de validación para controlara su operación entre líneas.
#       continua - flag que indica que se debe ignorar la linea actual y continuar con la siguiente. 
#
#
def valida_using(es_ajustado: bool, linea: str, flags: [str]) -> (bool, str, [str]):
    """Elimina las líneas con sentencia 'using'
       Activa el flag 'continua' que indica que se debe ignorar la línea \
        (no aparece en el archivo de salida).
    """
    # Si la línea ya fue ajustada, no se procesa mas. 
    if es_ajustado:
        return (es_ajustado, linea, flags)
    
    # \b marca limite de palabra, valida que el patrón no es un fragmento dentro de una palabra mas grande.
    # [] conjunto de caracteres.
    # \s espacio en blanco y  [\t\n\r\f\v]
    # + uno o mas caracteres que le preceden. 
    # \w identifica palabra.
    p = r'^using[\s]+\w+'
    rex = re.compile(p)
    if rex.match(linea):
        print("using identificado.")
        flags.append('continua')
        return (True, xlinea, flags)
    return (False, linea, flags)

def valida_namespace(es_ajustado: bool, linea: str, flags: [str]) -> (bool, str, [str]):
    """Si la linea tiene la definición de namespace, la ajusta.
    """

    # Si la línea ya fue ajustada, no se procesa mas. 
    if es_ajustado:
        return (es_ajustado, linea, flags)
    
    # namespace a aplicar.
    xnamespace = "POSGkSwaggerModel"
    # \b marca limite de palabra, valida que el patrón no es un fragmento dentro de una palabra mas grande.
    # [] conjunto de caracteres.
    # \s espacio en blanco y  [\t\n\r\f\v]
    # + uno o mas caracteres que le preceden. 
    # ( ) indica un grupo dentro de la expresión de búsqueda, contando desde 1 cada grupo.
    p = r'^namespace[\s]+\bIO.Swagger.Model\b'
    rex = re.compile(p)
    if rex.match(linea):
        xlinea = linea.replace("IO.Swagger.Model", xnamespace)
        print("namespace ajustado.")
        return (True, xlinea, flags)
    return (False, linea, flags)

def valida_anotaciones(es_ajustado: bool, linea: str, flags: [str]) -> (bool, str, [str]):
    """ Elimina las lineas que tiene anotaciones tales como: 
         [DataContract]
         [JsonConstructorAttribute]
         [DataMember(Name="businessUnitGroupID", EmitDefaultValue=false)]
        En el caso de las antociones, son la única entrada en la línea, es decir, \
        la línea de texto tiene la forma <blancos>[...texto...]<blancos>
    """
    # Si la línea ya fue ajustada, no se procesa mas. 
    if es_ajustado:
        return (es_ajustado, linea, flags)
    
    # [] conjunto de caracteres.
    # \s espacio en blanco y  [\t\n\r\f\v]
    # * cero uno o mas caracteres que le preceden. 
    # \w identifica palabra.
    # El grupo [^[\]]+ indica todos los caracteres que no son [ ni ]
    p = r'\[[^[\]]+\]'
    rex = re.compile(p)
    if rex.match(linea.strip()):
        print("anotación [...] identificada.")
        flags.append('continua')
        return (True, xlinea, flags)
    return (False, linea, flags)


def valida_clase(es_ajustado: bool, linea: str, flags: [str]) -> (bool, str, [str]):
    """Si la linea tiene la definición de una clase la cambia por definición de un record.
    """

    # Si la línea ya fue ajustada, no se procesa mas. 
    if es_ajustado:
        return (es_ajustado, linea, flags)
    
    # \s espacio en blanco y  [\t\n\r\f\v]
    # + uno o mas caracteres que le preceden. 
    # ( ) indica un grupo dentro de la expresión de búsqueda, contando desde 1 cada grupo.
    # busca coincidencias con patrón "public class" o "public partial class", donde "partial" es opcional.
    p = r'(public\s+(partial)*\s+class)'
    r = "public record"
    rex = re.compile(p)
    if rex.search(linea):
        xlinea = re.sub(p, r, linea)
        #print("clase ajustada.")
        return (True, xlinea, flags)
    return (False, linea, flags)

def valida_propiedad(es_ajustado: bool, linea: str, flags: [str]) -> (bool, str, [str]):
    """Si la linea tiene la definición de una propiedad, la ajusta.
       Ejm: 
        Original:
            public string BusinessUnitGroupID { get; set; }
        Ajustada: 
            public string BusinessUnitGroupID { get; init; }

        Activa el flag de 'propiedad' lo que significa que de aquí en adelante, las lineas que 
        no son propiedades serán ignoradas, hasta encontrar el } de cierre de la clase o record.
    """
    # Si la línea ya fue ajustada, no se procesa mas. 
    if es_ajustado:
        return (es_ajustado, linea, flags)

    # \s espacio en blanco y  [\t\n\r\f\v]
    # + uno o mas caracteres que le preceden. 
    # ( ) indica un grupo dentro de la expresión de búsqueda, contando desde 1 cada grupo.
    # busca coincidencias con patrón "public class" o "public partial class", donde "partial" es opcional.
    p = r'{ get; set; }'
    r = '{ get; init; }'
    rex = re.compile(p)
    if rex.search(linea):
        xlinea = re.sub(p, r, linea)
        #print("propiedad ajustada.")
        flags.append('propiedad')
        return (True, xlinea, flags)
    return (False, linea, flags)

def continua(flags: [str]) -> bool:
    """Retorna True si la lista de flags incluye 'continua'.
        Elimina el flag de la lista.
    """
    for s in flags:
        if s == 'continua':
            flags.remove('continua')
            return True
    return False

def propiedad(flags: [str]) -> bool:
    """Retorna True si la lista de flags incluye 'propiedad'.
    """
    for f in flags:
        if f == 'propiedad':
            return True
    return False

def idx_no_espacio(texto):
    """ Retorna indice del primer caracter que no es espacio, 
        o -1 si todos son espacio.
    """
    for indice, caracter in enumerate(texto):
        if caracter != ' ':
            return indice
    return -1  # Si la cadena está compuesta solo por espacios

def ajusta_nombre_record(linea: str) -> str:
    """ fija en minúscula primera letra del nombre del record declarado en la línea y elimina
        declaraciones no necesarias para tipo record (interfaces)

        Ejm:
        Original:
            public record ComGkSoftwareGkrApiTxpoolDtoTransactionKey :  IEquatable<ComGkSoftwareGkrApiTxpoolDtoTransactionKey>, IValidatableObject
        Resultado:
            public record comGkSoftwareGkrApiTxpoolDtoTransactionKey
    """
    # Extrae los caracteres antes de la definición de registro, para 
    # agregarlos a la cadana ajustada. Estos contienen la indentación.
    p = re.compile(r'\w+')
    patron = p.search(linea)
    if patron:
        caracteres_pre = linea[:patron.start()]
    else:
        caracteres_pre = ''
    # 
    p = re.compile(r'\w+')
    palabras = p.findall(linea)
    if len(palabras) >= 3:
        # toma el nombre del record
        nombre = palabras[2]
        # lo ajusta con la primera letra en minúscula
        nombre_minuscula = nombre[0].lower() + nombre[1:]
        palabras[2] = nombre_minuscula
        # borra la cuarta palabra en adelante, si las  hay
        del palabras[3:]
        # arma la cadena nuevamente
        linea = caracteres_pre + ' '.join(palabras)
    return linea

def ajusta_nombre_propiedad(linea: str) -> str:
    """ fija en minúscula primera letra del nombre de propieda

        Ejm:
        Original:
            public string BusinessUnitGroupID { get; init; }
        Resultado:
            public string businessUnitGroupID { get; init; }
    """
    # Extrae los caracteres antes de la definición de registro, para 
    # agregarlos a la cadana ajustada. Estos contienen la indentación.
    p = re.compile(r'\w+')
    palabras = p.findall(linea)
    if len(palabras) >= 2:
        # toma el nombre del record
        nombre = palabras[2]
        # lo ajusta con la primera letra en minúscula
        nombre_minuscula = nombre[0].lower() + nombre[1:]
        palabras[2] = nombre_minuscula
        # arma la cadena nuevamente
        #linea = caracteres_pre + ' '.join(palabras)
        linea = linea.replace(nombre, nombre_minuscula)
    return linea

def valida_comentario(linea: str) -> bool:
    """ Acumula lineas de comentario en varible global

        Retorna True/False si la linea es un comentario.
    """
    global comentarios
    p = re.compile(r'\s*///\s*<summary>')
    hay_comentarios = p.match(linea)
    if hay_comentarios:
        # Apertura de comentario, se inicializa lista de estos y se agrega.
        comentarios = []
        comentarios.append(linea)
    else:
        p = re.compile(r'\s*///')
        hay_comentarios = p.match(linea)
        if hay_comentarios:
            # Se agrega comentario
            comentarios.append(linea)
    return hay_comentarios

def aplica_comentarios(lineas: list[str]):
    """ Agrega lineas de comentario, si corresponde"""
    global comentarios
    global nro_linea_comentario
    global nro_linea
    # los comentarios están cerca a la linea actual...
    if (nro_linea - nro_linea_comentario <= 3):
        lineas.extend(comentarios)
        #print(f"comentarios {comentarios}")
        comentarios = []
        nro_linea_comentario = -1

def procesa_archivo_cs(archivo: str, carpeta_entrada: str, carpeta_salida: str):
    """ Aplica transformaciones al archivo cs para pasar de definir una clase a definir un record.
    """
    global comentarios
    global nro_linea_comentario
    global nro_linea
    # Lee archivo en una lista de lineas.
    entrada = carpeta_entrada + archivo
    print(f"Archivo entrada {entrada}")
    ptr_archivo = open(entrada, 'r', encoding='utf8')
    lineas_txt = ptr_archivo.readlines()
    ptr_archivo.close()

    comentarios = []

    # Recorre lineas del archivo y aplica ajustes.
    lineas_txt_ajustadas = []
    flags = []
    # clase on / off indica si ha detectado una clase
    clase_on = False
    # puntero de inicio comentarios en vector lineas_txt
    nro_linea_comentario = -1
    nro_linea = -1
    for linea in lineas_txt:
        nro_linea += 1

        hay_comenterios = valida_comentario(linea)
        if hay_comenterios:
            nro_linea_comentario = nro_linea
            continue
        
        ajusta, xlinea, flags = valida_namespace(False, linea, flags)
        if ajusta:
            aplica_comentarios(lineas_txt_ajustadas)
            lineas_txt_ajustadas.append(xlinea)
            lineas_txt_ajustadas.append('{\n')
            continue
        
        ajusta, xlinea, flags = valida_clase(False, linea, flags)
        if ajusta:
            # Hay apertura de clase previa ?, si la hay,
            # primero inserta '}' para cerrarla.
            if clase_on:
                clase_on = False
                lineas_txt_ajustadas.append('\t}\n')
            # si linea anterior es de comentario, agrega bloque comentarios.
            aplica_comentarios(lineas_txt_ajustadas)
            lineas_txt_ajustadas.append(xlinea)
            lineas_txt_ajustadas.append('\t{\n')
            clase_on = True
            continue
        
        ajusta, xlinea, flags = valida_propiedad(False, linea, flags)
        if ajusta:
            aplica_comentarios(lineas_txt_ajustadas)
            lineas_txt_ajustadas.append(xlinea)
            #lineas_txt_ajustadas.append(xlinea)
            continue

    # si hay flag de clase abierta, cierra la clase con '}'
    lineas_txt_ajustadas.append('\t}\n')
    # cierra el namespace.
    lineas_txt_ajustadas.append('}\n')
    # Escribe archivo salida.
    salida = carpeta_salida + archivo
    ptr_archivo = open(salida, 'w', encoding='utf8')
    ptr_archivo.writelines(lineas_txt_ajustadas)
    ptr_archivo.close()
    print(f"Archivo salida creado {salida}")


def obtener_archivos_cs_en_carpeta(ruta_carpeta):
    archivos_cs = []
    for archivo in os.listdir(ruta_carpeta):
        if archivo.endswith('.cs'):
            archivos_cs.append(archivo)
    return archivos_cs

# 
# --- Cuerpo principal del programa.
#
# lista con lineas de comentario
carpeta_entrada = "C:/jht/scogateway/dotnet/csharp-client-swagger/src/IO.Swagger/Model/"
carpeta_salida = "C:/jht/scogateway/dotnet/api-gateway-chec/gatewayUtilPython/salida/"
#archivo = "ComGkSoftwareGkrApiTxpoolDtoTransactionKey.cs"

archivos = obtener_archivos_cs_en_carpeta(carpeta_entrada)
i = 1
for archivo in archivos:
    comentarios = []
    nro_linea_comentario = -1
    nro_linea = 0
    procesa_archivo_cs(archivo, carpeta_entrada, carpeta_salida)
    i += 1

print(f'Total archivos {i}')


