# src/tab_coil.py

import tkinter as tk
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText

import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

import numpy as np
import math as m

# Se importan las integrales elípticas de SciPy para el cálculo preciso de la inductancia
from scipy.special import ellipk, ellipe

# -------------------------------------------------------------------
# Constantes y funciones para el cálculo de inductancia
# -------------------------------------------------------------------

MU0 = 4.0e-7 * m.pi  # Permeabilidad magnética del vacío (H/m)

def self_inductance_loop(r, a_eff=1e-4):
    """
    Autoinductancia aproximada de un anillo fino de radio r.
    Emplea la fórmula: L ~ mu0*r * [ln(8r/a_eff) - 2].
    """
    if r <= 0:
        return 0.0
    return MU0 * r * (m.log(8.0 * r / a_eff) - 2.0)

def mutual_inductance_coaxial(r1, r2, z_dist):
    """
    Inductancia mutua entre dos anillos coaxiales de radios r1 y r2,
    separados una distancia axial z_dist.
    Emplea las integrales elípticas de primera y segunda especie.
    """
    if r1 < 1e-12 or r2 < 1e-12:
        return 0.0

    k_sq = 4.0 * r1 * r2 / ((r1 + r2)**2 + z_dist**2)
    if k_sq > 1.0:
        # Caso no físico (puede ocurrir por redondeos), retornamos 0
        return 0.0

    k = np.sqrt(k_sq)

    # Caso casi degenerado (r1 ~ r2, z_dist ~ 0)
    if (1.0 - k_sq) < 1e-12:
        # Aproximación cuando r1 ≈ r2 y z ≈ 0
        return MU0 * min(r1, r2) * m.pi
    else:
        Kk = ellipk(k_sq)
        Ek = ellipe(k_sq)
        factor = MU0 * np.sqrt(r1 * r2)
        # Expresión: M = mu0 * sqrt(r1*r2)*((2-k)*K - 2E) / k
        return factor * ((2.0 - k) * Kk - 2.0 * Ek) / k

def compute_coil_geometry(N_total, R_int, R_ext, espesor_plast, H, wire_d):
    """
    Genera la lista de (r_i, z_i) para las N_total espiras, asumiendo
    que se llenan capas radiales y, en cada capa, vueltas a lo largo de la altura.
    Todos los parámetros deben estar en metros.
    """
    radial_thickness = R_ext - (R_int + 2 * espesor_plast)
    if radial_thickness <= 0:
        raise ValueError("No hay espacio radial para el bobinado.")

    max_layers = int(np.floor(radial_thickness / wire_d))
    if max_layers < 1:
        raise ValueError("No cabe ni una capa con el grosor de hilo dado.")

    max_turns_per_layer = int(np.floor(H / wire_d))
    if max_turns_per_layer < 1:
        raise ValueError("No cabe ni una vuelta en la altura dada.")

    max_turns_possible = max_layers * max_turns_per_layer
    if max_turns_possible < N_total:
        raise ValueError(
            f"Con {max_layers} capas y {max_turns_per_layer} vueltas/capa "
            f"sólo se pueden alojar {max_turns_possible} espiras, "
            f"pero se solicitan {N_total}."
        )

    espiras = []
    vueltas_restantes = N_total
    layer_index = 0

    while vueltas_restantes > 0 and layer_index < max_layers:
        r_layer = R_int + espesor_plast + layer_index * wire_d + wire_d / 2.0

        if vueltas_restantes >= max_turns_per_layer:
            n_vueltas = max_turns_per_layer
        else:
            n_vueltas = vueltas_restantes

        for turn in range(n_vueltas):
            z_i = (turn + 0.5) * wire_d
            espiras.append((r_layer, z_i))

        vueltas_restantes -= n_vueltas
        layer_index += 1

    if vueltas_restantes > 0:
        raise ValueError(
            f"No se pudo colocar toda la bobina. "
            f"Faltan {vueltas_restantes} espiras por ubicar."
        )

    return espiras

def inductance_from_spiralist(espiras, wire_radius):
    """
    Calcula la inductancia total (en henrios) a partir de la lista (r_i, z_i),
    sumando la autoinductancia de cada espira y la mutua entre pares (O(N^2)).
    """
    N = len(espiras)
    L_total = 0.0

    for i in range(N):
        r_i, z_i = espiras[i]
        # Autoinductancia
        L_total += self_inductance_loop(r_i, wire_radius)
        # Mutua con las espiras j > i
        for j in range(i + 1, N):
            r_j, z_j = espiras[j]
            dist_z = abs(z_j - z_i)
            M_ij = mutual_inductance_coaxial(r_i, r_j, dist_z)
            L_total += 2.0 * M_ij

    return abs(L_total)

# -------------------------------------------------------------------
# Importamos la función para dibujar la bobina 2D (tal y como estaba).
# -------------------------------------------------------------------
from tabs.geometry_viewer import plot_coil_in_frame

# -------------------------------------------------------------------
# Definición de la clase TabCoil, añadiendo la ventana de logs y
# el botón de cálculo de inductancia con mensajes de estado.
# -------------------------------------------------------------------
class TabCoil:
    def __init__(self, notebook, tab_simulator):
        """
        Pestaña para representar la bobina en 2D y ajustar sus parámetros.
        Recibe:
          - notebook: el ttk.Notebook donde se añadirá
          - tab_simulator: referencia a TabSimulator, si se necesita comunicar
        """
        self.notebook = notebook
        self.tab_simulator = tab_simulator

        # Frame principal dentro del notebook
        self.frame = tk.Frame(notebook)
        self.frame.pack(fill="both", expand=True)

        # Layout principal: lado izq (parámetros) y lado der (dibujo)
        frame_left = tk.Frame(self.frame)
        frame_left.pack(side="left", fill="y", padx=5, pady=5)

        frame_right = tk.Frame(self.frame, bd=2, relief="groove")
        frame_right.pack(side="right", fill="both", expand=True, padx=5, pady=5)

        # Este frame contendrá el canvas/figura para la bobina en 2D
        self.frame_2d = tk.Frame(frame_right)
        self.frame_2d.pack(fill="both", expand=True)

        #
        # Sección Izquierda: parámetros de la bobina
        #
        tk.Label(frame_left, text="Parámetros de la Bobina:").pack(anchor="w")

        # Variables
        self.var_N     = tk.DoubleVar(value=500)    # Número de espiras
        self.var_e_pvc = tk.DoubleVar(value=2)      # Espesor del plástico [mm]
        self.var_r_int = tk.DoubleVar(value=4.04)  # Radio interior [mm]
        self.var_r_ext = tk.DoubleVar(value=10.64)  # Radio exterior [mm]
        self.var_h_c   = tk.DoubleVar(value=53.12)  # Altura de la bobina [mm]
        self.var_d_cu  = tk.DoubleVar(value=0.5)    # Diámetro conductor [mm]

        # Variable para mostrar la inductancia (se mostrará en µH)
        self.var_L = tk.StringVar(value="")

        # Frame para agrupar los Entries
        self.frame_params = tk.Frame(frame_left, bd=1, relief="sunken")
        self.frame_params.pack(fill="x", pady=5)

        # Función auxiliar para añadir filas (etiqueta + entry)
        def add_param_row(label_text, var, row, width=8):
            lbl = tk.Label(self.frame_params, text=label_text)
            lbl.grid(row=row, column=0, sticky="w", padx=5, pady=2)
            ent = tk.Entry(self.frame_params, textvariable=var, width=width)
            ent.grid(row=row, column=1, padx=5, pady=2)

        rowcount = 0
        add_param_row("N (vueltas):",   self.var_N,     rowcount); rowcount += 1
        add_param_row("e_pvc (mm):",    self.var_e_pvc, rowcount); rowcount += 1
        add_param_row("r_int (mm):",    self.var_r_int, rowcount); rowcount += 1
        add_param_row("r_ext (mm):",    self.var_r_ext, rowcount); rowcount += 1
        add_param_row("h_c (mm):",      self.var_h_c,   rowcount); rowcount += 1
        add_param_row("d_cu (mm):",     self.var_d_cu,  rowcount); rowcount += 1

        # Campo para ver la inductancia en microHenrios (µH).
        lblL = tk.Label(self.frame_params, text="Induct. (µH):")
        lblL.grid(row=rowcount, column=0, sticky="w", padx=5, pady=2)
        entL = tk.Entry(self.frame_params, textvariable=self.var_L, width=10, state="readonly")
        entL.grid(row=rowcount, column=1, padx=5, pady=2)
        rowcount += 1

        # Botones
        btn_refrescar = tk.Button(
            frame_left, text="Refrescar Vista 2D",
            command=self.refrescar_2d
        )
        btn_refrescar.pack(pady=10)

        btn_calc_L = tk.Button(
            frame_left, text="Calcular Inductancia",
            command=self.calcular_inductancia
        )
        btn_calc_L.pack(pady=5)

        #
        # Sección de Logs
        #
        tk.Label(frame_left, text="Logs:").pack(anchor="w")
        self.log_area = ScrolledText(frame_left, wrap="word", height=8, width=35)
        self.log_area.pack(fill="x", padx=5, pady=5)
        self.log_area.configure(state="disabled")

        # Definimos etiquetas de color
        self.log_area.tag_config("info", foreground="green")
        self.log_area.tag_config("error", foreground="red")

        #
        # Figura y canvas para la bobina en 2D
        #
        self.fig = plt.Figure(figsize=(4,3), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.canvas_2d = FigureCanvasTkAgg(self.fig, master=self.frame_2d)
        self.canvas_2d_widget = self.canvas_2d.get_tk_widget()
        self.canvas_2d_widget.pack(fill="both", expand=True)

        # Dibujamos la bobina inicial
        self.refrescar_2d()

    def log_message(self, text, mode="info"):
        """
        Inserta un mensaje en el log_area, en color definido por 'mode'.
        mode puede ser 'info' (verde) o 'error' (rojo).
        """
        self.log_area.configure(state="normal")
        self.log_area.insert("end", text + "\n", mode)
        self.log_area.see("end")
        self.log_area.configure(state="disabled")

    def refrescar_2d(self):
        """
        Lee los valores de la bobina y llama a 'plot_coil_in_frame'
        para actualizar el dibujo en 2D.
        """
        N_val     = self.var_N.get()
        e_pvc_val = self.var_e_pvc.get()
        r_int_val = self.var_r_int.get()
        r_ext_val = self.var_r_ext.get()
        h_c_val   = self.var_h_c.get()
        d_cu_val  = self.var_d_cu.get()

        # Limpiar el Axes
        self.ax.clear()

        # Llamada a la función que inserta la figura en self.frame_2d
        plot_coil_in_frame(
            parent_frame=self.frame_2d,
            N=N_val,
            e_pvc=e_pvc_val,
            r_int=r_int_val,
            r_ext=r_ext_val,
            h_c=h_c_val,
            d_cu=d_cu_val
        )

    def calcular_inductancia(self):
        """
        Cálculo de la inductancia a partir de los parámetros introducidos.
        Muestra el resultado en µH en el campo correspondiente y en la log_area.
        """
        try:
            # Lectura de parámetros
            N_espiras = int(self.var_N.get())
            e_pvc_val = self.var_e_pvc.get()
            r_int_val = self.var_r_int.get()
            r_ext_val = self.var_r_ext.get()
            h_c_val   = self.var_h_c.get()
            d_cu_val  = self.var_d_cu.get()

            # Conversión mm -> m
            R_in = r_int_val / 1000.0
            R_out = r_ext_val / 1000.0
            plast = e_pvc_val / 1000.0
            altura = h_c_val / 1000.0
            diam_hilo = d_cu_val / 1000.0

            # Construir la geometría de la bobina
            espiras_xyz = compute_coil_geometry(
                N_total=N_espiras,
                R_int=R_in,
                R_ext=R_out,
                espesor_plast=plast,
                H=altura,
                wire_d=diam_hilo
            )

            # Radio efectivo para autoinductancia
            wire_radius = diam_hilo / 2.0

            # Calcular la inductancia total (en henrios)
            L_h = inductance_from_spiralist(espiras_xyz, wire_radius)
            L_uH = L_h * 1e6

            # Actualizar variable de resultado
            self.var_L.set(f"{L_uH:.3f}")

            # Log de éxito
            self.log_message("Inductancia calculada correctamente", "info")

        except ValueError as e:
            # Si hay algún problema con la geometría, etc.
            self.var_L.set("Error")
            self.log_message(f"Error en cálculo: {str(e)}", "error")
        except Exception as ex:
            # Errores generales
            self.var_L.set("Error")
            self.log_message(f"Error inesperado: {str(ex)}", "error")
