##################################################
#        Concentric Wire Loop Inductance         #
#            Author: Leon Teichroeb              #
#               Date: 16.04.2024                 #
##################################################
import numpy as np
from scipy.special import ellipkinc, ellipeinc

VAC_PERMEABILITY = 1.256637e-6


def loop_self_inductance(r, wire_diameter):
    """
    Calculate self-inductance of a circularly symmetric wire loop.
    """
    return VAC_PERMEABILITY * np.abs(r) * np.log((16*np.abs(r)/wire_diameter) - 2) 


def loop_mutual_inductance(r1, r2, z1, z2):
    """
    Calculate the mutual inductance between two wire loops, concentric and symmteric around the z axis.
    """
    k = (r1**2 + r2**2 + z1**2 + z2**2 - 2*z1*z2)/(2*r1*r2)
    fac = VAC_PERMEABILITY * (r1*r2/(2*(k-1)))**0.5
    ellip1 = ellipkinc(np.pi, -2/(k-1))
    ellip2 = ellipeinc(np.pi, -2/(k-1))
    return fac * (k*ellip1 + (1-k)*ellip2)


def inductance_for_paths(le1, le2, SI=False, D=1e-3):
    """
    Calculates the inductance between two wire loops with diameter D.
    The parameters le1 and le2 are lists with elements of the form (x, y, z, length, angle, factor).
    The factor is an arbitrary scaling factor, such as the number of turns for example.
    """
    le1_x   = le1[0, :]
    le2_x   = le2[0, :]
    le1_y   = le1[1, :]
    le2_y   = le2[1, :]
    le1_z   = le1[2, :]
    le2_z   = le2[2, :]
    le1_dx  = le1[3, :]
    le2_dx  = le2[3, :]
    le1_dy  = le1[4, :]
    le2_dy  = le2[4, :]
    le1_dz  = le1[5, :]
    le2_dz  = le2[5, :]
    le1_fac = le1[6, :]
    le2_fac = le2[6, :]

    distances = \
        ( np.subtract.outer(le1_x, le2_x)**2
        + np.subtract.outer(le1_y, le2_y)**2
        + np.subtract.outer(le1_z, le2_z)**2)**0.5
    
    if SI:
        # Limit the minimum distance between elements
        distances[distances < D/4] = D/4
    
    # Mutual inductance: double integral Neumann formula https://en.wikipedia.org/wiki/Inductance
    # M = u0/4pi * dx_m dot dx_n / |x_m-x_n|
    L = 1e-7 * np.outer(le1_fac, le2_fac) * \
        (np.outer(le1_dx, le2_dx) + np.outer(le1_dy, le2_dy) + np.outer(le1_dz, le2_dz)) / distances

    #if SI:
    #    L[range(len(le1)), range(len(le1))] = 0.0 #L11 * le1_fac[0] * le2_fac[0]
    #    d_length = np.linalg.norm([le1_dx, le1_dy, le1_dz], axis=0)
    #    length = np.sum(d_length)
    #    return np.sum(L) + 1e-7 * length * 0.5 * le1_fac[0] * le2_fac[0]
    #else:
    return np.sum(L)

def radial_inductance_numeric(r_in, r_out, n=1000):
    """
    Rough calculation of the self inductance of the radial connections.

    :param r_in: [position, vector, element length]
    :param r_out: Thickness of the wire [m], optional.
    :param n: number of divisions.
    :return: L (Mutual inductance matrix)
    """

    line_elements = []
    dr = r_out - r_in

    for i in np.linspace(0, 2 * np.pi, int(np.ceil(n)))[:-1]:
        line_elements.append([[(r_in + 0.5 * dr) * np.cos(i), (r_in + 0.5 * dr) * np.sin(i), 0.0], [np.cos(i), np.sin(i), 0.0], dr])
    D = 2 * np.pi * (r_in + dr) / n

    L = np.zeros((len(line_elements), 1))
    line_elements = [[np.array(line_elements[i][0]), np.array(line_elements[i][1]), line_elements[i][2]] for i in range(len(line_elements))]

    for i in range(len(line_elements)):
        for j in [0]:
            a = line_elements[i][2]
            b = line_elements[j][2]
            if j == i:
                # Self inductance (Link doesn't work anymore)
                L11 = 1e-9 * 2 * b * 100 * (
                        np.log(2 * b / D * (1 + (1 + (D / (2 * b)) ** 2) ** 0.5)) - (
                        1 + (D / (2 * b)) ** 2) ** 0.5 + 1 / 4 + D / (2 * b))
            else:
                # Mutual inductance: double integral Neumann formula https://en.wikipedia.org/wiki/Inductance
                # M = u0/4pi * dx_m dot dx_n / |x_m-x_n|
                Dxyz = np.linalg.norm(line_elements[i][0] - line_elements[j][0])
                ab = np.dot(line_elements[i][1], line_elements[j][1])
                abab = ab / (a * b)
                abab = 1.0 if abab > 1.0 else abab
                abab = -1.0 if abab < -1.0 else abab
                angle = np.arccos(abab)
                L11 = 1e-7 * a * b * np.cos(angle) / Dxyz if Dxyz > 1e-5 else 0.0
            L[i, j] = L11
    return np.sum(np.abs(L))/len(L)
