"""Animated ASCII logo for Oubli.

Displays "OUBLI" in Claude Code style:
- Solid orange filled blocks in foreground
- Single unified orange double-line outline shadow per letter (offset behind)
- Interactive mode: Q to quit, mouse click for explosion effects
"""

import math
import random
import sys
import time
from dataclasses import dataclass, field

# ANSI escape codes
CLEAR_SCREEN = "\033[2J"
HOME = "\033[H"
HIDE_CURSOR = "\033[?25l"
SHOW_CURSOR = "\033[?25h"

# Colors (24-bit true color)
ORANGE = "\033[38;2;224;123;83m"
DARK_ORANGE = "\033[38;2;160;88;59m"  # Darker for exploding particles
RESET = "\033[0m"

# Box drawing characters for shadow outline
BOX_CHARS = {
    # (top, right, bottom, left) -> character
    (True, False, False, False): "═",   # top edge only
    (False, False, True, False): "═",   # bottom edge only
    (False, True, False, False): "║",   # right edge only
    (False, False, False, True): "║",   # left edge only
    (True, True, False, False): "╔",    # top-right corner (outside)
    (True, False, False, True): "╗",    # top-left corner (outside)
    (False, True, True, False): "╚",    # bottom-right corner (outside)
    (False, False, True, True): "╝",    # bottom-left corner (outside)
    (True, True, True, False): "╠",     # T junction
    (True, False, True, True): "╣",     # T junction
    (True, True, False, True): "╦",     # T junction
    (False, True, True, True): "╩",     # T junction
    (True, True, True, True): "╬",      # cross
    (True, False, True, False): "═",    # horizontal through
    (False, True, False, True): "║",    # vertical through
}

# Solid block for main text
FILL = "█"

# Block dimensions (for main fill)
BLOCK_WIDTH = 4   # chars wide
BLOCK_HEIGHT = 2  # lines tall (reduced for Claude Code style)

# Shadow offset
SHADOW_OFFSET_ROW = 1
SHADOW_OFFSET_COL = 2


# =============================================================================
# Particle System for Explosion Effects
# =============================================================================

@dataclass
class Particle:
    """A single particle for explosion effects."""
    x: float
    y: float
    vx: float
    vy: float
    char: str = FILL
    color: str = DARK_ORANGE
    lifetime: int = 80
    landed: bool = False
    prev_x: float = 0.0
    prev_y: float = 0.0

    def update(self, gravity: float = 0.12, drag: float = 0.98, floor_y: int = 30):
        """Apply physics: gravity, drag, floor collision."""
        if self.landed:
            return

        # Store previous position for clearing
        self.prev_x = self.x
        self.prev_y = self.y

        # Gravity pulls down
        self.vy += gravity

        # Drag reduces velocity
        self.vx *= drag
        self.vy *= drag

        # Update position
        self.x += self.vx
        self.y += self.vy

        # Check floor collision (land at bottom)
        if self.y >= floor_y:
            self.y = floor_y
            self.vy = 0
            self.vx = 0
            self.landed = True

        self.lifetime -= 1

    def prev_screen_pos(self) -> tuple[int, int]:
        """Get previous integer screen coordinates for clearing."""
        return (int(self.prev_x), int(self.prev_y))

    def is_alive(self) -> bool:
        """Particle is alive if it has lifetime and is on screen."""
        return self.lifetime > 0

    def screen_pos(self) -> tuple[int, int]:
        """Get integer screen coordinates."""
        return (int(self.x), int(self.y))


@dataclass
class ParticleSystem:
    """Manages particles for explosion effects."""
    particles: list[Particle] = field(default_factory=list)

    def spawn_explosion(self, center_x: int, center_y: int, count: int = 16):
        """Spawn particles radiating outward from a point."""
        for i in range(count):
            # Radial distribution with some randomness
            angle = (i / count) * 2 * math.pi + random.uniform(-0.2, 0.2)
            speed = random.uniform(1.5, 3.5)

            vx = speed * math.cos(angle)
            vy = speed * math.sin(angle) - 0.5  # Slight upward bias

            particle = Particle(
                x=float(center_x),
                y=float(center_y),
                vx=vx,
                vy=vy,
                lifetime=random.randint(60, 100),
                prev_x=float(center_x),
                prev_y=float(center_y),
            )
            self.particles.append(particle)

    def spawn_from_logo_pixel(self, screen_x: int, screen_y: int,
                               click_x: int, click_y: int):
        """Spawn a particle from a logo pixel, flying away from click point."""
        # Direction away from click
        dx = screen_x - click_x
        dy = screen_y - click_y
        dist = math.sqrt(dx * dx + dy * dy) or 1

        # Normalize and scale velocity
        speed = random.uniform(2.0, 4.0)
        vx = (dx / dist) * speed + random.uniform(-0.3, 0.3)
        vy = (dy / dist) * speed + random.uniform(-0.3, 0.3) - 0.5

        particle = Particle(
            x=float(screen_x),
            y=float(screen_y),
            vx=vx,
            vy=vy,
            lifetime=random.randint(70, 120),
            prev_x=float(screen_x),
            prev_y=float(screen_y),
        )
        self.particles.append(particle)

    def update(self, floor_y: int = 30):
        """Update all particles."""
        for particle in self.particles:
            particle.update(floor_y=floor_y)

        # Remove dead particles (but keep landed ones for a bit)
        self.particles = [p for p in self.particles if p.is_alive()]

    def render(self, term, start_row: int, start_col: int,
               screen_width: int, screen_height: int):
        """Render particles to terminal."""
        for particle in self.particles:
            x, y = particle.screen_pos()
            # Check bounds (relative to logo area)
            if 0 <= x < screen_width + 20 and 0 <= y < screen_height + 10:
                screen_x = start_col + x
                screen_y = start_row + y
                if screen_x > 0 and screen_y > 0:
                    sys.stdout.write(f"\033[{screen_y};{screen_x}H")
                    sys.stdout.write(f"{particle.color}{particle.char}{RESET}")


# Custom pixel letters - each '#' becomes a block (6 rows for balanced look)
LETTERS = {
    'O': [
        " ### ",
        "#   #",
        "#   #",
        "#   #",
        "#   #",
        " ### ",
    ],
    'U': [
        "#   #",
        "#   #",
        "#   #",
        "#   #",
        "#   #",
        " ### ",
    ],
    'B': [
        "#### ",
        "#   #",
        "#### ",
        "#   #",
        "#   #",
        "#### ",
    ],
    'L': [
        "#    ",
        "#    ",
        "#    ",
        "#    ",
        "#    ",
        "#####",
    ],
    'I': [
        "#####",
        "  #  ",
        "  #  ",
        "  #  ",
        "  #  ",
        "#####",
    ],
}


def move_cursor(row: int, col: int) -> str:
    """Move cursor to row, col (1-indexed)."""
    return f"\033[{row};{col}H"


def get_letter_grid(word: str) -> list[list[bool]]:
    """Create a combined grid for all letters in the word."""
    height = 6  # Balanced letter height
    letter_spacing = 7  # 5 per letter + 2 spacing
    total_width = len(word) * letter_spacing - 2  # minus last spacing

    grid = [[False] * total_width for _ in range(height)]

    col_offset = 0
    for char in word:
        letter = LETTERS[char]
        for row_idx, row_str in enumerate(letter):
            for col_idx, c in enumerate(row_str):
                if c == '#':
                    grid[row_idx][col_offset + col_idx] = True
        col_offset += letter_spacing

    return grid


def expand_grid(grid: list[list[bool]], block_width: int, block_height: int) -> list[list[bool]]:
    """Expand pixel grid to screen resolution (each pixel becomes a block)."""
    height = len(grid) * block_height
    width = len(grid[0]) * block_width

    expanded = [[False] * width for _ in range(height)]

    for row_idx, row in enumerate(grid):
        for col_idx, filled in enumerate(row):
            if filled:
                for r in range(block_height):
                    for c in range(block_width):
                        expanded[row_idx * block_height + r][col_idx * block_width + c] = True

    return expanded


def get_outline_char(grid: list[list[bool]], row: int, col: int) -> str | None:
    """Get the appropriate box-drawing character for an outline position.

    Returns None if this position is not on the outline.
    """
    height = len(grid)
    width = len(grid[0])

    def cell(r, c):
        """Safely get cell value, False if out of bounds."""
        if 0 <= r < height and 0 <= c < width:
            return grid[r][c]
        return False

    # Check if this cell is filled
    is_filled = cell(row, col)

    if is_filled:
        return None  # Inside the shape, not on outline

    # Check adjacent cells
    top = cell(row - 1, col)
    bottom = cell(row + 1, col)
    left = cell(row, col - 1)
    right = cell(row, col + 1)

    # If no adjacent filled cells, not on outline
    if not (top or bottom or left or right):
        return None

    # Determine which edges are exposed (adjacent to filled)
    key = (top, right, bottom, left)

    # Handle corners specially by checking diagonals too
    top_left = cell(row - 1, col - 1)
    top_right = cell(row - 1, col + 1)
    bottom_left = cell(row + 1, col - 1)
    bottom_right = cell(row + 1, col + 1)

    # Outside corners
    if top_right and not top and not right:
        return "╔"
    if top_left and not top and not left:
        return "╗"
    if bottom_right and not bottom and not right:
        return "╚"
    if bottom_left and not bottom and not left:
        return "╝"

    # Inside corners (concave)
    if top and left and not top_left:
        return "╝"
    if top and right and not top_right:
        return "╚"
    if bottom and left and not bottom_left:
        return "╗"
    if bottom and right and not bottom_right:
        return "╔"

    # Edges
    if top or bottom:
        if left or right:
            return BOX_CHARS.get(key, "╬")
        return "═"
    if left or right:
        return "║"

    return BOX_CHARS.get(key)


def generate_logo_blocks() -> list[tuple[int, int]]:
    """Generate list of (grid_row, grid_col) for each pixel in OUBLI."""
    word = "OUBLI"
    blocks = []
    letter_spacing = 7  # 5 per letter + 2 spacing

    col_offset = 0
    for char in word:
        letter = LETTERS[char]
        for row_idx, row_str in enumerate(letter):
            for col_idx, c in enumerate(row_str):
                if c == '#':
                    blocks.append((row_idx, col_offset + col_idx))
        col_offset += letter_spacing

    return blocks


def animate_logo(speed: float = 0.02):
    """Animate the logo with left-to-right sweep."""
    # Get the pixel grid
    pixel_grid = get_letter_grid("OUBLI")

    # Expand to screen resolution
    expanded = expand_grid(pixel_grid, BLOCK_WIDTH, BLOCK_HEIGHT)

    # Get blocks for animation ordering
    blocks = generate_logo_blocks()

    if not blocks:
        print("No logo to render")
        return

    # Group blocks by their grid column for sweep animation
    from collections import defaultdict
    cols = defaultdict(list)
    for grid_row, grid_col in blocks:
        cols[grid_col].append((grid_row, grid_col))

    # Starting position on screen
    start_row = 2
    start_col = 3

    # Calculate dimensions
    max_row = max(r for r, c in blocks)
    screen_height = len(expanded) + SHADOW_OFFSET_ROW
    screen_width = len(expanded[0]) + SHADOW_OFFSET_COL

    # Pre-compute shadow outline
    shadow_chars = {}
    for row in range(-1, screen_height + 1):
        for col in range(-1, screen_width + 1):
            char = get_outline_char(expanded, row - SHADOW_OFFSET_ROW, col - SHADOW_OFFSET_COL)
            if char:
                shadow_chars[(row, col)] = char

    # Initialize screen
    sys.stdout.write(HIDE_CURSOR)
    sys.stdout.write(CLEAR_SCREEN)
    sys.stdout.write(HOME)
    sys.stdout.flush()

    try:
        # Sweep through grid columns left to right
        for grid_col in sorted(cols.keys()):
            for grid_row, _ in cols[grid_col]:
                # Calculate screen positions for this block
                base_row = grid_row * BLOCK_HEIGHT
                base_col = grid_col * BLOCK_WIDTH

                # Draw shadow outline chars in this block's shadow area
                for r in range(BLOCK_HEIGHT + 2):
                    for c in range(BLOCK_WIDTH + 2):
                        shadow_row = base_row + SHADOW_OFFSET_ROW + r - 1
                        shadow_col = base_col + SHADOW_OFFSET_COL + c - 1
                        if (shadow_row, shadow_col) in shadow_chars:
                            sys.stdout.write(move_cursor(start_row + shadow_row, start_col + shadow_col))
                            sys.stdout.write(f"{ORANGE}{shadow_chars[(shadow_row, shadow_col)]}{RESET}")

                # Draw main filled block on top
                for r in range(BLOCK_HEIGHT):
                    sys.stdout.write(move_cursor(start_row + base_row + r, start_col + base_col))
                    sys.stdout.write(f"{ORANGE}{FILL * BLOCK_WIDTH}{RESET}")

            sys.stdout.flush()
            time.sleep(speed)

        # Hold the final result
        time.sleep(0.5)

        # Move cursor below the logo
        final_row = start_row + screen_height + 2
        sys.stdout.write(move_cursor(final_row, start_col))
        sys.stdout.write(f"{ORANGE}Oubli{RESET} - Fractal Memory System\n\n")
        sys.stdout.flush()

    except KeyboardInterrupt:
        pass
    finally:
        sys.stdout.write(SHOW_CURSOR)
        sys.stdout.flush()


def animate_logo_interactive(speed: float = 0.02):
    """Interactive logo with mouse click explosions. Press Q to quit."""
    import select
    import termios
    import tty
    import shutil

    # Get terminal size for bounds checking
    term_size = shutil.get_terminal_size()
    term_cols = term_size.columns
    term_rows = term_size.lines

    # Get the pixel grid and expand it
    pixel_grid = get_letter_grid("OUBLI")
    expanded = expand_grid(pixel_grid, BLOCK_WIDTH, BLOCK_HEIGHT)

    # Get blocks for animation
    blocks = generate_logo_blocks()
    if not blocks:
        print("No logo to render")
        return

    # Group blocks by column for sweep animation
    from collections import defaultdict
    cols = defaultdict(list)
    for grid_row, grid_col in blocks:
        cols[grid_col].append((grid_row, grid_col))

    # Screen positioning
    start_row = 2
    start_col = 3
    screen_height = len(expanded) + SHADOW_OFFSET_ROW
    screen_width = len(expanded[0]) + SHADOW_OFFSET_COL
    floor_y = term_rows - start_row - 2  # Floor at bottom of terminal

    # Pre-compute shadow outline
    shadow_chars = {}
    for row in range(-1, screen_height + 1):
        for col in range(-1, screen_width + 1):
            char = get_outline_char(expanded, row - SHADOW_OFFSET_ROW, col - SHADOW_OFFSET_COL)
            if char:
                shadow_chars[(row, col)] = char

    # Track screen positions where logo pixels are rendered (actual terminal coords)
    # Will be populated during animation with actual screen coordinates
    logo_screen_pixels: set[tuple[int, int]] = set()

    # Track exploded pixels (screen coords, removed from logo)
    exploded_screen_pixels: set[tuple[int, int]] = set()

    # Particle system
    particles = ParticleSystem()

    # Animation state
    animation_complete = False
    animation_col_idx = 0
    sorted_cols = sorted(cols.keys())

    # Save terminal settings
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)

    # Use alternate screen buffer and clear (prevents scrolling issues)
    sys.stdout.write("\033[?1049h")  # Switch to alternate screen
    sys.stdout.write(CLEAR_SCREEN + HOME + HIDE_CURSOR)
    sys.stdout.flush()

    # Detect Warp terminal and apply mouse Y offset correction
    # Warp reports mouse coords relative to scrollback, not viewport
    import os
    term_program = os.environ.get("TERM_PROGRAM", "")
    if "warp" in term_program.lower() or "WarpTerminal" in term_program:
        # Warp has an offset - detect by checking scroll position
        # For now, use a reasonable default based on typical Warp layout
        mouse_y_offset = 0  # Will be auto-calibrated on first click
    else:
        mouse_y_offset = 0

    # Auto-calibrate: if first click is far from any logo pixel,
    # find nearest and calculate offset
    calibrated = False

    def read_input() -> str:
        """Non-blocking read of available input."""
        result = ""
        while select.select([sys.stdin], [], [], 0)[0]:
            ch = sys.stdin.read(1)
            if ch:
                result += ch
            else:
                break
        return result

    def parse_mouse_event(data: str) -> tuple[int, int] | None:
        """Parse mouse click from escape sequence. Returns (x, y) or None."""
        import re
        # SGR extended mode: [<btn;x;y;M (press) or m (release)
        # Note: \x1b may be missing or at end due to read splitting
        match = re.search(r'\[<(\d+);(\d+);(\d+)M', data)
        if match:
            btn, x, y = match.groups()
            if int(btn) == 0:  # Left button press
                return (int(x), int(y))
        return None

    try:
        # Set terminal to raw mode
        tty.setraw(fd)

        # Enable mouse tracking
        sys.stdout.write("\033[?1000h\033[?1006h")
        sys.stdout.flush()

        last_frame_time = time.time()
        input_buffer = ""

        while True:
            current_time = time.time()
            dt = current_time - last_frame_time

            # Read any available input (non-blocking)
            new_input = read_input()
            input_buffer += new_input

            # Check for Q to quit
            if 'q' in input_buffer or 'Q' in input_buffer:
                break

            # Check for mouse clicks
            mouse_pos = parse_mouse_event(input_buffer)
            if mouse_pos:
                mouse_x, mouse_y = mouse_pos

                # Auto-calibrate Y offset: try to find which offset gives hits
                if not calibrated and logo_screen_pixels:
                    best_offset = 0
                    best_hits = 0
                    # Try various offsets to find the one that matches
                    for test_offset in range(0, 100, 5):
                        test_y = mouse_y - test_offset
                        hits = sum(
                            1 for sx, sy in logo_screen_pixels
                            if math.sqrt((sx - mouse_x)**2 + (sy - test_y)**2) < 15
                        )
                        if hits > best_hits:
                            best_hits = hits
                            best_offset = test_offset
                    if best_hits > 0:
                        mouse_y_offset = best_offset
                        calibrated = True

                # Apply Y offset correction
                adjusted_y = mouse_y - mouse_y_offset

                # Find nearby logo pixels using adjusted coordinates
                explosion_radius = 10
                pixels_to_explode = []

                for (sx, sy) in list(logo_screen_pixels - exploded_screen_pixels):
                    dist = math.sqrt((sx - mouse_x)**2 + (sy - adjusted_y)**2)
                    if dist < explosion_radius:
                        pixels_to_explode.append((sx, sy))

                # Spawn particles from exploded pixels
                for (sx, sy) in pixels_to_explode:
                    exploded_screen_pixels.add((sx, sy))
                    # Particles use logo-relative coords (for rendering with start_* offset)
                    particles.spawn_from_logo_pixel(
                        sx - start_col, sy - start_row,
                        mouse_x - start_col, adjusted_y - start_row
                    )

                # Also spawn extra particles at adjusted click point
                particles.spawn_explosion(
                    mouse_x - start_col, adjusted_y - start_row, count=12
                )

            # Clear processed input
            input_buffer = ""

            # Advance intro animation
            if not animation_complete and dt >= speed:
                if animation_col_idx < len(sorted_cols):
                    grid_col = sorted_cols[animation_col_idx]
                    for grid_row, _ in cols[grid_col]:
                        base_row = grid_row * BLOCK_HEIGHT
                        base_col = grid_col * BLOCK_WIDTH

                        # Draw shadow
                        for r in range(BLOCK_HEIGHT + 2):
                            for c in range(BLOCK_WIDTH + 2):
                                shadow_row = base_row + SHADOW_OFFSET_ROW + r - 1
                                shadow_col = base_col + SHADOW_OFFSET_COL + c - 1
                                if (shadow_row, shadow_col) in shadow_chars:
                                    sys.stdout.write(move_cursor(
                                        start_row + shadow_row,
                                        start_col + shadow_col
                                    ))
                                    sys.stdout.write(
                                        f"{ORANGE}{shadow_chars[(shadow_row, shadow_col)]}{RESET}"
                                    )

                        # Draw filled blocks and track screen positions
                        for r in range(BLOCK_HEIGHT):
                            for c in range(BLOCK_WIDTH):
                                screen_y = start_row + base_row + r
                                screen_x = start_col + base_col + c
                                screen_pos = (screen_x, screen_y)
                                # Track this pixel's screen position
                                logo_screen_pixels.add(screen_pos)
                                if screen_pos not in exploded_screen_pixels:
                                    sys.stdout.write(move_cursor(screen_y, screen_x))
                                    sys.stdout.write(f"{ORANGE}{FILL}{RESET}")

                    animation_col_idx += 1
                    last_frame_time = current_time
                else:
                    animation_complete = True
                    # Show tagline
                    final_row = start_row + screen_height + 2
                    sys.stdout.write(move_cursor(final_row, start_col))
                    sys.stdout.write(f"{ORANGE}Oubli{RESET} - Fractal Memory System")
                    sys.stdout.write(move_cursor(final_row + 1, start_col))
                    sys.stdout.write(f"Click to explode | Press Q to quit")

            # Clear previous particle positions first
            for particle in particles.particles:
                px, py = particle.prev_screen_pos()
                if px != 0 or py != 0:  # Skip if no previous position
                    screen_x = start_col + px
                    screen_y = start_row + py
                    # Only clear within terminal bounds (avoid scrolling)
                    if 0 < screen_x < term_cols and 0 < screen_y < term_rows:
                        sys.stdout.write(f"\033[{screen_y};{screen_x}H ")

            # Update particle physics
            particles.update(floor_y=floor_y)

            # Clear exploded pixel positions (already in screen coords)
            for (screen_x, screen_y) in exploded_screen_pixels:
                if 0 < screen_x < term_cols and 0 < screen_y < term_rows:
                    sys.stdout.write(f"\033[{screen_y};{screen_x}H ")

            # Render particles at new positions
            for particle in particles.particles:
                x, y = particle.screen_pos()
                screen_x = start_col + x
                screen_y = start_row + y
                # Only render within terminal bounds
                if 0 < screen_x < term_cols and 0 < screen_y < term_rows:
                    sys.stdout.write(f"\033[{screen_y};{screen_x}H")
                    sys.stdout.write(f"{particle.color}{particle.char}{RESET}")

            sys.stdout.flush()
            time.sleep(0.016)  # ~60 FPS

    except KeyboardInterrupt:
        pass
    finally:
        # Disable mouse tracking
        sys.stdout.write("\033[?1006l\033[?1000l")
        # Restore terminal and exit alternate screen
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        sys.stdout.write(SHOW_CURSOR)
        sys.stdout.write("\033[?1049l")  # Exit alternate screen
        sys.stdout.flush()


def print_logo_static():
    """Print the logo without animation."""
    # Get the pixel grid
    pixel_grid = get_letter_grid("OUBLI")

    # Expand to screen resolution
    expanded = expand_grid(pixel_grid, BLOCK_WIDTH, BLOCK_HEIGHT)

    height = len(expanded)
    width = len(expanded[0])

    # Screen buffer with space for shadow offset
    screen_height = height + SHADOW_OFFSET_ROW
    screen_width = width + SHADOW_OFFSET_COL

    screen = [[' ' for _ in range(screen_width)] for _ in range(screen_height)]
    colors = [[None for _ in range(screen_width)] for _ in range(screen_height)]

    # Draw shadow outline first
    for row in range(-1, screen_height + 1):
        for col in range(-1, screen_width + 1):
            char = get_outline_char(expanded, row - SHADOW_OFFSET_ROW, col - SHADOW_OFFSET_COL)
            if char and 0 <= row < screen_height and 0 <= col < screen_width:
                screen[row][col] = char
                colors[row][col] = ORANGE

    # Draw main filled blocks on top (overwrites shadow where they overlap)
    for row in range(height):
        for col in range(width):
            if expanded[row][col]:
                screen[row][col] = FILL
                colors[row][col] = ORANGE

    # Print
    print()
    for row_idx in range(screen_height):
        line = "   "
        for col_idx in range(screen_width):
            char = screen[row_idx][col_idx]
            color = colors[row_idx][col_idx]
            if color:
                line += f"{color}{char}{RESET}"
            else:
                line += char
        print(line)
    print(f"\n   {ORANGE}Oubli{RESET} - Fractal Memory System\n")


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Oubli ASCII logo")
    parser.add_argument("--static", action="store_true", help="Show static logo")
    parser.add_argument("--no-interactive", action="store_true", help="Non-interactive animation")
    args = parser.parse_args()

    if args.static:
        print_logo_static()
    elif args.no_interactive:
        animate_logo()
    else:
        animate_logo_interactive()
