import bpy
import sys
import math
import json
from pathlib import Path
from mathutils import Vector

ROOT=Path(__file__).resolve().parent
sys.path.insert(0,str(ROOT))
import geometry as geo

D=geo.data()
(ROOT/'assets').mkdir(parents=True,exist_ok=True)
(ROOT/'design-data.json').write_text(json.dumps(D,ensure_ascii=False,indent=2),encoding='utf-8')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for c in list(bpy.data.collections):
    if c.name!='Collection' and not c.objects: bpy.data.collections.remove(c)
scene=bpy.context.scene
scene.unit_settings.system='METRIC'
scene.unit_settings.length_unit='MILLIMETERS'
scene.unit_settings.scale_length=1.0
unit=bpy.data.collections.new('R4 | terminal')
scene.collection.children.link(unit)
rig=bpy.data.collections.new('Studio and cameras')
scene.collection.children.link(rig)

def mat(name,color,metal=0,rough=.4,emission=0):
    m=bpy.data.materials.new(name);m.diffuse_color=(*color,1)
    m.use_nodes=True;p=m.node_tree.nodes.get('Principled BSDF')
    p.inputs['Base Color'].default_value=(*color,1)
    p.inputs['Metallic'].default_value=metal
    p.inputs['Roughness'].default_value=rough
    if emission:
        p.inputs['Emission Color'].default_value=(*color,1)
        p.inputs['Emission Strength'].default_value=emission
    return m
silver=mat('Satin silver folded metal',(.46,.53,.59),.7,.29)
dark=mat('Graphite powder coat',(.025,.034,.046),.05,.48)
black=mat('Tablet original housing',(.009,.013,.019),0,.46)
glass=mat('Screen surround',(.005,.009,.014),0,.4)
blue=mat('Screen blue',(.018,.31,.64),.03,.35,.3)
cyan=mat('Painted cyan accent',(.005,.48,.57),.15,.33)
white=mat('Screen light background',(.69,.81,.9),0,.48,.2)
ui_line=mat('Screen secondary lines',(.23,.39,.5),0,.4,.1)
green=mat('Board schematic green',(.08,.25,.19),.05,.5)
terminal=mat('Terminal placeholder',(.09,.3,.22),.05,.5)
amber=mat('Power-terminal shield',(.38,.23,.06),.05,.5)
steel=mat('Base structural steel',(.12,.15,.18),.55,.36)
floor=mat('Studio ground',(.72,.76,.8),0,.62)
for m in [dark,black,glass]:
    m.node_tree.nodes.get('Principled BSDF').inputs['Specular IOR Level'].default_value=.15

def move_collection(o,c=unit):
    for old in list(o.users_collection):old.objects.unlink(o)
    c.objects.link(o)
    return o
def material(o,m):
    o.data.materials.append(m);return o
def cube(name,xyz,whd,m,bevel=0,c=unit):
    bpy.ops.mesh.primitive_cube_add(size=1,location=tuple(v*.001 for v in xyz))
    o=bpy.context.object;o.name=name;o.dimensions=tuple(v*.001 for v in whd)
    bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
    move_collection(o,c);material(o,m)
    if bevel:
        b=o.modifiers.new('Small manufactured edge','BEVEL');b.width=bevel*.001;b.segments=3
        o.modifiers.new('Weighted corner normals','WEIGHTED_NORMAL')
    return o
def mesh(name,verts,faces,m,thick=0):
    data=bpy.data.meshes.new(name+' mesh')
    data.from_pydata([tuple(v*.001 for v in p) for p in verts],[],faces);data.update()
    o=bpy.data.objects.new(name,data);unit.objects.link(o);material(o,m)
    if thick:
        s=o.modifiers.new('Nominal sheet inward','SOLIDIFY');s.thickness=thick*.001;s.offset=-1
    return o
def cylinder(name,xyz,radius,depth,m,vertices=32):
    bpy.ops.mesh.primitive_cylinder_add(vertices=vertices,radius=radius*.001,depth=depth*.001,location=tuple(v*.001 for v in xyz))
    o=bpy.context.object;o.name=name;move_collection(o);material(o,m);return o
def annulus(name,outer,inner,m):
    n=len(outer);faces=[]
    for i in range(n):
        j=(i+1)%n;faces.append((i,n+i,n+j,j))
    return mesh(name,outer+inner,faces,m,1.2)
def face_point(x,z,offset=0):
    return (x,geo.front_y(z)+offset,z)
def scaled_face(scale,offset):
    return [face_point(x*scale,1235+(z-1235)*scale,offset) for x,z in geo.FACE_XZ]

# Main head side-wrap is an open shell; it does not fill the tablet's internal cavity.
n=len(geo.FRONT)
faces=[(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)]
head=mesh('HEAD | shared nominal outer contour',geo.FRONT+geo.REAR,faces,silver,1.5)
for sign in [-1,1]:
    def sx(z):return sign*(185+(1334-z)*13/192+.15)
    mesh('HEAD | graphite side inset',
         [(sx(1320),geo.front_y(1320)+12,1320),(sx(1157),geo.front_y(1157)+12,1157),
          (sx(1157),108,1157),(sx(1320),108,1320)],[(0,1,2,3)],dark)
inner=scaled_face(.935,-.2)
annulus('HEAD | metal front perimeter',geo.FRONT,inner,silver)

angle=math.radians(geo.TILT)
screen_center_z=1240
screen_center=Vector((0,geo.front_y(screen_center_z)-2,screen_center_z))
up=Vector((0,math.sin(angle),math.cos(angle)))
inward=Vector((0,math.cos(angle),-math.sin(angle)))
def panel_points(w,h,corner,offset=0):
    a=w/2;b=h/2
    points=[(-a+corner,b),(a-corner,b),(a,b-corner),(a,-b+corner),
            (a-corner,-b),(-a+corner,-b),(-a,-b+corner),(-a,b-corner)]
    return [tuple(screen_center+Vector((x,0,0))+up*y+inward*offset) for x,y in points]
tablet_front=panel_points(247.6,166.2,2,0)
annulus('HEAD | dark fascia around original tablet',inner,tablet_front,glass)
tablet=cube('TABLET | 247.6 x 166.2 x 33.7 original housing',
            tuple(screen_center+inward*(33.7/2)),(247.6,33.7,166.2),black,1.5)
tablet.rotation_euler[0]=-angle
def ui_rect(name,x,y,w,h,m,offset=-.55):
    cen=screen_center+Vector((x,0,0))+up*y+inward*offset
    o=cube(name,tuple(cen),(w,.4,h),m,.2);o.rotation_euler[0]=-angle;return o
ui_rect('Display area | indicative only',0,0,215,133,white,-.8)
ui_rect('UI header',0,57,215,19,blue,-1.1)
ui_rect('UI left tile',-50,-1,94,83,white,-1.2)
ui_rect('UI right tile',52,-1,91,83,white,-1.2)
for y in [34,20,6,-8,-22]:
    ui_rect('UI row',53,y,68,3,ui_line,-1.5)
ui_rect('UI action',59,-47,60,13,blue,-1.7)
ui_rect('UI car body',-51,2,57,18,blue,-1.7)
ui_rect('UI car roof',-51,18,37,12,blue,-1.8)
for x in [-71,-32]:ui_rect('UI wheel',x,-9,8,9,ui_line,-1.8)

rear_outline=[(x*.94,121,1235+(z-1235)*.93) for x,_,z in geo.REAR]
rear=mesh('HEAD | removable rear service cover',rear_outline,[tuple(range(n))],dark,1.5)
for z in [1298,1286,1274]:
    for x in [-68,-22,24,70]:
        cube('Head ventilation | conceptual', (x,122,z),(33,1,3.3),black,1.2)
for x in [-153,153]:
    for z in [1148,1314]:
        o=cylinder('Head cover fastener', (x,123,z),2.1,1.8,silver,12);o.rotation_euler[0]=math.pi/2

# Shoulder outer surfaces connect the true head and true column cross-sections.
mesh('SHOULDER | continuous folded transition',geo.SHOULDER_TOP+geo.SHOULDER_BOTTOM,
     [(0,4,5,1),(1,5,6,2),(2,6,7,3),(3,7,4,0)],silver,1.5)
mesh('SHOULDER | inset graphite center',
     [(-143,-180.4,1120),(143,-180.4,1120),(72,-90.5,1010),(-72,-90.5,1010)],
     [(0,3,2,1)],dark,1.2)
mesh('HEAD | narrow painted accent',
     [face_point(-184,1122,-1),face_point(184,1122,-1),face_point(187,1125,-1),face_point(-187,1125,-1)],
     [(0,3,2,1)],cyan)

# Body nominal bounds exactly 180 x 180, standing directly on the fixed base.
for x in [-89.25,89.25]:
    cube('COLUMN | structural side wall',(x,0,509),(1.5,180,1002),silver)
for x in [-90.02,90.02]:
    cube('COLUMN | recessed graphite side finish',(x,5,523),(.02,136,952),dark)
cube('COLUMN | fixed front panel',(0,-89.25,509),(177,1.5,1002),dark)
for x in [-82,82]:
    cube('COLUMN | silver face rail',(x,-89.6,524),(14,0.6,968),silver)
cube('COLUMN | shallow center groove',(0,-90.1,529),(3.5,.15,860),black,1.5)
upper_cover=cube('COLUMN | removable upper rear cover',(0,89.25,694),(164,1.5,598),dark,3)
lower_cover=cube('COLUMN | independent power bay rear cover',(0,89.25,287),(164,1.5,181),dark,3)
for z in [332,320,308]:
    for x in [-38,0,38]:cube('Power bay vent | conceptual',(x,90.2,z),(29,.8,3.3),black,1.2)
for x in [-73,73]:
    for z in [406,982,207,367]:
        o=cylinder('Column panel fastener',(x,90.7,z),1.6,1.3,silver,12);o.rotation_euler[0]=math.pi/2
cube('COLUMN | fixed field terminal bay',(0,89.25,113),(164,1.5,134),dark,3)
for x in [-48,48]:
    o=cylinder('Fixed cable gland placeholder',(x,94,113),8,10,black,24);o.rotation_euler[0]=math.pi/2
cube('Ethernet passthrough placeholder',(0,93,113),(15,6,14),black,1)

# Separate electrical mounting plates; quantities and planar sizes match user data.
upper_plate=cube('INTERIOR | removable plate 160 x 610',(0,-59.25,690),(160,1.5,610),silver)
lower_plate=cube('INTERIOR | PSU tray 160 x 180',(0,-59.25,285),(160,1.5,180),silver)
interior=[upper_plate,lower_plate]
for p in D['components']:
    z=995-p['y']-p['h']/2
    thickness=70
    o=cube('INTERIOR | '+p['id']+' planning thickness 70, not measured',(0,-58.5+thickness/2,z),(p['w'],thickness,p['h']),green if p['type']!='power' else silver,2)
    interior.append(o)
for x in [-68,68]:
    for z,h in [(690,600),(285,170)]:
        interior.append(cube('INTERIOR | split duct planning 18 wide',(x,-38,z),(18,35,h),dark,1))
for o in interior:o.hide_render=True

# Anchored base: no casters, adjustable feet or rubber pads.
base=cube('BASE | structural plate 480 x 440 x 8',(0,0,4),(480,440,8),steel)
for i,(x,y) in enumerate(geo.ANCHORS,1):
    cut=cylinder('CUT | anchor hole '+str(i),(x,y,4),7,20,steel)
    b=base.modifiers.new('Anchor hole '+str(i)+' diameter14 placeholder','BOOLEAN');b.operation='DIFFERENCE';b.solver='EXACT';b.object=cut
    bpy.context.view_layer.objects.active=base
    bpy.ops.object.modifier_apply(modifier=b.name);bpy.data.objects.remove(cut,do_unlink=True)
    cylinder('ANCHOR | above-floor stud placeholder '+str(i),(x,y,12),4,24,silver)
    cylinder('ANCHOR | washer placeholder '+str(i),(x,y,10),12,2,silver)
    cylinder('ANCHOR | nut placeholder '+str(i),(x,y,16),8.5,10,silver,6)

def cover_half(name,sign):
    # An open U notch lets each half slide away without lifting the column.
    pts=[(-240,-220),(240,-220),(240,0),(92,0),(92,-92),(-92,-92),(-92,0),(-240,0)]
    top=[(x,y*sign,42) for x,y in pts]
    faces=[tuple(range(8))]
    verts=list(top)
    for i,j in [(0,1),(1,2),(7,0)]:
        k=len(verts);verts.extend([(pts[i][0],pts[i][1]*sign,8),(pts[j][0],pts[j][1]*sign,8)])
        faces.append((i,j,k+1,k))
    return mesh(name,verts,faces,dark,1.2)
front_cap=cover_half('BASE | removable front cap',1)
rear_cap=cover_half('BASE | removable rear cap',-1)

# Shared geometry and all visible silhouettes are saved before rendering.
scene['R4_design_status']='Geometry-coordinated concept. Not a flat pattern, STEP solid, structural or anchor certification.'
scene['nominal_head_width_mm']=396.0
scene['nominal_column_width_mm']=180.0
scene['defined_width_ratio']=2.2
scene['anchor_hole_diameter_mm_placeholder']=14.0
scene['anchor_spacing_mm_placeholder']='380 x 340'
for o in unit.objects:
    o['design_revision']='R4'

def camera(name,position,target,ortho):
    c=bpy.data.cameras.new(name);o=bpy.data.objects.new(name,c);rig.objects.link(o)
    o.location=position;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler()
    c.type='ORTHO';c.ortho_scale=ortho;c.lens=55
    return o
front_cam=camera('CAM | true front',(0,-5,.675),(0,0,.675),1.53)
side_cam=camera('CAM | true right side',(5,0,.675),(0,0,.675),1.53)
rear_cam=camera('CAM | true rear',(0,5,.675),(0,0,.675),1.53)
hero_cam=camera('CAM | front 3-quarter',(2.8,-5,2.0),(0,0,.68),1.58)
base_cam=camera('CAM | base exposed',(1.3,-1.6,1.1),(0,0,.12),1.05)
ground=cube('Studio floor',(0,0,-5),(20000,20000,10),floor,c=rig)
def area(name,position,power,size):
    l=bpy.data.lights.new(name,'AREA');l.energy=power;l.shape='DISK';l.size=size
    o=bpy.data.objects.new(name,l);rig.objects.link(o);o.location=position
    o.rotation_euler=(Vector((0,0,.65))-o.location).to_track_quat('-Z','Y').to_euler()
area('Key softbox',(-3,-4,4),300,4)
area('Fill softbox',(3,-2,2.8),180,3)
area('Rim softbox',(1,3,4),350,3)
scene.world.color=(.22,.22,.22)
scene.render.engine='CYCLES';scene.cycles.device='CPU';scene.cycles.samples=20
scene.cycles.use_denoising=True
scene.render.image_settings.file_format='PNG'
scene.render.resolution_percentage=100
scene.view_settings.view_transform='AgX'
scene.render.film_transparent=True
scene.use_nodes=True
nodes=scene.node_tree.nodes;nodes.clear()
rl=nodes.new('CompositorNodeRLayers');over=nodes.new('CompositorNodeAlphaOver');comp=nodes.new('CompositorNodeComposite')
over.inputs[1].default_value=(1,1,1,1)
scene.node_tree.links.new(rl.outputs['Image'],over.inputs[2]);scene.node_tree.links.new(over.outputs['Image'],comp.inputs['Image'])
scene.camera=hero_cam
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'R4-terminal.blend'))

def render(filename,cam,width=900,height=1400):
    scene.camera=cam;scene.render.resolution_x=width;scene.render.resolution_y=height
    ground.hide_render=cam in [front_cam,side_cam,rear_cam]
    scene.render.filepath=str(ROOT/'assets'/filename)
    bpy.ops.render.render(write_still=True)
    print('RENDERED '+filename,flush=True)

if '--model-only' not in sys.argv:
    if '--base-only' not in sys.argv:
        render('front-perspective.png',hero_cam)
        render('front-orthographic.png',front_cam)
        render('side-orthographic.png',side_cam)
        render('rear-orthographic.png',rear_cam)
    front_cap.location.y-=.15;front_cap.location.z+=.12
    rear_cap.location.y+=.15;rear_cap.location.z+=.12
    render('base-open.png',base_cam,1200,1000)
    front_cap.location.y=0;front_cap.location.z=0
    rear_cap.location.y=0;rear_cap.location.z=0
    render('base-closed.png',base_cam,1200,1000)
    scene.camera=hero_cam
    bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'R4-terminal.blend'))
print('R4 model and render sequence finished',flush=True)
