import sys
import time

import minimalmodbus
import serial

# Configuracion de comunicacion
PUERTO = "COM4"
ID_ESCLAVO = 1
MODO_DIRECCIONES = "auto"  # auto | dec | hex

REG_DEC = {
    "modelo": 0,
    "version": 1,
    "slave_address": 3,
    "baudios": 6,
    "sens_media": 100,
    "filtro": 101,
    "peso_nominal": 102,
    "sens_cel1": 201,
    "sens_cel2": 202,
    "sens_cel3": 203,
    "sens_cel4": 204,
    "valores_fabrica": 500,
    "peso_proceso": 1000,
    "tara": 1001,
    "peso_muerto": 1002,
    "ganancia": 1003,
    "tarar": 1200,
    "coger_peso_muerto": 1300,
}

REG_HEX = {
    "modelo": 0x0000,
    "version": 0x0001,
    "slave_address": 0x0003,
    "baudios": 0x0006,
    "sens_media": 0x0100,
    "filtro": 0x0101,
    "peso_nominal": 0x0102,
    "sens_cel1": 0x0201,
    "sens_cel2": 0x0202,
    "sens_cel3": 0x0203,
    "sens_cel4": 0x0204,
    "valores_fabrica": 0x0500,
    "peso_proceso": 0x1000,
    "tara": 0x1001,
    "peso_muerto": 0x1002,
    "ganancia": 0x1003,
    "tarar": 0x1200,
    "coger_peso_muerto": 0x1300,
}

MODO_ACTIVO = "dec"
REG = REG_DEC

instrument = minimalmodbus.Instrument(PUERTO, ID_ESCLAVO)
instrument.serial.baudrate = 19200
instrument.serial.bytesize = 8
instrument.serial.parity = serial.PARITY_NONE
instrument.serial.stopbits = 1
instrument.serial.timeout = 1.0
instrument.clear_buffers_before_each_transaction = True


def _leer_registro(registro, signed=False):
    ultimo_error = None
    for fc in (3, 4):
        try:
            return instrument.read_register(registro, functioncode=fc, signed=signed)
        except Exception as e:
            ultimo_error = e
    raise RuntimeError(f"Error leyendo registro {registro}: {ultimo_error}")


def _escribir_registro(registro, valor, nombre="WRITE"):
    ultimo_error = None
    for fc in (6, 16):
        try:
            instrument.write_register(registro, valor, functioncode=fc)
            return
        except Exception as e:
            ultimo_error = e
    raise RuntimeError(f"{nombre}: sin respuesta valida (FC06/FC16). Ultimo error: {ultimo_error}")


def _u16_to_i16(valor):
    return valor - 65536 if valor >= 32768 else valor


def _leer_u16_i16(registro):
    u16 = _leer_registro(registro, signed=False)
    return u16, _u16_to_i16(u16)


def _leer_calibracion_mapa(mapa):
    keys = [
        "filtro",
        "peso_nominal",
        "sens_cel1",
        "sens_cel2",
        "sens_cel3",
        "sens_cel4",
        "ganancia",
        "tara",
        "peso_muerto",
        "peso_proceso",
    ]
    out = {}
    for k in keys:
        try:
            out[k] = _leer_registro(mapa[k], signed=True)
        except Exception:
            out[k] = None
    return out


def _score_calibracion(valores):
    score = 0
    if valores["filtro"] is not None and 1 <= valores["filtro"] <= 20:
        score += 1
    if valores["peso_nominal"] is not None and valores["peso_nominal"] > 0:
        score += 1
    if valores["ganancia"] is not None and 1000 <= valores["ganancia"] <= 50000:
        score += 1
    for k in ("sens_cel1", "sens_cel2", "sens_cel3", "sens_cel4"):
        v = valores[k]
        if v is not None and 500 <= v <= 5000:
            score += 1
    return score


def seleccionar_mapa_activo():
    global REG, MODO_ACTIVO

    modo = MODO_DIRECCIONES.lower().strip()
    if modo == "dec":
        REG = REG_DEC
        MODO_ACTIVO = "dec"
        print("Mapa activo forzado: DEC")
        return
    if modo == "hex":
        REG = REG_HEX
        MODO_ACTIVO = "hex"
        print("Mapa activo forzado: HEX")
        return

    dec = _leer_calibracion_mapa(REG_DEC)
    hexa = _leer_calibracion_mapa(REG_HEX)
    s_dec = _score_calibracion(dec)
    s_hex = _score_calibracion(hexa)

    if s_dec >= s_hex:
        REG = REG_DEC
        MODO_ACTIVO = "dec"
    else:
        REG = REG_HEX
        MODO_ACTIVO = "hex"

    print(f"Mapa activo AUTO: {MODO_ACTIVO.upper()} (score DEC={s_dec}, HEX={s_hex})")


def aplicar_tabla_manual_rs4():
    print("Aplicando tabla manual RS4 (fabrica) en mapa activo...")
    tabla = [
        ("filtro", 4),
        ("peso_nominal", 16000),
        ("sens_cel1", 2000),
        ("sens_cel2", 2000),
        ("sens_cel3", 2000),
        ("sens_cel4", 2000),
        ("tara", 0),
        ("peso_muerto", 0),
        ("ganancia", 10000),
        ("tarar", 0),
        ("coger_peso_muerto", 0),
    ]
    for k, v in tabla:
        _escribir_registro(REG[k], v, f"TABLA_{k.upper()}")
        vr = _leer_registro(REG[k], signed=True)
        print(f"OK {k:16s} set={v:6d} read={vr:6d}")


def limpiar_offsets(preservar_peso_muerto=False):
    _escribir_registro(REG["tarar"], 0, "CMD_TARA_OFF")
    _escribir_registro(REG["coger_peso_muerto"], 0, "CMD_PM_OFF")
    _escribir_registro(REG["tara"], 0, "SET_TARA_0")
    if not preservar_peso_muerto:
        _escribir_registro(REG["peso_muerto"], 0, "SET_PM_0")
    time.sleep(0.2)


def _captura_proceso_estable(titulo, segundos=3.0, pausa=0.1):
    n = max(5, int(segundos / pausa))
    vals = []
    for _ in range(n):
        _, v = _leer_u16_i16(REG["peso_proceso"])
        vals.append(v)
        time.sleep(pausa)

    ordenados = sorted(vals)
    recorte = max(1, int(len(ordenados) * 0.1))
    if len(ordenados) > 2 * recorte:
        nucleo = ordenados[recorte:-recorte]
    else:
        nucleo = ordenados

    promedio = sum(nucleo) / len(nucleo)
    dispersion = max(nucleo) - min(nucleo)
    print(
        f"{titulo}: media={promedio/100.0:.2f} kg "
        f"(crudo={promedio:.1f}) dispersion={dispersion/100.0:.2f} kg"
    )
    return promedio, dispersion


def guardar_peso_muerto_actual():
    _escribir_registro(REG["coger_peso_muerto"], 1, "PM_ON")
    time.sleep(0.2)
    _escribir_registro(REG["coger_peso_muerto"], 0, "PM_OFF")
    _escribir_registro(REG["tarar"], 0, "CMD_TARA_OFF")
    _escribir_registro(REG["coger_peso_muerto"], 0, "CMD_PM_OFF")
    pm = _leer_registro(REG["peso_muerto"], signed=True)
    print(f"Peso muerto fijado en 1002 = {pm} ({pm/100.0:.2f} kg)")


def _estado_final():
    p_u, p_i = _leer_u16_i16(REG["peso_proceso"])
    t_u, t_i = _leer_u16_i16(REG["tara"])
    pm_u, pm_i = _leer_u16_i16(REG["peso_muerto"])
    g = _leer_registro(REG["ganancia"], signed=True)
    cmd_tara = _leer_registro(REG["tarar"], signed=False)
    cmd_pm = _leer_registro(REG["coger_peso_muerto"], signed=False)

    print("--- Estado final ---")
    print(
        f"proceso={p_i/100.0:.2f} kg (u16={p_u}, i16={p_i}) | "
        f"tara={t_i/100.0:.2f} kg (u16={t_u}, i16={t_i}) | "
        f"peso_muerto={pm_i/100.0:.2f} kg (u16={pm_u}, i16={pm_i})"
    )
    print(f"ganancia(1003)={g} | cmd_tara(1200)={cmd_tara} | cmd_pm(1300)={cmd_pm}")


def setup_automatico():
    print("--- Setup automatico RS4 ---")
    print(f"Puerto={PUERTO} Slave={ID_ESCLAVO} Mapa={MODO_ACTIVO.upper()}")

    aplicar_tabla_manual_rs4()
    limpiar_offsets(preservar_peso_muerto=False)

    peso_ref = float(input("Peso patron real en kg (ej: 10): ").strip())
    if peso_ref <= 0:
        print("Peso patron invalido.")
        return

    print("Paso 1/4: Bascula VACIA. Pulsa Enter.")
    input()
    cero_inicial, disp_cero = _captura_proceso_estable("Cero inicial")

    print("Paso 2/4: Coloca el peso patron. Pulsa Enter.")
    input()
    carga_inicial, disp_carga = _captura_proceso_estable("Carga patron")

    delta = carga_inicial - cero_inicial
    kg_medidos = delta / 100.0
    if abs(kg_medidos) < 0.05:
        print("No hay variacion suficiente entre vacio y carga.")
        return

    ganancia_actual = _leer_registro(REG["ganancia"], signed=True)
    if ganancia_actual <= 0:
        ganancia_actual = 10000

    factor = peso_ref / kg_medidos
    ganancia_nueva = int(round(ganancia_actual * factor))
    ganancia_nueva = max(1, min(32767, ganancia_nueva))

    print(
        f"Ajuste ganancia: actual={ganancia_actual}, medida={kg_medidos:.3f} kg, "
        f"factor={factor:.5f}, nueva={ganancia_nueva}"
    )
    _escribir_registro(REG["ganancia"], ganancia_nueva, "AUTO_SET_GANANCIA")
    time.sleep(0.2)

    print("Paso 3/4: Quita el peso patron (vacio). Pulsa Enter para fijar peso muerto.")
    input()
    guardar_peso_muerto_actual()
    limpiar_offsets(preservar_peso_muerto=True)
    vacio_final, _ = _captura_proceso_estable("Vacio final")

    print("Paso 4/4: Vuelve a colocar el peso patron. Pulsa Enter para verificar.")
    input()
    carga_final, _ = _captura_proceso_estable("Carga final")

    lectura_final = (carga_final - vacio_final) / 100.0
    error = lectura_final - peso_ref
    error_pct = (error / peso_ref) * 100.0
    print(
        f"Verificacion final: {lectura_final:.2f} kg para patron {peso_ref:.2f} kg "
        f"(error {error:+.2f} kg, {error_pct:+.2f}%)"
    )

    if abs(vacio_final) > 20:
        print(
            f"Aviso: vacio final no esta cerca de 0 ({vacio_final/100.0:.2f} kg). "
            "Repite setup capturando vacio estable en paso 3."
        )
    if disp_cero > 80 or disp_carga > 80:
        print("Aviso: señal inestable (>0.8 kg de dispersion). Revisa montaje mecanico.")

    _estado_final()
    print("Setup completado.")


def _uso():
    print("Uso:")
    print("  python test.py --setup")
    print("  python test.py --help")


def main():
    seleccionar_mapa_activo()

    if len(sys.argv) > 1 and sys.argv[1].lower() in ("-h", "--help"):
        _uso()
        return

    # Flujo unico: setup automatico
    setup_automatico()


if __name__ == "__main__":
    main()
