"""
BDF Mass Extractor
==================

Extract mass from Nastran BDF/DAT file using pyNastran.
This is more reliable than OP2 GRDPNT extraction since the BDF always contains
material properties and element geometry.

Usage:
    extractor = BDFMassExtractor(bdf_file="model.dat")
    mass_kg = extractor.extract_mass()
"""

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

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


class BDFMassExtractor:
    """
    Extract mass from Nastran BDF/DAT file.

    This extractor calculates mass from:
    - Element geometry (GRID nodes, element definitions)
    - Material properties (MAT1, etc. with density)
    - Physical properties (PSOLID, PSHELL, etc. with thickness)
    """

    def __init__(self, bdf_file: str):
        """
        Args:
            bdf_file: Path to .dat or .bdf file
        """
        self.bdf_file = Path(bdf_file)
        self._bdf_model = None

    def _load_bdf(self):
        """Lazy load BDF file"""
        if self._bdf_model is None:
            if not self.bdf_file.exists():
                raise FileNotFoundError(f"BDF file not found: {self.bdf_file}")
            # Read BDF with xref=True to cross-reference cards
            self._bdf_model = read_bdf(str(self.bdf_file), xref=True, debug=False)
        return self._bdf_model

    def extract_mass(self) -> Dict[str, Any]:
        """
        Calculate total structural mass from BDF.

        Returns:
            dict: {
                'mass_kg': total mass in kg,
                'mass_g': total mass in grams,
                'mass_ton': total mass in metric tons,
                'cg': [x, y, z] center of gravity (if calculable),
                'num_elements': number of elements,
                'units': 'ton-mm-sec (Nastran default)',
                'breakdown': {element_type: mass} breakdown by element type
            }
        """
        bdf = self._load_bdf()

        # Calculate mass manually by iterating through elements
        total_mass_ton = 0.0
        breakdown = {}
        total_elements = 0
        cg_numerator = np.zeros(3)  # For weighted CG calculation

        for eid, elem in bdf.elements.items():
            try:
                # Get element mass
                elem_mass = elem.Mass()  # Returns mass in Nastran units (ton)

                total_mass_ton += elem_mass

                # Track by element type
                elem_type = elem.type
                if elem_type not in breakdown:
                    breakdown[elem_type] = 0.0
                breakdown[elem_type] += elem_mass

                # Get element centroid for CG calculation
                try:
                    centroid = elem.Centroid()
                    cg_numerator += elem_mass * np.array(centroid)
                except:
                    pass  # Some elements may not have centroid method

                total_elements += 1

            except Exception as e:
                # Some elements might not have Mass() method
                # (e.g., rigid elements, SPCs, etc.)
                continue

        # Calculate center of gravity
        if total_mass_ton > 0:
            cg = cg_numerator / total_mass_ton
        else:
            cg = np.zeros(3)

        # Convert units: NX uses kg-mm-s where "ton" = 1 kg (not 1000 kg!)
        # So the mass returned by pyNastran is already in kg
        mass_kg = float(total_mass_ton)  # Already in kg
        mass_g = mass_kg * 1000.0

        # Breakdown is already in kg
        breakdown_kg = {k: float(v) for k, v in breakdown.items()}

        return {
            'mass_kg': mass_kg,
            'mass_g': mass_g,
            'mass_ton': float(total_mass_ton),  # This is actually kg in NX units
            'cg': cg.tolist() if hasattr(cg, 'tolist') else list(cg),
            'num_elements': total_elements,
            'units': 'kg-mm-s (NX default)',
            'breakdown': breakdown,
            'breakdown_kg': breakdown_kg,
        }

    def get_material_info(self) -> Dict[str, Any]:
        """
        Get material property information from BDF.

        Returns:
            dict: Material ID -> {density, E, nu, ...}
        """
        bdf = self._load_bdf()

        materials = {}
        for mid, mat in bdf.materials.items():
            mat_info = {
                'type': mat.type,
                'mid': mid,
            }

            # MAT1 (isotropic)
            if hasattr(mat, 'rho') and mat.rho is not None:
                mat_info['density'] = float(mat.rho)  # ton/mm^3
                mat_info['density_kg_m3'] = float(mat.rho * 1e12)  # Convert to kg/m^3

            if hasattr(mat, 'e'):
                mat_info['E'] = float(mat.e) if mat.e is not None else None

            if hasattr(mat, 'nu'):
                mat_info['nu'] = float(mat.nu) if mat.nu is not None else None

            materials[mid] = mat_info

        return materials


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

    Args:
        bdf_file: Path to .dat or .bdf file

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


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

        # Extract mass
        mass_result = extractor.extract_mass()
        print(f"Mass: {mass_result['mass_kg']:.6f} kg ({mass_result['mass_g']:.2f} g)")
        print(f"CG: {mass_result['cg']}")
        print(f"Elements: {mass_result['num_elements']}")
        print(f"Element breakdown: {mass_result['breakdown']}")

        # Get material info
        materials = extractor.get_material_info()
        print(f"\nMaterials:")
        for mid, mat_info in materials.items():
            print(f"  MAT{mid}: {mat_info}")
