"""MFS outer/strip potential-flow representation with prescribed circulation."""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np


def _images(y0: float, H: float, layers: int, vortex: bool):
    for k in range(-layers,layers+1):
        yield y0+4*k*H, 1.0
        yield 2*H-y0+4*k*H, -1.0 if vortex else 1.0


def singular_velocity(points, source, *, H=None, layers=0, vortex=False):
    p=np.asarray(points,float); x0,y0=map(float,source); vel=np.zeros_like(p)
    imgs=[(y0,1.0)] if H is None else list(_images(y0,float(H),int(layers),vortex))
    for yi,sgn in imgs:
        dx=p[:,0]-x0; dy=p[:,1]-yi; r2=dx*dx+dy*dy
        if np.any(r2 <= 1e-24): raise ValueError("evaluation at singularity")
        if vortex: vel += sgn*np.column_stack((-dy/r2,dx/r2))/(2*np.pi)
        else: vel += sgn*np.column_stack((dx/r2,dy/r2))/(2*np.pi)
    return vel


@dataclass
class MFSSolution:
    centers: np.ndarray; radius: float; strengths: np.ndarray; source_points: np.ndarray
    circulations: np.ndarray; u_inf: float; H: float|None; image_layers: int
    def velocity(self, points):
        p=np.asarray(points,float); v=np.zeros_like(p); v[:,0]=self.u_inf
        for q,z in zip(self.strengths,self.source_points):
            v += q*singular_velocity(p,z,H=self.H,layers=self.image_layers)
        for g,z in zip(self.circulations,self.centers):
            v += g*singular_velocity(p,z,H=self.H,layers=self.image_layers,vortex=True)
        return v


def solve_mfs(centers, radius, circulations, *, u_inf=1.0, n_boundary=96,
              source_ratio=0.65, H=None, image_layers=20):
    c=np.asarray(centers,float); gam=np.asarray(circulations,float)
    if c.shape != (len(gam),2): raise ValueError("centers/circulations mismatch")
    th=(np.arange(n_boundary)+0.5)*2*np.pi/n_boundary
    normals=np.column_stack((np.cos(th),np.sin(th)))
    bpts=np.concatenate([ci+radius*normals for ci in c])
    nrms=np.tile(normals,(len(c),1))
    spts=np.concatenate([ci+source_ratio*radius*normals for ci in c])
    base=np.zeros_like(bpts); base[:,0]=u_inf
    for g,ci in zip(gam,c): base += g*singular_velocity(bpts,ci,H=H,layers=image_layers,vortex=True)
    rhs=-np.einsum('ij,ij->i',base,nrms)
    # Eliminate one source per body so every body's net source is exactly zero.
    cols=[]
    for ib in range(len(c)):
        ref=spts[ib*n_boundary+n_boundary-1]
        vr=singular_velocity(bpts,ref,H=H,layers=image_layers)
        for j in range(n_boundary-1):
            v=singular_velocity(bpts,spts[ib*n_boundary+j],H=H,layers=image_layers)-vr
            cols.append(np.einsum('ij,ij->i',v,nrms))
    A=np.column_stack(cols); coeff,resid,rank,sv=np.linalg.lstsq(A,rhs,rcond=1e-12)
    strengths=np.zeros(len(spts)); k=0
    for ib in range(len(c)):
        q=coeff[k:k+n_boundary-1]; k+=n_boundary-1
        strengths[ib*n_boundary:ib*n_boundary+n_boundary-1]=q
        strengths[ib*n_boundary+n_boundary-1]=-np.sum(q)
    sol=MFSSolution(c,float(radius),strengths,spts,gam,float(u_inf),H,int(image_layers))
    residual=np.einsum('ij,ij->i',sol.velocity(bpts),nrms)
    return sol,{"boundary_Linf":float(np.max(np.abs(residual))),"boundary_L2":float(np.sqrt(np.mean(residual**2))),
                "rank":int(rank),"unknowns":int(A.shape[1]),"condition":float(sv[0]/sv[-1])}
