import numpy as np
from stl import mesh
import struct
import sys
import os

def simplify_stl(input_path, output_path, keep_ratio=0.1):
    try:
        # Try using professional pymeshlab first
        import pymeshlab
        ms = pymeshlab.MeshSet()
        ms.load_new_mesh(input_path)
        
        target_faces = int(ms.current_mesh().face_number() * keep_ratio)
        
        # Optimize using quadric edge collapse to preserve 95% visual effect
        try:
            ms.apply_filter('meshing_decimation_quadric_edge_collapse', 
                            targetfacenum=target_faces, 
                            preserveboundary=True, 
                            preservenormal=True, 
                            preservetopology=True)
        except AttributeError:
            ms.meshing_decimation_quadric_edge_collapse(
                targetfacenum=target_faces,
                preserveboundary=True,
                preservenormal=True,
                preservetopology=True
            )
            
        ms.save_current_mesh(output_path)
        return True
    except ImportError:
        pass
    except Exception as e:
        print(f"Pymeshlab simplification failed: {e}. Falling back to Open3D...")
        
    try:
        # Try using open3d as second fallback
        import open3d as o3d
        o3d_mesh = o3d.io.read_triangle_mesh(input_path)
        if len(o3d_mesh.triangles) > 0:
            target_faces = max(1, int(len(o3d_mesh.triangles) * keep_ratio))
            smp_mesh = o3d_mesh.simplify_quadric_decimation(target_number_of_triangles=target_faces)
            smp_mesh.compute_triangle_normals()
            o3d.io.write_triangle_mesh(output_path, smp_mesh)
            return True
    except ImportError:
        pass
    except Exception as e:
        print(f"Open3D simplification failed: {e}. Falling back to PyFQMR...")
        
    try:
        # Try using pyfqmr as third fallback
        import pyfqmr
        import trimesh
        mesh_data = trimesh.load(input_path, file_type='stl')
        target_faces = max(1, int(len(mesh_data.faces) * keep_ratio))
        simplifier = pyfqmr.Simplify()
        simplifier.setMesh(mesh_data.vertices, mesh_data.faces)
        simplifier.simplify_mesh(target_count=target_faces, aggressiveness=7, preserve_border=True, verbose=False)
        
        mesh_parts = simplifier.getMesh()
        smp_mesh = trimesh.Trimesh(vertices=mesh_parts[0], faces=mesh_parts[1], process=False)
        smp_mesh.export(output_path, file_type='stl')
        return True
    except ImportError:
        pass
    except Exception as e:
        print(f"PyFQMR simplification failed: {e}. Falling back to custom algorithm...")

    try:
        try:
            import trimesh
            mesh_data = trimesh.load(input_path, file_type='stl')
            if hasattr(mesh_data, 'triangles'):
                vertices = mesh_data.triangles.reshape(-1, 3)
            else:
                vertices = mesh_data.vertices[mesh_data.faces].reshape(-1, 3)
            use_trimesh = True
        except ImportError:
            # Load mesh using numpy-stl fallback
            m = mesh.Mesh.from_file(input_path)
            vertices = m.vectors.reshape(-1, 3)
            use_trimesh = False
            
        min_v = vertices.min(axis=0)
        max_v = vertices.max(axis=0)
        bbox_size = max_v - min_v
        max_dim = np.max(bbox_size)
        
        if max_dim == 0:
            if use_trimesh:
                mesh_data.export(output_path, file_type='stl')
            else:
                m.save(output_path)
            return True
            
        # Target roughly a resolution that gives us keep_ratio faces.
        # This is a heuristic approach to grid-based vertex clustering.
        # Function to simplify given a grid size
        def do_simplify(g_size):
            v_idx = np.round((vertices - min_v) / g_size).astype(np.int64)
            
            # Fast 1D hash to avoid extremely slow np.unique(axis=0) on 2D arrays
            max_idx = v_idx.max(axis=0) + 1
            v_1d = v_idx[:, 0] + v_idx[:, 1] * max_idx[0] + v_idx[:, 2] * max_idx[0] * max_idx[1]
            
            # Find unique grid cells and map old vertices to them
            _, unique_idx, inv_idx = np.unique(v_1d, return_index=True, return_inverse=True)
            new_verts = vertices[unique_idx]
            
            # Map faces to new vertices
            faces = inv_idx.reshape(-1, 3)
            
            # Remove degenerate faces (faces where at least two vertices resolve to the same cell)
            valid = (faces[:,0] != faces[:,1]) & (faces[:,1] != faces[:,2]) & (faces[:,0] != faces[:,2])
            valid_faces = faces[valid]
            
            return new_verts, valid_faces
            
        target_faces = max(1, int((len(vertices) // 3) * keep_ratio))
        low_g = max_dim * 0.0005
        high_g = max_dim * 0.2
        best_verts = vertices
        best_faces = np.arange(len(vertices)).reshape(-1, 3)
        
        # Binary search for the right grid size
        for _ in range(8):
            g_size = (low_g + high_g) / 2
            v, f = do_simplify(g_size)
            best_verts, best_faces = v, f
            
            if len(f) > target_faces:
                # too many faces, make grid coarser (larger)
                low_g = g_size
            else:
                # too few faces, make grid finer (smaller)
                high_g = g_size
                
            if abs(len(f) - target_faces) < target_faces * 0.05:
                break
                
        new_vertices, valid_faces = best_verts, best_faces
        
        if use_trimesh:
            simplified = trimesh.Trimesh(vertices=new_vertices, faces=valid_faces, process=False)
            simplified.export(output_path, file_type='stl')
            return True

        # Build the simplified mesh using fallback
        new_m = mesh.Mesh(np.zeros(valid_faces.shape[0], dtype=mesh.Mesh.dtype))
        
        # Vectorized assignment
        new_m.vectors[:, 0, :] = new_vertices[valid_faces[:, 0]]
        new_m.vectors[:, 1, :] = new_vertices[valid_faces[:, 1]]
        new_m.vectors[:, 2, :] = new_vertices[valid_faces[:, 2]]
        
        # Calculate normals correctly
        new_m.update_normals()
        new_m.save(output_path)
        return True
        
    except Exception as e:
        print(f"Error simplifying STL: {e}")
        return False

if __name__ == "__main__":
    if len(sys.argv) > 2:
        simplify_stl(sys.argv[1], sys.argv[2])
