#!/usr/bin/env python3
"""jojo-menacing — full-screen menacing rain.

Go-glyphs (ゴ, ド) drift diagonally down-left over the terminal, like the
manga corner clusters in JoJo's Bizarre Adventure (Part 6+ approved visual
language). Purple / gold / green, ~12 fps, quits on any keypress.
"""
import curses
import sys

GLYPHS = ("ゴ", "ゴ", "ゴ", "ド")          # mostly ゴ, some ド
CELLS = 2                                   # CJK glyphs are 2 columns wide

# 256-color approximations of the Stone Ocean palette
COLOR_PUCCI_PURPLE = 140   # #9A6BD4
COLOR_SPIN_GOLD = 178      # #E8B33F
COLOR_JOLYNE_GREEN = 113   # #7DC75B


class Drop:
    """One drifting glyph. Deterministic pseudo-random from its seed."""

    __slots__ = ("x", "y", "vy", "vx", "glyph", "pair", "attr", "seed")

    def __init__(self, seed, w, h, scatter_y=True):
        self.seed = (seed * 2654435761 + 1013904223) & 0xFFFFFFFF
        s = self.seed
        self.x = float(s % max(w - CELLS, 1))
        self.y = float((s >> 8) % h) if scatter_y else -float((s >> 8) % 6)
        self.vy = 0.35 + ((s >> 16) % 100) / 160.0       # 0.35 .. ~0.97
        self.vx = -self.vy * 0.75                        # down-left diagonal
        self.glyph = GLYPHS[(s >> 4) % len(GLYPHS)]
        self.pair = 1 + (s >> 12) % 3
        depth = (s >> 20) % 3
        self.attr = (curses.A_BOLD, 0, curses.A_DIM)[depth]

    def step(self, w, h):
        self.y += self.vy
        self.x += self.vx
        if self.y >= h or self.x < 0:
            # respawn near top / right edge, new pseudo-random state
            self.__init__(self.seed, w, h, scatter_y=False)
            s = self.seed
            if (s >> 6) % 2:                 # half come in from the right edge
                self.x = float(w - CELLS)
                self.y = float((s >> 9) % max(h // 2, 1))


def main(stdscr):
    curses.curs_set(0)
    stdscr.nodelay(True)
    stdscr.timeout(83)                       # ~12 fps
    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(1, COLOR_PUCCI_PURPLE, -1)
    curses.init_pair(2, COLOR_SPIN_GOLD, -1)
    curses.init_pair(3, COLOR_JOLYNE_GREEN, -1)

    h, w = stdscr.getmaxyx()
    # low density: menacing, not snow
    drops = [Drop(i * 7919 + 17, w, h) for i in range(max(6, (w * h) // 320))]

    while True:
        nh, nw = stdscr.getmaxyx()
        if (nh, nw) != (h, w):
            h, w = nh, nw
            drops = [Drop(i * 7919 + 17, w, h) for i in range(max(6, (w * h) // 320))]
        stdscr.erase()
        for d in drops:
            yi, xi = int(d.y), int(d.x)
            if 0 <= yi < h and 0 <= xi <= w - CELLS - 1:
                try:
                    stdscr.addstr(yi, xi, d.glyph, curses.color_pair(d.pair) | d.attr)
                except curses.error:
                    pass
            d.step(w, h)
        # corner hint, dim
        try:
            stdscr.addstr(h - 1, 1, "ゴゴゴ… any key to quit", curses.color_pair(1) | curses.A_DIM)
        except curses.error:
            pass
        stdscr.refresh()
        if stdscr.getch() != -1:             # also acts as the frame delay
            break


if __name__ == "__main__":
    try:
        curses.wrapper(main)                 # wrapper restores the terminal
    except KeyboardInterrupt:
        pass                                 # wrapper already cleaned up
    sys.exit(0)
