#!/usr/bin/env catnip
# Riemann et Lebesgue à la frontière : Cantor ordinaire contre Cantor gras.
#
# Deux suites décroissantes d'ensembles fermés partent de [0, 1]. À chaque étape,
# leur indicatrice est Riemann-intégrable : elle n'a qu'un nombre fini de points
# de discontinuité, et son intégrale est la longueur totale des intervalles.
#
# Les limites ont pourtant des destins différents. Le Cantor ternaire a une
# mesure nulle ; les sommes de Darboux inférieure et supérieure de son indicatrice
# valent toutes deux zéro. Le Smith–Volterra–Cantor a une mesure 1/2 et un
# intérieur vide ; son indicatrice a une somme inférieure nulle et une somme
# supérieure 1/2. Elle est Lebesgue-intégrable, mais pas Riemann-intégrable.
#
# DEPS: matplotlib

mpl = import('matplotlib')
mpl.use('Agg')
plt = import('matplotlib.pyplot')
sys = import('sys')
import('pathlib', 'Path')

script_dir = Path(META.file).parent
output_dir = script_dir / 'output'
output_dir.mkdir(exist_ok=True)

STAGES = 7

struct Interval {
    left: float; right: float;

    length(self): float => { self.right - self.left }
}

struct StageReport {
    name: str; stage: int; intervals: list[Interval];
    cover_measure: float; expected: float; error: float;

    display(self): None => {
        print(
            f"  étape={self.stage}  intervalles={len(self.intervals):<3}  " +
                f"couverture={self.cover_measure:.9f}  formule={self.expected:.9f}"
        )
    }
}

union Verdict {
    riemann_and_lebesgue
    lebesgue_only

    label(self): str => {
        match self {
            Verdict.riemann_and_lebesgue => { "Riemann et Lebesgue" }
            Verdict.lebesgue_only        => { "Lebesgue seulement" }
        }
    }
}

struct LimitReport {
    name: str; lower_darboux: float; upper_darboux: float;
    lebesgue_integral: float; verdict: Verdict;

    display(self): None => {
        print(f"  {self.name}")
        print(
            f"    Darboux : inférieure={self.lower_darboux}, " +
                f"supérieure={self.upper_darboux}"
        )
        print(
            f"    intégrale de Lebesgue={self.lebesgue_integral}, " +
                f"verdict={self.verdict.label()}"
        )
    }
}

interval_measure = (intervals: list[Interval]): float => {
    total = 0.0
    for interval in intervals {
        total = total + interval.length()
    }
    total
}

# Cantor ternaire : chaque intervalle conserve ses tiers gauche et droit.
# Après n étapes, 2^n intervalles de longueur 3^-n couvrent la limite.
next_ternary_stage = (intervals: list[Interval]): list[Interval] => {
    result = list()
    for interval in intervals {
        third = interval.length() / 3.0
        result.append(Interval(interval.left, interval.left + third))
        result.append(Interval(interval.right - third, interval.right))
    }
    result
}

# Smith–Volterra–Cantor : à l'étape n, on retire au centre de chacun des
# 2^(n-1) intervalles un trou de longueur 4^-n. La longueur retirée tend vers 1/2.
next_fat_stage = (intervals: list[Interval], stage: int): list[Interval] => {
    hole = 1.0 / (4.0 ** stage)
    result = list()
    for interval in intervals {
        center = (interval.left + interval.right) / 2.0
        result.append(Interval(interval.left, center - hole / 2.0))
        result.append(Interval(center + hole / 2.0, interval.right))
    }
    result
}

make_stage_report = (name: str, stage: int, intervals: list[Interval], expected: float): StageReport => {
    measured = interval_measure(intervals)
    StageReport(name, stage, intervals, measured, expected, abs(measured - expected))
}

ternary_intervals = list(Interval(0.0, 1.0))
fat_intervals = list(Interval(0.0, 1.0))
ternary = list(make_stage_report('Cantor ternaire', 0, ternary_intervals, 1.0))
fat = list(make_stage_report('Smith–Volterra–Cantor', 0, fat_intervals, 1.0))

stage = 1
while stage <= STAGES {
    ternary_intervals = next_ternary_stage(ternary_intervals)
    fat_intervals = next_fat_stage(fat_intervals, stage)
    ternary_expected = (2.0 / 3.0) ** stage
    fat_expected = 0.5 + 1.0 / (2.0 ** (stage + 1))
    ternary.append(make_stage_report('Cantor ternaire', stage, ternary_intervals, ternary_expected))
    fat.append(make_stage_report('Smith–Volterra–Cantor', stage, fat_intervals, fat_expected))
    stage = stage + 1
}

# Les deux limites sont fermées et d'intérieur vide. Toute somme inférieure de
# leur indicatrice vaut donc zéro. Les couvertures finies calculées ci-dessus
# convergent vers leurs sommes supérieures : zéro pour le Cantor ternaire, 1/2
# pour le Smith–Volterra–Cantor.
limits = list(
    LimitReport('Cantor ternaire', 0.0, 0.0, 0.0, Verdict.riemann_and_lebesgue),
    LimitReport('Smith–Volterra–Cantor', 0.0, 0.5, 0.5, Verdict.lebesgue_only),
)

# Oracles : cardinal 2^n, formules exactes des couvertures et décroissance stricte.
oracle_ok = True
families = list(ternary, fat)
for reports in families {
    i = 0
    while i < len(reports) {
        report = reports[i]
        if len(report.intervals) != 2 ** report.stage or report.error > 1.0e-12 {
            oracle_ok = False
        }
        if i > 0 and report.cover_measure >= reports[i - 1].cover_measure {
            oracle_ok = False
        }
        i = i + 1
    }
}
if ternary[len(ternary) - 1].cover_measure > 6.0e-2 {
    oracle_ok = False
}
if abs(fat[len(fat) - 1].cover_measure - 0.5) > 4.0e-3 {
    oracle_ok = False
}

draw_stages = (axis, reports: list[StageReport], color: str, title: str): None => {
    for report in reports {
        for interval in report.intervals {
            axis.plot(
                list(interval.left, interval.right),
                list(report.stage, report.stage),
                color=color,
                linewidth=5.0,
                solid_capstyle='butt',
            )
        }
    }
    axis.invert_yaxis()
    axis.set_xlim(0.0, 1.0)
    axis.set_yticks(range(STAGES + 1))
    axis.set_title(title)
    axis.set_xlabel("x")
    axis.set_ylabel("étape")
}

# Planche : mêmes propriétés topologiques visibles, mesures différentes, puis
# conséquence directe sur l'écart entre sommes de Darboux à la limite.
fig, axes = plt.subplots(2, 2, figsize=tuple(13, 9))
axes = axes.ravel()

draw_stages(axes[0], ternary, '#4c78a8', "Cantor ternaire : mesure → 0")
draw_stages(axes[1], fat, '#54a24b', "Smith–Volterra–Cantor : mesure → 1/2")

stage_numbers = ternary.[(r) => { r.stage }]
ternary_measures = ternary.[(r) => { r.cover_measure }]
fat_measures = fat.[(r) => { r.cover_measure }]
axes[2].plot(stage_numbers, ternary_measures, marker='o', label='Cantor ternaire')
axes[2].plot(stage_numbers, fat_measures, marker='s', label='Smith–Volterra–Cantor')
axes[2].axhline(0.5, color='#777777', linewidth=1.0, linestyle='--', label='limite 1/2')
axes[2].axhline(0.0, color='#111111', linewidth=1.0)
axes[2].set_ylim(-0.03, 1.03)
axes[2].set_title("Intégrales des indicatrices finies")
axes[2].set_xlabel("étape n")
axes[2].set_ylabel("longueur de la couverture")
axes[2].grid(True, alpha=0.25)
axes[2].legend()

xpos = list(0, 1)
upper = limits.[(r) => { r.upper_darboux }]
lower = limits.[(r) => { r.lower_darboux }]
axes[3].bar(xpos, upper, width=0.55, color=list('#4c78a8', '#54a24b'), alpha=0.7, label='somme supérieure')
axes[3].scatter(xpos, lower, color='#111111', marker='_', s=500, linewidths=3.0, label='somme inférieure')
axes[3].set_xticks(xpos)
axes[3].set_xticklabels(list('Cantor\nordinaire', 'Cantor\ngras'))
axes[3].set_ylim(-0.04, 0.62)
axes[3].set_title("Écart de Darboux à la limite")
axes[3].set_ylabel("valeur")
axes[3].text(0, 0.05, "Riemann : oui", ha='center')
axes[3].text(1, 0.54, "Riemann : non", ha='center')
axes[3].legend(loc='upper left')

fig.suptitle("Même topologie, mesure différente, intégrabilité différente", fontsize=15)
fig.tight_layout()
output_path = output_dir / 'riemann_lebesgue.png'
fig.savefig(str(output_path), dpi=130, bbox_inches='tight')

print(f"⇒ Figure → {output_path}")

if not oracle_ok {
    raise RuntimeError("un invariant des constructions de Cantor a échoué")
}

# Aperçu one-shot par défaut : le PNG reste généré au runtime, puis embarqué
# dans une page autonome. `--no-browser` conserve un chemin headless pour les
# validations et les environnements sans interface graphique.
if '--no-browser' not in sys.argv {
    base64 = import('base64')
    http = import('http')
    image_data = base64.b64encode(output_path.read_bytes()).decode('ascii')

    page_start = '<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Riemann et Lebesgue — Catnip</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; background: #0d1117; color: #c9d1d9; font-family: system-ui, sans-serif; }
    main { width: min(1440px, 100%); margin: 0 auto; padding: 32px 20px 48px; }
    h1 { margin: 0 0 8px; color: #f0f6fc; font-size: clamp(1.5rem, 3vw, 2.4rem); }
    p { margin: 0 0 24px; color: #8b949e; }
    figure { margin: 0; padding: 16px; background: #ffffff; border: 1px solid #30363d; border-radius: 12px; }
    img { display: block; width: 100%; height: auto; }
    .verdicts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 16px; }
    .verdict { padding: 14px 16px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; }
    .verdict strong { display: block; color: #f0f6fc; margin-bottom: 4px; }
    @media (max-width: 680px) { .verdicts { grid-template-columns: 1fr; } }
  </style>
</head>
<body>
<main>
  <h1>Riemann et Lebesgue à la frontière</h1>
  <p>Même topologie, mesure différente, intégrabilité différente.</p>
  <figure><img alt="Comparaison des ensembles de Cantor ordinaire et gras" src="data:image/png;base64,'

    page_end = '"></figure>
  <section class="verdicts">
    <div class="verdict">
      <strong>Cantor ternaire</strong>
      Darboux inférieur = supérieur = 0. Riemann et Lebesgue.
    </div>
    <div class="verdict">
      <strong>Smith–Volterra–Cantor</strong>
      Darboux inférieur = 0, supérieur = 1/2. Lebesgue seulement.
    </div>
  </section>
</main>
</body>
</html>'

    http.serve(page_start + image_data + page_end, 0, 'text/html; charset=utf-8', True)
}