"""ASCII rendering of phase portraits and dynamics."""

from __future__ import annotations

from gamescape.dynamics import (
    PayoffMatrix,
    FixedPoint,
    find_fixed_points,
    replicator_dx,
    trajectory,
    classify_game,
)

# ANSI color codes
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"


def _arrow(dx: float) -> str:
    """Map a derivative to an arrow character."""
    if dx > 0.1:
        return ">>>"
    elif dx > 0.01:
        return ">>"
    elif dx > 0.001:
        return ">"
    elif dx > 0:
        return ">"
    elif dx < -0.1:
        return "<<<"
    elif dx < -0.01:
        return "<<"
    elif dx < -0.001:
        return "<"
    elif dx < 0:
        return "<"
    return "."


def render_flow_line(game: PayoffMatrix, width: int = 60, color: bool = True) -> str:
    """Render a 1D flow line showing direction of replicator dynamics.

    x=0 (all-D) on left, x=1 (all-C) on right.
    """
    fps = find_fixed_points(game)
    cells: list[str] = []

    for i in range(width):
        x = i / (width - 1)
        dx = replicator_dx(game, x)

        # Check if near a fixed point
        near_fp = None
        for fp in fps:
            if abs(x - fp.x) < 0.5 / width:
                near_fp = fp
                break

        if near_fp is not None:
            if color:
                if near_fp.stable:
                    cells.append(f"{GREEN}{BOLD}@{RESET}")
                else:
                    cells.append(f"{RED}{BOLD}o{RESET}")
            else:
                cells.append("@" if near_fp.stable else "o")
        else:
            arrow = _arrow(dx)
            if color:
                if dx > 0:
                    cells.append(f"{CYAN}{arrow[-1]}{RESET}")
                elif dx < 0:
                    cells.append(f"{MAGENTA}{arrow[-1]}{RESET}")
                else:
                    cells.append(f"{DIM}.{RESET}")
            else:
                cells.append(arrow[-1])

    line = "".join(cells)
    return f"  all-D |{line}| all-C"


def render_trajectory_plot(
    game: PayoffMatrix, x0_values: list[float] | None = None,
    width: int = 70, height: int = 20, color: bool = True,
) -> str:
    """Render x(t) trajectories as an ASCII plot."""
    if x0_values is None:
        x0_values = [0.1, 0.3, 0.5, 0.7, 0.9]

    trajectories = [trajectory(game, x0, steps=200) for x0 in x0_values]
    max_t = max(len(t) for t in trajectories)

    # Build the grid
    grid = [[" " for _ in range(width)] for _ in range(height)]
    symbols = ["*", "+", "~", "#", "^", "=", "%", "&"]

    for idx, traj in enumerate(trajectories):
        sym = symbols[idx % len(symbols)]
        for t_idx, x_val in enumerate(traj):
            col = int(t_idx / max_t * (width - 1))
            row = height - 1 - int(x_val * (height - 1))
            row = max(0, min(height - 1, row))
            col = max(0, min(width - 1, col))
            grid[row][col] = sym

    lines: list[str] = []
    lines.append(f"  x(t) trajectories from {len(x0_values)} initial conditions")
    lines.append(f"  {'':>4} {'':─<{width}}")

    for r in range(height):
        x_label = f"{1.0 - r / (height - 1):.1f}"
        row_str = "".join(grid[r])
        if color:
            # Colorize each trajectory symbol
            colored = []
            colors = [CYAN, GREEN, YELLOW, MAGENTA, RED, BLUE, CYAN, GREEN]
            for ch in row_str:
                if ch in symbols:
                    c = colors[symbols.index(ch) % len(colors)]
                    colored.append(f"{c}{ch}{RESET}")
                else:
                    colored.append(ch)
            row_str = "".join(colored)
        lines.append(f"  {x_label:>4}|{row_str}|")

    lines.append(f"  {'':>4} {'':─<{width}}")
    lines.append(f"  {'':>4} t=0{'':>{width - 6}}t=T")

    # Legend
    lines.append("")
    legend_parts = []
    for idx, x0 in enumerate(x0_values):
        sym = symbols[idx % len(symbols)]
        if color:
            c = [CYAN, GREEN, YELLOW, MAGENTA, RED, BLUE, CYAN, GREEN][idx % 8]
            legend_parts.append(f"{c}{sym}{RESET} x0={x0:.1f}")
        else:
            legend_parts.append(f"{sym} x0={x0:.1f}")
    lines.append("  " + "  ".join(legend_parts))

    return "\n".join(lines)


def render_payoff_table(game: PayoffMatrix, color: bool = True) -> str:
    """Render the payoff matrix as a formatted table."""
    header = f"{'':>12}{'Cooperate':>12}{'Defect':>12}"
    row_c = f"{'Cooperate':>12}"
    row_d = f"{'Defect':>12}"

    if color:
        row_c += f"{GREEN}{game.a:>12.1f}{RESET}{RED}{game.b:>12.1f}{RESET}"
        row_d += f"{YELLOW}{game.c:>12.1f}{RESET}{BLUE}{game.d:>12.1f}{RESET}"
    else:
        row_c += f"{game.a:>12.1f}{game.b:>12.1f}"
        row_d += f"{game.c:>12.1f}{game.d:>12.1f}"

    return f"  {header}\n  {row_c}\n  {row_d}"


def _strip_ansi(s: str) -> str:
    """Remove ANSI escape codes to get visible length."""
    import re
    return re.sub(r"\033\[[0-9;]*m", "", s)


def _visible_len(s: str) -> str:
    """Visible character count (ignoring ANSI codes)."""
    return len(_strip_ansi(s))


def _pad_to(s: str, width: int) -> str:
    """Pad a string (which may contain ANSI codes) to a visible width."""
    vis = _visible_len(s)
    if vis >= width:
        return s
    return s + " " * (width - vis)


def render_compact(name: str, game: PayoffMatrix, flow_width: int = 30,
                   traj_width: int = 30, traj_height: int = 12,
                   color: bool = True) -> list[str]:
    """Render a compact single-column card for one game. Returns list of lines."""
    fps = find_fixed_points(game)
    classification = classify_game(game)

    lines: list[str] = []

    # Title bar
    if color:
        lines.append(f"{BOLD}{CYAN}{'=' * 34}{RESET}")
        lines.append(f"{BOLD}{CYAN} {name:^32s} {RESET}")
        lines.append(f"{BOLD}{CYAN}{'=' * 34}{RESET}")
    else:
        lines.append("=" * 34)
        lines.append(f" {name:^32s} ")
        lines.append("=" * 34)

    # Payoff matrix (compact)
    lines.append(f"       {'C':>6s}{'D':>6s}")
    if color:
        lines.append(f"   C  {GREEN}{game.a:>6.0f}{RESET}{RED}{game.b:>6.0f}{RESET}")
        lines.append(f"   D  {YELLOW}{game.c:>6.0f}{RESET}{BLUE}{game.d:>6.0f}{RESET}")
    else:
        lines.append(f"   C  {game.a:>6.0f}{game.b:>6.0f}")
        lines.append(f"   D  {game.c:>6.0f}{game.d:>6.0f}")

    # Classification
    if color:
        lines.append(f" {BOLD}Type:{RESET} {YELLOW}{classification}{RESET}")
    else:
        lines.append(f" Type: {classification}")

    # Fixed points
    for fp in fps:
        marker = "@" if fp.stable else "o"
        stab = "stable" if fp.stable else "unstable"
        if color:
            sc = GREEN if fp.stable else RED
            lines.append(f"  {sc}{marker}{RESET} x={fp.x:.3f} {fp.label} {sc}{stab}{RESET}")
        else:
            lines.append(f"  {marker} x={fp.x:.3f} {fp.label} {stab}")

    # Flow line
    flow = render_flow_line(game, width=flow_width, color=color)
    # Strip the "  all-D |..| all-C" wrapper for compact version
    lines.append(f" D|{flow.split('|')[1]}|C")

    # Mini trajectory plot
    x0_values = [0.1, 0.3, 0.5, 0.7, 0.9]
    trajectories = [trajectory(game, x0, steps=200) for x0 in x0_values]
    max_t = max(len(t) for t in trajectories)
    symbols = [".", ":", "~", "#", "^"]

    grid = [[" " for _ in range(traj_width)] for _ in range(traj_height)]
    for idx, traj in enumerate(trajectories):
        sym = symbols[idx % len(symbols)]
        for t_idx, x_val in enumerate(traj):
            col = int(t_idx / max_t * (traj_width - 1))
            row = traj_height - 1 - int(x_val * (traj_height - 1))
            row = max(0, min(traj_height - 1, row))
            col = max(0, min(traj_width - 1, col))
            grid[row][col] = sym

    traj_colors = [CYAN, GREEN, YELLOW, MAGENTA, RED]
    lines.append(f" {'─' * (traj_width + 2)}")
    for r in range(traj_height):
        label = f"{1.0 - r / (traj_height - 1):.1f}" if r % 3 == 0 else "   "
        row_str = "".join(grid[r])
        if color:
            colored = []
            for ch in row_str:
                if ch in symbols:
                    c = traj_colors[symbols.index(ch) % len(traj_colors)]
                    colored.append(f"{c}{ch}{RESET}")
                else:
                    colored.append(ch)
            row_str = "".join(colored)
        lines.append(f"{label}|{row_str}|")
    lines.append(f" {'─' * (traj_width + 2)}")

    return lines


def render_comparison(games: dict[str, PayoffMatrix], color: bool = True) -> str:
    """Render multiple games side by side in compact columns."""
    # Render each game as a list of lines
    columns: list[list[str]] = []
    for name, game in games.items():
        col = render_compact(name, game, color=color)
        columns.append(col)

    # Pad all columns to same height
    max_height = max(len(c) for c in columns)
    for col in columns:
        while len(col) < max_height:
            col.append("")

    # Determine column width (visible chars) — use widest line per column
    col_widths = []
    for col in columns:
        w = max(_visible_len(line) for line in col)
        col_widths.append(w)

    # Join columns side by side with separator
    sep = "  "
    output_lines: list[str] = []
    for row_idx in range(max_height):
        parts = []
        for col_idx, col in enumerate(columns):
            line = col[row_idx] if row_idx < len(col) else ""
            parts.append(_pad_to(line, col_widths[col_idx]))
        output_lines.append(sep.join(parts))

    # Header
    header = ""
    if color:
        header = f"\n{BOLD}  Evolutionary Game Theory — All Classic 2x2 Games{RESET}\n"
    else:
        header = "\n  Evolutionary Game Theory — All Classic 2x2 Games\n"

    return header + "\n".join(output_lines) + "\n"


def render_analysis(game: PayoffMatrix, color: bool = True) -> str:
    """Full analysis output."""
    fps = find_fixed_points(game)
    classification = classify_game(game)

    lines: list[str] = []

    # Title
    if color:
        lines.append(f"\n{BOLD}  Game Analysis{RESET}")
    else:
        lines.append("\n  Game Analysis")
    lines.append("  " + "=" * 40)

    # Payoff matrix
    lines.append("")
    if color:
        lines.append(f"  {BOLD}Payoff Matrix:{RESET}")
    else:
        lines.append("  Payoff Matrix:")
    lines.append(render_payoff_table(game, color))

    # Classification
    lines.append("")
    if color:
        lines.append(f"  {BOLD}Classification:{RESET} {CYAN}{classification}{RESET}")
    else:
        lines.append(f"  Classification: {classification}")

    # Fixed points
    lines.append("")
    if color:
        lines.append(f"  {BOLD}Fixed Points:{RESET}")
    else:
        lines.append("  Fixed Points:")
    for fp in fps:
        stability = "stable" if fp.stable else "unstable"
        if color:
            s_color = GREEN if fp.stable else RED
            lines.append(
                f"    {s_color}{'@' if fp.stable else 'o'}{RESET} "
                f"x={fp.x:.4f} ({fp.label}, {s_color}{stability}{RESET})"
            )
        else:
            lines.append(
                f"    {'@' if fp.stable else 'o'} "
                f"x={fp.x:.4f} ({fp.label}, {stability})"
            )

    # Flow line
    lines.append("")
    if color:
        lines.append(f"  {BOLD}Phase Flow:{RESET}")
    else:
        lines.append("  Phase Flow:")
    lines.append(render_flow_line(game, color=color))

    # Trajectory plot
    lines.append("")
    if color:
        lines.append(f"  {BOLD}Trajectories:{RESET}")
    else:
        lines.append("  Trajectories:")
    lines.append(render_trajectory_plot(game, color=color))

    lines.append("")
    return "\n".join(lines)
