"""
Extract Part Mass and Material Properties from NX Part Files

This extractor reads mass and material data from a temp file written by
the NX journal: nx_journals/extract_part_mass_material.py

The journal uses NXOpen.MeasureManager to extract:
- Mass (kg)
- Volume (mm^3)
- Surface area (mm^2)
- Center of gravity (mm)
- Material name and density

NX Open APIs Used (by journal):
    - NXOpen.MeasureManager.NewMassProperties()
    - NXOpen.MeasureBodies
    - NXOpen.Body.GetBodies()
    - NXOpen.PhysicalMaterial

Author: Atomizer
Created: 2025-12-05
Version: 1.0
"""

from pathlib import Path
from typing import Dict, Any, Optional, Union
import json


def extract_part_mass_material(
    prt_file: Union[str, Path],
    properties_file: Optional[Union[str, Path]] = None
) -> Dict[str, Any]:
    """
    Extract mass and material properties from NX part file.

    This function reads from a temp JSON file that must be created by
    running the NX journal: nx_journals/extract_part_mass_material.py

    Args:
        prt_file: Path to .prt file (used to locate temp file)
        properties_file: Optional explicit path to _temp_part_properties.json
                        If not provided, looks in same directory as prt_file

    Returns:
        Dictionary containing:
        - 'mass_kg': Mass in kilograms (float)
        - 'mass_g': Mass in grams (float)
        - 'volume_mm3': Volume in mm^3 (float)
        - 'surface_area_mm2': Surface area in mm^2 (float)
        - 'center_of_gravity_mm': [x, y, z] in mm (list)
        - 'moments_of_inertia': {'Ixx', 'Iyy', 'Izz', 'unit'} or None
        - 'material': {'name', 'density', 'density_unit'} (dict)
        - 'num_bodies': Number of solid bodies (int)

    Raises:
        FileNotFoundError: If prt file or temp properties file not found
        ValueError: If temp file has invalid format or extraction failed

    Example:
        >>> result = extract_part_mass_material('model.prt')
        >>> print(f"Mass: {result['mass_kg']:.3f} kg")
        Mass: 1.234 kg
        >>> print(f"Material: {result['material']['name']}")
        Material: Aluminum_6061

    Note:
        Before calling this function, you must run the NX journal to
        create the temp file:
        ```
        run_journal.exe extract_part_mass_material.py model.prt
        ```
    """
    prt_file = Path(prt_file)

    if not prt_file.exists():
        raise FileNotFoundError(f"Part file not found: {prt_file}")

    # Determine properties file location
    if properties_file:
        props_file = Path(properties_file)
    else:
        props_file = prt_file.parent / "_temp_part_properties.json"

    if not props_file.exists():
        raise FileNotFoundError(
            f"Part properties temp file not found: {props_file}\n"
            f"Run the NX journal first:\n"
            f"  run_journal.exe extract_part_mass_material.py {prt_file}"
        )

    # Read and parse JSON
    try:
        with open(props_file, 'r') as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON in properties file: {e}")

    # Check for extraction errors
    if not data.get('success', False):
        error_msg = data.get('error', 'Unknown error during extraction')
        raise ValueError(f"NX extraction failed: {error_msg}")

    # Build result dictionary
    result = {
        'mass_kg': float(data.get('mass_kg', 0.0)),
        'mass_g': float(data.get('mass_g', 0.0)),
        'volume_mm3': float(data.get('volume_mm3', 0.0)),
        'surface_area_mm2': float(data.get('surface_area_mm2', 0.0)),
        'center_of_gravity_mm': data.get('center_of_gravity_mm', [0.0, 0.0, 0.0]),
        'moments_of_inertia': data.get('moments_of_inertia'),
        'material': data.get('material', {'name': None, 'density': None, 'density_unit': 'kg/mm^3'}),
        'num_bodies': int(data.get('num_bodies', 0)),
        'part_file': data.get('part_file', prt_file.name),
    }

    print(f"[OK] Part mass: {result['mass_kg']:.6f} kg ({result['mass_g']:.2f} g)")
    if result['material'].get('name'):
        print(f"[OK] Material: {result['material']['name']}")

    return result


def extract_part_mass(
    prt_file: Union[str, Path],
    properties_file: Optional[Union[str, Path]] = None
) -> float:
    """
    Convenience function to extract just the mass in kg.

    Args:
        prt_file: Path to .prt file
        properties_file: Optional explicit path to temp file

    Returns:
        Mass in kilograms (float)

    Example:
        >>> mass = extract_part_mass('model.prt')
        >>> print(f"Mass: {mass:.3f} kg")
        Mass: 1.234 kg
    """
    result = extract_part_mass_material(prt_file, properties_file)
    return result['mass_kg']


def extract_part_material(
    prt_file: Union[str, Path],
    properties_file: Optional[Union[str, Path]] = None
) -> Dict[str, Any]:
    """
    Convenience function to extract just material info.

    Args:
        prt_file: Path to .prt file
        properties_file: Optional explicit path to temp file

    Returns:
        Dictionary with 'name', 'density', 'density_unit'

    Example:
        >>> mat = extract_part_material('model.prt')
        >>> print(f"Material: {mat['name']}, Density: {mat['density']}")
        Material: Steel_304, Density: 7.93e-06
    """
    result = extract_part_mass_material(prt_file, properties_file)
    return result['material']


class PartMassExtractor:
    """
    Class-based extractor for part mass and material with caching.

    Use this when you need to extract properties from multiple parts
    or want to cache results.

    Example:
        >>> extractor = PartMassExtractor('model.prt')
        >>> result = extractor.extract()
        >>> print(result['mass_kg'])
        1.234
    """

    def __init__(
        self,
        prt_file: Union[str, Path],
        properties_file: Optional[Union[str, Path]] = None
    ):
        """
        Initialize the extractor.

        Args:
            prt_file: Path to .prt file
            properties_file: Optional explicit path to temp file
        """
        self.prt_file = Path(prt_file)
        self.properties_file = Path(properties_file) if properties_file else None
        self._cached_result = None

    def extract(self, use_cache: bool = True) -> Dict[str, Any]:
        """
        Extract mass and material properties.

        Args:
            use_cache: If True, returns cached result if available

        Returns:
            Dictionary with all extracted properties
        """
        if use_cache and self._cached_result is not None:
            return self._cached_result

        self._cached_result = extract_part_mass_material(
            self.prt_file,
            self.properties_file
        )
        return self._cached_result

    @property
    def mass_kg(self) -> float:
        """Get mass in kg (extracts if needed)."""
        return self.extract()['mass_kg']

    @property
    def mass_g(self) -> float:
        """Get mass in grams (extracts if needed)."""
        return self.extract()['mass_g']

    @property
    def material_name(self) -> Optional[str]:
        """Get material name (extracts if needed)."""
        return self.extract()['material'].get('name')

    @property
    def density(self) -> Optional[float]:
        """Get material density in kg/mm^3 (extracts if needed)."""
        return self.extract()['material'].get('density')

    def clear_cache(self):
        """Clear the cached result."""
        self._cached_result = None


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print(f"Usage: python {sys.argv[0]} <prt_file> [properties_file]")
        sys.exit(1)

    prt_file = Path(sys.argv[1])
    props_file = Path(sys.argv[2]) if len(sys.argv) > 2 else None

    try:
        result = extract_part_mass_material(prt_file, props_file)
        print("\n" + "="*50)
        print("PART MASS & MATERIAL EXTRACTION RESULTS")
        print("="*50)
        print(f"Part File: {result['part_file']}")
        print(f"Mass: {result['mass_kg']:.6f} kg ({result['mass_g']:.2f} g)")
        print(f"Volume: {result['volume_mm3']:.2f} mm^3")
        print(f"Surface Area: {result['surface_area_mm2']:.2f} mm^2")
        print(f"Center of Gravity: {result['center_of_gravity_mm']} mm")
        print(f"Num Bodies: {result['num_bodies']}")
        if result['material']['name']:
            print(f"Material: {result['material']['name']}")
            if result['material']['density']:
                print(f"Density: {result['material']['density']} {result['material']['density_unit']}")
        if result['moments_of_inertia']:
            moi = result['moments_of_inertia']
            print(f"Moments of Inertia: Ixx={moi['Ixx']}, Iyy={moi['Iyy']}, Izz={moi['Izz']} {moi['unit']}")
    except Exception as e:
        print(f"\nERROR: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
