import os
import sys
import comtypes.client
import pandas as pd
from shapely import points
pd.set_option('future.no_silent_downcasting', True)
import itertools
import tkinter as tk
from tkinter import *
from tkinter import ttk, messagebox, scrolledtext
from tkinter.filedialog import askopenfilename
import math
import numpy as np
import warnings
warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
import threading
import time

nombre_archivo = "resultados1.txt"




class TimeoutException(Exception):
    pass

def raise_timeout():
    raise TimeoutException()

timer = threading.Timer(20.0, raise_timeout) #se crea un timer de 20 segundos
timer.start()

try:
    #Conexion con SAP2000
    helper = comtypes.client.CreateObject('SAP2000v1.Helper')
    helper = helper.QueryInterface(comtypes.gen.SAP2000v1.cHelper)
    mySapObject = helper.GetObject("CSI.SAP2000.API.SapObject")
    SapModel = mySapObject.SapModel
    if SapModel.GetModelisLocked(): SapModel.SetModelisLocked(False)

except TimeoutException as exc:
    messagebox.showerror(
        "Error",
        "No se encuentra una instancia de SAP2000 abierta. Por favor, abra SAP2000 e intente de nuevo."
    )
    sys.exit(1)

finally:
    timer.cancel()


class SAPSectionDesignerGUI:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Diseñador de Secciones SAP2000 - Python API")
        self.root.geometry("600x800")
        
        self.SapModel = None
        
        self.crear_interfaz()
    
    def crear_interfaz(self):        
        # === Datos de la sección ===
        tk.Label(self.root, text="Datos de la Sección", font=("Arial", 12, "bold")).pack(pady=(20,5))
        
        frame_datos = tk.Frame(self.root)
        frame_datos.pack(pady=5, padx=20, fill="x")

        con_ref = tk.BooleanVar(value=False)
        chk_con_ref = ttk.Checkbutton(frame_datos, text="Con Refuerzos verticales", variable=con_ref)
        chk_con_ref.pack(anchor="w")
        self.con_ref = con_ref
        # Frame para parámetros fijos
        frame_fixed = ttk.LabelFrame(self.root, text="Parámetros Fijos", padding=5)
        frame_fixed.pack(fill=tk.X, pady=(0, 10))

        # Variables para toggle fijo/barrido
        self.sweep_toggle_vars = {}
        self.sweep_fixed_entries = {}
        self.sweep_range_entries = {}

        params_ipe = ['H', 'b', 'tf', 'tw', 'e_ref', 'L_ref']
        for param in params_ipe:
            container = ttk.Frame(frame_fixed)
            container.pack(pady=2)

            var = tk.BooleanVar(value=True)
            self.sweep_toggle_vars[param] = var
            chk = ttk.Checkbutton(container, text=f"Fijo: {param}", variable=var,
                                 command=lambda p=param: self.on_sweep_toggle(p))
            chk.pack(side=tk.LEFT, fill=tk.X, expand=True)

            entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=10)
            entry.pack(side=tk.LEFT, padx=5)
            self.sweep_fixed_entries[param] = entry

        # Cargar valores actuales
        self.sweep_fixed_entries['H'].set("1.0")
        self.sweep_fixed_entries['b'].set("0.45")
        self.sweep_fixed_entries['tf'].set("0.01")
        self.sweep_fixed_entries['tw'].set("0.01")
        self.sweep_fixed_entries['e_ref'].set("0.01")
        self.sweep_fixed_entries['L_ref'].set("0.01")

        # Frame para parámetros a barrer
        frame_sweep = ttk.LabelFrame(self.root, text="Parámetros a Barrer", padding=5)
        frame_sweep.pack(fill=tk.X, pady=(0, 10))

        self.sweep_range_labels = {}
        self.sweep_range_entries = {}

        for param in params_ipe:
            container = ttk.Frame(frame_sweep)
            container.pack(fill=tk.X, pady=2)

            lbl = ttk.Label(container, text=f"{param}:", width=5)
            lbl.pack(side=tk.LEFT)
            self.sweep_range_labels[param] = lbl

            ttk.Label(container, text="min:").pack(side=tk.LEFT, padx=(10, 2))
            min_entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=8)
            min_entry.pack(side=tk.LEFT, padx=2)

            ttk.Label(container, text="max:").pack(side=tk.LEFT, padx=(10, 2))
            max_entry = ttk.Spinbox(container, from_=0.0, to=1000.0, increment=0.001, width=8)
            max_entry.pack(side=tk.LEFT, padx=2)

            ttk.Label(container, text="pasos:").pack(side=tk.LEFT, padx=(10, 2))
            steps_entry = ttk.Spinbox(container, from_=2, to=20, increment=1, width=8)
            steps_entry.pack(side=tk.LEFT, padx=2)
            steps_entry.set(5)

            self.sweep_range_entries[param] = {
                'min': min_entry,
                'max': max_entry,
                'steps': steps_entry
            }

        # Botones de control
        button_frame = ttk.Frame(self.root)
        button_frame.pack(fill=tk.X, pady=(10, 0))

        # === Botón Crear ===
        btn_crear = tk.Button(self.root, text="🚀 Crear Sección en Section Designer", 
                             command=self.execute_parametric_sweep, width=40, height=2, 
                             bg="#2196F3", fg="white", font=("Arial", 10, "bold"))
        btn_crear.pack(pady=20)
        
        tk.Label(self.root, text="Nota: Todas las unidades estan en [m].", 
                fg="gray").pack()
    
    def on_sweep_toggle(self, param):
        """Alterna parámetro entre fijo y barrido"""
        is_fixed = self.sweep_toggle_vars[param].get()
        # Los rangos se mostrarán solo si no está fijo
        # Esta lógica se puede mejorar si es necesario

    def execute_parametric_sweep(self):
        """Ejecuta el barrido paramétrico"""

        if SapModel.GetModelisLocked(): SapModel.SetModelisLocked(False)
        
        try:
            # Recopilar configuración
            fixed_params = {}
            sweep_configs = {}
            
            for param in ['H', 'b', 'tf', 'tw', 'e_ref', 'L_ref'] if self.con_ref.get() else ['H', 'b', 'tf', 'tw']:
                is_fixed = self.sweep_toggle_vars[param].get()
                
                if is_fixed:
                    value = float(self.sweep_fixed_entries[param].get())
                    fixed_params[param] = value
                else:
                    min_val = float(self.sweep_range_entries[param]['min'].get())
                    max_val = float(self.sweep_range_entries[param]['max'].get())
                    steps = int(self.sweep_range_entries[param]['steps'].get())

                    if steps < 1:
                        messagebox.showerror("Error", f"El número de pasos para {param} debe ser al menos 1")
                        return

                    if min_val <= 0:
                        messagebox.showerror("Error", f"Valores de {param} deben ser positivos")
                        return
                    
                    if min_val >= max_val:
                        messagebox.showerror("Error", f"Rango inválido para {param}: min >= max")
                        return
                    
                    sweep_configs[param] = {
                        'min': min_val,
                        'max': max_val,
                        'steps': steps
                    }
            
            
            if not sweep_configs:
                messagebox.showerror("Error", "Debe barrer al menos un parámetro")
                return
            
            if not fixed_params:
                messagebox.showwarning("Advertencia", "No hay parámetros fijos")
            

            combinations = self.generar_matriz_sweep(sweep_configs, fixed_params)
            
            if not combinations:
                messagebox.showerror("Error", "No se generaron combinaciones para el barrido")
                return
            
            results = []

            f = open(nombre_archivo, "w")
            if self.con_ref.get():
                f.write("H\tb\ttf\ttw\te\tL\tFrecuencia\tCoef_Impacto\tFlecha_38D\tFlecha_86E\tFlecha_Media\tArea\tIxg\tIyg\tImax\tImin\tPeso\n")
            else:
                f.write("H\tb\ttf\ttw\tFrecuencia\tCoef_Impacto\tFlecha_38D\tFlecha_86E\tFlecha_Media\tArea\tIxg\tIyg\tImax\tImin\tPeso\n")
            
            for params in combinations:
                SapModel.SetModelisLocked(False)
                SapModel.SetPresentUnits(10) #unidades en niutons metros

                if self.con_ref.get():
                    points_ipe = self.generate_ipe_points_ref(params['H'], params['b'], params['tf'], params['tw'], params['e_ref'], params['L_ref'])
                    points_hueco_inf = self.generate_hueco_inf_points_ref(params['H'], params['b'], params['tf'], params['tw'], params['e_ref'], params['L_ref'])
                    points_hueco_sup = self.generate_hueco_sup_points_ref(params['H'], params['b'], params['tf'], params['tw'], params['e_ref'], params['L_ref'])
                    points_completo = self.generate_completo_points_ref(params['H'], params['b'], params['tf'], params['tw'], params['e_ref'], params['L_ref'])
                else:
                    points_ipe = self.generate_ipe_points(params['H'], params['b'], params['tf'], params['tw'])
                    points_hueco_inf = self.generate_hueco_inf_points(params['H'], params['b'], params['tf'], params['tw'])
                    points_hueco_sup = self.generate_hueco_sup_points(params['H'], params['b'], params['tf'], params['tw'])
                    points_completo = self.generate_completo_points(params['H'], params['b'], params['tf'], params['tw'])
                
                properties_ipe = self.calculate_section_properties(points_ipe)

                self.crear_seccion(points_ipe, tipo="ipe", nombre_poligono = "Polygon1")
                self.crear_seccion(points_hueco_inf, tipo="hueco", nombre_poligono = "Polygon1")
                self.crear_seccion(points_hueco_sup, tipo="hueco", nombre_poligono = "Polygon2")
                self.crear_seccion(points_completo, tipo="completo", nombre_poligono = "Polygon1")
                
                ret = SapModel.Analyze.RunAnalysis()

                frequency = self.obtain_frequency()

                coef_impacto = self.calc_coef_impacto(frequency)
                
                ret = SapModel.SetModelisLocked(False)
                
                ret = SapModel.LoadCases.StaticLinear.SetLoads("H3. Sobrecarga UIC 71", 1, ["Load"],
                                                               ["H3. Sobrecarga UIC"], [coef_impacto])
                
                if ret[3] != 0:
                    messagebox.showerror("Error de carga", f"Error al aplicar cargas para la combinación: {params}\nCódigo de error: {ret}")
                    return
        
                ret = SapModel.Analyze.RunAnalysis()

                SapModel.SetPresentUnits(9)  # Unidades niutons milimetos
                ret = SapModel.Results.Setup.DeselectAllCasesAndCombosForOutput()
                ret = SapModel.Results.Setup.SetComboSelectedForOutput("3. ENV ELS CAR")

                FieldKeyList = []
                GroupName = 'All'
                TableVersion = 1
                FieldsKeysIncluded = []
                NumberRecords = 1
                TableData = []

                ret = SapModel.DatabaseTables.GetTableforDisplayArray("Joint Displacements", FieldKeyList, GroupName, 
                                                                TableVersion, FieldsKeysIncluded, NumberRecords, TableData)
                
                if ret[-1] != 0:
                    messagebox.showerror("Error de resultados", f"Error al obtener resultados para la combinación: {params}\nCódigo de error: {ret}")
                    return
                
                big_tuple = ret[4]
                items = list(big_tuple)
                
                filas = []

                j = 0
                while j < len(items):
                    if j + 10 <= len(items):
                        fila = items[j:j+10]
                        filas.append(fila)
                        j += 10
                    else:
                        break



                flecha1 = self.filtrar_por_joint(filas, "38D")
                flecha2 = self.filtrar_por_joint(filas, "86E")

                flecha_media = (flecha1 + flecha2) / 2

                

                f = open(nombre_archivo, "a")
                if self.con_ref.get():
                    string = (f"{params['H'].item() if isinstance(params['H'], np.ndarray) else params['H']}\
                            {params['b'].item() if isinstance(params['b'], np.ndarray) else params['b']}\
                            {params['tf'].item() if isinstance(params['tf'], np.ndarray) else params['tf']}\
                            {params['tw'].item() if isinstance(params['tw'], np.ndarray) else params['tw']}\
                            {params['e_ref'].item() if isinstance(params['e_ref'], np.ndarray) else params['e_ref']}\
                            {params['L_ref'].item() if isinstance(params['L_ref'], np.ndarray) else params['L_ref']}\
                            {frequency}\
                            {coef_impacto}\
                            {flecha1}\
                            {flecha2}\
                            {flecha_media}\
                            {properties_ipe['area'].item()}\
                            {properties_ipe['ixg'].item()}\
                            {properties_ipe['iyg'].item()}\
                            {properties_ipe['imax'].item()}\
                            {properties_ipe['imin'].item()}\
                            {properties_ipe['peso'].item()}\n").replace(".", ",")
                else:
                    string = (f"{params['H'].item() if isinstance(params['H'], np.ndarray) else params['H']}\
                            {params['b'].item() if isinstance(params['b'], np.ndarray) else params['b']}\
                            {params['tf'].item() if isinstance(params['tf'], np.ndarray) else params['tf']}\
                            {params['tw'].item() if isinstance(params['tw'], np.ndarray) else params['tw']}\
                            {frequency}\
                            {coef_impacto}\
                            {flecha1}\
                            {flecha2}\
                            {flecha_media}\
                            {properties_ipe['area'].item()}\
                            {properties_ipe['ixg'].item()}\
                            {properties_ipe['iyg'].item()}\
                            {properties_ipe['imax'].item()}\
                            {properties_ipe['imin'].item()}\
                            {properties_ipe['peso'].item()}\n").replace(".", ",")
                f.write(string)
                f.close()
                
                
            return



        
        except Exception as e:
            messagebox.showerror("Error en configuración", f"Revisa los valores ingresados:\n{e}")
            return
        
    
    def filtrar_por_joint(self, filas, joint_id):

        for fila in filas:
            if fila[0] == joint_id and fila[3] == "Min":
                return float(fila[6].replace(",", "."))
            
        return None
        
            
        
    def calc_coef_impacto(self, frecuencia):

        #calculo segun EC1.2 trenes reales

        L_phi = 15 # m
        v = 80  # km/h
        r = 1 # calidad de mantenimiento de la vía

        alpha = 1 if v/3.6 > 22 else v/(3.6*22)

        K = v/(3.6*L_phi*frecuencia)

        phi1 = 1.325 if K >= 0.76 else K/(1-K+K**4)

        phi2 = alpha*(56*math.exp(-((L_phi/10)**2))+50*(L_phi*frecuencia/80-1)*math.exp(-((L_phi/20)**2)))/100

        return 1.21 * (1 + phi1 + r * phi2)  #aplicado el factor alpha 


    def obtain_frequency(self):
        Num_results = 0
        LoadCase = ""
        Steptype = []
        Stepnum = []
        Period = []
        Ux = []
        Uy = []
        Uz = []
        SumUx = []
        SumUy = []
        SumUz = []
        Rx = []
        Ry = []
        Rz = []
        SumRx = []
        SumRy = []
        SumRz = []

        ret = SapModel.Results.ModalParticipatingMassRatios(Num_results, LoadCase, Steptype, Stepnum, Period,
                                                            Ux, Uy, Uz, SumUx, SumUy, SumUz, 
                                                            Rx, Ry, Rz, SumRx, SumRy, SumRz)
        
        if ret[17] != 0:
            messagebox.showerror("Error al obtener frecuencias", f"Código de error: {ret}")
            return None
 
        return 1/ret[4][min(ret[7].index(max(ret[7])), ret[12].index(max(ret[12])))]


    def calculate_section_properties(self, points):
        """Calcula propiedades de la sección a partir de puntos"""
        # Constantes de material
        DENS = 7850  # kg/m3
        FY = 355  # MPa
        GAMMAS = 1.05
        EYOUNG = 210000  # MPa
        NU = 0.3
        FYD = FY * 10**6 / GAMMAS

        npuntos = len(points)
        px = points[:, 0]
        py = points[:, 1]

        bmax = np.amax(px) - np.amin(px)
        hmax = np.amax(py) - np.amin(py)

        # Perímetro
        long_i = np.zeros(npuntos - 1)
        for i in range(npuntos - 1):
            long_i[i] = ((points[i+1, 0] - points[i, 0])**2 + (points[i+1, 1] - points[i, 1])**2)**(1/2)
        perimetro = abs(sum(long_i))

        # Área
        area_i = np.zeros(npuntos - 1)
        for i in range(npuntos - 1):
            area_i[i] = (points[i+1, 0] - points[i, 0]) * (points[i+1, 1] + points[i, 1]) / 2
        area = abs(sum(area_i))

        # Peso
        peso = area * DENS

        # Centro de gravedad
        cdg_i = np.zeros([npuntos, 2])
        for i in range(npuntos - 1):
            h1 = points[i, 1]
            h2 = points[i+1, 1]
            b = points[i+1, 0] - points[i, 0]
            d = points[i, 0]

            if h1 + h2 == 0:
                cdg_i[i, 1] = 0
            else:
                cdg_i[i, 1] = 1/3 * (h1*h1 + h1*h2 + h2*h2) / (h1 + h2)

            if h1 + h2 == 0:
                cdg_i[i, 0] = d + b/2
            else:
                cdg_i[i, 0] = d + b/3 * (h1 + 2*h2) / (h1 + h2)

        statico_i = np.zeros([npuntos, 2])
        for i in range(npuntos - 1):
            statico_i[i, 1] = area_i[i] * cdg_i[i, 1]
            statico_i[i, 0] = area_i[i] * cdg_i[i, 0]

        cdg = sum(statico_i) / sum(area_i)
        xg = cdg[0]
        yg = cdg[1]

        # Fibras más alejadas
        v1y = np.amax(py) - yg
        v2y = np.amin(py) - yg
        v1x = np.amax(px) - xg
        v2x = np.amin(px) - xg

        # Momentos de inercia
        inercia_i = np.zeros([npuntos, 3])
        for i in range(npuntos - 1):
            h1 = points[i, 1]
            h2 = points[i+1, 1]
            b = points[i+1, 0] - points[i, 0]
            d = points[i, 0]
            xgi = cdg_i[i, 0]
            ygi = cdg_i[i, 1]
            ai = area_i[i]

            if h2 >= h1:
                ixcuad_G_local = 1/12 * b * (h1**3) + b * h1 * (h1/2 - ygi)**2
                ixtriang_G_loc = 1/36 * b * (h2-h1)**3 + 1/2 * b * (h2-h1) * ((2*h1+h2)/3 - ygi)**2
            else:
                ixcuad_G_local = 1/12 * b * (h2**3) + b * h2 * (h2/2 - ygi)**2
                ixtriang_G_loc = 1/36 * b * (h1-h2)**3 + 1/2 * b * (h1-h2) * ((2*h2+h1)/3 - ygi)**2
            inercia_i[i, 0] = ixcuad_G_local + ixtriang_G_loc + ai * (yg - ygi)**2

            if h2 >= h1:
                iycuad = 1/12 * h1 * b**3 + h1 * b * (b/2 + d - xgi)**2
                iytrian = 1/36 * (h2-h1) * b**3 + 1/2 * b * (h2-h1) * (2/3*b + d - xgi)**2
            else:
                iycuad = 1/12 * h2 * b**3 + h2 * b * (b/2 + d - xgi)**2
                iytrian = 1/36 * (h1-h2) * b**3 + 1/2 * b * (h1-h2) * (1/3*b + d - xgi)**2
            inercia_i[i, 1] = iycuad + iytrian + ai * (xg - xgi)**2

            if h2 >= h1:
                pxygcuadrado = b * h1 * (-h1/2 + ygi) * (-d - b/2 + xgi)
                pxytriangulo = b*b * (h2-h1)**2 / 72 + b * (h2-h1) / 2 * (-(h2-h1)/3 - h1 + ygi) * (-d - 2/3*b + xgi)
            else:
                pxygcuadrado = b * h2 * (-h2/2 + ygi) * (-d - b/2 + xgi)
                pxytriangulo = -b*b * (h1-h2)**2 / 72 + b * (h1-h2) / 2 * (-(h1-h2)/3 - h2 + ygi) * (-d - 1/3*b + xgi)
            inercia_i[i, 2] = pxygcuadrado + pxytriangulo + ai * (-xg + xgi) * (-yg + ygi)

        ig = sum(inercia_i)
        ixg = abs(ig[0])
        iyg = abs(ig[1])
        pxyg = ig[2]
        if sum(area_i) >= 0:
            pxyg = pxyg
        else:
            pxyg = -pxyg

        # Radios de giro
        rx = (ixg / area)**0.5
        ry = (iyg / area)**0.5

        # Ejes principales
        ic = (ixg + iyg) / 2
        ir = (((ixg - iyg)/2)**2 + pxyg**2)**0.5
        imax = ic + ir
        imin = ic - ir
        rmax = (imax / area)**0.5
        rmin = (imin / area)**0.5

        # Conversión a unidades prácticas (cm, cm2, cm3, cm4, kN)
        pot = 2
        area_cm2 = area * 10**(pot*2)
        ixg_cm4 = ixg * 10**(pot*4)
        iyg_cm4 = iyg * 10**(pot*4)
        imax_cm4 = imax * 10**(pot*4)
        imin_cm4 = imin * 10**(pot*4)

        return {
            'area': area_cm2,
            'ixg': ixg_cm4,
            'iyg': iyg_cm4,
            'imax': imax_cm4,
            'imin': imin_cm4,
            'peso': peso,
        }
    

    def generate_completo_points(self, H, b, tf, tw):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - .3) / 2
        x_alma_fin = (b + .3) / 2

        points = np.array([
            [0, 0],
            [b, 0],
            [b, tf],
            [x_alma_fin, 0.169],
            [x_alma_fin, H - .169],
            [b, H - tf],
            [b, H],
            [0, H],
            [0, H - tf],
            [x_alma_ini, H - .169],
            [x_alma_ini, 0.169],
            [0, tf],
        ])

        return points
    
    def generate_completo_points_ref(self, H, b, tf, tw, e_ref, L_ref):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - .3) / 2
        x_alma_fin = (b + .3) / 2

        points = np.array([
            [0, 0],
            [b, 0],
            [b, L_ref + tf],
            [b - e_ref, L_ref + tf],
            [x_alma_fin, 0.169],
            [x_alma_fin, H - .169],
            [b - e_ref, H - L_ref - tf],
            [b, H - L_ref - tf],
            [b, H],
            [0, H],
            [0, H - L_ref - tf],
            [e_ref, H - L_ref - tf],
            [x_alma_ini, H - 0.169],
            [x_alma_ini, 0.169],
            [e_ref, L_ref + tf],
            [0, L_ref + tf],
        ])

        return points


    def generate_hueco_inf_points(self, H, b, tf, tw):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - tw) / 2
        x_alma_fin = (b + tw) / 2

        points = np.array([
            [0, 0],
            [b, 0],
            [b, tf],
            [x_alma_fin, tf],
            [x_alma_fin, H -.36 - .169],
            [x_alma_ini, H - .36 - .169],
            [x_alma_ini, tf],
            [0, tf],          
        ])

        return points
    
    def generate_hueco_inf_points_ref(self, H, b, tf, tw, e_ref, L_ref):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - tw) / 2
        x_alma_fin = (b + tw) / 2

        points = np.array([
            [0, 0],
            [b, 0],
            [b, L_ref + tf],
            [b - e_ref, L_ref + tf],
            [b - e_ref, tf],
            [x_alma_fin, tf],
            [x_alma_fin, H -.36 - .169],
            [x_alma_ini, H - .36 - .169],
            [x_alma_ini, tf],
            [e_ref, tf],
            [e_ref, L_ref + tf],
            [0, L_ref + tf],
        ])

        return points
    
    def generate_hueco_sup_points(self, H, b, tf, tw):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - tw) / 2
        x_alma_fin = (b + tw) / 2

        points = np.array([
            [x_alma_fin, H - tf - .169],
            [x_alma_fin, H - tf],
            [b, H - tf],
            [b, H],
            [0, H],
            [0, H - tf],
            [x_alma_ini, H - tf],
            [x_alma_ini, H - tf - .169],
        ])

        return points
    
    def generate_hueco_sup_points_ref(self, H, b, tf, tw, e_ref, L_ref):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - tw) / 2
        x_alma_fin = (b + tw) / 2

        points = np.array([
            [x_alma_fin, H - .169],
            [x_alma_fin, H - tf],
            [b - e_ref, H - tf],
            [b - e_ref, H - L_ref - tf],
            [b, H - L_ref - tf],
            [b, H],
            [0, H],
            [0, H - L_ref - tf],
            [e_ref, H - L_ref - tf],
            [e_ref, H - tf],
            [x_alma_ini, H - tf],
            [x_alma_ini, H - .169],
        ])

        return points

    def generate_ipe_points(self, H, b, tf, tw):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - tw) / 2
        x_alma_fin = (b + tw) / 2

        points = np.array([
            [0, 0],
            [b, 0],
            [b, tf],
            [x_alma_fin, tf],
            [x_alma_fin, H - tf],
            [b, H - tf],
            [b, H],
            [0, H],
            [0, H - tf],
            [x_alma_ini, H - tf],
            [x_alma_ini, tf],
            [0, tf],
        ])

        return points
    
    def generate_ipe_points_ref(self, H, b, tf, tw, e_ref, L_ref):
        """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
        h_alma = H - 2*tf
        x_alma_ini = (b - tw) / 2
        x_alma_fin = (b + tw) / 2

        points = np.array([
            [0, 0],
            [b, 0],
            [b, L_ref + tf],
            [b - e_ref, L_ref + tf],
            [b - e_ref, tf],
            [x_alma_fin, tf],
            [x_alma_fin, H - tf],
            [b - e_ref, H - tf],
            [b - e_ref, H - L_ref - tf],
            [b, H - L_ref - tf],
            [b, H],
            [0, H],
            [0, H - L_ref - tf],
            [e_ref, H - L_ref - tf],
            [e_ref, H - tf],
            [x_alma_ini, H - tf],
            [x_alma_ini, tf],
            [e_ref, tf],
            [e_ref, L_ref + tf],
            [0, L_ref + tf],
        ])

        return points
    
    def _valid_geometry(self, params):
        """Valida que la combinación de parámetros genere una geometría válida"""
        H = params.get('H', 0)
        b = params.get('b', 0)
        tf = params.get('tf', 0)
        tw = params.get('tw', 0)

        if H <= 0 or b <= 0 or tf <= 0 or tw <= 0:
            return False
        if H < 0.360 + 0.169 + tf:
            return False
        if b < 0.3:
            return False
        if tf > H/2 or tf > 0.169:
            return False
        if tw > b:
            return False
        
        if self.con_ref.get():
            e_ref = params.get('e_ref', 0)
            L_ref = params.get('L_ref', 0)

            if e_ref > 0.3:
                return False
            if L_ref + tf > 0.169:
                return False
        
        return True 

    def generar_matriz_sweep(self, sweep_configs, fixed_params):
        sweep_params = list(sweep_configs.keys())

        param_values = {}
        for param in sweep_params:
            config = sweep_configs[param]
            values = np.linspace(config['min'], config['max'], config['steps'])
            param_values[param] = values
        
        combinations = []
        for combo in itertools.product(*param_values.values()):
            params = fixed_params.copy()
            for i, param in enumerate(sweep_params):
                params[param] = combo[i]
            if self._valid_geometry(params):
                combinations.append(params)
        
        return combinations


    def crear_seccion(self, puntos = [], tipo="ipe", nombre_poligono="Nueva sección"):        
        
        material = "S355"
        
        if not len(puntos) or not tipo:
            messagebox.showerror("Error", "Faltan puntos o tipos de sección")
            return
        
        # Leer coordenadas
        try:
            X = [float(p[0]) for p in puntos]
            Y = [float(p[1]) for p in puntos]
                  
            num_points = len(X)
            Radius = [0.0] * num_points
            
            if num_points < 3:
                raise ValueError("Se necesitan al menos 3 puntos")
            
        except Exception as e:
            messagebox.showerror("Error en coordenadas", str(e))
            return
        
        if tipo == "hueco":
            nombre = "hueca"
        elif tipo == "completo":
            nombre = "rigida"
        elif tipo == "ipe":
            nombre = "entera"
        else:
            messagebox.showerror("Error", f"Tipo de sección desconocido: {tipo}")
            return
        
        try:
            
            ret = SapModel.PropFrame.SetSDSection(
                nombre, material, 1, -1, "", "Default"
            )
            
            if not (ret == 0 or ret == 1):  # 1 = sección ya existe, lo cual está bien
                messagebox.showerror("Error SetSDSection", f"Código: {ret}")
                return
            
            if not (nombre == "hueca" and nombre_poligono == "Polygon2"): #si es la sección hueca, el polígono 2 es el hueco, no se borra
                ret = SapModel.PropFrame.SDShape.Delete(
                    nombre,
                    "",
                    True
                )
            
            if ret != 0 :  
                messagebox.showerror("Error Delete", f"Código: {ret}")
                return
                
            ret = SapModel.PropFrame.SDShape.SetPolygon(
                nombre,                  # Nombre de la sección SD
                nombre_poligono,         # Nombre del shape
                material,                # Material
                "Default",
                num_points,              # Número de puntos
                X,                       # Lista normal de Python (X)
                Y,                       # Lista normal (Y)
                Radius,                  # Lista normal (Radius)
                -1,                      # Color
                False,
                ""
            )
            
            if ret[4] != 0:
                messagebox.showerror("Error SAP", 
                    f"SetPolygon devolvió código de error: {ret}\n\nRevisa que el material exista y las coordenadas sean correctas.")
                
        except Exception as e:
            messagebox.showerror("Error SAP", str(e))


    def run(self):
        self.root.mainloop()

if __name__ == "__main__":
    app = SAPSectionDesignerGUI()
    app.run()