"""Pressed-wheel / hollow tyre prototypes from factory nominal sizes.

The fitted markings are unknown. The reference profile is unloaded and does
not prescribe tyre fitment, operating clearance or wheel manufacturing.
"""
from pathlib import Path
import json
import math
import cadquery as cq
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRepTools import BRepTools
from front_wing_geometry import absolute_tessellation

OUT=Path(__file__).parent
B=json.loads((OUT.parent/'body-data.json').read_text())
R=B['wheel_profile_reference'];N=R['factory_nominals'];S=2032/2.4
X=cq.Vector(1,0,0)
BEAD_R=N['bead_seat_diameter_mm']/2
BEAD_X=N['pressed_bead_seat_width_mm']/2
SECTION_HALF=N['tyre_designation_section_width_mm']/2
OUTER_R=.335*S  # retain old unmeasured drawing radius; do not infer aspect ratio
GAUGE=2.5       # radial construction offset, not a measured normal steel gauge
PCD=B['rear_hub_reference']['catalog_pcd_mm']
parts=[]
def v(x,r):return cq.Vector(x,r,0)
def revolved_polygon(points):
    return cq.Workplane('XY').polyline(points).close().revolve(360,(0,0,0),(1,0,0)).val()
def line(a,b):return cq.Edge.makeLine(v(*a),v(*b))
def bezier(points):return cq.Edge.makeBezier([v(*p) for p in points])
def reflected(points):return [(-x,r) for x,r in points]
def cyl(r,l,x,y=0,z=0):return cq.Solid.makeCylinder(r,l,cq.Vector(x,y,z),X)
def add(name,shape,color):
    assert shape.isValid() and shape.Volume()>0,name
    parts.append((name,shape,color))

# Rim seats are separated by the nominal4½ inch width at nominal13 inch
# diameter. Flange lips, drop center and radial gauge are estimated profiles.
rim_outer=[(-BEAD_X-10,178),(-BEAD_X-3.85,178),(-BEAD_X,BEAD_R),
           (-45,BEAD_R),(-32,150),(32,150),(45,BEAD_R),
           (BEAD_X,BEAD_R),(BEAD_X+3.85,178),(BEAD_X+10,178)]
rim=revolved_polygon(rim_outer+[(x,r-GAUGE) for x,r in reversed(rim_outer)])
# Four formed spokes share the retained stud-sector mounting lands. Paired vents
# are separated by a recessed web in each quadrant, as visible in IMG6341.
# Radial/axial sections below are construction estimates, not a pressed part
# survey, stress model or manufacturing/tooling geometry.
RADII=[29,38,45,65,72,80,90,100,110,120,130,140,147,150]
SPOKE_RISE_XR=[(29,0),(38,0),(45,8),(65,20),(90,23),(105,22),(125,16),(145,7),(150,0)]
def front_face_x(r,angle):
    spoke=max(0,math.cos(4*angle))**2
    amplitude=next(y+(q-y)*(r-a)/(b-a)for (a,y),(b,q)in zip(SPOKE_RISE_XR,SPOKE_RISE_XR[1:])if a<=r<=b)
    u=max(0,(r-65)/85)
    base=25+5*u+5*math.sin(math.pi*u)
    return base+amplitude*spoke
def pressing_section(angle):
    front=[cq.Vector(front_face_x(r,angle),r*math.cos(angle),r*math.sin(angle))for r in RADII]
    rear=[p-cq.Vector(5 if r<=65 else 5-2.5*(r-65)/85,0,0)for p,r in zip(front,RADII)]
    return cq.Wire.makePolygon(front+list(reversed(rear))+[front[0]])
# Fine ruled angular sections preserve the flat sectors and forming bounds;
# the former smooth angular loft overshot them near the mounting face.
quadrants=[cq.Solid.makeLoft([pressing_section(math.radians(q*90+i*3.75))for i in range(25)],ruled=True)for q in range(4)]
center=quadrants[0].fuse(*quadrants[1:]).clean()
center=center.cut(cyl(29,120,-20))
vent_tools=[]
for q in range(4):
    for sign in [-1,1]:
        points=[]
        for radius,angle in [(110,3),(142,3),(142,32),(110,32)]:
            angle=math.radians(q*90+45+sign*angle)
            points.append((radius*math.cos(angle),radius*math.sin(angle)))
        tool=cq.Workplane('YZ',origin=(-80,0,0)).polyline(points).close().extrude(180).edges('|X').fillet(4).val()
        vent_tools.append(tool);center=center.cut(tool)
bolt_axes=[]
for i in range(4):
    a=math.pi/4+i*math.pi/2;y,z=PCD/2*math.sin(a),PCD/2*math.cos(a)
    bolt_axes.append([y,z])
    center=center.cut(cyl(5.2,50,0,y,z))
wheel=rim.fuse(center).clean()
assert len(wheel.Solids())==1
# A separate thin bright trim-ring envelope follows the photographed outer
# face. Clip geometry and actual attachment are not inferred.
trim_profile=[(36,150),(46,158),(59,162),(61,168),(69,175),(70,178)]
trim=revolved_polygon(trim_profile+[(x+.6,r)for x,r in reversed(trim_profile)])
assert trim.isValid()and len(trim.Solids())==1

# Curved, hollow rubber section. Convex Bezier control hulls bound the nominal
# designation width and retained outer radius; subsidiary dimensions are guesses.
outer_curves=[[(BEAD_X+10,178),(70,185),(SECTION_HALF,204),(SECTION_HALF,220)],
              [(SECTION_HALF,220),(SECTION_HALF,250),(68,280.5844444444),(55,282)],
              [(55,282),(40,OUTER_R),(20,OUTER_R),(0,OUTER_R)]]
inner_curves=[[(45,BEAD_R),(45,177),(64,187),(64,220)],
              [(64,220),(64,248),(58,273),(45,275)],
              [(45,275),(30,OUTER_R-7),(15,OUTER_R-7),(0,OUTER_R-7)]]
edges=[line((BEAD_X,BEAD_R),(BEAD_X+3.85,178)),
       line((BEAD_X+3.85,178),(BEAD_X+10,178))]
edges.extend(bezier(p) for p in outer_curves)
edges.extend(bezier(list(reversed(reflected(p)))) for p in reversed(outer_curves))
edges.extend([line((-BEAD_X-10,178),(-BEAD_X-3.85,178)),
              line((-BEAD_X-3.85,178),(-BEAD_X,BEAD_R)),
              line((-BEAD_X,BEAD_R),(-45,BEAD_R))])
edges.extend(bezier(reflected(p)) for p in inner_curves)
edges.extend(bezier(list(reversed(p))) for p in reversed(inner_curves))
edges.append(line((45,BEAD_R),(BEAD_X,BEAD_R)))
profile=cq.Wire.assembleEdges(edges)
tyre=cq.Solid.revolve(profile,[],360,cq.Vector(0,0,0),X)
assert tyre.isValid() and len(tyre.Solids())==1
assert tyre.intersect(wheel).Volume()<1e-4
cap=cyl(35,8,27).cut(cyl(31,10,26)).fuse(cyl(35,2,35)).clean()
for sign,label in [(1,'left'),(-1,'right')]:
    for role,shape,color in [('rim_and_pressed_center',wheel,(.16,.18,.16)),
                             ('hollow_tyre',tyre,(.08,.095,.085)),
                             ('center_cap',cap,(.60,.64,.59)),
                             ('bright_rim_trim',trim,(.72,.75,.72))]:
        add('wheel_'+label+'_'+role+'_reference',shape if sign>0 else shape.mirror('YZ'),color)

# All four installed instances use these two handed prototypes at their old
# track/axle centers. The STEP is a local prototype, not an installed car survey.
assembly=cq.Assembly(name='unregistered_pressed_wheel_and_tyre_prototypes');meshes=[]
for name,shape,color in parts:
    assembly.add(shape,name=name,color=cq.Color(*color))
    BRepTools.Clean_s(shape.wrapped)
    m=BRepMesh_IncrementalMesh(shape.wrapped,.12,False,.18,False);assert m.IsDone()
    vs,ts=absolute_tessellation(shape,.06,.12)if 'rim_and_pressed_center'in name else shape.tessellate(.15,.18)
    meshes.append(dict(name=name,color=color,vertices=[round(q,4) for p in vs for q in p.toTuple()],triangles=[j for t in ts for j in t]))
assembly.export(str(OUT/'wheel-profile-reference.step'))
data=dict(units='mm',source_frame='Local wheel-center mm: +X outward on left prototype; right prototype is geometrically mirrored. +Y up, +Z vehicle front.',
    owner_dimensions_applied=False,factory_source=R['factory_source'],factory_nominals=N,
    basis=R['basis'],limits=R['limits'],factory_parts_source=R.get('factory_parts_source'),factory_rostyle_alternatives=R.get('factory_rostyle_alternatives'),supplier_shape_comparison=R.get('supplier_shape_comparison'),
    construction=dict(retained_estimated_unloaded_outer_radius_mm=OUTER_R,estimated_rim_radial_gauge_mm=GAUGE,
        estimated_rim_flange_radius_mm=178,estimated_rim_total_width_mm=2*(BEAD_X+10),
        retained_mount_face_x_mm=20,retained_stud_sector_x_bounds_mm=[20,25],retained_center_bore_radius_mm=29,
        retained_catalog_pcd_mm=PCD,retained_fixing_bore_radius_mm=5.2,fixing_axes_yz_mm=bolt_axes,
        estimated_clock_correction_degrees=45,
        formed_rostyle_center=dict(owner_photos=['IMG_6329','IMG_6341'],spoke_axes_degrees=[0,90,180,270],vent_quadrant_axes_degrees=[45,135,225,315],vents_per_quadrant=2,vent_radial_bounds_mm=[110,142],vent_angular_bounds_per_half_quadrant_degrees=[3,32],vent_corner_radius_mm=4,radial_section_stations_mm=RADII,angular_section_step_degrees=3.75,angular_interpolation="ruled_to_preserve_forming_bounds_and_flat_stud_sectors",estimated_spoke_rise_xr_mm=SPOKE_RISE_XR,section_basis='Estimated four-spoke polar forming surface; retained20..25mm stud-sector mounting lands and hub axes. No fitted steel gauge, profile, offset, strength or operating fit.',bright_trim_profile_xr_mm=trim_profile,bright_trim_projected_x_gauge_mm=.6,trim_retention='Unmodeled; no fitted clip, seat or attachment claim.'),
        rim_outer_profile_xr_mm=rim_outer,tyre_outer_bezier_xr_mm=outer_curves,tyre_inner_bezier_xr_mm=inner_curves,
        profile_limits='Nominal designation width is used as an unloaded reference envelope, not a measured maximum on the fitted tyre. Sidewall and crown sections are estimated; no aspect ratio is assigned. Rim gauge is radial, not a normal sheet offset. Center/windows/weld boundary, bead contour, valve and retention details are unverified.'),parts=meshes)
(OUT/'wheel-profile-reference.json').write_text(json.dumps(data,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'wheel-profile-reference.step')).val();assert restored.isValid()
assert abs(restored.Volume()-sum(s.Volume() for n,s,c in parts))<restored.Volume()*1e-7
print(json.dumps(dict(meshes=len(parts),step_solids=len(restored.Solids()),step_valid=True,connected_steel_wheel=True,connected_hollow_tyre=True,nominal_tyre_rim_material_overlap_mm3=tyre.intersect(wheel).Volume(),reference_bead_width_mm=2*BEAD_X)),flush=True)
