#!/usr/bin/env python
"""
Compare all Zernike extraction methods on M1 Mirror data.

Methods compared:
1. Standard - Z-displacement only at original (x,y)
2. Parabola OPD - Uses parabola approximation with prescription focal length
3. Figure OPD - Uses actual figure.dat geometry (most rigorous)

Run:
    python tests/test_zernike_methods_comparison.py studies/M1_Mirror/m1_mirror_cost_reduction_V9
"""

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent))

import numpy as np


def run_full_comparison(study_dir: Path):
    """Run comparison of all three Zernike methods."""
    from optimization_engine.extractors.extract_zernike import ZernikeExtractor
    from optimization_engine.extractors.extract_zernike_opd import ZernikeAnalyticExtractor
    from optimization_engine.extractors.extract_zernike_figure import ZernikeOPDExtractor

    # Find OP2 file
    op2_files = list(study_dir.glob('3_results/best_design_archive/**/*.op2'))
    if not op2_files:
        op2_files = list(study_dir.glob('2_iterations/iter1/*.op2'))
    if not op2_files:
        raise FileNotFoundError(f"No OP2 file found in {study_dir}")

    op2_file = op2_files[0]

    # Figure file is optional - extractor will use BDF geometry filtered to OP2 nodes
    figure_file = study_dir / '1_setup' / 'model' / 'figure.dat'
    use_figure_file = figure_file.exists()

    print("=" * 80)
    print("ZERNIKE METHODS COMPARISON - M1 MIRROR")
    print("=" * 80)
    print(f"\nOP2 file: {op2_file.name}")
    if use_figure_file:
        print(f"Figure file: {figure_file.name}")
    else:
        print("Figure file: Not found (using BDF geometry filtered to OP2 nodes)")

    # Initialize extractors
    print("\nInitializing extractors...")

    std_extractor = ZernikeExtractor(op2_file)
    print(f"  Standard: {len(std_extractor.node_geometry)} nodes in BDF")

    analytic_extractor = ZernikeAnalyticExtractor(op2_file, focal_length=1445.0, concave=True)
    print(f"  Analytic (Parabola f=1445mm): {len(analytic_extractor.node_geometry)} nodes")

    # OPD extractor - ALWAYS use BDF geometry filtered to OP2 nodes (RECOMMENDED)
    # (figure.dat may have mismatched coordinates from different model state)
    opd_extractor = ZernikeOPDExtractor(
        op2_file,
        figure_path=None  # Force use of BDF geometry
    )
    print(f"  OPD (BDF geometry): {len(opd_extractor.figure_geometry)} figure nodes")

    print()

    # Get available subcases
    subcases = list(std_extractor.displacements.keys())
    print(f"Available subcases: {subcases}")

    # Results table header
    print("\n" + "=" * 80)
    print(f"{'Subcase':<10} {'Standard':<15} {'Analytic':<15} {'OPD':<15} {'Max Lat (um)':<12}")
    print("-" * 80)

    all_results = {}

    for sc in subcases:
        try:
            # Extract with each method
            std_res = std_extractor.extract_subcase(sc)
            analytic_res = analytic_extractor.extract_subcase(sc)
            opd_res = opd_extractor.extract_subcase(sc)

            std_rms = std_res['filtered_rms_nm']
            analytic_rms = analytic_res['filtered_rms_nm']
            opd_rms = opd_res['filtered_rms_nm']
            max_lat = opd_res.get('max_lateral_displacement_um', 0)

            print(f"{sc:<10} {std_rms:<15.2f} {analytic_rms:<15.2f} {opd_rms:<15.2f} {max_lat:<12.3f}")

            all_results[sc] = {
                'standard': std_res,
                'analytic': analytic_res,
                'opd': opd_res
            }

        except Exception as e:
            print(f"{sc:<10} ERROR: {e}")

    print("-" * 80)

    # Detailed comparison for each subcase
    print("\n" + "=" * 80)
    print("DETAILED COMPARISON")
    print("=" * 80)

    for sc, results in all_results.items():
        print(f"\n{'-' * 40}")
        print(f"SUBCASE {sc}")
        print(f"{'-' * 40}")

        std = results['standard']
        analytic = results['analytic']
        opd = results['opd']

        print(f"\n{'Metric':<25} {'Standard':<15} {'Analytic':<15} {'OPD':<15}")
        print("-" * 70)

        # RMS metrics
        print(f"{'Filtered RMS (nm)':<25} {std['filtered_rms_nm']:<15.2f} {analytic['filtered_rms_nm']:<15.2f} {opd['filtered_rms_nm']:<15.2f}")
        print(f"{'Global RMS (nm)':<25} {std['global_rms_nm']:<15.2f} {analytic['global_rms_nm']:<15.2f} {opd['global_rms_nm']:<15.2f}")

        # Aberrations
        for aberr in ['defocus', 'astigmatism', 'coma', 'trefoil', 'spherical']:
            key = f'{aberr}_nm'
            if key in std and key in analytic and key in opd:
                print(f"{aberr.capitalize():<25} {std[key]:<15.2f} {analytic[key]:<15.2f} {opd[key]:<15.2f}")

        # Node count
        print(f"{'Nodes':<25} {std['n_nodes']:<15} {analytic['n_nodes']:<15} {opd['n_nodes']:<15}")

        # Lateral displacement (only for OPD methods)
        print()
        print(f"Lateral displacement (OPD method):")
        print(f"  Max:  {opd.get('max_lateral_displacement_um', 0):.3f} um")
        print(f"  RMS:  {opd.get('rms_lateral_displacement_um', 0):.3f} um")
        print(f"  Mean: {opd.get('mean_lateral_displacement_um', 0):.3f} um")

        # Differences
        print()
        diff_std_opd = opd['filtered_rms_nm'] - std['filtered_rms_nm']
        diff_analytic_opd = opd['filtered_rms_nm'] - analytic['filtered_rms_nm']

        print(f"Difference from Standard:")
        print(f"  OPD:      {diff_std_opd:+.2f} nm ({100*diff_std_opd/std['filtered_rms_nm']:+.1f}%)")
        print(f"  Analytic: {analytic['filtered_rms_nm'] - std['filtered_rms_nm']:+.2f} nm")
        print()
        print(f"Difference OPD vs Analytic: {diff_analytic_opd:+.2f} nm")

    # Tracking WFE analysis
    if '2' in all_results and '3' in all_results and '4' in all_results:
        print("\n" + "=" * 80)
        print("TRACKING WFE ANALYSIS (elevation changes)")
        print("=" * 80)

        for method_name, method_key in [('Standard', 'standard'), ('Analytic', 'analytic'), ('OPD', 'opd')]:
            print(f"\n{method_name}:")

            z20 = np.array(all_results['2'][method_key].get('coefficients', [0]*36))
            z40 = np.array(all_results['3'][method_key].get('coefficients', [0]*36))
            z60 = np.array(all_results['4'][method_key].get('coefficients', [0]*36))

            if len(z20) > 4:
                # Differential (J5+)
                diff_40_20 = z40 - z20
                diff_60_20 = z60 - z20

                rms_40_20 = np.sqrt(np.sum(diff_40_20[4:]**2))
                rms_60_20 = np.sqrt(np.sum(diff_60_20[4:]**2))

                print(f"  40-20 deg tracking: {rms_40_20:.2f} nm RMS (J5+ filtered)")
                print(f"  60-20 deg tracking: {rms_60_20:.2f} nm RMS (J5+ filtered)")
            else:
                print(f"  (coefficients not available)")

    print("\n" + "=" * 80)
    print("SUMMARY")
    print("=" * 80)
    print("""
Key findings:
- Standard method: Uses Z-displacement only at original (x,y) - fast but ignores lateral shift
- Analytic method: Accounts for lateral shift using parabola formula (requires focal length)
- OPD method: Uses actual mesh geometry - MOST RIGOROUS, no shape assumption

Recommendation: Use OPD method (ZernikeOPDExtractor) for all mirror optimization.
The Analytic method is useful for comparison against theoretical parabola.
""")


def main():
    import argparse
    parser = argparse.ArgumentParser(description='Compare Zernike extraction methods')
    parser.add_argument('study_dir', type=str, help='Path to study directory')
    args = parser.parse_args()

    study_dir = Path(args.study_dir).resolve()
    if not study_dir.exists():
        print(f"Study directory not found: {study_dir}")
        sys.exit(1)

    run_full_comparison(study_dir)


if __name__ == '__main__':
    main()
