import bpy, math, random

scene = bpy.context.scene
CX = 6.8

def link_to(obj, cname):
    col = bpy.data.collections.get(cname)
    if col is None:
        col = bpy.data.collections.new(cname); scene.collection.children.link(col)
    for c in list(obj.users_collection): c.objects.unlink(obj)
    col.objects.link(obj)

def pmat(name, color, rough=0.7):
    m = bpy.data.materials.get(name)
    if m: return m
    m = bpy.data.materials.new(name); m.use_nodes=True
    nt = m.node_tree; nt.nodes.clear()
    out = nt.nodes.new('ShaderNodeOutputMaterial'); out.location=(500,0)
    b = nt.nodes.new('ShaderNodeBsdfPrincipled'); b.location=(250,0)
    nt.links.new(b.outputs['BSDF'], out.inputs['Surface'])
    b.inputs['Roughness'].default_value = rough
    texco = nt.nodes.new('ShaderNodeTexCoord'); texco.location=(-600,-300)
    noise = nt.nodes.new('ShaderNodeTexNoise'); noise.location=(-400,-300)
    noise.inputs['Scale'].default_value=14.0; noise.inputs['Detail'].default_value=8
    noise.inputs['Distortion'].default_value=0.4
    ramp = nt.nodes.new('ShaderNodeValToRGB'); ramp.location=(-200,-300)
    ramp.color_ramp.elements[0].position=0.35
    ramp.color_ramp.elements[0].color=(*[c*0.72 for c in color],1)
    ramp.color_ramp.elements[1].position=0.65
    ramp.color_ramp.elements[1].color=(*[min(1,c*1.25) for c in color],1)
    nt.links.new(texco.outputs['Object'], noise.inputs['Vector'])
    nt.links.new(noise.outputs['Fac'], ramp.inputs['Fac'])
    nt.links.new(ramp.outputs['Color'], b.inputs['Base Color'])
    bmp = nt.nodes.new('ShaderNodeBump'); bmp.location=(40,-260)
    bmp.inputs['Strength'].default_value=0.10
    nt.links.new(noise.outputs['Fac'], bmp.inputs['Height'])
    nt.links.new(bmp.outputs['Normal'], b.inputs['Normal'])
    return m

MAT_LOG   = pmat("LogMat",  (0.30,0.185,0.095), 0.68)
MAT_PLANK = pmat("PlankMat",(0.235,0.145,0.075), 0.60)
MAT_DARK  = pmat("DarkWood",(0.085,0.052,0.028), 0.62)
MAT_ROOF  = pmat("RoofMat", (0.115,0.065,0.042), 0.55)
MAT_TRIM  = pmat("TrimWood",(0.16,0.105,0.06), 0.55)
MAT_STONE = bpy.data.materials["StoneMat"]

if "GlassPane" not in bpy.data.materials:
    m=bpy.data.materials.new("GlassPane"); m.use_nodes=True; nt=m.node_tree; nt.nodes.clear()
    out=nt.nodes.new('ShaderNodeOutputMaterial'); out.location=(420,0)
    t=nt.nodes.new('ShaderNodeBsdfTransparent'); t.location=(120,120)
    g=nt.nodes.new('ShaderNodeBsdfGlossy'); g.location=(120,-40)
    g.inputs['Roughness'].default_value=0.03; g.inputs['Color'].default_value=(0.85,0.88,0.9,1)
    add=nt.nodes.new('ShaderNodeAddShader'); add.location=(280,40)
    nt.links.new(t.outputs['BSDF'], add.inputs[0]); nt.links.new(g.outputs['BSDF'], add.inputs[1])
    nt.links.new(add.outputs['Shader'], out.inputs['Surface'])
if "WinLight" not in bpy.data.materials:
    m=bpy.data.materials.new("WinLight"); m.use_nodes=True; nt=m.node_tree; nt.nodes.clear()
    out=nt.nodes.new('ShaderNodeOutputMaterial'); out.location=(400,0)
    em=nt.nodes.new('ShaderNodeEmission'); em.location=(180,20)
    em.inputs['Color'].default_value=(1.0,0.62,0.28,1); em.inputs['Strength'].default_value=14.0
    nt.links.new(em.outputs['Emission'], out.inputs['Surface'])
if "NearBlack" not in bpy.data.materials:
    rm_=bpy.data.materials.new("NearBlack"); rm_.use_nodes=True; ntx=rm_.node_tree; ntx.nodes.clear()
    o2=ntx.nodes.new('ShaderNodeOutputMaterial'); b2=ntx.nodes.new('ShaderNodeBsdfPrincipled')
    b2.inputs['Base Color'].default_value=(0.01,0.008,0.006,1); b2.inputs['Roughness'].default_value=1.0
    ntx.links.new(b2.outputs['BSDF'], o2.inputs['Surface'])

def cyl_log(name, r, length, mat=MAT_LOG, verts=14):
    bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=r, depth=length,
                                        location=(0,0,0), rotation=(math.pi/2,0,0))
    ob = bpy.context.active_object; ob.name=name
    for p in ob.data.polygons: p.use_smooth=True
    ob.data.materials.append(mat)
    return ob

def box(name, sx,sy,sz, loc, rot=(0,0,0), mat=None):
    bpy.ops.mesh.primitive_cube_add(size=1, location=loc, rotation=rot)
    ob=bpy.context.active_object; ob.name=name
    ob.scale=(sx/2, sy/2, sz/2)
    bpy.ops.object.transform_apply(scale=True)
    if mat: ob.data.materials.append(mat)
    return ob

CAB="Cabin"
W=8.4; D=6.2; LOG_R=0.155
z = LOG_R*0.9; row=0
while z < 3.02:
    if row % 2 == 0:
        lf = cyl_log(f"LogF{row}", LOG_R, W); lf.location=(CX,-D/2,z); link_to(lf,CAB)
        lb = cyl_log(f"LogB{row}", LOG_R, W); lb.location=(CX, D/2,z); link_to(lb,CAB)
        for sgn in (-1,1):
            l1 = cyl_log(f"LogS{row}_{sgn}", LOG_R, D/2-W/2, verts=12)
            l1.rotation_euler=(math.pi/2,0,0); l1.location=(CX+sgn*(W/2-LOG_R), -D/4, z); link_to(l1,CAB)
            l2 = cyl_log(f"LogS{row}b_{sgn}", LOG_R, D/2-W/2, verts=12)
            l2.rotation_euler=(math.pi/2,0,0); l2.location=(CX+sgn*(W/2-LOG_R), D/4, z); link_to(l2,CAB)
    else:
        ls = cyl_log(f"LogL{row}", LOG_R, D); ls.rotation_euler=(0,math.pi/2,0)
        ls.location=(CX-W/2,0,z); link_to(ls,CAB)
        rs_ = cyl_log(f"LogR{row}", LOG_R, D); rs_.rotation_euler=(0,math.pi/2,0)
        rs_.location=(CX+W/2,0,z); link_to(rs_,CAB)
        seglen = W-2*LOG_R
        for sgn in (-1,1):
            fseg = cyl_log(f"LogFs{row}_{sgn}", LOG_R*0.92, seglen/2, verts=12)
            fseg.location=(CX+sgn*(W/4+LOG_R/2), -D/2+LOG_R*0.15, z+LOG_R*0.55); link_to(fseg,CAB)
            bseg = cyl_log(f"LogBs{row}_{sgn}", LOG_R*0.92, seglen/2, verts=12)
            bseg.location=(CX+sgn*(W/4+LOG_R/2), D/2-LOG_R*0.15, z+LOG_R*0.55); link_to(bseg,CAB)
    row += 1; z += LOG_R*1.62

TOP_Z = z
steps=9; peak_h=2.35
for s in range(steps):
    tz = TOP_Z + (s+0.5)*peak_h/steps
    halfw = (W/2)*(1-(s+0.5)/steps)+0.18
    fg=cyl_log(f"GableF{s}", LOG_R*0.95, halfw*2, verts=12); fg.location=(CX,-D/2+LOG_R*0.1,tz); link_to(fg,CAB)
    bg=cyl_log(f"GableB{s}", LOG_R*0.95, halfw*2, verts=12); bg.location=(CX, D/2-LOG_R*0.1,tz); link_to(bg,CAB)

floor = box("Floor", W+0.7, D+0.7, 0.22, (CX,0,-0.02), mat=MAT_PLANK); link_to(floor,"Cabin")
found = box("Foundation", W+1.1, D+1.1, 0.34, (CX,0,-0.26), mat=MAT_STONE); link_to(found,"Cabin")

DOOR_W, DOOR_H = 1.06, 2.12
door_x = CX-2.35; door_z = DOOR_H/2 + 0.09
for nm,loc,sz in (("DoorFrameL",(door_x-DOOR_W/2-0.05,-D/2-0.06,door_z),(0.14,0.24,DOOR_H+0.24)),
                  ("DoorFrameR",(door_x+DOOR_W/2+0.05,-D/2-0.06,door_z),(0.14,0.24,DOOR_H+0.24)),
                  ("DoorFrameT",(door_x,-D/2-0.06,DOOR_H+0.16),(DOOR_W+0.38,0.24,0.14))):
    o=box(nm,*sz,loc,mat=MAT_TRIM); link_to(o,"Cabin")
recess = box("DoorRecess", DOOR_W+0.1, 0.5, DOOR_H+0.05, (door_x,-D/2+0.18,door_z))
recess.data.materials.append(bpy.data.materials["NearBlack"]); link_to(recess,"Cabin")
door = box("DoorLeaf", DOOR_W-0.06, 0.075, DOOR_H-0.04, (0,0,0), mat=MAT_DARK)
door.location=(door_x-0.42,-D/2-0.42,door_z+0.02)
door.rotation_euler=(0,0,math.radians(28)); link_to(door,"Cabin")
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.045, location=(door_x+0.28,-D/2-0.78,1.15))
h=bpy.context.active_object; h.name="DoorHandle"; h.data.materials.append(MAT_DARK)
bpy.ops.object.shade_smooth(); link_to(h,"Cabin")

def make_window(name,cx_,cy_,cz_,w,hh,rotz=0.0):
    rec=box(f"{name}_Rec", w, 0.46, hh,(cx_,cy_,cz_),rot=(0,0,rotz))
    rec.data.materials.append(bpy.data.materials["NearBlack"]); link_to(rec,"Cabin")
    fr_b=box(f"{name}_FrB", w+0.22,0.16,0.13,(cx_,cy_,cz_-hh/2-0.05),rot=(0,0,rotz),mat=MAT_TRIM)
    fr_t=box(f"{name}_FrT", w+0.22,0.16,0.13,(cx_,cy_,cz_+hh/2+0.05),rot=(0,0,rotz),mat=MAT_TRIM)
    dxl=-(w/2+0.05)*math.cos(rotz); dyl=-(w/2+0.05)*math.sin(rotz)
    fr_l=box(f"{name}_FrL",0.13,0.16,hh+0.22,(cx_+dxl,cy_+dyl,cz_),rot=(0,0,rotz),mat=MAT_TRIM)
    dxr=(w/2+0.05)*math.cos(rotz); dyr=(w/2+0.05)*math.sin(rotz)
    fr_r=box(f"{name}_FrR",0.13,0.16,hh+0.22,(cx_+dxr,cy_+dyr,cz_),rot=(0,0,rotz),mat=MAT_TRIM)
    mb_h=box(f"{name}_BarH", w,0.09,0.06,(cx_,cy_,cz_),rot=(0,0,rotz),mat=MAT_TRIM)
    mb_v=box(f"{name}_BarV",0.06,0.09,hh,(cx_,cy_,cz_),rot=(0,0,rotz),mat=MAT_TRIM)
    pane=box(f"{name}_Pane", w+0.02,0.02,hh+0.02,(cx_,cy_+0.02*math.cos(rotz),cz_),rot=(0,0,rotz))
    pane.data.materials.append(bpy.data.materials["GlassPane"])
    lit=box(f"{name}_Lit", w-0.04,0.01,hh-0.04,(cx_,cy_+0.07*math.cos(rotz)+0.04,cz_+0.3*hh),rot=(0,0,rotz))
    lit.data.materials.append(bpy.data.materials["WinLight"])
    for o in (fr_b,fr_t,fr_l,fr_r,mb_h,mb_v,pane,lit): link_to(o,"Cabin")

make_window("WinF1", CX+1.75,-D/2-0.02,1.55,1.15,1.05)
make_window("WinL1", CX-W/2+0.02,0.4,1.55,1.05,1.15,rotz=math.pi/2)
make_window("WinR1", CX+W/2-0.02,-0.9,1.55,1.05,1.15,rotz=-math.pi/2)
make_window("WinB1", CX-0.6, D/2+0.02,1.55,1.15,1.05,rotz=math.pi)

result={"rows":row,"top_z":TOP_Z,"objects":len(bpy.data.objects)}