"""Параметрическая геометрия ТВВ-320. Встроенный Python FEMM 5.

Самостоятельная реализация геометрических построений. Размеры читаются из
machine.json; программы и модели sim не импортируются. Координаты — миллиметры.
"""
import math


def hypot(x,y):
    # MicroPython FEMM 5 does not provide math.hypot.
    return math.sqrt(x*x+y*y)


def rotate_point(point, angle):
    c, s = math.cos(angle), math.sin(angle)
    return (c * point[0] - s * point[1], s * point[0] + c * point[1])


def polar_point(radius, angle):
    return (radius * math.cos(angle), radius * math.sin(angle))


def fillet_corner(before, corner, after, radius):
    """Касательные и центр окружности, вписанной в угол двух прямых."""
    a = (before[0] - corner[0], before[1] - corner[1])
    b = (after[0] - corner[0], after[1] - corner[1])
    la, lb = hypot(*a), hypot(*b)
    a, b = (a[0]/la, a[1]/la), (b[0]/lb, b[1]/lb)
    half = math.acos(max(-1.0, min(1.0, a[0]*b[0] + a[1]*b[1]))) / 2
    distance = radius / math.tan(half)
    if distance >= min(la, lb):
        raise ValueError('Скругление не помещается между соседними вершинами')
    start = (corner[0]+a[0]*distance, corner[1]+a[1]*distance)
    end = (corner[0]+b[0]*distance, corner[1]+b[1]*distance)
    direction = (a[0]+b[0], a[1]+b[1])
    norm = hypot(*direction)
    center = (corner[0]+direction[0]/norm*radius/math.sin(half),
              corner[1]+direction[1]/norm*radius/math.sin(half))
    angle = math.atan2(end[1]-center[1], end[0]-center[0]) - math.atan2(start[1]-center[1], start[0]-center[0])
    angle = (angle + math.pi) % (2*math.pi) - math.pi
    return start, end, center, angle * 180 / math.pi


def stator_profile(machine):
    g, s = machine['geometry'], machine['stator']
    radius = g['rotor_body_diameter_mm']/2 + g['air_gap_mm']
    a, b = s['slot_width_mm']/2, s['slot_wedge_width_mm']/2
    top = math.sqrt(radius*radius-a*a)
    bottom = radius + s['slot_height_mm']
    shelf = bottom - s['slot_straight_height_mm']
    neck = shelf - s['slot_wedge_height_mm']
    slope = neck + (b-a)/math.tan(math.radians(s['slot_wedge_angle_deg']))
    if not top < neck < slope < shelf < bottom:
        raise ValueError('Неверный порядок уровней паза статора')
    points = [(-a,top),(-a,neck),(-b,slope),(-b,shelf),(-a,shelf),(-a,bottom),
              (a,bottom),(a,shelf),(b,shelf),(b,slope),(a,neck),(a,top)]
    return [(points[i],points[i+1],0.0) for i in range(len(points)-1)]


def rotor_profile(machine):
    g, r = machine['geometry'], machine['rotor']
    radius = g['rotor_body_diameter_mm']/2
    a, b = r['slot_width_mm']/2, r['slot_wedge_width_mm']/2
    top = math.sqrt(radius*radius-a*a)
    ledge = radius-r['slot_l2_mm']
    shelf = ledge-r['slot_l3_mm']
    corner = (-a,radius-r['slot_l1_mm'])
    start,end,center,angle = fillet_corner((-a,top),corner,(-b,ledge),r['slot_fillet_radius_mm'])
    bottom = radius-r['slot_height_mm']
    rb = r['slot_bottom_radius_mm']
    if abs(rb-a)>1e-9:
        raise ValueError('В этом профиле полукруглое дно: R = b/2')
    left = [((-a,top),start,0.0),(start,end,angle),(end,(-b,ledge),0.0),
            ((-b,ledge),(-b,shelf),0.0),((-b,shelf),(-a,shelf),0.0),
            ((-a,shelf),(-a,bottom+rb),0.0)]
    edges = left + [((-a,bottom+rb),(a,bottom+rb),180.0)]
    for p,q,arc in reversed(left):
        edges.append(((-q[0],q[1]),(-p[0],p[1]),arc))
    return edges


def make_geometry(machine, winding, theta_deg=0.0, eccentricity_mm=0.0, rotor_discrete=True):
    """Создать геометрические объекты в памяти; здесь нет вызовов решателя."""
    theta = math.radians(theta_deg)
    g, s, r = machine['geometry'],machine['stator'],machine['rotor']
    edges, labels = [], []

    def edge(a,b,angle=0.0,group=0,boundary=''):
        if hypot(a[0]-b[0],a[1]-b[1])<1e-8:
            raise ValueError('Вырожденное ребро')
        edges.append({'a':a,'b':b,'angle':angle,'group':group,'boundary':boundary})

    def circle(radius,group,cx=0.0,boundary=''):
        edge((cx+radius,0),(cx-radius,0),180,group,boundary)
        edge((cx-radius,0),(cx+radius,0),180,group,boundary)

    def transform(p,phi,offset=0.0):
        x,y=rotate_point(p,phi-math.pi/2)
        return (x+offset,y)

    def label(p,material,group,circuit='',turns=0,role=''):
        labels.append({'x':p[0],'y':p[1],'material':material,'group':group,
                       'circuit':circuit,'turns':turns,'role':role})

    def rectangle(x,y,width,height,phi,offset,group,material,circuit,turns,role):
        pts=[(x-width/2,y),(x+width/2,y),(x+width/2,y+height),(x-width/2,y+height)]
        pts=[transform(p,phi,offset) for p in pts]
        for i in range(4):edge(pts[i],pts[(i+1)%4],0,group)
        label(transform((x,y+height/2),phi,offset),material,group,circuit,turns,role)

    def slotted_circle(profile,angles,radius,group,offset=0.0):
        angles=sorted(a%(2*math.pi) for a in angles)
        alpha=math.asin(abs(profile[0][0][0])/radius)
        for i,phi in enumerate(angles):
            for a,b,arc in profile:edge(transform(a,phi,offset),transform(b,phi,offset),arc,group)
            nxt=angles[(i+1)%len(angles)]
            sweep=(nxt-phi)%(2*math.pi)-2*alpha
            if sweep<=0:raise ValueError('Пазы пересекаются на поверхности')
            edge(transform(profile[0][0],phi,offset),transform(profile[-1][1],nxt,offset),
                 math.degrees(sweep),group)

    rr=g['rotor_body_diameter_mm']/2
    rs=rr+g['air_gap_mm']
    stator_angles=[math.radians(machine['conventions']['stator_slot1_deg'])+2*math.pi*k/s['slots'] for k in range(s['slots'])]
    slotted_circle(stator_profile(machine),stator_angles,rs,20)
    circle(g['stator_back_diameter_mm']/2,20)
    rotor_angles=[]
    for pole in range(2):
        for k in range(r['working_slots']//2):
            rotor_angles.append(theta+math.pi/2+pole*math.pi+(k-(r['working_slots']/2-1)/2)*2*math.pi/r['slot_divisions'])
    slotted_circle(rotor_profile(machine),rotor_angles,rr,10,eccentricity_mm)
    circle(g['rotor_inner_diameter_mm']/2,10,eccentricity_mm)
    circle(g['outer_air_radius_factor']*g['stator_back_diameter_mm']/2,90,0,'Внешняя граница A=0')
    label((eccentricity_mm,0),'Воздух',10,role='центральное отверстие')
    label((eccentricity_mm+rr*.5,0),'Сталь ротора',10,role='бочка ротора')
    label(polar_point((rs+g['stator_back_diameter_mm']/2)/2,math.radians(3)),'Сталь статора',20,role='ярмо статора')
    label((rr+g['air_gap_mm']/2,0),'Воздух',0,role='зазор и открытые пазы')
    label((g['stator_back_diameter_mm']*.75,0),'Воздух',90,role='внешний воздух')

    # Пара катушки: нижняя сторона s и верхняя сторона s+y с противоположным знаком.
    by_slot={int(row['slot']):row for row in winding}
    for k,phi in enumerate(stator_angles):
        for layer in ('lower','upper'):
            row=by_slot[k+1]
            origin=k if layer=='lower' else (k-s['coil_span_slots'])%s['slots']
            source=by_slot[origin+1]
            phase=source['lower_phase']
            sign=int(source['lower_sign'])*(1 if layer=='lower' else -1)
            if phase!=row[layer+'_phase'] or sign!=int(row[layer+'_sign']):
                raise ValueError('Обмоточная таблица не согласована с шагом катушки')
            branch=1 if int(source['lower_sign'])>0 else 2
            off=s['lower_winding_offset_mm'] if layer=='lower' else s['upper_winding_offset_mm']
            rectangle(0,rs+s['slot_height_mm']-off-s['conductor_height_mm'],
                      s['conductor_width_mm'],s['conductor_height_mm'],phi,0,100+2*'ABC'.index(phase)+branch,
                      'Область обмотки',phase+str(branch)+' — ветвь статора',sign,
                      'паз %d, %s'%(k+1,layer))
    for k,phi in enumerate(rotor_angles):
        bottom=rr-r['slot_height_mm']+r['winding_bottom_offset_mm']
        sign=1 if k<r['working_slots']//2 else -1
        if rotor_discrete:
            pitch=(r['winding_height_mm']-r['conductor_height_mm'])/(r['turns_per_slot']-1)
            for turn in range(r['turns_per_slot']):
                rectangle(0,bottom+turn*pitch,r['conductor_width_mm'],r['conductor_height_mm'],phi,eccentricity_mm,
                          11,'Область обмотки','F — возбуждение ротора',sign,'ротор, паз %d, сторона %d'%(k+1,turn+1))
        else:
            rectangle(0,bottom,r['conductor_width_mm'],r['winding_height_mm'],phi,eccentricity_mm,
                      11,'Область обмотки','F — возбуждение ротора',sign*r['turns_per_slot'],'ротор, паз %d'%(k+1))
    return {'edges':edges,'labels':labels,'theta_deg':theta_deg,'eccentricity_mm':eccentricity_mm,
            'rotor_discrete':rotor_discrete,'stator_slots':s['slots'],'rotor_slots':r['working_slots']}


def emit_geometry(geometry, api, mesh):
    """Передать отдельные узлы, отрезки, дуги и метки штатному API FEMM."""
    nodes={}
    for item in geometry['edges']:
        for x,y in (item['a'],item['b']):
            key='%.8f,%.8f'%(x,y)
            if key not in nodes:
                api['mi_addnode'](x,y)
                nodes[key]=True
    for item in geometry['edges']:
        a,b,angle=item['a'],item['b'],item['angle']
        if angle:
            if angle<0:a,b,angle=b,a,-angle
            api['mi_addarc'](a[0],a[1],b[0],b[1],angle,mesh['arc_deg'])
            # Середина дуги с учётом её кривизны, а не середина хорды.
            chord=(b[0]-a[0],b[1]-a[1]);half=math.radians(angle)/2
            cot=math.cos(half)/math.sin(half)
            center=((a[0]+b[0])/2-chord[1]*cot/2,(a[1]+b[1])/2+chord[0]*cot/2)
            vec=rotate_point((a[0]-center[0],a[1]-center[1]),half)
            api['mi_selectarcsegment'](center[0]+vec[0],center[1]+vec[1])
            api['mi_setarcsegmentprop'](mesh['arc_deg'],item['boundary'],0,item['group'])
        else:
            api['mi_addsegment'](a[0],a[1],b[0],b[1])
            api['mi_selectsegment']((a[0]+b[0])/2,(a[1]+b[1])/2)
            api['mi_setsegmentprop'](item['boundary'],mesh['edge_mm'],0,0,item['group'])
        api['mi_clearselected']()
    for row in geometry['labels']:
        api['mi_addblocklabel'](row['x'],row['y'])
        api['mi_selectlabel'](row['x'],row['y'])
        size=mesh['copper_mm'] if row['circuit'] else mesh['iron_mm']
        if row['material']=='Воздух':size=mesh['outer_mm'] if row['group']==90 else mesh['gap_mm']
        api['mi_setblockprop'](row['material'],0,size,row['circuit'],0,row['group'],row['turns'])
        api['mi_clearselected']()
    return len(nodes)
