#!/usr/bin/env python3
# 「99%」の映像フレームを描いて ffmpeg に渡す
import math, random, subprocess, sys
from PIL import Image, ImageDraw, ImageFont

W, H, FPS = 1280, 720, 30
DUR = 63.0
FONT = "/usr/share/fonts/opentype/ipafont-gothic/ipagp.ttf"
_fc = {}
def F(size):
    if size not in _fc:
        _fc[size] = ImageFont.truetype(FONT, size)
    return _fc[size]

def ease_out(x):
    x = max(0.0, min(1.0, x)); return 1 - (1 - x) ** 3
def ease_io(x):
    x = max(0.0, min(1.0, x)); return x * x * (3 - 2 * x)
def lerp(a, b, x): return a + (b - a) * x
def seg(t, a, b): return max(0.0, min(1.0, (t - a) / (b - a)))

# ---- 吹き出し（バーのつぶやき） ----
BUBBLES = [
    (15.5, 18.1, "……あと1%なんです"),
    (18.4, 21.0, "あの、あんまり見ないでもらえますか"),
    (22.6, 27.4, "待って待って待って待って"),
    (27.7, 29.4, "……ありがとうございます"),
    (29.7, 31.6, "いきます"),
    (31.9, 39.0, "ふんぬぬぬぬぬぬぬ"),
    (39.6, 42.6, "1%って、全体の100分の1ですよ"),
    (42.9, 45.8, "……けっこうありますよ"),
    (48.6, 51.6, "やった……！ やりました……！"),
    (54.6, 56.3, "……"),
    (56.5, 58.4, "……え？"),
]

REMAIN = [(0, None), (7.6, "約2秒"), (9.5, "約5分"), (11.0, "約3時間"), (12.6, "約14年"),
          (46.5, None)]

def pct_value(t):
    if t < 0.6: return 0.0
    if t < 4.5: return 87 * ease_out(seg(t, 0.6, 4.5))
    if t < 6.6: return lerp(87, 98, ease_out(seg(t, 4.5, 6.6)))
    if t < 7.6: return lerp(98, 99, seg(t, 6.6, 7.6))
    if t < 46.5: return 99.0
    if t < 52.0: return 100.0
    return 0.0

def pct_text(t):
    v = pct_value(t)
    if t < 31.9 or t >= 39.0:
        return f"{int(v)}%"
    steps = [(31.9, "99.1%"), (32.6, "99.4%"), (33.2, "99.9%"), (33.8, "99.99%"), (34.4, "99.999%")]
    if t < 34.4:
        s = "99%"
        for a, txt in steps:
            if t >= a: s = txt
        return s
    n = 3 + int((t - 34.4) ** 1.5 * 9)
    return "99." + "9" * n + "%"

def remain_text(t):
    if t < 7.6:
        return f"約{max(2, int(9 - t))}秒"
    r = None
    for a, s in REMAIN:
        if t >= a: r = s
    return r

def shake(t):
    a = 0
    if 22.6 <= t < 27.2: a = 6
    if 31.9 <= t < 39.0: a = lerp(1, 9, seg(t, 31.9, 39.0))
    if a == 0: return 0, 0
    return a * math.sin(t * 91.0), a * 0.6 * math.cos(t * 77.0)

def cursor(t):
    # 画面右下から「キャンセル」へ、そして戻る
    tgt = (W // 2 + int(WW/2) - int(110*K), H // 2 + 60 + int(WH/2) - int(30*K))
    start = (W + 40, H + 60)
    if t < 21.0 or t > 28.6: return None
    if t < 23.6: x = ease_io(seg(t, 21.0, 23.6))
    elif t < 26.0:
        x = 1.0
        # ためらって少し揺れる
        jit = 4 * math.sin(t * 13)
        return (tgt[0] + jit, tgt[1] + jit * 0.5)
    else: x = 1 - ease_io(seg(t, 26.0, 28.6))
    return (lerp(start[0], tgt[0], x), lerp(start[1], tgt[1], x))

random.seed(4)
CONF = [(random.uniform(0, W), random.uniform(-400, -10), random.uniform(80, 260),
         random.uniform(-60, 60), random.choice([(255, 90, 90), (255, 210, 60), (90, 200, 255), (120, 230, 120), (230, 120, 255)]),
         random.uniform(0, 6.28), random.uniform(3, 9)) for _ in range(220)]

BG_TOP, BG_BOT = (24, 64, 92), (14, 32, 52)
bg = Image.new("RGB", (W, H))
bd = ImageDraw.Draw(bg)
for y in range(H):
    c = tuple(int(lerp(BG_TOP[i], BG_BOT[i], y / H)) for i in range(3))
    bd.line([(0, y), (W, y)], fill=c)

K = 1.3
WW, WH = int(760*K), int(300*K)

def draw_window(d, ox, oy, t, second=False, done=False):
    x0, y0 = ox - WW // 2, oy - WH // 2
    d.rounded_rectangle([x0 + int(K*8), y0 + int(K*10), x0 + WW + int(K*8), y0 + WH + int(K*10)], int(K*12), fill=(8, 16, 26))
    d.rounded_rectangle([x0, y0, x0 + WW, y0 + WH], int(K*12), fill=(240, 242, 246))
    d.rounded_rectangle([x0, y0, x0 + WW, y0 + int(K*44)], int(K*12), fill=(212, 218, 228))
    d.rectangle([x0, y0 + int(K*30), x0 + WW, y0 + int(K*44)], fill=(212, 218, 228))
    d.text((x0 + int(K*18), y0 + int(K*9)), "コピー中", font=F(int(K*22)), fill=(40, 44, 52))
    d.text((x0 + WW - int(K*36), y0 + int(K*7)), "×", font=F(int(K*26)), fill=(90, 94, 100))
    if second:
        msg = "ファイルをコピーしています…（2 / 3,481）"
    elif done:
        msg = "コピーが完了しました"
    else:
        msg = "ファイルをコピーしています…（1 / 3,481）"
    d.text((x0 + int(K*40), y0 + int(K*66)), msg, font=F(int(K*28)), fill=(30, 32, 38))
    # バー
    bx0, by0, bx1, by1 = x0 + int(K*40), y0 + int(K*122), x0 + WW - int(K*40), y0 + int(K*166)
    d.rounded_rectangle([bx0, by0, bx1, by1], int(K*8), fill=(205, 210, 218))
    v = 0.0 if second else pct_value(t)
    fw = int((bx1 - bx0) * v / 100)
    col = (70, 200, 110) if (done and not second) else (40, 140, 240)
    if fw > 16:
        d.rounded_rectangle([bx0, by0, bx0 + fw, by1], int(K*8), fill=col)
    # 目（バーの先端）
    ex = bx0 + max(fw, int(K*34)) - int(K*24)
    talking = any(a <= t < b for a, b, _ in BUBBLES)
    eye_r = int(K*7)
    if 22.6 <= t < 27.2: eye_r = int(K*11)
    blink = (t % 3.7) < 0.12
    ecol = (255, 255, 255) if fw > 16 else (60, 70, 90)
    if t > 7.6 or second:
        for dx in (-int(K*12), int(K*12)):
            cx, cy = ex + dx, (by0 + by1) // 2 - 2
            if blink:
                d.line([cx - eye_r, cy, cx + eye_r, cy], fill=ecol, width=3)
            elif done and not second:
                d.arc([cx - int(K*7), cy - int(K*4), cx + int(K*7), cy + int(K*8)], 200, 340, fill=ecol, width=3)
            else:
                d.ellipse([cx - eye_r, cy - eye_r, cx + eye_r, cy + eye_r], fill=ecol)
    # 数字・残り時間
    pt = "0%" if second else pct_text(t)
    size = int(K*42)
    d.text((bx0, by1 + int(K*14)), pt, font=F(size), fill=(20, 22, 28))
    rt = ("約14年" if t < 57.4 else "約4万8千年") if second else remain_text(t)
    if rt and not done and not (31.9 <= t < 39.0 and not second):
        d.text((bx1 - int(K*270), by1 + int(K*22)), "残り時間: " + rt, font=F(int(K*26)), fill=(70, 74, 82))
    # キャンセルボタン
    hover = (cursor(t) is not None and 23.4 <= t < 26.2) and not second
    cbx, cby = x0 + WW - int(K*190), y0 + WH - int(K*58)
    if not done or second:
        d.rounded_rectangle([cbx, cby, cbx + int(K*150), cby + int(K*42)], int(K*8),
                            fill=(255, 225, 225) if hover else (250, 250, 252),
                            outline=(200, 80, 80) if hover else (150, 155, 165), width=2)
        d.text((cbx + int(K*27), cby + int(K*7)), "キャンセル", font=F(int(K*24)), fill=(40, 44, 52))
    else:
        d.rounded_rectangle([cbx, cby, cbx + int(K*150), cby + int(K*42)], int(K*8), fill=(40, 140, 240))
        d.text((cbx + int(K*52), cby + int(K*7)), "閉じる", font=F(int(K*24)), fill=(255, 255, 255))
    return ex, by0

def draw_bubble(d, tx, ty, text, t, a):
    f = F(40)
    tw = d.textlength(text, font=f)
    pad = 26
    bw, bh = tw + pad * 2, 76
    pop = ease_out(seg(t, a, a + 0.18))
    bx = max(20, min(W - 20 - bw, tx - bw / 2))
    by = ty - 250 - (1 - pop) * 12
    d.rounded_rectangle([bx, by, bx + bw, by + bh], 22, fill=(255, 255, 255), outline=(30, 30, 30), width=3)
    d.polygon([(tx - 14, by + bh - 2), (tx + 14, by + bh - 2), (tx + 2, ty - 18)], fill=(255, 255, 255))
    d.line([(tx - 14, by + bh), (tx + 2, ty - 18), (tx + 14, by + bh)], fill=(30, 30, 30), width=3)
    d.text((bx + pad, by + 15), text, font=f, fill=(20, 20, 20))

def draw_cursor(d, x, y):
    pts = [(0, 0), (0, 34), (9, 26), (15, 40), (21, 37), (15, 24), (27, 24)]
    pts = [(x + px * 1.3, y + py * 1.3) for px, py in pts]
    d.polygon(pts, fill=(255, 255, 255), outline=(0, 0, 0))
    d.line(pts + [pts[0]], fill=(0, 0, 0), width=2)

def frame(t):
    if t >= 58.6:
        img = Image.new("RGB", (W, H), (0, 0, 0))
        d = ImageDraw.Draw(img)
        a = ease_io(seg(t, 59.2, 60.2)) * (1 - ease_io(seg(t, 62.2, 63.0)))
        c = int(255 * a)
        f = F(150); s = "99%"
        d.text(((W - d.textlength(s, font=f)) / 2, 230), s, font=f, fill=(c, c, c))
        a2 = ease_io(seg(t, 60.2, 61.0)) * (1 - ease_io(seg(t, 62.2, 63.0)))
        c2 = int(200 * a2); f2 = F(32); s2 = "すべての進捗バーに捧ぐ"
        d.text(((W - d.textlength(s2, font=f2)) / 2, 430), s2, font=f2, fill=(c2, c2, c2))
        return img
    img = bg.copy()
    d = ImageDraw.Draw(img)
    sx, sy = shake(t)
    done = 46.5 <= t
    # 1枚目のウィンドウ（52秒以降は左へよける）
    ox = W // 2 + sx
    oy = H // 2 + 60 + sy
    second_in = ease_io(seg(t, 52.0, 52.8))
    if t >= 52.0:
        ox -= 900 * second_in
    talk_anchor = None
    if second_in < 1:
        talk_anchor = draw_window(d, ox, oy, t, done=done)
    if t >= 52.0:
        ox2 = W // 2 + 900 * (1 - second_in)
        talk_anchor = draw_window(d, ox2, H // 2 + 60, t, second=True)
    # 100%の白い閃光
    if 46.5 <= t < 47.1:
        a = 1 - seg(t, 46.5, 47.1)
        img = Image.blend(img, Image.new("RGB", (W, H), (255, 255, 255)), a * 0.7)
        d = ImageDraw.Draw(img)
    # 紙吹雪
    if 46.5 <= t < 56:
        dt = t - 46.5
        for x, y, vy, vx, col, ph, sz in CONF:
            px = x + vx * dt + 20 * math.sin(ph + dt * 3)
            py = y + vy * dt + 60 * dt * dt
            if -20 < py < H + 20:
                w = sz * abs(math.cos(ph + dt * 5)) + 1
                d.rectangle([px, py, px + w, py + sz], fill=col)
    for a, b, txt in BUBBLES:
        if a <= t < b and talk_anchor:
            draw_bubble(d, talk_anchor[0], talk_anchor[1], txt, t, a)
    c = cursor(t)
    if c: draw_cursor(d, *c)
    # フェードイン
    if t < 0.5:
        img = Image.blend(Image.new("RGB", (W, H)), img, t / 0.5)
    return img

def main():
    out = sys.argv[1]
    only = float(sys.argv[2]) if len(sys.argv) > 2 else None
    if only is not None:
        frame(only).save(out); return
    p = subprocess.Popen(["ffmpeg", "-y", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgb24",
                          "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-", "-c:v", "libx264",
                          "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p", out], stdin=subprocess.PIPE)
    n = int(DUR * FPS)
    for i in range(n):
        p.stdin.write(frame(i / FPS).tobytes())
    p.stdin.close(); p.wait()

if __name__ == "__main__":
    main()
