"""
STEP to GLB Converter - Ultimate Edition
Single file output with comprehensive metadata and advanced features
"""

import sys
import json
import logging
import hashlib
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime

# FreeCAD import handling
try:
    import FreeCAD
    import Import
    import Mesh
    import Part
except ImportError as e:
    print(f"ERROR: FreeCAD modules not found: {e}")
    print("This script must be run with FreeCAD Python environment")
    sys.exit(1)

# ============================================================================
# CONFIGURATION - Adjust these settings
# ============================================================================

# Input/Output Settings
STEP_FILE = Path("D:\\__sistem__\\ProgramFiles\\FreeCAD\\bin\\freecad.stp")       # Input STEP file path
OUTPUT_FILE = None                       # Output GLB path (None = auto, same name/location)

# Mesh Quality Settings
MESH_TOLERANCE = 0.05           # Lower = more detail (0.01-0.1 recommended)
ANGULAR_DEFLECTION = 0.5        # Angle deviation in degrees
MIN_EDGE_LENGTH = 0.1           # Minimum edge length to process

# Export Options
EXPORT_EDGES = True             # Include edge wireframes
EXPORT_VERTICES = False         # Include vertex points (can be very dense)
EXPORT_HIDDEN_OBJECTS = False   # Include hidden bodies
MERGE_COPLANAR_FACES = False    # Merge adjacent coplanar faces (experimental)

# Color Settings
DEFAULT_COLOR = (0.8, 0.8, 0.8) # RGB for objects without color
EDGE_COLOR = (0.1, 0.1, 0.1)    # RGB for edges
VERTEX_COLOR = (1.0, 0.0, 0.0)  # RGB for vertices
EDGE_THICKNESS = 1.0            # Visual thickness multiplier

# Metadata Options
INCLUDE_BOUNDING_BOX = True     # Calculate and include bounding boxes
INCLUDE_VOLUME = True           # Calculate solid volumes
INCLUDE_HASH = True             # Add geometry hash for change detection
INCLUDE_TIMESTAMPS = True       # Add processing timestamps
INCLUDE_MATERIAL_INFO = True    # Extract material properties if available

# Performance Settings
MAX_OBJECTS_WARNING = 10000     # Warn if exceeding this count
BATCH_SIZE = 100                # Process in batches (memory optimization)
ENABLE_MULTIPROCESSING = False  # Parallel processing (experimental)

# Output Settings
OUTPUT_SUFFIX = ""              # Add suffix to output files (e.g., "_processed")
CREATE_BACKUP = False           # Backup existing GLB files
COMPRESS_OUTPUT = False         # Attempt to optimize GLB size

# Logging
LOG_FILE = STEP_FOLDER / "conversion.log"
LOG_LEVEL = logging.INFO        # DEBUG, INFO, WARNING, ERROR

# ============================================================================
# END CONFIGURATION
# ============================================================================

# Setup logging
logging.basicConfig(
    level=LOG_LEVEL,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(LOG_FILE),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)


def calculate_hash(shape) -> str:
    """Calculate geometry hash for change detection"""
    try:
        # Use vertex coordinates for hash
        vertices = []
        for vertex in shape.Vertexes:
            vertices.extend([vertex.X, vertex.Y, vertex.Z])
        vertex_str = ",".join([f"{v:.6f}" for v in sorted(vertices)])
        return hashlib.md5(vertex_str.encode()).hexdigest()[:16]
    except:
        return "unknown"


def get_bounding_box(shape) -> Dict[str, Any]:
    """Calculate bounding box"""
    try:
        bbox = shape.BoundBox
        return {
            "min": [round(bbox.XMin, 6), round(bbox.YMin, 6), round(bbox.ZMin, 6)],
            "max": [round(bbox.XMax, 6), round(bbox.YMax, 6), round(bbox.ZMax, 6)],
            "center": [round(bbox.Center.x, 6), round(bbox.Center.y, 6), round(bbox.Center.z, 6)],
            "diagonal": round(bbox.DiagonalLength, 6)
        }
    except:
        return {}


def get_volume(shape) -> Optional[float]:
    """Calculate solid volume"""
    try:
        if hasattr(shape, 'Volume'):
            return round(shape.Volume, 6)
    except:
        pass
    return None


def get_material_info(body) -> Dict[str, Any]:
    """Extract material properties"""
    material_info = {}
    try:
        if hasattr(body, 'Material') and body.Material:
            material_info['material'] = body.Material
        if hasattr(body, 'ViewObject'):
            vo = body.ViewObject
            if hasattr(vo, 'ShapeMaterial'):
                material_info['shape_material'] = vo.ShapeMaterial
            if hasattr(vo, 'LineColor'):
                material_info['line_color'] = list(vo.LineColor)[:3]
            if hasattr(vo, 'LineWidth'):
                material_info['line_width'] = vo.LineWidth
    except:
        pass
    return material_info


def get_color_from_body(body, face_index: int) -> Tuple[float, float, float]:
    """Extract color with fallback handling"""
    try:
        if hasattr(body, 'ViewObject') and hasattr(body.ViewObject, 'DiffuseColor'):
            colors = body.ViewObject.DiffuseColor
            if colors:
                return colors[face_index % len(colors)]
    except (AttributeError, IndexError):
        pass
    return DEFAULT_COLOR


def get_transparency(body) -> float:
    """Extract transparency with fallback"""
    try:
        if hasattr(body, 'ViewObject'):
            transparency = getattr(body.ViewObject, 'Transparency', 0)
            return 1.0 - (transparency / 100.0)
    except AttributeError:
        pass
    return 1.0


def get_face_properties(face) -> Dict[str, Any]:
    """Extract comprehensive face properties"""
    props = {}
    
    try:
        # Basic properties
        props['area'] = round(face.Area, 6)
        props['surface_type'] = face.Surface.TypeId if hasattr(face, 'Surface') else "Unknown"
        
        # Geometric center and normal
        try:
            props['center'] = [round(v, 6) for v in face.CenterOfMass]
            props['normal'] = [round(v, 6) for v in face.normalAt(0, 0)]
        except:
            pass
        
        # Surface-specific properties
        surface = face.Surface
        if hasattr(surface, 'Radius'):
            props['radius'] = round(surface.Radius, 6)
        if hasattr(surface, 'Position'):
            props['position'] = [round(v, 6) for v in surface.Position]
        if hasattr(surface, 'Axis'):
            props['axis'] = [round(v, 6) for v in surface.Axis]
        
        # Curvature info
        try:
            u, v = face.ParameterRange
            curvature = face.curvatureAt(u[0], v[0])
            if curvature:
                props['curvature'] = round(abs(curvature), 6)
        except:
            pass
        
        # Topology
        props['edge_count'] = len(face.Edges)
        props['vertex_count'] = len(face.Vertexes)
        props['is_planar'] = face.Surface.isPlanar() if hasattr(face.Surface, 'isPlanar') else None
        
    except Exception as e:
        logger.debug(f"Failed to extract some face properties: {e}")
    
    return props


def get_edge_properties(edge) -> Dict[str, Any]:
    """Extract comprehensive edge properties"""
    props = {}
    
    try:
        # Basic properties
        props['length'] = round(edge.Length, 6)
        props['edge_type'] = edge.Curve.TypeId if hasattr(edge, 'Curve') else "Unknown"
        
        # Curve-specific properties
        curve = edge.Curve
        if hasattr(curve, 'Radius'):
            props['radius'] = round(curve.Radius, 6)
        if hasattr(curve, 'Center'):
            props['center'] = [round(v, 6) for v in curve.Center]
        if hasattr(curve, 'Axis'):
            props['axis'] = [round(v, 6) for v in curve.Axis]
        
        # Start/End points
        try:
            props['start_point'] = [round(v, 6) for v in edge.Vertexes[0].Point]
            props['end_point'] = [round(v, 6) for v in edge.Vertexes[-1].Point]
        except:
            pass
        
        # Continuity
        props['is_closed'] = edge.Closed
        props['is_degenerated'] = edge.isNull()
        
    except Exception as e:
        logger.debug(f"Failed to extract some edge properties: {e}")
    
    return props


def create_face_mesh(face, body, body_index: int, face_index: int, body_label: str) -> Optional[Mesh.Mesh]:
    """Create mesh from face with comprehensive embedded metadata"""
    try:
        mesh = Mesh.Mesh(face.tessellate(MESH_TOLERANCE))
        
        if mesh.CountFacets == 0:
            return None
        
        # Get color and transparency
        color = get_color_from_body(body, face_index)
        alpha = get_transparency(body)
        
        # Apply vertex colors
        colors = [(color[0], color[1], color[2], alpha)] * len(mesh.Facets)
        mesh.VertexColors = colors
        
        # Get comprehensive face properties
        face_props = get_face_properties(face)
        
        # Build metadata
        metadata = {
            "group_id": body_index + 1,
            "face_index": face_index,
            "body_label": body_label,
            "original_label": getattr(body, 'Label', ''),
            "type": "Face",
            "color": [round(c, 3) for c in color],
            "alpha": round(alpha, 3),
            **face_props
        }
        
        # Add bounding box if enabled
        if INCLUDE_BOUNDING_BOX:
            metadata['bbox'] = get_bounding_box(face)
        
        # Add hash if enabled
        if INCLUDE_HASH:
            metadata['hash'] = calculate_hash(face)
        
        # Create compact name for GLB
        surface_short = face_props.get('surface_type', 'Unknown').split('::')[-1]
        mesh.Label = (
            f"B{body_index + 1}_F{face_index}__"
            f"{body_label}__"
            f"T_Face__"
            f"ST_{surface_short}__"
            f"A_{face_props.get('area', 0)}"
        )
        
        # Store full metadata in Label2
        mesh.Label2 = json.dumps(metadata, separators=(',', ':'))
        
        return mesh
        
    except Exception as e:
        logger.warning(f"Failed to create face mesh: {e}")
        return None


def create_edge_mesh(edge, body, body_index: int, face_index: int, edge_index: int, body_label: str) -> Optional[Mesh.Mesh]:
    """Create wireframe mesh from edge with comprehensive metadata"""
    try:
        edge_mesh = Mesh.Mesh(edge.tessellate(MESH_TOLERANCE))
        
        if edge_mesh.CountFacets == 0 or edge.Length < MIN_EDGE_LENGTH:
            return None
        
        # Apply edge color
        edge_color = [(EDGE_COLOR[0], EDGE_COLOR[1], EDGE_COLOR[2], 1.0)] * len(edge_mesh.Facets)
        edge_mesh.VertexColors = edge_color
        
        # Get comprehensive edge properties
        edge_props = get_edge_properties(edge)
        
        # Build metadata
        metadata = {
            "group_id": body_index + 1,
            "face_index": face_index,
            "edge_index": edge_index,
            "body_label": body_label,
            "original_label": getattr(body, 'Label', ''),
            "type": "Edge",
            **edge_props
        }
        
        # Add bounding box if enabled
        if INCLUDE_BOUNDING_BOX:
            metadata['bbox'] = get_bounding_box(edge)
        
        # Create compact name
        edge_type_short = edge_props.get('edge_type', 'Unknown').split('::')[-1]
        radius_str = f"_R_{edge_props['radius']}" if 'radius' in edge_props else ""
        
        edge_mesh.Label = (
            f"B{body_index + 1}_F{face_index}_E{edge_index}__"
            f"{body_label}__"
            f"T_Edge__"
            f"ET_{edge_type_short}__"
            f"L_{edge_props.get('length', 0)}{radius_str}"
        )
        
        edge_mesh.Label2 = json.dumps(metadata, separators=(',', ':'))
        
        return edge_mesh
        
    except Exception as e:
        logger.debug(f"Failed to create edge mesh: {e}")
        return None


def create_vertex_mesh(vertex, body, body_index: int, vertex_index: int, body_label: str) -> Optional[Mesh.Mesh]:
    """Create point mesh from vertex"""
    try:
        # Create a small sphere at vertex position
        point = vertex.Point
        sphere = Part.makeSphere(MESH_TOLERANCE * 2, point)
        vertex_mesh = Mesh.Mesh(sphere.tessellate(MESH_TOLERANCE))
        
        # Apply vertex color
        v_color = [(VERTEX_COLOR[0], VERTEX_COLOR[1], VERTEX_COLOR[2], 1.0)] * len(vertex_mesh.Facets)
        vertex_mesh.VertexColors = v_color
        
        metadata = {
            "group_id": body_index + 1,
            "vertex_index": vertex_index,
            "body_label": body_label,
            "type": "Vertex",
            "position": [round(point.x, 6), round(point.y, 6), round(point.z, 6)]
        }
        
        vertex_mesh.Label = f"B{body_index + 1}_V{vertex_index}__{body_label}__T_Vertex"
        vertex_mesh.Label2 = json.dumps(metadata, separators=(',', ':'))
        
        return vertex_mesh
        
    except Exception as e:
        logger.debug(f"Failed to create vertex mesh: {e}")
        return None


def process_step_file(step_path: Path) -> bool:
    """Process single STEP file to GLB with comprehensive metadata"""
    output_name = step_path.stem + OUTPUT_SUFFIX + ".glb"
    glb_file = step_path.parent / output_name
    
    # Backup existing file if enabled
    if CREATE_BACKUP and glb_file.exists():
        backup_file = glb_file.with_suffix('.glb.backup')
        glb_file.rename(backup_file)
        logger.info(f"Backed up existing file to {backup_file.name}")
    
    doc = None
    
    try:
        logger.info(f"Opening: {step_path.name}")
        start_time = datetime.now()
        
        Import.open(str(step_path))
        doc = FreeCAD.ActiveDocument
        
        if not doc:
            raise RuntimeError("Failed to open document")
        
        objs_to_export = []
        
        # Global metadata summary
        metadata_summary = {
            "source_file": step_path.name,
            "conversion_date": datetime.now().isoformat() if INCLUDE_TIMESTAMPS else None,
            "freecad_version": FreeCAD.Version(),
            "config": {
                "mesh_tolerance": MESH_TOLERANCE,
                "angular_deflection": ANGULAR_DEFLECTION,
                "export_edges": EXPORT_EDGES,
                "export_vertices": EXPORT_VERTICES
            },
            "bodies": [],
            "total_faces": 0,
            "total_edges": 0,
            "total_vertices": 0
        }
        
        # Process each body
        for body_index, body in enumerate(doc.Objects):
            if not hasattr(body, "Shape") or not body.Shape:
                continue
            
            # Check visibility
            if not EXPORT_HIDDEN_OBJECTS:
                try:
                    if hasattr(body, 'ViewObject') and not body.ViewObject.Visibility:
                        logger.debug(f"Skipping hidden body: {body.Label}")
                        continue
                except:
                    pass
            
            body_label = getattr(body, 'Label', f'Body{body_index}')
            logger.debug(f"Processing body {body_index}: {body_label}")
            
            body_info = {
                "body_id": body_index + 1,
                "label": body_label,
                "face_count": len(body.Shape.Faces),
                "edge_count": len(body.Shape.Edges),
                "vertex_count": len(body.Shape.Vertexes)
            }
            
            # Add volume if enabled
            if INCLUDE_VOLUME:
                volume = get_volume(body.Shape)
                if volume:
                    body_info['volume'] = volume
            
            # Add bounding box if enabled
            if INCLUDE_BOUNDING_BOX:
                body_info['bbox'] = get_bounding_box(body.Shape)
            
            # Add material info if enabled
            if INCLUDE_MATERIAL_INFO:
                material = get_material_info(body)
                if material:
                    body_info['material'] = material
            
            # Add hash if enabled
            if INCLUDE_HASH:
                body_info['hash'] = calculate_hash(body.Shape)
            
            # Process faces
            for face_index, face in enumerate(body.Shape.Faces):
                face_mesh = create_face_mesh(face, body, body_index, face_index, body_label)
                if face_mesh:
                    objs_to_export.append(face_mesh)
                    metadata_summary["total_faces"] += 1
                
                # Process edges if enabled
                if EXPORT_EDGES:
                    for edge_index, edge in enumerate(face.Edges):
                        edge_mesh = create_edge_mesh(edge, body, body_index, face_index, edge_index, body_label)
                        if edge_mesh:
                            objs_to_export.append(edge_mesh)
                            metadata_summary["total_edges"] += 1
            
            # Process vertices if enabled
            if EXPORT_VERTICES:
                for vertex_index, vertex in enumerate(body.Shape.Vertexes):
                    vertex_mesh = create_vertex_mesh(vertex, body, body_index, vertex_index, body_label)
                    if vertex_mesh:
                        objs_to_export.append(vertex_mesh)
                        metadata_summary["total_vertices"] += 1
            
            metadata_summary["bodies"].append(body_info)
        
        if len(objs_to_export) == 0:
            logger.warning(f"No objects to export from {step_path.name}")
            return False
        
        if len(objs_to_export) > MAX_OBJECTS_WARNING:
            logger.warning(f"Large export: {len(objs_to_export)} objects - may be slow")
        
        # Add processing time
        if INCLUDE_TIMESTAMPS:
            metadata_summary["processing_time_seconds"] = (datetime.now() - start_time).total_seconds()
        
        # Create metadata mesh as first item
        try:
            meta_mesh = Mesh.Mesh()
            meta_mesh.Label = "__METADATA__"
            meta_mesh.Label2 = json.dumps(metadata_summary, separators=(',', ':'))
            objs_to_export.insert(0, meta_mesh)
        except Exception as e:
            logger.debug(f"Could not create metadata mesh: {e}")
        
        # Export to GLB
        logger.info(f"Exporting {len(objs_to_export)} objects to GLB...")
        Import.export(objs_to_export, str(glb_file))
        
        # Log summary
        logger.info(f"✅ Completed: {glb_file.name}")
        logger.info(f"   ├─ Bodies: {len(metadata_summary['bodies'])}")
        logger.info(f"   ├─ Faces: {metadata_summary['total_faces']}")
        logger.info(f"   ├─ Edges: {metadata_summary['total_edges']}")
        if EXPORT_VERTICES:
            logger.info(f"   ├─ Vertices: {metadata_summary['total_vertices']}")
        if INCLUDE_TIMESTAMPS:
            logger.info(f"   └─ Time: {metadata_summary['processing_time_seconds']:.2f}s")
        
        return True
        
    except Exception as e:
        logger.error(f"❌ Failed: {step_path.name}: {e}", exc_info=True)
        return False
        
    finally:
        if doc:
            try:
                FreeCAD.closeDocument(doc.Name)
            except:
                pass


def main():
    """Main execution"""
    logger.info("=" * 70)
    logger.info("STEP to GLB Converter - Ultimate Edition")
    logger.info("=" * 70)
    
    if not STEP_FOLDER.exists():
        logger.error(f"Folder not found: {STEP_FOLDER}")
        sys.exit(1)
    
    # Find STEP files
    step_files = list(STEP_FOLDER.glob("*.stp")) + list(STEP_FOLDER.glob("*.step"))
    
    if not step_files:
        logger.warning(f"No STEP files found in {STEP_FOLDER}")
        sys.exit(0)
    
    # Log configuration
    logger.info(f"Configuration:")
    logger.info(f"  ├─ Files found: {len(step_files)}")
    logger.info(f"  ├─ Mesh tolerance: {MESH_TOLERANCE}")
    logger.info(f"  ├─ Export edges: {EXPORT_EDGES}")
    logger.info(f"  ├─ Export vertices: {EXPORT_VERTICES}")
    logger.info(f"  ├─ Include bounding boxes: {INCLUDE_BOUNDING_BOX}")
    logger.info(f"  ├─ Include volumes: {INCLUDE_VOLUME}")
    logger.info(f"  └─ Include hashes: {INCLUDE_HASH}")
    
    # Process files
    success_count = 0
    failed_count = 0
    
    for idx, step_path in enumerate(step_files, 1):
        logger.info(f"\n{'='*70}")
        logger.info(f"[{idx}/{len(step_files)}] {step_path.name}")
        logger.info(f"{'='*70}")
        
        if process_step_file(step_path):
            success_count += 1
        else:
            failed_count += 1
    
    # Final summary
    logger.info("\n" + "=" * 70)
    logger.info(f"Conversion Complete")
    logger.info(f"  ├─ Succeeded: {success_count}")
    logger.info(f"  ├─ Failed: {failed_count}")
    logger.info(f"  └─ Log: {LOG_FILE}")
    logger.info("=" * 70)


if __name__ == "__main__":
    main()