"""
Create Pareto front visualizations for M1 Mirror optimization data.
Shows relationship between geometric parameters and 60/20 WFE performance.
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.patches as mpatches

# Set style for publication-quality plots
plt.rcParams.update({
    'font.family': 'sans-serif',
    'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans'],
    'font.size': 11,
    'axes.titlesize': 16,
    'axes.labelsize': 13,
    'xtick.labelsize': 11,
    'ytick.labelsize': 11,
    'legend.fontsize': 10,
    'figure.dpi': 150,
    'savefig.dpi': 150,
    'axes.spines.top': False,
    'axes.spines.right': False,
})

# Load data
df = pd.read_csv(r'c:\Users\antoi\Atomizer\studies\m1_mirror_all_trials_export.csv')

print("=== Data Overview ===")
print(f"Total rows: {len(df)}")
print(f"\nColumn data availability:")
for col in df.columns:
    non_null = df[col].notna().sum()
    if non_null > 0:
        print(f"  {col}: {non_null} ({100*non_null/len(df):.1f}%)")

print(f"\nStudies: {df['study'].unique()}")

# Filter for rows with the key parameters
thickness_col = 'center_thickness'
angle_col = 'blank_backface_angle'
wfe_col = 'rel_filtered_rms_60_vs_20'

print(f"\n=== Key columns ===")
print(f"center_thickness non-null: {df[thickness_col].notna().sum()}")
print(f"blank_backface_angle non-null: {df[angle_col].notna().sum()}")
print(f"rel_filtered_rms_60_vs_20 non-null: {df[wfe_col].notna().sum()}")

# Create filtered dataset with valid WFE values
df_valid = df[df[wfe_col].notna()].copy()
print(f"\nRows with valid WFE data (before outlier removal): {len(df_valid)}")

if len(df_valid) == 0:
    print("No valid WFE data found!")
    exit()

# Remove outliers - WFE values above 1000 are clearly failed simulations
WFE_THRESHOLD = 100  # Reasonable upper bound for WFE ratio
df_valid = df_valid[df_valid[wfe_col] < WFE_THRESHOLD].copy()
print(f"Rows with valid WFE data (after outlier removal, WFE < {WFE_THRESHOLD}): {len(df_valid)}")

# Show ranges
print(f"\n=== Value ranges (clean data) ===")
if df_valid[thickness_col].notna().any():
    print(f"center_thickness: {df_valid[thickness_col].min():.2f} - {df_valid[thickness_col].max():.2f} mm")
if df_valid[angle_col].notna().any():
    print(f"blank_backface_angle: {df_valid[angle_col].min():.2f} - {df_valid[angle_col].max():.2f}°")
print(f"rel_filtered_rms_60_vs_20: {df_valid[wfe_col].min():.4f} - {df_valid[wfe_col].max():.4f}")

# Also check mass
if 'mass_kg' in df_valid.columns and df_valid['mass_kg'].notna().any():
    print(f"mass_kg: {df_valid['mass_kg'].min():.2f} - {df_valid['mass_kg'].max():.2f} kg")


def compute_pareto_front(x, y, minimize_x=True, minimize_y=True):
    """
    Compute Pareto front indices.
    Returns indices of points on the Pareto front.
    """
    # Create array of points
    points = np.column_stack([x, y])
    n_points = len(points)

    # Adjust for minimization/maximization
    if not minimize_x:
        points[:, 0] = -points[:, 0]
    if not minimize_y:
        points[:, 1] = -points[:, 1]

    # Find Pareto front
    pareto_mask = np.ones(n_points, dtype=bool)

    for i in range(n_points):
        if pareto_mask[i]:
            # Check if any other point dominates point i
            for j in range(n_points):
                if i != j and pareto_mask[j]:
                    # j dominates i if j is <= in all objectives and < in at least one
                    if (points[j, 0] <= points[i, 0] and points[j, 1] <= points[i, 1] and
                        (points[j, 0] < points[i, 0] or points[j, 1] < points[i, 1])):
                        pareto_mask[i] = False
                        break

    return np.where(pareto_mask)[0]


def create_pareto_plot(df_plot, x_col, y_col, x_label, y_label, title, filename,
                       minimize_x=True, minimize_y=True, color_by=None, color_label=None):
    """Create a publication-quality Pareto front plot."""

    # Filter valid data
    mask = df_plot[x_col].notna() & df_plot[y_col].notna()
    df_clean = df_plot[mask].copy()

    if len(df_clean) < 2:
        print(f"Not enough data for {title}")
        return

    x = df_clean[x_col].values
    y = df_clean[y_col].values

    # Compute Pareto front
    pareto_idx = compute_pareto_front(x, y, minimize_x, minimize_y)

    # Sort Pareto points by x for line drawing
    pareto_points = np.column_stack([x[pareto_idx], y[pareto_idx]])
    sort_idx = np.argsort(pareto_points[:, 0])
    pareto_sorted = pareto_points[sort_idx]

    # Create figure with professional styling
    fig, ax = plt.subplots(figsize=(12, 8))
    fig.patch.set_facecolor('white')

    # Professional color palette
    bg_color = '#f8f9fa'
    grid_color = '#dee2e6'
    point_color = '#6c757d'
    pareto_color = '#dc3545'
    pareto_fill = '#ffc107'

    ax.set_facecolor(bg_color)

    # Color scheme - use mass as color if available
    if color_by is not None and color_by in df_clean.columns and df_clean[color_by].notna().sum() > 10:
        # Only use color if we have enough colored points
        color_mask = df_clean[color_by].notna()
        colors = df_clean.loc[color_mask, color_by].values

        # Plot non-colored points in gray
        ax.scatter(x[~color_mask.values], y[~color_mask.values],
                  c=point_color, alpha=0.3, s=40,
                  edgecolors='white', linewidth=0.3, zorder=2)

        # Plot colored points
        scatter = ax.scatter(x[color_mask.values], y[color_mask.values],
                            c=colors, cmap='plasma', alpha=0.7, s=60,
                            edgecolors='white', linewidth=0.5, zorder=2)
        cbar = plt.colorbar(scatter, ax=ax, pad=0.02, shrink=0.8)
        cbar.set_label(color_label or color_by, fontsize=12, fontweight='bold')
        cbar.ax.tick_params(labelsize=10)
    else:
        ax.scatter(x, y, c=point_color, alpha=0.4, s=50,
                  edgecolors='white', linewidth=0.3, zorder=2, label='Design candidates')

    # Draw Pareto front fill area (visual emphasis)
    if len(pareto_sorted) > 1:
        # Smooth interpolation for the Pareto front line
        from scipy.interpolate import interp1d
        if len(pareto_sorted) >= 4:
            # Use cubic interpolation for smooth curve
            try:
                f = interp1d(pareto_sorted[:, 0], pareto_sorted[:, 1], kind='cubic')
                x_smooth = np.linspace(pareto_sorted[:, 0].min(), pareto_sorted[:, 0].max(), 100)
                y_smooth = f(x_smooth)
                ax.plot(x_smooth, y_smooth, color=pareto_color, linewidth=3, alpha=0.9, zorder=3)
            except:
                ax.plot(pareto_sorted[:, 0], pareto_sorted[:, 1], color=pareto_color,
                       linewidth=3, alpha=0.9, zorder=3)
        else:
            ax.plot(pareto_sorted[:, 0], pareto_sorted[:, 1], color=pareto_color,
                   linewidth=3, alpha=0.9, zorder=3)

    # Plot Pareto front points with emphasis
    ax.scatter(x[pareto_idx], y[pareto_idx], c=pareto_fill, s=180,
              edgecolors=pareto_color, linewidth=2.5, zorder=5,
              label=f'Pareto optimal ({len(pareto_idx)} designs)')

    # Styling
    ax.set_xlabel(x_label, fontsize=14, fontweight='bold', labelpad=12)
    ax.set_ylabel(y_label, fontsize=14, fontweight='bold', labelpad=12)
    ax.set_title(title, fontsize=18, fontweight='bold', pad=20, color='#212529')

    # Refined grid
    ax.grid(True, alpha=0.5, linestyle='-', linewidth=0.5, color=grid_color)
    ax.set_axisbelow(True)

    # Add minor grid
    ax.minorticks_on()
    ax.grid(True, which='minor', alpha=0.2, linestyle=':', linewidth=0.3, color=grid_color)

    # Legend with professional styling
    legend = ax.legend(loc='upper right', fontsize=11, framealpha=0.95,
                      edgecolor=grid_color, fancybox=True, shadow=True)

    # Add annotation for best point
    if minimize_y:
        best_idx = pareto_idx[np.argmin(y[pareto_idx])]
    else:
        best_idx = pareto_idx[np.argmax(y[pareto_idx])]

    # Professional annotation box - position dynamically based on data location
    # Determine best quadrant for annotation
    x_range = x.max() - x.min()
    y_range = y.max() - y.min()
    x_mid = x.min() + x_range / 2
    y_mid = y.min() + y_range / 2

    # Place annotation away from the best point
    if x[best_idx] < x_mid:
        x_offset = 50
    else:
        x_offset = -120
    if y[best_idx] < y_mid:
        y_offset = 50
    else:
        y_offset = -60

    ax.annotate(f'Best WFE: {y[best_idx]:.2f}\n{x_label.split()[0]}: {x[best_idx]:.1f}',
                xy=(x[best_idx], y[best_idx]),
                xytext=(x_offset, y_offset), textcoords='offset points',
                fontsize=11, fontweight='bold',
                bbox=dict(boxstyle='round,pad=0.6', facecolor='white',
                         edgecolor=pareto_color, linewidth=2, alpha=0.95),
                arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.2',
                               color=pareto_color, lw=2))

    # Statistics box in bottom left
    stats_text = f'Total designs explored: {len(df_clean):,}\nPareto optimal: {len(pareto_idx)}'
    ax.text(0.02, 0.02, stats_text, transform=ax.transAxes, fontsize=10,
            verticalalignment='bottom',
            bbox=dict(boxstyle='round,pad=0.5', facecolor='white',
                     edgecolor=grid_color, alpha=0.9))

    # Adjust spines
    for spine in ax.spines.values():
        spine.set_color(grid_color)
        spine.set_linewidth(1.5)

    plt.tight_layout()
    plt.savefig(filename, dpi=200, bbox_inches='tight', facecolor='white',
               edgecolor='none', pad_inches=0.2)
    plt.close()
    print(f"Saved: {filename}")

    return pareto_sorted, pareto_idx


# Create plots
output_dir = r'c:\Users\antoi\Atomizer\studies'

# 1. Blank Thickness vs 60/20 WFE
print("\n--- Creating Blank Thickness vs WFE plot ---")
if df_valid[thickness_col].notna().any():
    result = create_pareto_plot(
        df_valid,
        x_col=thickness_col,
        y_col=wfe_col,
        x_label='Blank Thickness (mm)',
        y_label='60/20 WFE (Relative RMS)',
        title='M1 Mirror Optimization\nBlank Thickness vs Wavefront Error',
        filename=f'{output_dir}\\pareto_thickness_vs_wfe.png',
        minimize_x=False,  # Thinner may be desirable
        minimize_y=True,   # Lower WFE is better
        color_by='mass_kg' if 'mass_kg' in df_valid.columns else None,
        color_label='Mass (kg)'
    )
else:
    print("No thickness data available")

# 2. Blank Backface Angle vs 60/20 WFE
print("\n--- Creating Backface Angle vs WFE plot ---")
if df_valid[angle_col].notna().any():
    result = create_pareto_plot(
        df_valid,
        x_col=angle_col,
        y_col=wfe_col,
        x_label='Blank Backface Angle (degrees)',
        y_label='60/20 WFE (Relative RMS)',
        title='M1 Mirror Optimization\nBackface Angle vs Wavefront Error',
        filename=f'{output_dir}\\pareto_angle_vs_wfe.png',
        minimize_x=False,
        minimize_y=True,
        color_by='mass_kg' if 'mass_kg' in df_valid.columns else None,
        color_label='Mass (kg)'
    )
else:
    print("No backface angle data available")

# 3. Combined 2D Design Space plot
print("\n--- Creating Design Space plot ---")
if df_valid[thickness_col].notna().any() and df_valid[angle_col].notna().any():
    mask = df_valid[thickness_col].notna() & df_valid[angle_col].notna()
    df_both = df_valid[mask].copy()

    if len(df_both) > 0:
        fig, ax = plt.subplots(figsize=(12, 9))
        fig.patch.set_facecolor('white')

        bg_color = '#f8f9fa'
        grid_color = '#dee2e6'
        ax.set_facecolor(bg_color)

        # Use a perceptually uniform colormap
        scatter = ax.scatter(
            df_both[thickness_col],
            df_both[angle_col],
            c=df_both[wfe_col],
            cmap='RdYlGn_r',  # Red=bad (high WFE), Green=good (low WFE)
            s=100,
            alpha=0.8,
            edgecolors='white',
            linewidth=0.5,
            vmin=df_both[wfe_col].quantile(0.05),
            vmax=df_both[wfe_col].quantile(0.95)
        )

        cbar = plt.colorbar(scatter, ax=ax, pad=0.02, shrink=0.85)
        cbar.set_label('60/20 WFE (Relative RMS)\nLower = Better Performance',
                      fontsize=12, fontweight='bold')
        cbar.ax.tick_params(labelsize=10)

        ax.set_xlabel('Blank Thickness (mm)', fontsize=14, fontweight='bold', labelpad=12)
        ax.set_ylabel('Blank Backface Angle (degrees)', fontsize=14, fontweight='bold', labelpad=12)
        ax.set_title('M1 Mirror Design Space Exploration\nGeometric Parameters vs Optical Performance',
                    fontsize=18, fontweight='bold', pad=20)

        ax.grid(True, alpha=0.5, color=grid_color)
        ax.minorticks_on()
        ax.grid(True, which='minor', alpha=0.2, linestyle=':', color=grid_color)

        # Mark best point with star
        best_idx = df_both[wfe_col].idxmin()
        best_row = df_both.loc[best_idx]
        ax.scatter(best_row[thickness_col], best_row[angle_col],
                  c='#ffc107', s=400, marker='*', edgecolors='#dc3545', linewidth=3,
                  zorder=5, label=f'Best Design (WFE={best_row[wfe_col]:.2f})')

        # Add annotation for best point - position in upper left to avoid overlap
        ax.annotate(f'Best Design\nThickness: {best_row[thickness_col]:.1f}mm\nAngle: {best_row[angle_col]:.2f}°\nWFE: {best_row[wfe_col]:.2f}',
                   xy=(best_row[thickness_col], best_row[angle_col]),
                   xytext=(-100, 60), textcoords='offset points',
                   fontsize=10, fontweight='bold',
                   bbox=dict(boxstyle='round,pad=0.6', facecolor='white',
                            edgecolor='#dc3545', linewidth=2, alpha=0.95),
                   arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.3',
                                  color='#dc3545', lw=2))

        ax.legend(loc='upper right', fontsize=11, framealpha=0.95, fancybox=True, shadow=True)

        # Stats
        stats_text = f'Designs evaluated: {len(df_both):,}'
        ax.text(0.02, 0.02, stats_text, transform=ax.transAxes, fontsize=10,
               verticalalignment='bottom',
               bbox=dict(boxstyle='round,pad=0.5', facecolor='white',
                        edgecolor=grid_color, alpha=0.9))

        for spine in ax.spines.values():
            spine.set_color(grid_color)
            spine.set_linewidth(1.5)

        plt.tight_layout()
        plt.savefig(f'{output_dir}\\design_space_wfe.png',
                   dpi=200, bbox_inches='tight', facecolor='white', pad_inches=0.2)
        plt.close()
        print(f"Saved: design_space_wfe.png")
else:
    print("Not enough data for combined design space plot")

print("\n" + "="*60)
print("PARETO VISUALIZATION COMPLETE")
print("="*60)
print(f"\nOutput files saved to: {output_dir}")
print("\nFiles created:")
print("  1. pareto_thickness_vs_wfe.png - Thickness vs WFE Pareto front")
print("  2. pareto_angle_vs_wfe.png - Backface Angle vs WFE Pareto front")
print("  3. design_space_wfe.png - Combined design space heatmap")
