"""Benchmark suite per LineShapeMatcher.

Usage:
    python -m pm2d.bench [--quick]

Misura tempi find() su 3 template-tipo × 3 scene-tipo × N config:
- Template: rettangolo 80×80, L-shape 120×120, cerchio 150×150
- Scene: pulita 800×600, cluttered 1080×1920, multi-pezzo 1080×1920
- Config: baseline, polarity, gpu, pyramid_propagate, greediness=0.7

Per ogni config stampa: ms/find, ms per fase (profile), n. match.
Output tabellare per detectare regressioni in CI.
"""

from __future__ import annotations

import argparse
import time

import cv2
import numpy as np

from pm2d.line_matcher import LineShapeMatcher, opencl_available


# ---------- Sintetizzatori template/scena ----------

def _tpl_rect() -> np.ndarray:
    t = np.zeros((80, 80, 3), np.uint8)
    cv2.rectangle(t, (15, 15), (65, 65), (255, 255, 255), 3)
    return t


def _tpl_lshape() -> np.ndarray:
    t = np.zeros((120, 120, 3), np.uint8)
    cv2.rectangle(t, (20, 20), (50, 100), (255, 255, 255), -1)
    cv2.rectangle(t, (20, 70), (100, 100), (255, 255, 255), -1)
    return t


def _tpl_circle() -> np.ndarray:
    t = np.zeros((150, 150, 3), np.uint8)
    cv2.circle(t, (75, 75), 60, (255, 255, 255), 4)
    return t


def _scene_clean(W: int, H: int, n_pieces: int = 1) -> np.ndarray:
    np.random.seed(0)
    s = np.zeros((H, W, 3), np.uint8)
    for _ in range(n_pieces):
        cx = np.random.randint(80, W - 80)
        cy = np.random.randint(80, H - 80)
        cv2.rectangle(s, (cx - 25, cy - 25), (cx + 25, cy + 25), (255, 255, 255), 3)
    return s


def _scene_cluttered(W: int, H: int) -> np.ndarray:
    np.random.seed(0)
    s = np.random.randint(50, 200, (H, W, 3), np.uint8)
    cv2.rectangle(s, (300, 200), (350, 250), (255, 255, 255), 3)
    cv2.rectangle(s, (1500, 800), (1550, 850), (255, 255, 255), 3)
    return s


# ---------- Single benchmark ----------

def _bench_config(template, scene, config_name: str,
                  init_kw: dict, find_kw: dict,
                  n_iter: int = 5) -> dict:
    m = LineShapeMatcher(**init_kw)
    t0 = time.perf_counter()
    n_var = m.train(template)
    t_train = time.perf_counter() - t0

    # Warmup (Numba JIT)
    m.find(scene, **find_kw)
    m.find(scene, **find_kw)

    # Run
    times_ms = []
    for _ in range(n_iter):
        t0 = time.perf_counter()
        matches = m.find(scene, **find_kw)
        times_ms.append((time.perf_counter() - t0) * 1000.0)

    # Profile (1 iter)
    m.find(scene, profile=True, **find_kw)
    prof = m.get_last_profile() or {}

    return {
        "config": config_name,
        "n_variants": n_var,
        "t_train_s": round(t_train, 3),
        "ms_avg": round(float(np.mean(times_ms)), 1),
        "ms_min": round(float(np.min(times_ms)), 1),
        "ms_max": round(float(np.max(times_ms)), 1),
        "n_matches": len(matches),
        "profile_ms": {k: round(v, 1) for k, v in prof.items()},
    }


# ---------- Suite ----------

CONFIGS = [
    ("baseline",
     {"angle_step_deg": 10, "pyramid_levels": 2},
     {"min_score": 0.4, "verify_threshold": 0.2}),
    ("polarity",
     {"angle_step_deg": 10, "pyramid_levels": 2, "use_polarity": True},
     {"min_score": 0.4, "verify_threshold": 0.2}),
    ("propagate",
     {"angle_step_deg": 10, "pyramid_levels": 3},
     {"min_score": 0.4, "verify_threshold": 0.2,
      "pyramid_propagate": True, "propagate_topk": 4}),
    ("greedy_07",
     {"angle_step_deg": 10, "pyramid_levels": 2},
     {"min_score": 0.4, "verify_threshold": 0.2, "greediness": 0.7}),
    ("stride2",
     {"angle_step_deg": 10, "pyramid_levels": 2},
     {"min_score": 0.4, "verify_threshold": 0.2, "coarse_stride": 2}),
]

if opencl_available():
    CONFIGS.append(
        ("gpu_umat",
         {"angle_step_deg": 10, "pyramid_levels": 2, "use_gpu": True},
         {"min_score": 0.4, "verify_threshold": 0.2})
    )


SCENARIOS = [
    ("rect_80 vs scene_800x600", _tpl_rect, lambda: _scene_clean(800, 600, 1)),
    ("lshape_120 vs scene_1080x1920_clutter",
     _tpl_lshape, lambda: _scene_cluttered(1920, 1080)),
    ("circle_150 vs scene_clean_3pieces",
     _tpl_circle, lambda: _scene_clean(1920, 1080, 3)),
]


def run(quick: bool = False) -> int:
    n_iter = 2 if quick else 5
    print(f"=== PM2D Benchmark Suite ({len(SCENARIOS)} scenarios x "
          f"{len(CONFIGS)} configs, n_iter={n_iter}) ===\n")
    rows = []
    for sc_name, tpl_fn, scn_fn in SCENARIOS:
        template = tpl_fn()
        scene = scn_fn()
        print(f"--- Scenario: {sc_name} (tpl={template.shape}, "
              f"scn={scene.shape}) ---")
        for cfg_name, init_kw, find_kw in CONFIGS:
            r = _bench_config(template, scene, cfg_name, init_kw, find_kw,
                              n_iter=n_iter)
            r["scenario"] = sc_name
            rows.append(r)
            prof_str = " ".join(
                f"{k}={v:.1f}" for k, v in r["profile_ms"].items()
            )
            print(f"  {cfg_name:14s}  {r['ms_avg']:6.1f}ms  "
                  f"(min {r['ms_min']:.1f} max {r['ms_max']:.1f})  "
                  f"vars={r['n_variants']:3d}  "
                  f"matches={r['n_matches']:2d}")
            if prof_str:
                print(f"     profile: {prof_str}")
        print()
    print("=== Done ===")
    return 0


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description="PM2D benchmark suite")
    p.add_argument("--quick", action="store_true",
                   help="2 iterazioni per config invece di 5 (smoke test)")
    args = p.parse_args(argv)
    return run(quick=args.quick)


if __name__ == "__main__":
    import sys
    sys.exit(main())
