#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Generateur de "ruban trigonometrique" recto/verso, a coller sur un cylindre.

CE QUE FAIT CE SCRIPT
----------------------
Il produit un fichier PDF de 2 pages (page 1 = RECTO, page 2 = VERSO) contenant
chacune 10 rubans gradues en radians (0 a environ 4,4 tours), a decouper et a
coller autour d'un cylindre. L'echelle est calee EXACTEMENT sur 1 cm = 1 radian,
ce qui correspond a un cylindre de 2 cm de diametre (circonference = 2*pi cm).
Une petite reglette en cm (bleue) accompagne chaque ruban, calee sur le meme zero
que les radians (la graduation "pi" tombe donc juste apres le "3" de la reglette,
puisque pi ~ 3.14159).

Quand ce fichier est imprime en recto-verso, le ruban verso (angles negatifs,
sens horaire) est l'image miroir horizontale exacte du recto : mathematiquement,
les deux pages sont structurellement identiques en hauteur (seule l'horizontale
change), donc les graduations doivent se superposer parfaitement par
transparence. En pratique, une imprimante ou un photocopieur a toujours une
petite tolerance mecanique de calage recto-verso (souvent 0 a quelques
dixiemes de mm) : si vos graduations sont legerement decalees a l'impression,
utilisez les 2 reglages ci-dessous pour compenser.

COMMENT S'EN SERVIR
--------------------
1) Installer la dependance (une seule fois) :
       pip install matplotlib
2) Lancer :
       python3 ruban_trigonometrique.py
   Le fichier "ruban_trigonometrique_RECTO_VERSO.pdf" est cree a cote du script.
3) Imprimer le PDF sans mise a l'echelle (100 %, "taille reelle"), en recto-verso,
   en choisissant le retournement "petit cote" / "flip on short edge" (parfois
   appele "tumble") dans les options d'impression -- pas "grand cote"/"long edge",
   qui inverserait le haut et le bas.
4) Si un leger decalage subsiste apres impression (mesure a la regle ou en
   tenant la feuille a la lumiere), ajustez VERSO_Y_SHIFT_MM et/ou
   VERSO_X_SHIFT_MM ci-dessous (en millimetres), relancez le script, et
   reimprimez juste la page concernee pour verifier avant de tout imprimer.

Aucune donnee n'est envoyee nulle part : ce script tourne entierement en local.
"""

import math
from fractions import Fraction

try:
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    from matplotlib.backends.backend_pdf import PdfPages
except ImportError:
    raise SystemExit(
        "Ce script a besoin de la bibliotheque 'matplotlib'.\n"
        "Installez-la avec :   pip install matplotlib\n"
        "puis relancez ce script."
    )

# =================================================================
# REGLAGES A PERSONNALISER PAR L'UTILISATEUR
# =================================================================

# Nom du fichier PDF produit (a cote de ce script)
OUTPUT_FILENAME = "ruban_trigonometrique_RECTO_VERSO.pdf"

# --- Correction fine pour VOTRE imprimante / photocopieur ---
# Si, apres un test d'impression recto-verso, les graduations du VERSO sont
# legerement decalees par rapport au RECTO (vu par transparence), ajustez
# ces deux valeurs en millimetres puis relancez le script. Une valeur
# positive decale le ruban verso vers le bas / vers la droite.
VERSO_Y_SHIFT_MM = 0.0   # decalage vertical du verso (mm)
VERSO_X_SHIFT_MM = 0.0   # decalage horizontal du verso (mm)

# --- Dimensions physiques ---
CYLINDER_DIAMETER_CM = 2.0   # diametre du cylindre cible (determine l'echelle 1cm = 1 rad)
TAB_WIDTH_CM = 1.3           # longueur de la zone de collage "COLLER ICI" en debut de ruban
N_STRIPS = 10                # nombre de rubans par page

# =================================================================
# A partir d'ici : logique de generation (vous pouvez aussi ajuster
# ces constantes si vous voulez changer la mise en page, mais ce n'est
# pas necessaire pour un simple calage d'imprimante).
# =================================================================

PAGE_W = 29.7   # cm (A4 paysage)
PAGE_H = 21.0   # cm
CIRC = math.pi * CYLINDER_DIAMETER_CM  # circonference : doit valoir 2*pi pour DIAM=2cm -> 1cm = 1 rad

RED = "#b02a1e"
BLACK = "#1a1a1a"
TAB_FACE = "#e7e7e7"
TAB_EDGE = "#555555"
BLUE = "#1a5fb0"

LEFT_MARGIN = 0.35
RIGHT_MARGIN = 0.35
TOP_MARGIN = 0.30       # simple marge de page (pas de titre)
GAP_BETWEEN = 0.15      # espace blanc entre deux rubans (pas de ligne de coupe)

FONT_RAD = 7.4
FONT_RULER = 5.4
FONT_TAB = 5.3

TICK_H = 0.15
TICK_GAP = 0.06
RAD_LABEL_H = 0.76
TOP_PAD = 0.15
RULER_GAP = 0.08
RULER_H = 0.32
BOTTOM_PAD = 0.08

ABOVE_AXIS = TICK_H + TICK_GAP + RAD_LABEL_H + TOP_PAD
BELOW_AXIS = TICK_H + TICK_GAP + RULER_GAP + RULER_H + BOTTOM_PAD
strip_h = ABOVE_AXIS + BELOW_AXIS  # hauteur de bande = strict besoin (pas de gaspillage)

BOTTOM_MARGIN = PAGE_H - TOP_MARGIN - N_STRIPS*strip_h - (N_STRIPS-1)*GAP_BETWEEN
if BOTTOM_MARGIN < 0:
    raise SystemExit(
        f"Mise en page impossible : {N_STRIPS} rubans ne tiennent pas sur la page "
        f"avec ces reglages (marge basse calculee = {BOTTOM_MARGIN:.2f} cm). "
        f"Reduisez N_STRIPS ou GAP_BETWEEN."
    )

VERSO_Y_SHIFT_CM = VERSO_Y_SHIFT_MM / 10.0
VERSO_X_SHIFT_CM = VERSO_X_SHIFT_MM / 10.0

CYCLE_OFFSETS = [0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330]


def frac_pi_label(deg):
    f = Fraction(deg, 180).limit_denominator(1000)
    num, den = f.numerator, f.denominator
    if num == 0:
        return "0"
    if den == 1:
        return "\u03c0" if num == 1 else f"{num}\u03c0"
    return f"\u03c0/{den}" if num == 1 else f"{num}\u03c0/{den}"


def build_special_angles(max_deg):
    angles = []
    k = 0
    while True:
        base = k * 360
        added = False
        for off in CYCLE_OFFSETS:
            deg = base + off
            if deg > max_deg + 1e-9:
                break
            angles.append(deg)
            added = True
        if base > max_deg or not added:
            break
        k += 1
    return sorted(set(angles))


ruler_start_offset = LEFT_MARGIN + TAB_WIDTH_CM
ruler_len = PAGE_W - LEFT_MARGIN - TAB_WIDTH_CM - RIGHT_MARGIN
if ruler_len <= 0:
    raise SystemExit("TAB_WIDTH_CM est trop grand : il ne reste plus de place pour la reglette.")

max_deg = ruler_len * 180 / math.pi
ANGLES = build_special_angles(max_deg)
max_angle_deg = ANGLES[-1]
max_angle_rad = max_angle_deg * math.pi / 180


def minor_ticks(max_rad, major_rads, step=0.15):
    marked = set(round(a, 4) for a in major_rads)
    out = []
    r = 0.0
    while r <= max_rad + 1e-9:
        if round(r, 4) not in marked:
            out.append(r)
        r += step
    return out


MAJOR_RADS = [d * math.pi / 180 for d in ANGLES]
MINOR_RADS = minor_ticks(max_angle_rad, MAJOR_RADS)
MAX_CM = int(math.floor(ruler_len))


def draw_page(mirror, x_shift=0.0, y_shift=0.0):
    fig = plt.figure(figsize=(PAGE_W / 2.54, PAGE_H / 2.54))
    ax = fig.add_axes([0, 0, 1, 1])
    ax.set_xlim(0, PAGE_W)
    ax.set_ylim(0, PAGE_H)
    ax.axis("off")

    ax.add_patch(plt.Rectangle((0.05, 0.05), PAGE_W - 0.1, PAGE_H - 0.1, fill=False,
                                edgecolor="#999999", linewidth=0.5))

    def X(x_recto):
        base = PAGE_W - x_recto if mirror else x_recto
        return base + x_shift

    y_top_of_strips = PAGE_H - TOP_MARGIN - y_shift

    for s in range(N_STRIPS):
        y_strip_top = y_top_of_strips - s * (strip_h + GAP_BETWEEN)
        y_strip_bot = y_strip_top - strip_h
        y_axis = y_strip_top - ABOVE_AXIS

        tx0 = X(LEFT_MARGIN)
        tx1 = X(LEFT_MARGIN + TAB_WIDTH_CM)
        xlo, xhi = min(tx0, tx1), max(tx0, tx1)
        ax.add_patch(plt.Rectangle((xlo, y_strip_bot + 0.03), xhi - xlo, strip_h - 0.06,
                                    facecolor=TAB_FACE, edgecolor=TAB_EDGE, linewidth=0.5))
        ax.text((xlo + xhi) / 2, (y_strip_bot + y_strip_top) / 2, "COLLER\nICI",
                ha="center", va="center", fontsize=FONT_TAB, color="#555555")

        a0 = X(ruler_start_offset)
        a1 = X(ruler_start_offset + ruler_len)
        ax.plot([min(a0, a1), max(a0, a1)], [y_axis, y_axis], color="black", linewidth=0.7)

        for r in MINOR_RADS:
            x = X(ruler_start_offset + r)
            ax.plot([x, x], [y_axis - 0.045, y_axis + 0.045], color="#bcbcbc", linewidth=0.35)

        for deg in ANGLES:
            rad = deg * math.pi / 180
            x = X(ruler_start_offset + rad)
            is_pi = (deg % 180 == 0)
            color = RED if is_pi else BLACK
            tick_h = TICK_H if is_pi else TICK_H * 0.62
            ax.plot([x, x], [y_axis - tick_h, y_axis + tick_h], color=color,
                    linewidth=0.9 if is_pi else 0.55)

            rad_txt = frac_pi_label(deg)
            if mirror and deg != 0:
                rad_txt = "-" + rad_txt

            ax.text(x, y_axis + TICK_H + TICK_GAP, rad_txt, rotation=90, ha="center",
                    va="bottom", fontsize=FONT_RAD, color=color)

        y_ruler_top = y_axis - TICK_H - TICK_GAP - RULER_GAP
        for cm in range(0, MAX_CM + 1):
            xr = X(ruler_start_offset + cm)
            th = 0.18 if cm % 5 == 0 else 0.10
            ax.plot([xr, xr], [y_ruler_top - th, y_ruler_top], color=BLUE, linewidth=0.45)
            if cm % 5 == 0:
                ax.text(xr, y_ruler_top - th - 0.04, str(cm), fontsize=FONT_RULER,
                        ha="center", va="top", color=BLUE)

    return fig


def main():
    fig_recto = draw_page(mirror=False, x_shift=0.0, y_shift=0.0)
    fig_verso = draw_page(mirror=True, x_shift=VERSO_X_SHIFT_CM, y_shift=VERSO_Y_SHIFT_CM)

    with PdfPages(OUTPUT_FILENAME) as pdf:
        pdf.savefig(fig_recto)
        pdf.savefig(fig_verso)
        d = pdf.infodict()
        d["Title"] = "Ruban trigonometrique - recto/verso"
        d["Subject"] = f"Echelle 1 cm = 1 radian, cylindre diametre {CYLINDER_DIAMETER_CM:g} cm"

    print(f"Fichier cree : {OUTPUT_FILENAME}")
    print(f"  - {N_STRIPS} rubans par page, hauteur de bande = {strip_h:.3f} cm")
    print(f"  - graduations jusqu'a {frac_pi_label(max_angle_deg)} rad "
          f"({max_angle_deg/360:.2f} tours)")
    print(f"  - decalage verso applique : {VERSO_X_SHIFT_MM} mm (horizontal), "
          f"{VERSO_Y_SHIFT_MM} mm (vertical)")


if __name__ == "__main__":
    main()
