"""Put a visibly separated raised reference tail onto a downward donor rest chain.
No cuts, caps, remeshing or invented tail geometry. Preserve cross-sections/UVs.
"""
import numpy as np
from mathutils import Vector

def conform(o,rig,height):
 bones=[b for b in rig.data.bones if b.name.lower().startswith('tail')]
 if not bones:return {'applied':False}
 roots=[b for b in bones if b.parent not in bones]
 if len(roots)!=1:raise ValueError('Ambiguous tail chain')
 chain=[roots[0]]
 while True:
  children=[b for b in bones if b.parent==chain[-1]]
  if not children:break
  if len(children)!=1:raise ValueError('Branched tail chain')
  chain+=children
 root=rig.matrix_world@chain[0].head_local;tip=rig.matrix_world@chain[-1].tail_local
 if tip.z>=root.z-height*.2:return {'applied':False,'reason':'Donor already has a raised or horizontal rest tail'}
 points=np.array([list(v.co) for v in o.data.vertices]);cut=root.y+height*.075
 # Find the narrow shaft behind the rump, not an arbitrary donor-space cut.
 eligible=points[(points[:,1]>root.y)&(points[:,2]>height*.65)]
 edges=np.linspace(root.y,float(eligible[:,1].max()),33);narrow=[]
 for left,right in zip(edges[:-1],edges[1:]):
  slab=eligible[(eligible[:,1]>=left)&(eligible[:,1]<right)]
  narrow.append(len(slab)>=3 and np.ptp(slab[:,0])<height*.08)
 starts=[i for i in range(len(narrow)-1) if narrow[i] and narrow[i+1]]
 if not starts:raise ValueError('No narrow tail shaft separated from rump')
 cut=float(edges[starts[0]])
 ids=np.flatnonzero((points[:,1]>cut)&(points[:,2]>height*.65))
 if len(ids)<20:raise ValueError('Raised source tail cannot be isolated for rest-pose fit')
 cloud=points[ids];start=cloud[cloud[:,1]<np.percentile(cloud[:,1],10)].mean(0);end=cloud[cloud[:,1]>np.percentile(cloud[:,1],90)].mean(0)
 direction=end-start;direction/=np.linalg.norm(direction);u=(cloud-start)@direction;lo,hi=float(u.min()),float(u.max());u=(u-lo)/(hi-lo)
 centers=[];ts=[]
 for t in np.linspace(0,1,16):
  sub=cloud[abs(u-t)<.09]
  if len(sub):centers.append(np.median(sub,axis=0));ts.append(t)
 target=np.array([list(rig.matrix_world@b.head_local) for b in chain]+[list(tip)])
 target+=centers[0]-target[0]
 lengths=np.r_[0,np.cumsum(np.linalg.norm(np.diff(target,axis=0),axis=1))];lengths/=lengths[-1]
 centers=np.array(centers);ts=np.array(ts)
 attr=o.data.attributes.get('_TAIL_REGION') or o.data.attributes.new('_TAIL_REGION','FLOAT','POINT')
 maximum=0
 for i,t,p in zip(ids,u,cloud):
  source=np.array([np.interp(t,ts,centers[:,j]) for j in range(3)]);dest=np.array([np.interp(t,lengths,target[:,j]) for j in range(3)])
  eps=.035;ta=max(0,t-eps);tb=min(1,t+eps)
  sa=Vector([np.interp(ta,ts,centers[:,j]) for j in range(3)]);sb=Vector([np.interp(tb,ts,centers[:,j]) for j in range(3)])
  da=Vector([np.interp(ta,lengths,target[:,j]) for j in range(3)]);db=Vector([np.interp(tb,lengths,target[:,j]) for j in range(3)])
  rotation=(sb-sa).normalized().rotation_difference((db-da).normalized())
  radial=rotation@Vector(p-source);radius=height*(.012+.020*max(0.,min(1.,(float(t)-.72)/.20)))
  if radial.length>radius:radial*=radius/radial.length
  moved=Vector(dest)+radial;blend=min(1,float(t)/.16);blend=blend*blend*(3-2*blend)
  result=Vector(p).lerp(moved,blend);maximum=max(maximum,(result-Vector(p)).length);o.data.vertices[int(i)].co=result;attr.data[int(i)].value=1
 # Remove narrow spikes on the transported tuft without changing topology/UVs.
 selected=set(map(int,ids));adj={i:set() for i in selected}
 for edge in o.data.edges:
  a,b=edge.vertices
  if a in selected and b in selected:adj[a].add(b);adj[b].add(a)
 for _ in range(5):
  updates={i:o.data.vertices[i].co.lerp(sum((o.data.vertices[j].co for j in adj[i]),Vector())/len(adj[i]),.35) for i,t in zip(ids,u) if t>.18 and adj[int(i)]}
  for i,co in updates.items():o.data.vertices[int(i)].co=co
 return {'applied':True,'shaft_start_y':cut,'vertices':len(ids),'max_displacement_m':maximum,'radial_cap_height_fraction':[.012,.032],'smoothing_iterations':5,'method':'reference centreline transported onto donor rest chain; preserved radial shape and UVs'}
