"""Hourglass Timer TUI — animated ASCII hourglass with falling sand."""

import signal
import sys
import time

from rich.console import Console
from rich.live import Live
from rich.text import Text

# Sand capacities per row (top-to-bottom of top bulb, then bottom bulb)
# Top bulb: 3 rows of 21, then taper rows 19,17,15,13,11,9,7,5,3  → total 162
# Bottom taper: 3,5,7,9,11,13,15,17,19, then 3 rows of 21        → total 162
# Grand total: 162
TOP_WIDTHS = [21, 21, 21, 19, 17, 15, 13, 11, 9, 7, 5, 3]  # 3 bulb + 9 taper
BOT_WIDTHS = [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 21, 21]
TOTAL_SAND = sum(TOP_WIDTHS)  # 162 grains of sand

# 3-frame falling grain cycle: (top, mid, bot)
NECK_FRAMES = [
    ("·", " ", " "),   # grain at top
    (" ", "·", " "),   # grain in middle
    (" ", " ", "·"),   # grain at bottom
]

# Color scheme for dark terminals
STYLE_CAP   = "color(39)"         # sky blue — caps
STYLE_FRAME = "color(248)"        # light grey — bulb borders (║)
STYLE_TAPER = "color(245)"        # medium grey — taper lines (╲╱)
STYLE_SAND  = "bold color(214)"   # amber/orange — sand
STYLE_GRAIN = "bold color(214)"   # amber — falling neck grain
STYLE_TIMER = "bold color(39)"    # sky blue — big digit timer

# 5-line tall ASCII art digits using block characters
BIG_DIGITS = {
    "0": [
        "█████",
        "█   █",
        "█   █",
        "█   █",
        "█████",
    ],
    "1": [
        "  █  ",
        " ██  ",
        "  █  ",
        "  █  ",
        "█████",
    ],
    "2": [
        "█████",
        "    █",
        "█████",
        "█    ",
        "█████",
    ],
    "3": [
        "█████",
        "    █",
        "█████",
        "    █",
        "█████",
    ],
    "4": [
        "█   █",
        "█   █",
        "█████",
        "    █",
        "    █",
    ],
    "5": [
        "█████",
        "█    ",
        "█████",
        "    █",
        "█████",
    ],
    "6": [
        "█████",
        "█    ",
        "█████",
        "█   █",
        "█████",
    ],
    "7": [
        "█████",
        "    █",
        "   █ ",
        "  █  ",
        "  █  ",
    ],
    "8": [
        "█████",
        "█   █",
        "█████",
        "█   █",
        "█████",
    ],
    "9": [
        "█████",
        "█   █",
        "█████",
        "    █",
        "█████",
    ],
    ":": [
        "     ",
        "  █  ",
        "     ",
        "  █  ",
        "     ",
    ],
}

BIG_DIGIT_HEIGHT = 5
BIG_DIGIT_WIDTH = 5
BIG_DIGIT_GAP = 1  # space between digits


def render_big_time(remaining_secs: float) -> list[str]:
    """Render MM:SS as large block-character text. Returns list of strings (one per line)."""
    mins = int(remaining_secs) // 60
    secs = int(remaining_secs) % 60
    time_str = f"{mins:02d}:{secs:02d}"

    rows = [""] * BIG_DIGIT_HEIGHT
    for i, ch in enumerate(time_str):
        glyph = BIG_DIGITS[ch]
        for row in range(BIG_DIGIT_HEIGHT):
            if i > 0:
                rows[row] += " " * BIG_DIGIT_GAP
            rows[row] += glyph[row]
    return rows


def compute_sand(progress: float) -> tuple[list[int], list[int], bool]:
    """Return (top_fills, bot_fills, grain_active) for a given progress 0→1."""
    sand_fallen = int(progress * TOTAL_SAND)
    sand_fallen = max(0, min(TOTAL_SAND, sand_fallen))
    sand_remaining = TOTAL_SAND - sand_fallen

    # Top bulb: fill from bottom rows first (sand settles), then remove from bottom
    top_fills = list(TOP_WIDTHS)  # start full
    to_remove = sand_fallen
    # Remove sand from bottom of top bulb (it falls out)
    for i in range(len(top_fills) - 1, -1, -1):
        if to_remove <= 0:
            break
        take = min(top_fills[i], to_remove)
        top_fills[i] -= take
        to_remove -= take

    # Bottom bulb: fill from bottom rows first (sand piles up)
    bot_fills = [0] * len(BOT_WIDTHS)
    to_add = sand_fallen
    for i in range(len(bot_fills) - 1, -1, -1):
        if to_add <= 0:
            break
        give = min(BOT_WIDTHS[i], to_add)
        bot_fills[i] = give
        to_add -= give

    grain_active = 0 < progress < 1
    return top_fills, bot_fills, grain_active


def format_sand(width: int, fill: int) -> str:
    """Return a string of `fill` sand chars centered in `width` space."""
    sand = "░" * fill
    pad = width - fill
    left = pad // 2
    right = pad - left
    return " " * left + sand + " " * right


def build_frame(progress: float, remaining_secs: float, frame_idx: int,
                 term_width: int = 80, term_height: int = 24) -> Text:
    """Build one frame of the hourglass as a Rich Text object, centered in the terminal."""
    top_fills, bot_fills, grain_active = compute_sand(progress)

    BULB_W = 21  # inner width of bulb rows

    # Neck grain animation (3-row falling cycle)
    if grain_active:
        neck_top, neck_mid, neck_bot = NECK_FRAMES[frame_idx % len(NECK_FRAMES)]
    else:
        neck_top, neck_mid, neck_bot = " ", " ", " "

    # Build raw hourglass lines (plain strings for width calculation, styled parts for rendering)
    raw_lines: list[str] = []
    styled_lines: list[list[tuple[str, str]]] = []

    def add_line(parts: list[tuple[str, str]]):
        raw = "".join(content for content, _ in parts)
        raw_lines.append(raw)
        styled_lines.append(parts)

    # Top cap
    add_line([("╔" + "═" * BULB_W + "╗", STYLE_CAP)])

    # Top bulb rows (indices 0-2, width 21)
    for i in range(3):
        inner = format_sand(BULB_W, top_fills[i])
        add_line([("║", STYLE_FRAME), (inner, STYLE_SAND), ("║", STYLE_FRAME)])

    # Top taper rows (indices 3-11, widths 19→3)
    for i in range(3, len(TOP_WIDTHS)):
        w = TOP_WIDTHS[i]
        pad = (BULB_W - w) // 2
        inner = format_sand(w, top_fills[i])
        add_line([
            (" " * pad + "╲", STYLE_TAPER),
            (inner, STYLE_SAND),
            ("╱", STYLE_TAPER),
        ])

    # Neck — 3 rows
    neck_pad = (BULB_W - 1) // 2
    add_line([(" " * neck_pad + "╲", STYLE_TAPER), (neck_top, STYLE_GRAIN), ("╱", STYLE_TAPER)])
    add_line([(" " * neck_pad + "│", STYLE_TAPER), (neck_mid, STYLE_GRAIN), ("│", STYLE_TAPER)])
    add_line([(" " * neck_pad + "╱", STYLE_TAPER), (neck_bot, STYLE_GRAIN), ("╲", STYLE_TAPER)])

    # Bottom taper rows (indices 0-8, widths 3→19)
    for i in range(len(BOT_WIDTHS) - 3):
        w = BOT_WIDTHS[i]
        pad = (BULB_W - w) // 2
        inner = format_sand(w, bot_fills[i])
        add_line([
            (" " * pad + "╱", STYLE_TAPER),
            (inner, STYLE_SAND),
            ("╲", STYLE_TAPER),
        ])

    # Bottom bulb rows (last 3, width 21)
    for i in range(len(BOT_WIDTHS) - 3, len(BOT_WIDTHS)):
        inner = format_sand(BULB_W, bot_fills[i])
        add_line([("║", STYLE_FRAME), (inner, STYLE_SAND), ("║", STYLE_FRAME)])

    # Bottom cap
    add_line([("╚" + "═" * BULB_W + "╝", STYLE_CAP)])

    # Big timer lines
    big_time_rows = render_big_time(remaining_secs)

    # Blank line between hourglass and timer
    raw_lines.append("")
    styled_lines.append([("", "")])

    for row in big_time_rows:
        raw_lines.append(row)
        styled_lines.append([(row, STYLE_TIMER)])

    # Compute centering
    max_content_width = max(len(line) for line in raw_lines)
    total_height = len(raw_lines)

    top_pad = max(0, (term_height - total_height) // 2)

    # Assemble into Rich Text
    text = Text()

    # Vertical padding
    if top_pad > 0:
        text.append("\n" * top_pad)

    for i, (raw, parts) in enumerate(zip(raw_lines, styled_lines)):
        if i > 0:
            text.append("\n")
        # Horizontal centering: pad each line so it's centered
        left_pad = max(0, (term_width - max_content_width) // 2)
        if left_pad > 0:
            text.append(" " * left_pad)
        for content, style in parts:
            if content or style:
                text.append(content, style=style)

    return text


def build_small_frame(remaining_secs: float) -> Text:
    """Fallback for small terminals: just show the countdown."""
    mins = int(remaining_secs) // 60
    secs = int(remaining_secs) % 60
    text = Text()
    text.append(f"  ⏳ {mins:02d}:{secs:02d}", style="bold bright_white")
    return text


def run_timer(duration_secs: float) -> None:
    """Run the hourglass timer for the given duration in seconds."""
    console = Console()
    interrupted = False

    def handle_sigint(sig, frame):
        nonlocal interrupted
        interrupted = True

    signal.signal(signal.SIGINT, handle_sigint)

    start = time.monotonic()
    frame_idx = 0

    try:
        with Live(
            console=console,
            refresh_per_second=8,
            screen=True,
            transient=True,
        ) as live:
            while not interrupted:
                elapsed = time.monotonic() - start
                remaining = max(0.0, duration_secs - elapsed)
                progress = min(1.0, elapsed / duration_secs) if duration_secs > 0 else 1.0

                # Check terminal size for fallback
                size = console.size
                if size.width < 30 or size.height < 38:
                    frame = build_small_frame(remaining)
                else:
                    frame = build_frame(progress, remaining, frame_idx,
                                        size.width, size.height)

                live.update(frame)
                frame_idx += 1

                if progress >= 1.0:
                    break

                time.sleep(0.125)

            # Completion notification
            if not interrupted and progress >= 1.0:
                # Bell
                console.bell()

                # Flash "TIME'S UP!" 3 times
                for _ in range(3):
                    size = console.size
                    msg = "TIME'S UP!"
                    h_pad = " " * max(0, (size.width - len(msg)) // 2)
                    v_pad = "\n" * max(0, size.height // 2)
                    text = Text()
                    text.append(v_pad + h_pad + msg, style="bold red on white")
                    live.update(text)
                    time.sleep(0.4)

                    size = console.size
                    live.update(build_frame(1.0, 0, frame_idx,
                                            size.width, size.height))
                    time.sleep(0.4)
                    frame_idx += 1

    except KeyboardInterrupt:
        pass

    signal.signal(signal.SIGINT, signal.SIG_DFL)
    console.print("\n  [bold green]Timer complete![/bold green]\n")


def main():
    if len(sys.argv) < 2:
        print("Usage: hglass <minutes>")
        print("  e.g. hglass 25    — 25-minute pomodoro")
        print("       hglass 0.1   — 6-second test")
        sys.exit(1)

    try:
        minutes = float(sys.argv[1])
    except ValueError:
        print(f"Error: '{sys.argv[1]}' is not a valid number of minutes.")
        sys.exit(1)

    if minutes <= 0:
        print("Error: duration must be positive.")
        sys.exit(1)

    run_timer(minutes * 60)


if __name__ == "__main__":
    main()
