#!/usr/bin/env python3
"""Looping bathtub-and-duck terminal animation for Downterm demos."""

from __future__ import annotations

import argparse
import atexit
import math
import shutil
import signal
import sys
import time
from typing import Sequence


CSI = "\x1b["
RESET = f"{CSI}0m"
HIDE_CURSOR = f"{CSI}?25l"
SHOW_CURSOR = f"{CSI}?25h"
ALT_SCREEN_ON = f"{CSI}?1049h"
ALT_SCREEN_OFF = f"{CSI}?1049l"
CLEAR = f"{CSI}2J"
HOME = f"{CSI}H"

TUB = "tub"
TUB_DARK = "tub_dark"
WATER = "water"
WATER_DEEP = "water_deep"
FOAM = "foam"
BUBBLE = "bubble"
DUCK = "duck"
DUCK_LIGHT = "duck_light"
BEAK = "beak"
EYE = "eye"
LABEL = "label"
LABEL_SHADOW = "label_shadow"

RGB = {
    TUB: (171, 177, 191),
    TUB_DARK: (112, 118, 133),
    WATER: (71, 185, 214),
    WATER_DEEP: (36, 121, 161),
    FOAM: (196, 242, 250),
    BUBBLE: (184, 233, 245),
    DUCK: (255, 208, 64),
    DUCK_LIGHT: (255, 232, 132),
    BEAK: (255, 154, 58),
    EYE: (251, 251, 253),
    LABEL: (207, 212, 222),
    LABEL_SHADOW: (74, 79, 92),
}

WATER_SHADE = " .:-=+*#"


def parse_args(argv: Sequence[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Render a looping bathtub-and-duck animation."
    )
    parser.add_argument(
        "--fps",
        type=float,
        default=18.0,
        help="Target frames per second. Default: 18",
    )
    parser.add_argument(
        "--speed",
        type=float,
        default=1.0,
        help="Animation speed multiplier. Default: 1.0",
    )
    parser.add_argument(
        "--duration",
        type=float,
        default=0.0,
        help="Optional run duration in seconds. Default: infinite",
    )
    parser.add_argument(
        "--label",
        default="",
        help="Optional centered label, e.g. --label Downterm",
    )
    parser.add_argument(
        "--mono",
        action="store_true",
        help="Disable color and render as monochrome ASCII.",
    )
    return parser.parse_args(argv)


def terminal_size() -> tuple[int, int]:
    size = shutil.get_terminal_size((90, 28))
    return max(40, size.columns), max(12, size.lines)


def clamp(value: float, low: float, high: float) -> float:
    return max(low, min(high, value))


def scene_size(width: int, height: int) -> tuple[int, int]:
    visible_height = max(8, height - 1)
    scene_width = min(max(48, width - 2), width, 110)
    scene_height = min(max(16, visible_height - 1), visible_height, 24)
    return scene_width, scene_height


def make_canvas(width: int, height: int) -> list[list[tuple[str, str | None]]]:
    return [[(" ", None) for _ in range(width)] for _ in range(height)]


def put(canvas: list[list[tuple[str, str | None]]], x: int, y: int, char: str, color: str | None) -> None:
    if 0 <= y < len(canvas) and 0 <= x < len(canvas[y]) and char:
        canvas[y][x] = (char[0], color)


def draw_text(
    canvas: list[list[tuple[str, str | None]]],
    x: int,
    y: int,
    text: str,
    color: str | None,
) -> None:
    for index, char in enumerate(text):
        if char != " ":
            put(canvas, x + index, y, char, color)


def draw_sprite_text(
    canvas: list[list[tuple[str, str | None]]],
    x: int,
    y: int,
    text: str,
    color: str | None,
) -> None:
    for index, char in enumerate(text):
        if char == " ":
            continue
        if char == "~":
            put(canvas, x + index, y, " ", None)
            continue
        put(canvas, x + index, y, char, color)


def water_char(intensity: float) -> str:
    index = int(clamp(intensity, 0.0, 0.9999) * len(WATER_SHADE))
    return WATER_SHADE[index]


def tub_geometry(scene_width: int, scene_height: int) -> dict[str, int]:
    top_y = 1
    notch_width = max(28, min(scene_width - 18, int(scene_width * 0.44)))
    rim_left = max(8, (scene_width - notch_width) // 2)
    rim_right = min(scene_width - 9, rim_left + notch_width)
    wall_left = rim_left + 1
    wall_right = rim_right - 1
    body_bottom = min(scene_height - 10, top_y + max(6, scene_height // 3))
    water_top = top_y + 2
    return {
        "rim_left": rim_left,
        "rim_right": rim_right,
        "top_y": top_y,
        "wall_left": wall_left,
        "wall_right": wall_right,
        "body_bottom": body_bottom,
        "water_top": water_top,
    }


def draw_tub(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int]) -> None:
    rim_left = geometry["rim_left"]
    rim_right = geometry["rim_right"]
    top_y = geometry["top_y"]
    wall_left = geometry["wall_left"]
    wall_right = geometry["wall_right"]
    body_bottom = geometry["body_bottom"]

    draw_text(canvas, 2, top_y, "_" * max(0, rim_left - 2), TUB)
    put(canvas, rim_left, top_y, "_", TUB)
    draw_text(canvas, rim_right + 1, top_y, "_" * max(0, len(canvas[0]) - rim_right - 3), TUB)
    put(canvas, rim_right, top_y, "_", TUB)

    for y in range(top_y + 1, body_bottom):
        put(canvas, wall_left, y, "|", TUB)
        put(canvas, wall_right, y, "|", TUB)

    put(canvas, wall_left, body_bottom, "\\", TUB_DARK)
    draw_text(canvas, wall_left + 1, body_bottom, "_" * max(0, wall_right - wall_left - 1), TUB_DARK)
    put(canvas, wall_right, body_bottom, "/", TUB_DARK)


def draw_water_fill(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
    rim_left = geometry["wall_left"] + 1
    rim_right = geometry["wall_right"] - 1
    water_top = geometry["water_top"]
    body_bottom = geometry["body_bottom"] - 1

    depth = max(1, body_bottom - water_top)
    for row in range(depth + 1):
        y = water_top + row
        for x in range(rim_left, rim_right + 1):
            wave = math.sin((x * 0.23) + (t * 2.1) + row * 0.55)
            shimmer = math.sin((x * 0.07) - (t * 1.35))
            if row == 0:
                char = "~" if wave > -0.22 else "-"
                color = FOAM if shimmer > 0.25 else WATER
            else:
                density = 0.40 + row / max(1, depth)
                value = 0.45 + (wave * 0.25) + (shimmer * 0.10) + density * 0.30
                char = water_char(value)
                color = WATER if row <= 2 else WATER_DEEP
            put(canvas, x, y, char, color)


def draw_water_surface(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
    left = geometry["wall_left"] + 1
    right = geometry["wall_right"] - 1
    y = geometry["water_top"]

    for x in range(left, right + 1):
        wave = math.sin((x * 0.23) + (t * 2.1))
        shimmer = math.sin((x * 0.11) - (t * 1.15))
        char = "~" if wave > -0.18 else "-"
        color = FOAM if shimmer > 0.35 else WATER
        put(canvas, x, y, char, color)


def draw_bubbles(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
    rim_left = geometry["wall_left"]
    water_top = geometry["water_top"]

    seeds = (
        (0.00, rim_left + 11, 0.9),
        (0.29, rim_left + 19, 1.1),
        (0.57, rim_left + 27, 0.8),
        (0.82, rim_left + 35, 1.0),
    )

    for offset, base_x, speed in seeds:
        progress = (t * 0.11 * speed + offset) % 1.0
        y = water_top + 4 - int(progress * 9)
        x = base_x + int(round(math.sin((t * 0.8 * speed) + offset * 8.0) * 2))
        put(canvas, x, y, "o" if progress < 0.5 else ".", BUBBLE)


def draw_duck(canvas: list[list[tuple[str, str | None]]], geometry: dict[str, int], t: float) -> None:
    rim_left = geometry["wall_left"]
    rim_right = geometry["wall_right"]
    water_top = geometry["water_top"]

    span = rim_right - rim_left
    bob = math.sin(t * 1.35) * 0.55
    drift = math.sin(t * 0.55) * 2.0

    blink_phase = int(t * 2.1) % 12
    eye_char = "-" if blink_phase == 7 else "."
    x = int(rim_left + span * 0.34 + drift)
    y = int(water_top - 1 + bob)

    rows = [
        "      __",
        f" ___(~{eye_char})>",
        " \\~<_.~) ",
        "  `---'  ",
    ]
    colors = [DUCK_LIGHT, DUCK, DUCK, DUCK]

    for index, row in enumerate(rows):
        draw_sprite_text(canvas, x, y + index, row, colors[index])

    put(canvas, x + 8, y + 1, ">", BEAK)
    put(canvas, x + 6, y + 1, eye_char, EYE)

    if 0.74 < (t * 0.18) % 1.0 < 0.90:
        splash_x = x + 9
        splash_y = geometry["water_top"] - 1
        for dx, dy, char in ((0, 0, "."), (2, -1, "o"), (4, 0, "."), (5, -2, "o")):
            put(canvas, splash_x + dx, splash_y + dy, char, BUBBLE)


def logo_rows(text: str) -> list[str]:
    if text == "commandNotch":
        return [
            "  ██████╗ ██████╗ ███╗   ███╗███╗   ███╗ █████╗ ███╗   ██╗██████╗ ",
            " ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗  ██║██╔══██╗",
            " ██║     ██║   ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║  ██║",
            " ██║     ██║   ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║  ██║",
            " ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║  ██║██║ ╚████║██████╔╝",
            "  ╚═════╝ ╚═════╝ ╚═╝     ╚═╝╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝╚═════╝ ",
            "",
            " ███╗   ██╗ ██████╗ ████████╗ ██████╗██╗  ██╗",
            " ████╗  ██║██╔═══██╗╚══██╔══╝██╔════╝██║  ██║",
            " ██╔██╗ ██║██║   ██║   ██║   ██║     ███████║",
            " ██║╚██╗██║██║   ██║   ██║   ██║     ██╔══██║",
            " ██║ ╚████║╚██████╔╝   ██║   ╚██████╗██║  ██║",
            " ╚═╝  ╚═══╝ ╚═════╝    ╚═╝    ╚═════╝╚═╝  ╚═╝",
        ]

    return [text]


def draw_label(canvas: list[list[tuple[str, str | None]]], label: str) -> None:
    logo = logo_rows(label or "commandNotch")
    start_y = len(canvas) - len(logo) - 1

    for index, row in enumerate(logo):
        start_x = max(0, (len(canvas[0]) - len(row)) // 2)
        draw_text(canvas, start_x, start_y + index, row, LABEL)


def scene_canvas(scene_width: int, scene_height: int, t: float, label: str) -> list[list[tuple[str, str | None]]]:
    canvas = make_canvas(scene_width, scene_height)
    geometry = tub_geometry(scene_width, scene_height)
    draw_tub(canvas, geometry)
    draw_water_fill(canvas, geometry, t)
    draw_water_surface(canvas, geometry, t)
    draw_duck(canvas, geometry, t)
    draw_bubbles(canvas, geometry, t)
    draw_label(canvas, label)
    return canvas


def ansi_rgb(color_name: str | None) -> str:
    if color_name is None:
        return RESET
    red, green, blue = RGB[color_name]
    return f"{CSI}38;2;{red};{green};{blue}m"


def render_scene_line(cells: list[tuple[str, str | None]], mono: bool) -> str:
    if mono:
        return "".join(char for char, _ in cells)

    chunks: list[str] = []
    current_color: str | None = None
    for char, color in cells:
        if color != current_color:
            chunks.append(ansi_rgb(color))
            current_color = color
        chunks.append(char)
    chunks.append(RESET)
    return "".join(chunks)


def render_frame(width: int, height: int, t: float, label: str, mono: bool) -> str:
    visible_height = max(8, height - 1)
    scene_width, scene_height = scene_size(width, height)
    scene = scene_canvas(scene_width, scene_height, t, label)
    top_padding = max(0, (visible_height - scene_height) // 2)
    left_padding = max(0, (width - scene_width) // 2)

    pieces: list[str] = [HOME]
    blank_line = " " * width

    for row in range(visible_height):
        if top_padding <= row < top_padding + scene_height:
            scene_row = scene[row - top_padding]
            rendered = render_scene_line(scene_row, mono)
            right_padding = max(0, width - left_padding - scene_width)
            line = (" " * left_padding) + rendered + (" " * right_padding)
        else:
            line = blank_line

        pieces.append(line)
        if row != visible_height - 1:
            pieces.append("\n")

    return "".join(pieces)


def restore_terminal() -> None:
    sys.stdout.write(f"{RESET}{SHOW_CURSOR}{ALT_SCREEN_OFF}")
    sys.stdout.flush()


def install_cleanup() -> None:
    def handle_signal(signum, _frame) -> None:
        restore_terminal()
        raise SystemExit(128 + signum)

    atexit.register(restore_terminal)
    signal.signal(signal.SIGINT, handle_signal)
    signal.signal(signal.SIGTERM, handle_signal)


def main(argv: Sequence[str]) -> int:
    args = parse_args(argv)
    if args.fps <= 0:
        raise SystemExit("--fps must be greater than 0")
    if args.speed <= 0:
        raise SystemExit("--speed must be greater than 0")

    install_cleanup()

    sys.stdout.write(f"{ALT_SCREEN_ON}{HIDE_CURSOR}{CLEAR}")
    sys.stdout.flush()

    frame_interval = 1.0 / args.fps
    start = time.perf_counter()
    frame_index = 0

    while True:
        now = time.perf_counter()
        elapsed = now - start
        if args.duration > 0 and elapsed >= args.duration:
            break

        width, height = terminal_size()
        t = elapsed * args.speed
        sys.stdout.write(render_frame(width, height, t, args.label, args.mono))
        sys.stdout.flush()

        frame_index += 1
        target = start + (frame_index * frame_interval)
        sleep_for = target - time.perf_counter()
        if sleep_for > 0:
            time.sleep(sleep_for)

    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
