use crate::*;
use crate::drop::{finish::{FinishContext,invalid,polyline,MAX_SOURCE_SEGMENTS},DropCutter};
use std::collections::BTreeMap;
type VertexKey=[u64;3];
fn key(p:Point3)->VertexKey {[p.x,p.y,p.z].map(|x|if x==0. {0} else {x.to_bits()})}
struct Trace {a:Point3,b:Point3,first:u32,second:u32,transverse:Point3}
fn clip_facet(t:&Triangle,a:Point3,b:Point3,r:f64,lo:&mut f64,hi:&mut f64)->bool {
let n=t.normal;
let contact=|p:Point3| {let center=p.add(Point3::Z.scale(r));center.sub(n.scale(n.dot(center.sub(t.a))))};
let pa=contact(a);let pb=contact(b);
for (u,v) in [(t.a,t.b),(t.b,t.c),(t.c,t.a)] {
let side=|p:Point3|v.sub(u).cross(p.sub(u)).dot(n);
let x=side(pa);let y=side(pb);let delta=y-x;
if delta==0. {if x<0. {return false;}} else if delta>0. {*lo=lo.max(-x/delta);} else {*hi=hi.min(-x/delta);}
}
*lo<=*hi
}
fn traces(ctx:&FinishContext<'_>,p:&PencilParams,runner:&mut Runner<'_>)->Result<Vec<Trace>,CamError> {
if ctx.cutter.kind()!=CutterKind::Ball {return Err(invalid("pencil bitangent tracing currently requires a ball cutter"));}
if !(0.0..180.0).contains(&p.bitangency_angle_deg) || p.extra_passes>1000 {return Err(invalid("invalid pencil bitangency angle or pass budget"));}
if ctx.operation.stock_to_leave.radial!=0. || ctx.operation.stock_to_leave.axial!=0. {return Err(invalid("pencil bitangent tracing requires zero stock allowance"));}
let mut edges:BTreeMap<(VertexKey,VertexKey),Vec<(u32,Point3,Point3)>>=BTreeMap::new();
for t in &ctx.mesh.triangles {
let f=&ctx.mesh.faces[t.face as usize];
if f.protected_only || !ctx.operation.targets.target_solids.contains(&f.solid) {continue;}
for (a,b) in [(t.a,t.b),(t.b,t.c),(t.c,t.a)] {let (ka,kb)=(key(a),key(b));let k=if ka<kb {(ka,kb)} else {(kb,ka)};edges.entry(k).or_default().push((t.id,a,b));}
runner.work(1,"pencil-adjacency",t.id as u64,ctx.mesh.triangle_count() as u64)?;
}
let r=ctx.cutter.radius();let mut out=Vec::new();
for entries in edges.values() {
if entries.len()!=2 {continue;}
let (id,a,b)=entries[0];let other=entries[1].0;let t1=ctx.mesh.triangle(id);let t2=ctx.mesh.triangle(other);
let (n1,n2)=(t1.normal,t2.normal);
if n1.z < -ctx.tolerance.angle_eps || n2.z < -ctx.tolerance.angle_eps {continue;}
let cosine=n1.dot(n2).clamp(-1.,1.);let angle=cosine.acos().to_degrees();
if angle<=p.bitangency_angle_deg.max(ctx.tolerance.angle_eps.to_degrees()) || 1.+cosine<=ctx.tolerance.angle_eps {continue;}
if t2.vertices().iter().map(|v|n1.dot(v.sub(a))).fold(f64::NEG_INFINITY,f64::max)<=ctx.tolerance.distance_eps {continue;}
let d=b.sub(a);let len=d.x.hypot(d.y);if len<=ctx.tolerance.sample_eps {continue;}
let transverse=Point3::new(-d.y/len,d.x/len,0.);
let clearance=(ctx.operation.tolerance/32.).max(ctx.tolerance.sample_eps*2.+ctx.tolerance.distance_eps*4.);
if n1.add(n2).scale(clearance/(1.+cosine)).length()>ctx.operation.tolerance*0.125 {return Err(invalid("pencil numeric clearance exceeds contact tolerance"));}
let offset=n1.add(n2).scale((r+clearance)/(1.+cosine)).sub(Point3::Z.scale(r));
let start=a.add(offset);let end=b.add(offset);
let (mut lo,mut hi)=(0.,1.);
if !clip_facet(t1,start,end,r,&mut lo,&mut hi)||!clip_facet(t2,start,end,r,&mut lo,&mut hi)||hi-lo<=ctx.tolerance.sample_eps/len {continue;}
out.push(Trace {a:start.lerp(end,lo),b:start.lerp(end,hi),first:id,second:other,transverse});
if out.len()>MAX_SOURCE_SEGMENTS {return Err(invalid("pencil trace work budget exceeded"));}
runner.work(1,"pencil-bitangents",out.len() as u64,edges.len() as u64)?;
}
if out.is_empty() {return Err(invalid("no finite concave bitangent pencil traces satisfy the angle threshold"));}Ok(out)
}
pub fn generate(operation:&Operation,cutter:&Cutter,mesh:&MeshSnapshot,index:Option<&dyn TriangleIndex>,heights:&Heights,feeds:&FeedsSpeeds,tolerance:Tolerance,runner:&mut Runner<'_>)->Result<Toolpath,CamError> {
let p=match &operation.strategy {Strategy::Pencil(p)=>p,_=>return Err(invalid("pencil generator requires Pencil parameters"))};
let ctx=FinishContext {operation,cutter,mesh,index,heights,feeds,tolerance};ctx.validate()?;
let raw=traces(&ctx,p,runner)?;let drop=DropCutter::new(mesh,cutter,index,tolerance)?;
let mut source=Vec::new();let mut count=WorkCounts::default();let mut traces=Vec::new();let mut clipped=false;
for trace in raw {
let n=(trace.a.distance(trace.b)/operation.tolerance.min(cutter.radius()*0.25)).ceil().max(1.);
if n>MAX_SOURCE_SEGMENTS as f64 || count.samples.saturating_add(n as u64+1)>MAX_SOURCE_SEGMENTS as u64 {return Err(invalid("pencil clearance sampling budget exceeded"));}
let mut start=None;let mut last=None;
for i in 0..=n as usize {
let point=trace.a.lerp(trace.b,i as f64/n);
let hit=drop.point(point.x,point.y,ctx.sampling().floor_z,runner)?;
count.samples+=1;count.candidates+=hit.candidate_count;count.contacts+=hit.contact_count;
let clear=hit.point.contact.is_some() && (hit.point.z-point.z).abs()<=operation.tolerance*0.25;
if clear {if start.is_none(){start=Some(point);}last=Some(point);}else{clipped=true;}
if !clear || i==n as usize {
if let (Some(a),Some(b))=(start,last) {if a.distance_xy(b)>tolerance.sample_eps {traces.push(Trace{a,b,first:trace.first,second:trace.second,transverse:trace.transverse});}}
start=None;last=None;
}
}
}
if traces.is_empty() {return Err(invalid("all pencil bitangent traces are blocked by protected geometry"));}
for trace in &traces {
for t in [0.,0.5,1.] {
let a=trace.a.lerp(trace.b,t);let center=a.add(Point3::Z.scale(cutter.radius()));
for id in [trace.first,trace.second] {let facet=mesh.triangle(id);if (facet.normal.dot(center.sub(facet.a))-cutter.radius()).abs()>(operation.tolerance/32.).max(tolerance.sample_eps*2.+tolerance.distance_eps*4.)+tolerance.distance_eps*4. {return Err(invalid("pencil offset planes lost bitangency"));}}
let result=drop.point(a.x,a.y,ctx.sampling().floor_z,runner)?;
count.samples+=1;count.candidates+=result.candidate_count;count.contacts+=result.contact_count;
if (result.point.z-a.z).abs()>operation.tolerance {return Err(invalid("pencil bitangent is blocked by protected geometry"));}
}
for extra in 0..=p.extra_passes {
let offset=if extra==0 {0.} else {((extra+1)/2) as f64*cutter.radius()*0.5*if extra%2==1 {1.} else {-1.}};
let a=trace.a.add(trace.transverse.scale(offset));let b=trace.b.add(trace.transverse.scale(offset));
source.push(polyline(&[Point2::new(a.x,a.y),Point2::new(b.x,b.y)])?);
}
}
let mut result=ctx.generate(&p.containment,&source,runner)?;
for motion in &result.motions {
if let MotionKind::Cut {to}=motion.kind {
for trace in &traces {
let d=trace.b.sub(trace.a);let len=d.x.hypot(d.y);
let lateral=((to.x-trace.a.x)*d.y-(to.y-trace.a.y)*d.x).abs()/len;
if lateral<=tolerance.point_eps {
let along=((to.x-trace.a.x)*d.x+(to.y-trace.a.y)*d.y)/(len*len);
if along < -tolerance.point_eps/len || along>1.+tolerance.point_eps/len {continue;}
let expected=trace.a.z+d.z*along;
if (to.z-expected).abs()>operation.tolerance+tolerance.distance_eps {return Err(invalid("pencil path lost its two-surface contact"));}
}
}
}
}
result.report.approximations.push(ApproximationLimit{kind:"pencil-facet-clearance".into(),bound:Some((operation.tolerance/32.).max(tolerance.sample_eps*2.+tolerance.distance_eps*4.)),detail:Some("Outward normal clearance from each contact plane prevents ambiguous first contact at a vertical wall top; bounded inside operation tolerance".into())});
if clipped {result.report.warn(Diagnostic::new("pencil-trimmed-contact","Two-facet traces were clipped where additional protected surfaces prevent bitangent contact"));}
result.report.counts.samples+=count.samples;result.report.counts.candidates+=count.candidates;result.report.counts.contacts+=count.contacts;
if p.extra_passes>0 {result.report.approximations.push(ApproximationLimit {kind:"pencil-extra-pass-spacing".into(),bound:Some(cutter.radius()*0.5),detail:Some("Extra passes alternate sides at half-tool-radius increments; current PencilParams has no spacing field".into())});}
Ok(result)
}