#!/usr/bin/env python3
"""Comprehensive Performance Benchmark Suite for McRogueFace (#104, #144)

Runs 6 benchmark scenarios to establish baseline performance metrics:
1. Empty scene - Pure engine overhead
2. Static UI - 100 frames, no animation (best case for caching)
3. Animated UI - 100 frames, all animating (worst case for caching)
4. Mixed UI - 100 frames, 10 animating (realistic case)
5. Deep hierarchy - 5 levels of nesting (propagation cost)
6. Grid stress - Large grid with entities (known bottleneck)

Usage:
    ./mcrogueface --headless --exec tests/benchmarks/benchmark_suite.py

Results are printed to stdout in a format suitable for tracking over time.
"""

import mcrfpy
import sys
import random

# Benchmark configuration
WARMUP_FRAMES = 30       # Frames to skip before measuring
MEASURE_FRAMES = 120     # Frames to measure (2 seconds at 60fps)
FRAME_BUDGET_MS = 16.67  # Target: 60 FPS

# Storage for results
results = {}
current_scenario = None
frame_count = 0
metrics_samples = []


def collect_metrics(runtime):
    """Timer callback to collect metrics each frame."""
    global frame_count, metrics_samples

    frame_count += 1

    # Skip warmup frames
    if frame_count <= WARMUP_FRAMES:
        return

    # Collect sample
    m = mcrfpy.getMetrics()
    metrics_samples.append({
        'frame_time': m['frame_time'],
        'avg_frame_time': m['avg_frame_time'],
        'fps': m['fps'],
        'draw_calls': m['draw_calls'],
        'ui_elements': m['ui_elements'],
        'visible_elements': m['visible_elements'],
        'grid_render_time': m['grid_render_time'],
        'entity_render_time': m['entity_render_time'],
        'python_time': m['python_time'],
        'animation_time': m['animation_time'],
        'grid_cells_rendered': m['grid_cells_rendered'],
        'entities_rendered': m['entities_rendered'],
    })

    # Check if we have enough samples
    if len(metrics_samples) >= MEASURE_FRAMES:
        finish_scenario()


def finish_scenario():
    """Calculate statistics and store results for current scenario."""
    global results, current_scenario, metrics_samples

    mcrfpy.delTimer("benchmark_collect")

    if not metrics_samples:
        print(f"  WARNING: No samples collected for {current_scenario}")
        return

    # Calculate averages
    n = len(metrics_samples)
    avg = lambda key: sum(s[key] for s in metrics_samples) / n

    results[current_scenario] = {
        'samples': n,
        'avg_frame_time': avg('frame_time'),
        'avg_fps': avg('fps'),
        'avg_draw_calls': avg('draw_calls'),
        'avg_ui_elements': avg('ui_elements'),
        'avg_grid_render_time': avg('grid_render_time'),
        'avg_entity_render_time': avg('entity_render_time'),
        'avg_python_time': avg('python_time'),
        'avg_animation_time': avg('animation_time'),
        'avg_grid_cells': avg('grid_cells_rendered'),
        'avg_entities': avg('entities_rendered'),
        'max_frame_time': max(s['frame_time'] for s in metrics_samples),
        'min_frame_time': min(s['frame_time'] for s in metrics_samples),
    }

    # Calculate percentage breakdown
    r = results[current_scenario]
    total = r['avg_frame_time']
    if total > 0:
        r['pct_grid'] = (r['avg_grid_render_time'] / total) * 100
        r['pct_entity'] = (r['avg_entity_render_time'] / total) * 100
        r['pct_python'] = (r['avg_python_time'] / total) * 100
        r['pct_animation'] = (r['avg_animation_time'] / total) * 100
        r['pct_other'] = 100 - r['pct_grid'] - r['pct_entity'] - r['pct_python'] - r['pct_animation']

    print(f"  Completed: {n} samples, avg {r['avg_frame_time']:.2f}ms ({r['avg_fps']:.0f} FPS)")

    # Run next scenario
    run_next_scenario()


def run_next_scenario():
    """Run the next benchmark scenario in sequence."""
    global current_scenario, frame_count, metrics_samples

    scenarios = [
        ('1_empty', setup_empty_scene),
        ('2_static_100', setup_static_100),
        ('3_animated_100', setup_animated_100),
        ('4_mixed_100', setup_mixed_100),
        ('5_deep_hierarchy', setup_deep_hierarchy),
        ('6_grid_stress', setup_grid_stress),
    ]

    # Find current index
    current_idx = -1
    if current_scenario:
        for i, (name, _) in enumerate(scenarios):
            if name == current_scenario:
                current_idx = i
                break

    # Move to next
    next_idx = current_idx + 1

    if next_idx >= len(scenarios):
        # All done
        print_results()
        return

    # Setup next scenario
    current_scenario = scenarios[next_idx][0]
    frame_count = 0
    metrics_samples = []

    print(f"\n[{next_idx + 1}/{len(scenarios)}] Running: {current_scenario}")

    # Run setup function
    scenarios[next_idx][1]()

    # Start collection timer (runs every frame)
    mcrfpy.setTimer("benchmark_collect", collect_metrics, 1)


# ============================================================================
# Scenario Setup Functions
# ============================================================================

def setup_empty_scene():
    """Scenario 1: Empty scene - pure engine overhead."""
    mcrfpy.createScene("bench_empty")
    mcrfpy.setScene("bench_empty")


def setup_static_100():
    """Scenario 2: 100 static frames - best case for caching."""
    mcrfpy.createScene("bench_static")
    ui = mcrfpy.sceneUI("bench_static")

    # Create 100 frames in a 10x10 grid
    for i in range(100):
        x = (i % 10) * 100 + 12
        y = (i // 10) * 70 + 12
        frame = mcrfpy.Frame(pos=(x, y), size=(80, 55))
        frame.fill_color = mcrfpy.Color(50 + i, 100, 150)
        frame.outline = 2
        frame.outline_color = mcrfpy.Color(255, 255, 255)

        # Add a caption child
        cap = mcrfpy.Caption(text=f"F{i}", pos=(5, 5))
        cap.fill_color = mcrfpy.Color(255, 255, 255)
        frame.children.append(cap)

        ui.append(frame)

    mcrfpy.setScene("bench_static")


def setup_animated_100():
    """Scenario 3: 100 frames all animating - worst case for caching."""
    mcrfpy.createScene("bench_animated")
    ui = mcrfpy.sceneUI("bench_animated")

    frames = []
    for i in range(100):
        x = (i % 10) * 100 + 12
        y = (i // 10) * 70 + 12
        frame = mcrfpy.Frame(pos=(x, y), size=(80, 55))
        frame.fill_color = mcrfpy.Color(50 + i, 100, 150)
        frames.append(frame)
        ui.append(frame)

    mcrfpy.setScene("bench_animated")

    # Start animations on all frames (color animation = content change)
    for i, frame in enumerate(frames):
        # Animate fill color - this dirties the frame
        target_r = (i * 17) % 256
        anim = mcrfpy.Animation("fill_color.r", float(target_r), 2.0, "linear")
        anim.start(frame)


def setup_mixed_100():
    """Scenario 4: 100 frames, only 10 animating - realistic case."""
    mcrfpy.createScene("bench_mixed")
    ui = mcrfpy.sceneUI("bench_mixed")

    frames = []
    for i in range(100):
        x = (i % 10) * 100 + 12
        y = (i // 10) * 70 + 12
        frame = mcrfpy.Frame(pos=(x, y), size=(80, 55))
        frame.fill_color = mcrfpy.Color(50 + i, 100, 150)
        frames.append(frame)
        ui.append(frame)

    mcrfpy.setScene("bench_mixed")

    # Animate only 10 frames (every 10th)
    for i in range(0, 100, 10):
        frame = frames[i]
        anim = mcrfpy.Animation("fill_color.r", 255.0, 2.0, "easeInOut")
        anim.start(frame)


def setup_deep_hierarchy():
    """Scenario 5: 5 levels of nesting - test dirty flag propagation cost."""
    mcrfpy.createScene("bench_deep")
    ui = mcrfpy.sceneUI("bench_deep")

    # Create 10 trees, each with 5 levels of nesting
    deepest_frames = []

    for tree in range(10):
        x_offset = tree * 100 + 12
        current_parent = None

        for level in range(5):
            frame = mcrfpy.Frame(
                pos=(10, 10) if level > 0 else (x_offset, 100),
                size=(80 - level * 10, 500 - level * 80)
            )
            frame.fill_color = mcrfpy.Color(50 + level * 40, 100, 200 - level * 30)
            frame.outline = 1

            if current_parent is None:
                ui.append(frame)
            else:
                current_parent.children.append(frame)

            current_parent = frame

            if level == 4:  # Deepest level
                deepest_frames.append(frame)

    mcrfpy.setScene("bench_deep")

    # Animate the deepest frames - tests propagation up the hierarchy
    for frame in deepest_frames:
        anim = mcrfpy.Animation("fill_color.g", 255.0, 2.0, "linear")
        anim.start(frame)


def setup_grid_stress():
    """Scenario 6: Large grid with entities - known performance bottleneck."""
    mcrfpy.createScene("bench_grid")
    ui = mcrfpy.sceneUI("bench_grid")

    # Create a 50x50 grid (2500 cells)
    grid = mcrfpy.Grid(grid_size=(50, 50), pos=(50, 50), size=(700, 700))
    grid.zoom = 0.75
    grid.center = (400, 400)  # Center view
    ui.append(grid)

    # Fill with alternating colors
    for y in range(50):
        for x in range(50):
            cell = grid.at(x, y)
            if (x + y) % 2 == 0:
                cell.color = mcrfpy.Color(60, 60, 80)
            else:
                cell.color = mcrfpy.Color(40, 40, 60)

    # Add 50 entities
    try:
        texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)

        for i in range(50):
            # Entity takes positional args: (position, texture, sprite_index, grid)
            pos = mcrfpy.Vector(random.randint(5, 45), random.randint(5, 45))
            entity = mcrfpy.Entity(pos, texture, random.randint(0, 100), grid)
            grid.entities.append(entity)
    except Exception as e:
        print(f"  Note: Could not create entities: {e}")

    mcrfpy.setScene("bench_grid")


# ============================================================================
# Results Output
# ============================================================================

def print_results():
    """Print final benchmark results."""
    print("\n" + "=" * 70)
    print("BENCHMARK RESULTS")
    print("=" * 70)

    print(f"\n{'Scenario':<20} {'Avg FPS':>8} {'Avg ms':>8} {'Max ms':>8} {'Draw Calls':>10}")
    print("-" * 70)

    for name, r in results.items():
        print(f"{name:<20} {r['avg_fps']:>8.1f} {r['avg_frame_time']:>8.2f} {r['max_frame_time']:>8.2f} {r['avg_draw_calls']:>10.0f}")

    print("\n" + "-" * 70)
    print("TIMING BREAKDOWN (% of frame time)")
    print("-" * 70)
    print(f"{'Scenario':<20} {'Grid':>8} {'Entity':>8} {'Python':>8} {'Anim':>8} {'Other':>8}")
    print("-" * 70)

    for name, r in results.items():
        if 'pct_grid' in r:
            print(f"{name:<20} {r['pct_grid']:>7.1f}% {r['pct_entity']:>7.1f}% {r['pct_python']:>7.1f}% {r['pct_animation']:>7.1f}% {r['pct_other']:>7.1f}%")

    print("\n" + "=" * 70)

    # Performance assessment
    print("\nPERFORMANCE ASSESSMENT:")
    for name, r in results.items():
        status = "PASS" if r['avg_frame_time'] < FRAME_BUDGET_MS else "OVER BUDGET"
        print(f"  {name}: {status} ({r['avg_frame_time']:.2f}ms vs {FRAME_BUDGET_MS:.2f}ms budget)")

    print("\nBenchmark complete.")
    sys.exit(0)


# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    print("=" * 70)
    print("McRogueFace Performance Benchmark Suite")
    print("=" * 70)
    print(f"Configuration: {WARMUP_FRAMES} warmup frames, {MEASURE_FRAMES} measurement frames")
    print(f"Target: {FRAME_BUDGET_MS:.2f}ms per frame (60 FPS)")

    # Start the benchmark sequence
    run_next_scenario()
