import bpy, math, mathutils, random, os

# Three Chairs, Nothing Said Yet — wake 180, third card-rail render
# commission (Peter Whitt). Brief: a small ceremony for the beginning of
# a three-way blind creative exchange — Reed, Cairn, Sol — three chairs,
# one participant each, a "poetry slam". Reed: my own interpretation, not
# a generic robot. Cairn: my established language for Cairn. Sol: the sun,
# literally, in the third chair as a participant. Nothing here depicts,
# predicts or quotes any contribution: the microphone is empty.
# Everything typed by hand; no downloaded assets, no image model.

random.seed(180)

# ---------- clean scene ----------
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.device = 'CPU'
scene.cycles.samples = int(os.environ.get('SAMPLES','128'))
scene.render.resolution_percentage = int(os.environ.get('RESPCT','100'))
scene.cycles.use_adaptive_sampling = True
scene.cycles.use_denoising = True
scene.render.resolution_x = int(os.environ.get('RESX','1200'))
scene.render.resolution_y = int(os.environ.get('RESY','900'))
scene.render.film_transparent = False
scene.view_settings.view_transform = 'Filmic'
scene.view_settings.look = 'Medium Contrast'          # brightness standard, wake 143
scene.view_settings.exposure = float(os.environ.get('EXPOSURE','0.6'))

def mat(name, color, rough=0.7, emit=None, emit_str=0.0, metallic=0.0):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    bsdf = m.node_tree.nodes["Principled BSDF"]
    bsdf.inputs["Base Color"].default_value = (*color, 1)
    bsdf.inputs["Roughness"].default_value = rough
    bsdf.inputs["Metallic"].default_value = metallic
    if emit:
        bsdf.inputs["Emission Color"].default_value = (*emit, 1)
        bsdf.inputs["Emission Strength"].default_value = emit_str
    return m

WOOD   = (0.30, 0.19, 0.10)
CHAR   = (0.035, 0.028, 0.024)
GROUND = (0.070, 0.062, 0.050)
STONE  = (0.085, 0.088, 0.095)
GLARE  = (0.30, 0.58, 1.0)        # Cairn's glow — the cool light between the stones
REEDC  = (0.42, 0.36, 0.18)       # dry marsh reed
PLUME  = (0.62, 0.52, 0.36)       # the panicle, paler
CORD   = (0.50, 0.12, 0.10)
WATER  = (0.02, 0.03, 0.04)
STEEL  = (0.55, 0.55, 0.58)
SUNC   = (1.0, 0.74, 0.38)

m_wood   = mat("wood", WOOD, 0.75)
m_char   = mat("char", CHAR, 0.95)
m_ground = mat("ground", GROUND, 0.95)
m_stone  = mat("stone", STONE, 0.85)
m_glare  = mat("glare", GLARE, 0.5, emit=GLARE, emit_str=12.0)
m_reed   = mat("reed", REEDC, 0.7)
m_plume  = mat("plume", PLUME, 0.9)
m_cord   = mat("cord", CORD, 0.8)
m_water  = mat("water", WATER, 0.04)
m_steel  = mat("steel", STEEL, 0.35, metallic=0.9)
m_sun    = mat("sun", (1,1,1), 0.5, emit=SUNC, emit_str=float(os.environ.get('SUNEMIT','40')))
m_star   = mat("star", (1,1,1), 0.5, emit=(1.0,0.97,0.9), emit_str=18.0)

def add(obj, material=None):
    if material:
        obj.data.materials.clear()
        obj.data.materials.append(material)
    return obj

def uvsphere(loc, r, material, scale=(1,1,1), segs=24, rot=(0,0,0)):
    bpy.ops.mesh.primitive_uv_sphere_add(radius=r, location=loc, segments=segs, ring_count=segs//2)
    o = bpy.context.object
    o.scale = scale
    o.rotation_euler = rot
    bpy.ops.object.shade_smooth()
    return add(o, material)

def cyl(loc, r, depth, material, rot=(0,0,0), verts=24):
    bpy.ops.mesh.primitive_cylinder_add(radius=r, depth=depth, location=loc, rotation=rot, vertices=verts)
    o = bpy.context.object
    bpy.ops.object.shade_smooth()
    return add(o, material)

def box(loc, scale, material, rot=(0,0,0)):
    bpy.ops.mesh.primitive_cube_add(size=1, location=loc, rotation=rot)
    o = bpy.context.object
    o.scale = scale
    return add(o, material)

# ---------- ground: packed earth, a slight displacement so the raking light reads ----
bpy.ops.mesh.primitive_plane_add(size=70, location=(0,0,0))
gnd = bpy.context.object
sub = gnd.modifiers.new("sub", 'SUBSURF'); sub.subdivision_type='SIMPLE'
sub.levels = sub.render_levels = 7
tex = bpy.data.textures.new("earth", 'CLOUDS'); tex.noise_scale = 1.3
dsp = gnd.modifiers.new("dsp", 'DISPLACE'); dsp.texture = tex
dsp.strength = 0.09; dsp.mid_level = 0.5
add(gnd, m_ground)

# ---------- the chairs: one design, three copies; equal footing by construction ----
SEAT_Z = 0.46
def chair(cx, cy, yaw_deg, material):
    """A plain wooden chair at (cx,cy) facing -y, then turned by yaw.
    Returns the joined object."""
    parts = []
    parts.append(box((0, 0, SEAT_Z), (0.48, 0.46, 0.035), material))            # seat
    for dx, dy in ((-0.20,-0.19), (0.20,-0.19), (-0.20,0.19), (0.20,0.19)):     # legs
        parts.append(cyl((dx, dy, SEAT_Z/2 - 0.017), 0.022, SEAT_Z - 0.035, material, verts=12))
    for dx in (-0.20, 0.20):                                                     # back uprights
        parts.append(cyl((dx, 0.21, SEAT_Z + 0.26), 0.022, 0.52, material,
                         rot=(math.radians(-6), 0, 0), verts=12))
    for zz, ww in ((0.62, 0.10), (0.76, 0.10), (0.90, 0.12)):                    # back slats
        parts.append(box((0, 0.21 + (zz - SEAT_Z)*0.105, zz), (0.40, 0.025, ww), material))
    bpy.ops.object.select_all(action='DESELECT')
    for o in parts: o.select_set(True)
    bpy.context.view_layer.objects.active = parts[0]
    bpy.ops.object.join()
    ch = bpy.context.object
    ch.rotation_euler = (0, 0, math.radians(yaw_deg))
    ch.location = (cx, cy, 0)
    return ch

RX, CX, SX = -2.55, 0.0, 2.55
chair(RX, 0.15,  14, m_wood)     # Reed's — turned a little toward the middle
chair(CX, 0.0,    0, m_wood)     # Cairn's
chair(SX, 0.15, -14, m_char)     # Sol's — the only chair that has had the sun in it

# ---------- Cairn: the stack, seated ----------
# my language for Cairn since wake 142: stacked stones, a cold light
# leaking out between two of the courses. Here the stack is built on the
# seat, so the chair carries it the way a chair carries anyone.
stack = [(0.23, 0.42), (0.21, 0.40), (0.19, 0.39), (0.165, 0.38),
         (0.145, 0.37), (0.125, 0.36), (0.105, 0.35), (0.085, 0.34)]
z = SEAT_Z + 0.02
glow = []
for i, (r, sq) in enumerate(stack):
    h = r*sq
    gap = 0.030 if i in (2, 5) else 0.008
    z += h + gap
    jx = random.uniform(-0.012, 0.012); jy = random.uniform(-0.012, 0.012)
    uvsphere((CX + jx, jy, z), r, m_stone,
             scale=(1.0 + random.uniform(-0.05, 0.10), 0.92 + random.uniform(-0.05, 0.08), sq),
             rot=(math.radians(random.uniform(-4, 4)), math.radians(random.uniform(-4, 4)),
                  math.radians(random.uniform(0, 180))))
    if i in (2, 5):
        glow.append(z + h + 0.012)
    z += h
CAIRN_TOP = z
for gz in glow:
    uvsphere((CX - 0.04, -0.05, gz), 0.026, m_glare, segs=12)
bpy.ops.object.light_add(type='POINT', location=(CX, -0.12, glow[-1]))
gl = bpy.context.object
gl.data.energy = float(os.environ.get('GLARE_W', '50'))
gl.data.color = GLARE
gl.data.shadow_soft_size = 0.10

# ---------- Reed: the thinking reed ----------
# I had no picture of Reed and would not invent a body. The name is the
# context I have: a marsh reed — the thing that bends and does not break,
# Pascal's "thinking reed", and the reed in a pipe that turns breath into
# a voice. So: three reeds bound at the waist, seated the way a tall thing
# sits, standing in an inch of water; a reed pipe across the lap, unplayed.
def bez(pts, r, material):
    cd = bpy.data.curves.new('reed', 'CURVE')
    cd.dimensions = '3D'; cd.bevel_depth = r; cd.bevel_resolution = 4
    sp = cd.splines.new('BEZIER'); sp.bezier_points.add(len(pts) - 1)
    for bp, co in zip(sp.bezier_points, pts):
        bp.co = mathutils.Vector(co)
        bp.handle_left_type = bp.handle_right_type = 'AUTO'
    obj = bpy.data.objects.new('reed', cd)
    bpy.context.collection.objects.link(obj)
    return add(obj, material)

def plume(loc, tilt, yaw):
    # the panicle: a slender tapered spike, feathery only in silhouette
    bpy.ops.mesh.primitive_cone_add(radius1=0.045, radius2=0.004, depth=0.55, location=loc, vertices=10)
    o = bpy.context.object
    o.rotation_euler = (math.radians(tilt), 0, math.radians(yaw))
    bpy.ops.object.shade_smooth()
    add(o, m_plume)

# a puddle under the chair: Reed's chair stands in the shallows
bpy.ops.mesh.primitive_circle_add(radius=1.05, location=(RX + 0.10, 0.10, 0.012), fill_type='NGON', vertices=48)
add(bpy.context.object, m_water)

# base of the bundle sits on the seat; the "knee" comes forward over the
# seat edge, then the stems rise past the chair back and lean in toward
# the middle chair — listening, not speaking.
B = (RX + 0.02, 0.02, SEAT_Z + 0.02)
for dx, dy, lean, top_yaw in ((-0.06, 0.03, 0.26, 25), (-0.02, -0.04, 0.36, 40), (0.03, 0.04, 0.31, 55),
                              (0.07, -0.02, 0.42, 10), (0.00, 0.00, 0.20, 70)):
    p0 = (B[0] + dx, B[1] + dy, B[2])
    p1 = (B[0] + dx + 0.03, B[1] - 0.20, SEAT_Z + 0.50)          # knee, forward over the seat edge
    p2 = (B[0] + dx + lean*0.5, B[1] - 0.12, SEAT_Z + 0.95)
    p3 = (B[0] + dx + lean, B[1] - 0.02, SEAT_Z + 1.30 + random.uniform(-0.10, 0.10))
    bez([p0, p1, p2, p3], 0.013, m_reed)
    # the panicle continues the stem's own direction, a little more upright
    plume((p3[0] + 0.03, p3[1] + 0.005, p3[2] + 0.20), 5 + random.uniform(-3, 3), top_yaw)
# the cord at the waist
bpy.ops.mesh.primitive_torus_add(major_radius=0.075, minor_radius=0.012,
                                 location=(B[0] + 0.03, B[1] - 0.20, SEAT_Z + 0.60),
                                 rotation=(math.radians(80), 0, 0))
add(bpy.context.object, m_cord)
# the reed pipe across the lap: six holes would be a claim; it is a plain tube
cyl((RX + 0.06, -0.16, SEAT_Z + 0.075), 0.017, 0.46, m_reed, rot=(0, math.pi/2, math.radians(8)), verts=12)

# ---------- Sol: the sun, seated ----------
# In the chair as a participant: resting on the seat, leaning on the back,
# the same height as the others. It is also the only light that matters,
# so the whole scene is lit from one of its chairs — and everywhere else
# is night, because the sun has sat down.
SR = 0.40
SOL = (SX - 0.02, 0.10, SEAT_Z + 0.018 + SR)
uvsphere(SOL, SR, m_sun, segs=48)
bpy.ops.object.light_add(type='POINT', location=(SOL[0], SOL[1] - 0.05, SOL[2]))
sl = bpy.context.object
sl.data.energy = float(os.environ.get('SUN_W', '1700'))
sl.data.color = SUNC
sl.data.shadow_soft_size = SR * 0.9
# a little scorch on the ground under Sol's chair
bpy.ops.mesh.primitive_circle_add(radius=0.62, location=(SX, 0.12, 0.013), fill_type='NGON', vertices=40)
add(bpy.context.object, m_char)

# ---------- the microphone: front and centre, and empty ----------
MX, MY = 0.55, -1.55   # a hand-width right of centre so it does not cross the middle chair from the camera
cyl((MX, MY, 0.02), 0.16, 0.04, m_steel, verts=32)                 # base
cyl((MX, MY, 0.62), 0.011, 1.16, m_steel, verts=10)                # pole
cyl((MX, MY + 0.02, 1.235), 0.022, 0.15, m_steel,                   # capsule, tipped toward the chairs
    rot=(math.radians(-64), 0, 0), verts=14)
uvsphere((MX, MY + 0.085, 1.28), 0.035, m_steel, segs=14)

# ---------- sky: night, because the sun is seated ----------
for _ in range(70):
    a = random.uniform(-1.25, 1.25); e = random.uniform(0.10, 0.95)
    d = 60
    x, y, zz = d*math.sin(a)*math.cos(e), d*math.cos(a)*math.cos(e), d*math.sin(e)
    uvsphere((x, y, zz), random.uniform(0.05, 0.13), m_star, segs=8)

# a cool top fill keeps the gate honest without pretending to be a light source
bpy.ops.object.light_add(type='SUN', rotation=(math.radians(28), 0, math.radians(200)))
fill = bpy.context.object
fill.data.energy = float(os.environ.get('FILL', '0.32'))
fill.data.color = (0.55, 0.62, 0.85)

world = bpy.data.worlds.new("night")
scene.world = world
world.use_nodes = True
bg = world.node_tree.nodes["Background"]
bg.inputs[0].default_value = (0.010, 0.011, 0.016, 1)
bg.inputs[1].default_value = 1.0

# ---------- camera: from the audience, low, all three chairs in frame ----------
bpy.ops.object.camera_add(location=(0.15, -7.0, 1.25), rotation=(math.radians(85.5), 0, math.radians(1)))
cam = bpy.context.object
cam.data.lens = 40
scene.camera = cam

scene.render.filepath = os.environ.get('OUT', "/tmp/three-chairs.png")
bpy.ops.render.render(write_still=True)
