import sys
import time
import serial
import serial.tools.list_ports

COMMON_BAUDS = [9600, 115200, 19200, 38400, 57600, 4800, 2400, 14400, 128000, 256000]
PROBE_PAYLOADS = [
    b"\r\n",
    # Comandos genéricos / AT / Protocolo $
    b"AT\r\n",
    b"at\r\n",
    b"$\r\n",
    b"$$$\r\n",
    b"$P\r\n",
    b"$PING\r\n",
    b"$PMTK000*32\r\n",
    b"$VERSION\r\n",
    b"$STATUS\r\n",
    b"$INFO\r\n",
    b"$?\r\n",
    b"?\r\n",
    b"v\r\n",
    b"VERSION\r\n",
    # Peticiones típicas de Básculas (Scale request probes)
    b"P\r\n",           # Solicitud de peso estándar (Print / P)
    b"W\r\n",           # Solicitud de peso (Weight)
    b"Q\r\n",           # Solicitud Query
    b"S\r\n",           # Send weight
    b"+\r\n",
    b"-\r\n",
    b"\x02P\x03\r\n",   # STX + P + ETX
    b"\x05",            # ENQ (Consulta de estado/peso)
    b"\x03",            # ETX / Ctrl+C
]

def format_hex(data: bytes) -> str:
    return " ".join(f"{b:02X}" for b in data)

def format_ascii(data: bytes) -> str:
    return "".join(chr(b) if 32 <= b <= 126 else "." for b in data)

def try_parse_weight(cadena: str):
    """ Intenta convertir la subcadena limpia a float """
    clean = cadena.replace(' ', '').replace(',', '.')
    try:
        return float(clean)
    except ValueError:
        return None

def analyze_scale_framing(data: bytes) -> str:
    """
    Analiza si la trama de datos recibida coincide con la extracción de peso de Delphi:
    - Tipo 0: Start '+' o '-', Copy(str, pos, 9), replace '.', ' ' -> StringToFloat
    - Tipo 1: Start '0' o '\'', Copy(str, pos, 6), replace '.', ' ' -> StringToFloat
    - Tipo 4: Start '+' o '-', Copy(str, pos, 10), replace '.', ' ' -> StringToFloat
    - Tipo 5: Start '+' o '-', Copy(str, pos, 7) -> StringToFloat
    - Tipo 10: Dibal u otras con inicio STX / RxChar
    """
    notes = []
    text = data.decode('latin1', errors='ignore')

    # Básculas Tipo 0, 4 y 5 (Inicio '+' o '-')
    for start_char in ['+', '-']:
        pos1 = text.find(start_char)
        if pos1 != -1:
            # Tipo 0: Subcadena 9 caracteres
            sub0 = text[pos1:pos1+9]
            w0 = try_parse_weight(sub0)
            if w0 is not None:
                notes.append(f"Báscula Tipo 0 (Cadena: '{sub0.strip()}', Peso extraído: {w0:.3f})")

            # Tipo 4: Subcadena 10 caracteres
            sub4 = text[pos1:pos1+10]
            w4 = try_parse_weight(sub4)
            if w4 is not None:
                notes.append(f"Báscula Tipo 4 (Cadena: '{sub4.strip()}', Peso extraído: {w4:.3f})")

            # Tipo 5: Subcadena 7 caracteres
            sub5 = text[pos1:pos1+7]
            w5 = try_parse_weight(sub5)
            if w5 is not None:
                notes.append(f"Báscula Tipo 5 (Cadena: '{sub5.strip()}', Peso extraído: {w5:.3f})")

    # Básculas Tipo 1 (Inicio '0' o '\'')
    for start_char in ['0', "'"]:
        pos1 = text.find(start_char)
        if pos1 != -1:
            sub1 = text[pos1:pos1+6]
            w1 = try_parse_weight(sub1)
            if w1 is not None:
                notes.append(f"Báscula Tipo 1 (Cadena: '{sub1.strip()}', Peso extraído: {w1:.3f})")

    if b"$" in data:
        notes.append("Protocolo $ / NMEA detectado")
    if b"\x02" in data:
        notes.append("Trama STX (0x02) / Báscula Tipo 10 (Dibal)")

    if notes:
        return " | ".join(set(notes))
    return ""

def test_serial_config(port_name, baudrate):
    print(f"\n==================================================")
    print(f" Iniciando prueba en puerto: {port_name} | Velocidad: {baudrate} bps")
    print(f"==================================================\n")

    parities = [serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD]
    databits_list = [serial.EIGHTBITS, serial.SEVENBITS]
    stopbits_list = [serial.STOPBITS_ONE, serial.STOPBITS_TWO]

    found = False

    for parity in parities:
        parity_str = {serial.PARITY_NONE: 'N', serial.PARITY_EVEN: 'E', serial.PARITY_ODD: 'O'}.get(parity, '?')
        for bytesize in databits_list:
            for stopbits in stopbits_list:
                stop_str = "1" if stopbits == serial.STOPBITS_ONE else "2"
                config_label = f"{bytesize}{parity_str}{stop_str}"
                
                print(f"\n--------------------------------------------------")
                print(f"[+] Probando configuración: {config_label} (Baud: {baudrate})")
                print(f"--------------------------------------------------")

                try:
                    ser = serial.Serial(
                        port=port_name,
                        baudrate=baudrate,
                        bytesize=bytesize,
                        parity=parity,
                        stopbits=stopbits,
                        timeout=0.4,
                        write_timeout=0.4
                    )
                except Exception as e:
                    print(f" [ERROR al abrir puerto: {e}]")
                    continue

                try:
                    # Activar líneas de control DTR y RTS por si el dispositivo lo requiere
                    ser.dtr = True
                    ser.rts = True
                    time.sleep(0.1)

                    # 1. Lectura pasiva por si el dispositivo emite continuamente (ej. Básculas, GPS, sensores)
                    passive_data = ser.read(100)
                    if passive_data:
                        scale_info = analyze_scale_framing(passive_data)
                        print("  [RX <- RECIBIDO PASIVO]: ASCII: '%s' | HEX: %s" % (format_ascii(passive_data), format_hex(passive_data)))
                        print(" ¡RESPUESTA DETECTADA EN MODO PASIVO!")
                        print(f"    --> Configuración exitosa: {baudrate} bps, {bytesize} DataBits, Paridad '{parity_str}', StopBits {stop_str}")
                        if scale_info:
                            print(f"    --> Detección de protocolo: {scale_info}")
                        found = True
                        ser.close()
                        return (bytesize, parity_str, stop_str, passive_data)
                    else:
                        print("  [RX <- RECIBIDO PASIVO]: Sin emisión pasiva detectada.")

                    # 2. Envío de sondas activas
                    for probe in PROBE_PAYLOADS:
                        ser.reset_input_buffer()
                        ser.write(probe)
                        print(f"  [TX -> EMITIDO ]: Raw: {repr(probe):<18} | ASCII: '{format_ascii(probe)}' | HEX: {format_hex(probe)}")
                        time.sleep(0.15)
                        resp = ser.read(100)
                        if resp:
                            scale_info = analyze_scale_framing(resp)
                            print(f"  [RX <- RECIBIDO]: ASCII: '{format_ascii(resp)}' | HEX: {format_hex(resp)}")
                            print(" ¡RESPUESTA DETECTADA!")
                            print(f"    --> Configuración exitosa: {baudrate} bps, {bytesize} DataBits, Paridad '{parity_str}', StopBits {stop_str}")
                            if scale_info:
                                print(f"    --> Detección de protocolo: {scale_info}")
                            found = True
                            ser.close()
                            return (bytesize, parity_str, stop_str, resp)
                        else:
                            print("  [RX <- RECIBIDO]: (Sin respuesta / Timeout)")

                except Exception as ex:
                    print(f" [Error durante comunicación: {ex}]")
                finally:
                    if ser.is_open:
                        ser.close()

    if not found:
        print("\n[!] Se han probado todas las combinaciones estándar sin obtener respuesta.")
        return None

def interactive_terminal(port_name, baudrate, bytesize, parity_str, stop_str):
    parity_map = {'N': serial.PARITY_NONE, 'E': serial.PARITY_EVEN, 'O': serial.PARITY_ODD}
    stop_map = {'1': serial.STOPBITS_ONE, '2': serial.STOPBITS_TWO}
    
    print("\n--------------------------------------------------")
    print(f" Abriendo terminal interactivo en {port_name} ({baudrate} {bytesize}{parity_str}{stop_str})")
    print(" Escribe cualquier texto y presiona Enter para enviar. (Escribe 'EXIT' para salir)")
    print("--------------------------------------------------\n")

    try:
        ser = serial.Serial(
            port=port_name,
            baudrate=baudrate,
            bytesize=bytesize,
            parity=parity_map[parity_str],
            stopbits=stop_map[stop_str],
            timeout=0.3
        )
        ser.dtr = True
        ser.rts = True
        
        while True:
            # Leer lo que llegue
            incoming = ser.read(256)
            if incoming:
                print(f"\n[RECIBIDO]: {format_ascii(incoming)} (HEX: {format_hex(incoming)})")
                print("> ", end="", flush=True)

            # Prompt para enviar
            cmd = input("> ")
            if cmd.strip().upper() == "EXIT":
                break
            if cmd:
                ser.write((cmd + "\r\n").encode('utf-8', errors='ignore'))
                time.sleep(0.2)
                resp = ser.read(256)
                if resp:
                    print(f"[RECIBIDO]: {format_ascii(resp)} (HEX: {format_hex(resp)})")

        ser.close()
    except Exception as e:
        print(f"Error en terminal interactivo: {e}")

def get_available_ports():
    ports = serial.tools.list_ports.comports()
    return [p.device for p in ports]

def main():
    print("==================================================")
    print("      COMPROBADOR DE PUERTO SERIE AUTO-CONFIG     ")
    print("==================================================")

    available = get_available_ports()
    if available:
        print(f"Puertos detectados en el sistema: {', '.join(available)}")
    else:
        print("Atención: No se detectaron puertos COM activos en este momento.")

    port_input = ""
    if len(sys.argv) > 1:
        port_input = sys.argv[1]
    else:
        port_input = input("\nIntroduce el nombre o número del puerto (ej: COM1 o 1): ").strip()

    if not port_input:
        print("Puerto no válido. Saliendo.")
        return

    # Normalizar puerto
    if port_input.isdigit():
        port_name = f"COM{port_input}"
    elif not port_input.upper().startswith("COM") and not port_input.startswith("/dev/"):
        port_name = f"COM{port_input}"
    else:
        port_name = port_input.upper()

    baud_input = ""
    if len(sys.argv) > 2:
        baud_input = sys.argv[2]
    else:
        baud_input = input("Introduce la velocidad en baudios (ej: 9600): ").strip()

    try:
        baudrate = int(baud_input) if baud_input else 9600
    except ValueError:
        print("Velocidad no válida. Usando 9600 por defecto.")
        baudrate = 9600

    res = test_serial_config(port_name, baudrate)

    if res:
        bytesize, parity_str, stop_str, payload = res
        ans = input("\n¿Deseas iniciar una terminal interactiva con esta configuración? (S/N): ").strip().lower()
        if ans == 's':
            interactive_terminal(port_name, baudrate, bytesize, parity_str, stop_str)
    
    print("\nProceso finalizado. Presiona Enter para salir.")
    input()

if __name__ == "__main__":
    main()
