"""Canapé convertible mid-century paramétrique (inspiré OPP Drevovyroba, 1970s).

Mécanisme fidèle à l'original :
- Les bras latéraux (teck) sont SOLIDAIRES du dossier (vissés).
- L'ensemble bras+dossier bascule VERS L'AVANT (~110°) autour d'un PIVOT
  UNIQUE par côté (tourillon bas sur le caisson). Le dossier décrit un arc
  par-dessus l'assise (l'arc monte : aucune collision), et se pose DEVANT
  elle pour former la moitié avant du lit.
- L'accoudoir devient jambe de force : son extrémité touche le sol en
  position lit et soutient le porte-à-faux. Sa position (courte, vers
  l'arrière) est imposée par cette contrainte d'atterrissage.
- Le dossier est garni sur ses DEUX faces : sa face arrière devient la
  surface de couchage.

Le pivot est calculé exactement : intersection des médiatrices des deux
poses (canapé / lit). Amélioration vs original : assise et dossier de même
épaisseur -> couchage parfaitement uniforme.

Usage : modifier PARAMS puis relancer le script dans Blender.
Animation : frame 1 = canapé, frame 61 = lit.
"""
import bpy, bmesh, math

# ============================== PARAMÈTRES ==================================
PARAMS = dict(
    largeur=2.00,          # largeur hors tout (m)
    h_assise=0.39,         # hauteur du dessus d'assise (m)
    ep_coussin=0.145,      # épaisseur commune assise + dossier (m)
    prof_assise=0.80,      # profondeur de la plateforme d'assise (m)
    prof_utile=0.50,       # profondeur utile nez d'assise -> face du dossier (m) ;
                           # le dossier est AVANCÉ sur l'assise (comme l'original),
                           # la zone derrière lui devient couchage au dépliage
    long_dossier=0.46,     # longueur du dossier = profondeur ajoutée en lit (m)
    inclinaison=20.0,      # inclinaison dossier vs verticale, position canapé (°)
    h_caisson=0.140,       # hauteur du bandeau caisson (m)
    ep_cp=0.018,           # épaisseur contreplaqué (m)
    ep_coussin_dos=0.0,    # option coussins dorsaux amovibles (0 = aucun ;
                           # inutiles si prof_utile est correcte)
)

# ========================= PIVOT UNIQUE (plan YZ) ===========================
# y+ = arrière, z+ = haut.
# Pose A (canapé) : dossier incliné, pied niché au-dessus de l'arrière d'assise.
# Pose B (lit)    : dossier à plat DEVANT l'assise, face arrière vers le haut.
def solve_pivot(P):
    L, t = P['long_dossier'], P['ep_coussin']
    SEAT_TOP = P['h_assise']
    y_front, y_rear = -P['prof_assise'] / 2, P['prof_assise'] / 2
    aA = math.radians(90 - P['inclinaison'])
    # pied du dossier positionné pour donner prof_utile devant sa face avant
    P1A = (y_front + P['prof_utile'] + (t / 2) * math.sin(aA), SEAT_TOP + 0.015)
    P2A = (P1A[0] + L * math.cos(aA), P1A[1] + L * math.sin(aA))
    P1B = (y_front, SEAT_TOP - t / 2)                # pied contre le bord avant
    P2B = (P1B[0] - L, P1B[1])                       # sommet vers l'avant

    def bisector(Pa, Pb):
        M = ((Pa[0] + Pb[0]) / 2, (Pa[1] + Pb[1]) / 2)
        d = (Pb[0] - Pa[0], Pb[1] - Pa[1])
        return M, (-d[1], d[0])

    (M1, D1), (M2, D2) = bisector(P1A, P1B), bisector(P2A, P2B)
    det = D1[0] * (-D2[1]) - (-D2[0]) * D1[1]
    rx, ry = M2[0] - M1[0], M2[1] - M1[1]
    s = (rx * (-D2[1]) + D2[0] * ry) / det
    F = (M1[0] + s * D1[0], M1[1] + s * D1[1])
    rot = math.pi - aA                               # bascule avant : aA -> 180°
    # garde pendant la bascule : le pied du dossier culmine au-dessus du pivot
    r1 = math.hypot(P1A[0] - F[0], P1A[1] - F[1])
    print(f"Pivot unique F=({F[0]*1000:.0f},{F[1]*1000:.0f})mm, rotation avant {math.degrees(rot):.0f}°, "
          f"apogée du pied du dossier {(F[1]+r1)*100:.0f}cm, couchage {(P['prof_assise']+L)*100:.0f} x {P['largeur']*100:.0f} cm")
    return dict(F=F, P1A=P1A, P2A=P2A, aA=aA, rot=rot,
                frame_top=SEAT_TOP - t, frame_bot=SEAT_TOP - t - P['h_caisson'])

def rotated(p, F, ang):
    """rotation 2D de p autour de F"""
    c, s = math.cos(ang), math.sin(ang)
    ry, rz = p[0] - F[0], p[1] - F[1]
    return (F[0] + c * ry - s * rz, F[1] + s * ry + c * rz)

# ============================== CONSTRUCTION ================================
def build(P, K):
    W = P['largeur']; L = P['long_dossier']; t = P['ep_coussin']
    frame_top, frame_bot = K['frame_top'], K['frame_bot']
    F = K['F']; aA = K['aA']
    y_av, y_ar = -P['prof_assise'] / 2 + 0.02, P['prof_assise'] / 2 - 0.02

    col = bpy.data.collections.get('Sofa')
    if col:
        for o in list(col.objects): bpy.data.objects.remove(o, do_unlink=True)
    else:
        col = bpy.data.collections.new('Sofa'); bpy.context.scene.collection.children.link(col)

    def mat(name, color, rough=0.6):
        m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
        m.use_nodes = True
        m.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (*color, 1)
        m.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value = rough
        m.diffuse_color = (*color, 1)
        return m
    M_hetre = mat('Hetre', (0.72, 0.45, 0.16), 0.45)
    M_teck = mat('Teck', (0.25, 0.08, 0.045), 0.4)
    M_tissu = mat('TissuVert', (0.13, 0.30, 0.16), 0.9)

    def add_obj(name, m):
        o = bpy.data.objects.new(name, m); col.objects.link(o); return o

    def make_box(name, sx, sy, sz, origin='center'):
        m = bpy.data.meshes.new(name); bm = bmesh.new()
        bmesh.ops.create_cube(bm, size=1)
        bmesh.ops.scale(bm, vec=(sx, sy, sz), verts=bm.verts)
        if origin == 'ymin':
            bmesh.ops.translate(bm, vec=(0, sy / 2, 0), verts=bm.verts)
        bm.to_mesh(m); bm.free()
        return m

    def extrude_profile(name, profil, x0, dx):
        m = bpy.data.meshes.new(name); bm = bmesh.new()
        vs = [bm.verts.new((x0, y, z)) for y, z in profil]
        f = bm.faces.new(vs)
        bmesh.ops.triangulate(bm, faces=[f])
        ret = bmesh.ops.extrude_face_region(bm, geom=bm.faces[:])
        bmesh.ops.translate(bm, vec=(dx, 0, 0),
                            verts=[v for v in ret['geom'] if isinstance(v, bmesh.types.BMVert)])
        bm.normal_update(); bm.to_mesh(m); bm.free()
        return add_obj(name, m)

    # Caisson (profil trapèze, prow avant)
    caisson = extrude_profile('Caisson',
        [(y_ar, frame_top), (y_av - 0.045, frame_top), (y_av + 0.02, frame_bot), (y_ar, frame_bot)],
        -W / 2, W)
    caisson.data.materials.append(M_hetre)

    # Pieds fuselés évasés
    for i, (lx, ly) in enumerate([(-W/2+0.18, y_av+0.10), (W/2-0.18, y_av+0.10),
                                  (-W/2+0.18, y_ar-0.06), (W/2-0.18, y_ar-0.06)]):
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=16, radius1=0.021, radius2=0.012, depth=frame_bot)
        mm = bpy.data.meshes.new(f'Pied{i}'); bm.to_mesh(mm); bm.free()
        leg = add_obj(f'Pied{i}', mm); leg.location = (lx, ly, frame_bot / 2)
        s = 4.5
        leg.rotation_euler = (math.radians(-s if ly < 0 else s), math.radians(-s if lx < 0 else s), 0)
        leg.data.materials.append(M_hetre)

    # Coussins d'assise
    marge = 0.030
    n = 3; cw = (W - 2 * marge) / n
    for i in range(n):
        cx = -W / 2 + marge + cw / 2 + i * cw
        c = add_obj(f'CoussinAssise{i}', make_box(f'CoussinAssise{i}', cw - 0.008, P['prof_assise'], t))
        c.location = (cx, 0.0, frame_top + t / 2)
        b = c.modifiers.new('Bevel', 'BEVEL'); b.width = 0.02; b.segments = 4
        c.data.materials.append(M_tissu)

    # Coussins dorsaux amovibles : appuyés contre le dossier, posés sur l'assise.
    # On les retire pour déplier le lit -> masqués à partir de la frame 5.
    t_dos = P.get('ep_coussin_dos', 0)
    if t_dos > 0:
        aA_deg = aA
        for i in range(n):
            cx = -W / 2 + marge + cw / 2 + i * cw
            cd = add_obj(f'CoussinDos{i}', make_box(f'CoussinDos{i}', cw - 0.008, 0.40, t_dos, origin='ymin'))
            # bord bas mi-épaisseur : contre la face avant du dossier, posé sur l'assise
            y0 = K['P1A'][0] - (t + t_dos) / 2 * math.cos(aA_deg - math.pi / 2) + 0.010
            cd.location = (cx, y0, frame_top + t + 0.005)
            cd.rotation_euler = (aA_deg, 0, 0)
            b = cd.modifiers.new('Bevel', 'BEVEL'); b.width = 0.025; b.segments = 4
            cd.data.materials.append(M_tissu)
            cd.hide_viewport = False; cd.hide_render = False
            cd.keyframe_insert('hide_viewport', frame=1); cd.keyframe_insert('hide_render', frame=1)
            cd.hide_viewport = True; cd.hide_render = True
            cd.keyframe_insert('hide_viewport', frame=5); cd.keyframe_insert('hide_render', frame=5)
            cd.hide_viewport = False; cd.hide_render = False

    # Pivot : Empty au pôle calculé — dossier et bras en sont enfants
    pivot = bpy.data.objects.new('PivotDossier', None)
    pivot.empty_display_type = 'SPHERE'; pivot.empty_display_size = 0.03
    pivot.location = (0, F[0], F[1])
    col.objects.link(pivot)
    bpy.context.view_layer.update()   # matrix_world du pivot à jour AVANT parentage

    # Axe central du dossier : le dossier fait un demi-tour sur lui-même en vol,
    # la même face moelleuse sert de dossier (canapé) et de couchage (lit).
    # L'axe passe par le centre du dossier ; le palier est porté par les bras.
    u = (math.cos(aA), math.sin(aA))
    C = (K['P1A'][0] + (L / 2) * u[0], K['P1A'][1] + (L / 2) * u[1])
    axe = bpy.data.objects.new('AxeDossier', None)
    axe.empty_display_type = 'SINGLE_ARROW'; axe.empty_display_size = 0.05
    axe.location = (0, C[0], C[1])
    col.objects.link(axe)
    bpy.context.view_layer.update()
    axe.parent = pivot
    axe.matrix_parent_inverse = pivot.matrix_world.inverted()
    bpy.context.view_layer.update()

    # Dossier (garni 2 faces, origine bord bas mi-épaisseur), pose canapé
    Wd = W - 2 * marge
    dossier = add_obj('Dossier', make_box('Dossier', Wd, L, t, origin='ymin'))
    b = dossier.modifiers.new('Bevel', 'BEVEL'); b.width = 0.02; b.segments = 4
    dossier.data.materials.append(M_tissu)
    # dos du dossier (face ferme, habillage sombre) — la face moelleuse verte
    # doit se retrouver dessus en position lit grâce au demi-tour
    M_dos = mat('TissuDos', (0.10, 0.09, 0.08), 0.9)
    dossier.data.materials.append(M_dos)
    for poly in dossier.data.polygons:
        if poly.normal.z < -0.5:
            poly.material_index = 1
    dossier.location = (0, K['P1A'][0], K['P1A'][1])
    dossier.rotation_euler = (aA, 0, 0)

    # Bras sculptural en Y, solidaire du dossier (pose canapé, plan YZ).
    # L'extrémité de l'accoudoir est calculée pour toucher le sol en pose lit.
    # Silhouette du bras : ruban en S (~6-8 cm) échantillonné sur des Béziers,
    # du lobe du pivot à la pointe d'accoudoir puis retour au palier central.
    # La tranche p7-p8 de la pointe atterrit à plat au sol en pose lit.
    def qbez(p0, c, p1, n=9):
        ts = [i / (n - 1) for i in range(n)]
        return [((1 - t) ** 2 * p0[0] + 2 * (1 - t) * t * c[0] + t * t * p1[0],
                 (1 - t) ** 2 * p0[1] + 2 * (1 - t) * t * c[1] + t * t * p1[1]) for t in ts]

    p7 = rotated((-0.47, 0.0), F, -K['rot'])       # pointe accoudoir bas -> sol lit
    p8 = rotated((-0.53, 0.0), F, -K['rot'])       # pointe accoudoir haut -> sol lit
    kA = (F[0] - 0.046, F[1] - 0.025)              # lobe pivot, avant-bas
    kB = (F[0] - 0.050, F[1] + 0.028)              # lobe pivot, avant-haut
    # Topologie : montant diagonal pivot -> palier ; l'accoudoir déborde en
    # avant du montant (creux kK dessous), pointe p7-p8 posée à plat en lit.
    kK = (p7[0] + 0.166, p7[1] - 0.012)            # creux : montant / dessous accoudoir
    arm_profile = (
        [kA, kB]
        + qbez(kB, ((kB[0] + kK[0]) / 2 - 0.062, (kB[1] + kK[1]) / 2 + 0.02), kK)[1:]  # bord avant montant
        + [p7, p8]                                                    # dessous pointe + tranche-pied
        + qbez(p8, ((p8[0] + C[0]) / 2, C[1] + 0.078), (C[0] - 0.015, C[1] + 0.054))[1:]  # dessus accoudoir
        + [(C[0] + 0.045, C[1] + 0.035),                              # lobe du palier
           (C[0] + 0.061, C[1] - 0.010),
           (C[0] + 0.035, C[1] - 0.057)]
        + qbez((C[0] + 0.035, C[1] - 0.057),
               ((C[0] + F[0]) / 2 + 0.018, (C[1] + F[1]) / 2 - 0.072),
               (F[0] + 0.050, F[1] + 0.030))[1:]                      # bord arrière montant
        + [(F[0] + 0.046, F[1] - 0.020), (F[0] + 0.010, F[1] - 0.047)]
    )
    def arm_object(name, profile, x_center, thickness):
        """profil (y,z) -> courbe 2D remplie extrudée en X (concavités ok)"""
        from mathutils import Matrix
        cu = bpy.data.curves.new(name, 'CURVE')
        cu.dimensions = '2D'; cu.fill_mode = 'BOTH'; cu.extrude = thickness / 2
        sp = cu.splines.new('POLY')
        sp.points.add(len(profile) - 1)
        for i, (y, z) in enumerate(profile):
            sp.points[i].co = (y, z, 0, 1)
        sp.use_cyclic_u = True
        o = bpy.data.objects.new(name, cu)
        col.objects.link(o)
        o.matrix_world = Matrix(((0, 0, 1, x_center), (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 0, 1)))
        return o

    for side in (-1, 1):
        bras = arm_object(f'Bras{side}', arm_profile, side * (W / 2 + 0.0195), 0.035)
        bras.data.materials.append(M_teck)
        bras.parent = pivot
        bras.matrix_parent_inverse = pivot.matrix_world.inverted()
        # tourillon apparent (fixe, sur le caisson)
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=0.016, radius2=0.016, depth=0.085)
        mm = bpy.data.meshes.new(f'Tourillon{side}'); bm.to_mesh(mm); bm.free()
        tk = add_obj(f'Tourillon{side}', mm)
        tk.rotation_euler = (0, math.radians(90), 0)
        tk.location = (side * (W / 2 + 0.02), F[0], F[1])
        tk.data.materials.append(M_teck)

    dossier.parent = axe
    dossier.matrix_parent_inverse = axe.matrix_world.inverted()

    # Palier de l'axe central : cylindre traversant, porté par les bras
    for side in (-1, 1):
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=0.016, radius2=0.016, depth=0.09)
        mm = bpy.data.meshes.new(f'AxeCentral{side}'); bm.to_mesh(mm); bm.free()
        ax = add_obj(f'AxeCentral{side}', mm)
        ax.rotation_euler = (0, math.radians(90), 0)
        ax.location = (side * (W / 2 + 0.02), C[0], C[1])
        ax.data.materials.append(M_teck)
        ax.parent = pivot
        ax.matrix_parent_inverse = pivot.matrix_world.inverted()

    # Guidage-butée de la rotation du dossier : un téton (solidaire du dossier)
    # court dans une GORGE semi-circulaire de 180°, concentrique à l'axe,
    # creusée dans la face intérieure du bras. Les extrémités de la gorge sont
    # les butées des deux poses (canapé / lit) : indexation automatique, pas
    # de verrou à manipuler. Les charges statiques plaquent le téton contre
    # sa butée dans les deux positions.
    def ring_segment(name, center, r_in, r_out, a_start, a_end, x0, dx, nseg=24):
        m = bpy.data.meshes.new(name); bm = bmesh.new()
        prev = None
        for i in range(nseg + 1):
            a = a_start + (a_end - a_start) * i / nseg
            ca, sa = math.cos(a), math.sin(a)
            v1 = bm.verts.new((x0, center[0] + r_in * ca, center[1] + r_in * sa))
            v2 = bm.verts.new((x0, center[0] + r_out * ca, center[1] + r_out * sa))
            if prev:
                bm.faces.new((prev[0], prev[1], v2, v1))
            prev = (v1, v2)
        ret = bmesh.ops.extrude_face_region(bm, geom=bm.faces[:])
        bmesh.ops.translate(bm, vec=(dx, 0, 0),
                            verts=[v for v in ret['geom'] if isinstance(v, bmesh.types.BMVert)])
        bm.normal_update(); bm.to_mesh(m); bm.free()
        return add_obj(name, m)

    d_g = 0.042                                     # rayon de la gorge
    a0 = math.atan2(-u[1], -u[0])                   # téton en pose canapé (repère bras)
    Tc = (C[0] + d_g * math.cos(a0), C[1] + d_g * math.sin(a0))
    M_laiton = mat('Laiton', (0.55, 0.42, 0.15), 0.35)
    for side in (-1, 1):
        # téton, rigide avec le dossier
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=0.004, radius2=0.004, depth=0.030)
        mm = bpy.data.meshes.new(f'Teton{side}'); bm.to_mesh(mm); bm.free()
        tet = add_obj(f'Teton{side}', mm)
        tet.rotation_euler = (0, math.radians(90), 0)
        tet.location = (side * (Wd / 2 + 0.021), Tc[0], Tc[1])
        tet.data.materials.append(M_laiton)
        tet.parent = axe
        tet.matrix_parent_inverse = axe.matrix_world.inverted()
        # gorge de 180° (a0 -> a0+pi, sens du demi-tour), rigide avec le bras
        gorge = ring_segment(f'Gorge{side}', C, d_g - 0.006, d_g + 0.006,
                             a0, a0 + math.pi, side * (W / 2 + 0.0015), side * 0.004)
        gorge.data.materials.append(M_dos)
        gorge.parent = pivot
        gorge.matrix_parent_inverse = pivot.matrix_world.inverted()

    # Animation : bascule avant (pivot) + demi-tour du dossier en vol (axe)
    scn = bpy.context.scene
    scn.frame_start, scn.frame_end = 1, 61
    for o in (pivot, axe):
        if o.animation_data: o.animation_data_clear()
    pivot.rotation_euler = (0, 0, 0)
    pivot.keyframe_insert('rotation_euler', frame=1)
    pivot.rotation_euler = (K['rot'], 0, 0)
    pivot.keyframe_insert('rotation_euler', frame=61)
    axe.rotation_euler = (0, 0, 0)
    axe.keyframe_insert('rotation_euler', frame=18)
    axe.rotation_euler = (math.pi, 0, 0)
    axe.keyframe_insert('rotation_euler', frame=48)
    scn.frame_set(1)


if __name__ == '__main__':
    K = solve_pivot(PARAMS)
    build(PARAMS, K)
    print('Canapé v3 : pivot unique, bascule avant, accoudoir-pied.')
