"""Erzeugt die Sollwerte, gegen die `app/tests/fsrs.test.ts` prüft.

Die Formeln sind wörtlich aus der Referenzimplementierung übernommen:
`open-spaced-repetition/py-fsrs`, Datei `fsrs/scheduler.py` (MIT).
Bewusst ein zweiter, unabhängiger Weg zum selben Ergebnis – ein Tippfehler in
der TypeScript-Fassung fällt dadurch auf, statt sich still in die
Wiedervorlagetermine fortzusetzen.

Dieses Skript gehört nicht zur ausgelieferten Anwendung. Es läuft nur, wenn
die Sollwerte neu erzeugt werden müssen, etwa nach einer neuen FSRS-Version:

    python data-pipeline/fsrs_referenz.py > app/tests/fixtures/fsrs-referenz.json

Die Sollwerte dürfen niemals von Hand angepasst werden. Wird der Test rot, ist
zuerst zu klären, ob sich die Referenz geändert hat oder die Umsetzung falsch
ist – nicht, welche Zahl den Test wieder grün macht.
"""

import math, json

w = (0.212,1.2931,2.3065,8.2956,6.4133,0.8334,3.0194,0.001,1.8722,0.1666,
     0.796,1.4835,0.0614,0.2629,1.6483,0.6014,1.8729,0.5425,0.0912,0.0658,0.1542)
DECAY = -w[20]
FACTOR = 0.9 ** (1 / DECAY) - 1
S_MIN = 0.001

def R(s, t):
    return (1 + FACTOR * max(0, t) / s) ** DECAY

def interval(s, dr):
    return (s / FACTOR) * ((dr ** (1 / DECAY)) - 1)

def d0(g, clamp=True):
    d = w[4] - (math.e ** (w[5] * (g - 1))) + 1
    return min(max(d, 1.0), 10.0) if clamp else d

def s0(g):
    return max(w[g - 1], S_MIN)

def next_d(d, g):
    arg1 = d0(4, clamp=False)
    dd = -(w[6] * (g - 3))
    arg2 = d + (10.0 - d) * dd / 9.0
    return min(max(w[7] * arg1 + (1 - w[7]) * arg2, 1.0), 10.0)

def s_recall(d, s, r, g):
    hard = w[15] if g == 2 else 1
    easy = w[16] if g == 4 else 1
    return s * (1 + (math.e ** w[8]) * (11 - d) * (s ** -w[9])
                * ((math.e ** ((1 - r) * w[10])) - 1) * hard * easy)

def s_forget(d, s, r):
    lt = w[11] * (d ** -w[12]) * (((s + 1) ** w[13]) - 1) * (math.e ** ((1 - r) * w[14]))
    st = s / (math.e ** (w[17] * w[18]))
    return min(lt, st)

def s_short(s, g):
    inc = (math.e ** (w[17] * (g - 3 + w[18]))) * (s ** -w[19])
    if g in (2, 3, 4):
        inc = max(inc, 1.0)
    return max(s * inc, S_MIN)

def naechster(stand, g, abstand):
    if stand is None:
        return {"stabilitaet": s0(g), "schwierigkeit": d0(g)}
    d = next_d(stand["schwierigkeit"], g)
    if abstand < 1:
        return {"stabilitaet": s_short(stand["stabilitaet"], g), "schwierigkeit": d}
    r = R(stand["stabilitaet"], abstand)
    s = (s_forget(stand["schwierigkeit"], stand["stabilitaet"], r) if g == 1
         else s_recall(stand["schwierigkeit"], stand["stabilitaet"], r, g))
    return {"stabilitaet": max(s, S_MIN), "schwierigkeit": d}

out = {
  "konstanten": {"ABFALL": DECAY, "STRECKUNG": FACTOR},
  "erstStand": {str(g): naechster(None, g, 0) for g in (1,2,3,4)},
  "kurve": [{"s": s, "t": t, "r": R(s, t)}
            for s, t in ((1,1),(2.3065,2.3065),(10,0),(10,5),(10,30),(50,100),(0.212,1))],
  "intervall": [{"s": s, "ziel": q, "tage": interval(s, q)}
                for s, q in ((2.3065,0.9),(10,0.9),(10,0.95),(10,0.8),(100,0.97))],
  "folgen": [],
}
# Mehrschrittige Verlaeufe - der harte Test, weil sich Fehler aufschaukeln.
for name, grade, abstaende in (
    ("nur_gut",      [3,3,3,3,3],  [0,3,8,20,50]),
    ("mit_fehlern",  [3,1,3,2,4],  [0,2,1,5,12]),
    ("nur_leicht",   [4,4,4],      [0,10,60]),
    ("selber_tag",   [3,3,2,1],    [0,0,0,0]),
    ("dauerfehler",  [1,1,1,1],    [0,1,1,1]),
):
    stand, schritte = None, []
    for g, a in zip(grade, abstaende):
        stand = naechster(stand, g, a)
        schritte.append({"grad": g, "abstand": a, **stand})
    out["folgen"].append({"name": name, "schritte": schritte})

print(json.dumps(out, indent=1))
