"""
Generic OP2 Extractor
====================

Reusable extractor for NX Nastran OP2 files using pyNastran.
Extracts mass properties, forces, displacements, stresses, etc.

Usage:
    extractor = OP2Extractor(op2_file="model.op2")
    mass = extractor.extract_mass()
    forces = extractor.extract_grid_point_forces()
"""

from pathlib import Path
from typing import Dict, Any, Optional, List
import numpy as np

try:
    from pyNastran.op2.op2 import read_op2
except ImportError:
    raise ImportError("pyNastran is required. Install with: pip install pyNastran")


class OP2Extractor:
    """
    Generic extractor for Nastran OP2 files.

    Supports:
    - Mass properties
    - Grid point forces
    - Displacements
    - Stresses
    - Strains
    - Element forces
    """

    def __init__(self, op2_file: str):
        """
        Args:
            op2_file: Path to .op2 file
        """
        self.op2_file = Path(op2_file)
        self._op2_model = None

    def _load_op2(self):
        """Lazy load OP2 file"""
        if self._op2_model is None:
            if not self.op2_file.exists():
                raise FileNotFoundError(f"OP2 file not found: {self.op2_file}")
            self._op2_model = read_op2(str(self.op2_file), debug=False)
        return self._op2_model

    def extract_mass(self, subcase_id: Optional[int] = None) -> Dict[str, Any]:
        """
        Extract mass properties from OP2.

        Returns:
            dict: {
                'mass_kg': total mass in kg,
                'mass_g': total mass in grams,
                'cg': [x, y, z] center of gravity,
                'inertia': 3x3 inertia matrix
            }
        """
        op2 = self._load_op2()

        # Get grid point weight (mass properties)
        if not hasattr(op2, 'grid_point_weight') or not op2.grid_point_weight:
            raise ValueError("No mass properties found in OP2 file")

        gpw = op2.grid_point_weight

        # Mass is typically in the first element of MO matrix (reference point mass)
        # OP2 stores mass in ton, mm, sec units typically
        mass_matrix = gpw.MO[0, 0] if hasattr(gpw, 'MO') else None

        # Get reference point
        if hasattr(gpw, 'reference_point') and gpw.reference_point:
            ref_point = gpw.reference_point
        else:
            ref_point = 0

        # Extract mass (convert based on units)
        # Nastran default: ton-mm-sec → need to convert to kg
        if mass_matrix is not None:
            mass_ton = mass_matrix
            mass_kg = mass_ton * 1000.0  # 1 ton = 1000 kg
        else:
            raise ValueError("Could not extract mass from OP2")

        # Extract CG if available
        cg = [0.0, 0.0, 0.0]
        if hasattr(gpw, 'cg'):
            cg = gpw.cg.tolist() if hasattr(gpw.cg, 'tolist') else list(gpw.cg)

        return {
            'mass_kg': mass_kg,
            'mass_g': mass_kg * 1000.0,
            'mass_ton': mass_ton,
            'cg': cg,
            'reference_point': ref_point,
            'units': 'ton-mm-sec (converted to kg)',
        }

    def extract_grid_point_forces(
        self,
        subcase_id: Optional[int] = None,
        component: str = "total"  # total, fx, fy, fz, mx, my, mz
    ) -> Dict[str, Any]:
        """
        Extract grid point forces from OP2.

        Args:
            subcase_id: Subcase ID (if None, uses first available)
            component: Force component to extract

        Returns:
            dict: {
                'force': resultant force value,
                'all_forces': list of forces at each grid point,
                'max_force': maximum force,
                'total_force': sum of all forces
            }
        """
        op2 = self._load_op2()

        if not hasattr(op2, 'grid_point_forces') or not op2.grid_point_forces:
            raise ValueError("No grid point forces found in OP2 file")

        # Get first subcase if not specified
        if subcase_id is None:
            subcase_id = list(op2.grid_point_forces.keys())[0]

        gpf = op2.grid_point_forces[subcase_id]

        # Extract forces based on component
        # Grid point forces table typically has columns: fx, fy, fz, mx, my, mz
        if component == "total":
            # Calculate resultant force: sqrt(fx^2 + fy^2 + fz^2)
            forces = np.sqrt(gpf.data[:, 0]**2 + gpf.data[:, 1]**2 + gpf.data[:, 2]**2)
        elif component == "fx":
            forces = gpf.data[:, 0]
        elif component == "fy":
            forces = gpf.data[:, 1]
        elif component == "fz":
            forces = gpf.data[:, 2]
        else:
            raise ValueError(f"Unknown component: {component}")

        return {
            'force': float(np.max(np.abs(forces))),
            'all_forces': forces.tolist(),
            'max_force': float(np.max(forces)),
            'min_force': float(np.min(forces)),
            'total_force': float(np.sum(forces)),
            'component': component,
            'subcase_id': subcase_id,
        }

    def extract_applied_loads(self, subcase_id: Optional[int] = None) -> Dict[str, Any]:
        """
        Extract applied loads from OP2 file.

        This attempts to get load vector information if available.
        Note: Not all OP2 files contain this data.

        Returns:
            dict: Load information
        """
        op2 = self._load_op2()

        # Try to get load vectors
        if hasattr(op2, 'load_vectors') and op2.load_vectors:
            if subcase_id is None:
                subcase_id = list(op2.load_vectors.keys())[0]

            lv = op2.load_vectors[subcase_id]
            loads = lv.data

            return {
                'total_load': float(np.sum(np.abs(loads))),
                'max_load': float(np.max(np.abs(loads))),
                'load_resultant': float(np.linalg.norm(loads)),
                'subcase_id': subcase_id,
            }
        else:
            # Fallback: use grid point forces as approximation
            return self.extract_grid_point_forces(subcase_id)


def extract_mass_from_op2(op2_file: str) -> float:
    """
    Convenience function to extract mass in kg.

    Args:
        op2_file: Path to .op2 file

    Returns:
        Mass in kilograms
    """
    extractor = OP2Extractor(op2_file)
    result = extractor.extract_mass()
    return result['mass_kg']


def extract_force_from_op2(
    op2_file: str,
    component: str = "fz"
) -> float:
    """
    Convenience function to extract force component.

    Args:
        op2_file: Path to .op2 file
        component: Force component (fx, fy, fz, or total)

    Returns:
        Force value
    """
    extractor = OP2Extractor(op2_file)
    result = extractor.extract_grid_point_forces(component=component)
    return result['force']


if __name__ == "__main__":
    # Example usage
    import sys
    if len(sys.argv) > 1:
        op2_file = sys.argv[1]
        extractor = OP2Extractor(op2_file)

        # Extract mass
        mass_result = extractor.extract_mass()
        print(f"Mass: {mass_result['mass_kg']:.6f} kg")
        print(f"CG: {mass_result['cg']}")

        # Extract forces
        try:
            force_result = extractor.extract_grid_point_forces(component="fz")
            print(f"Max Fz: {force_result['force']:.2f} N")
        except ValueError as e:
            print(f"Forces not available: {e}")
