"""
3D mAP (mean Average Precision) 계산 모듈
3D 모델과 참조 모델 간의 3D 객체 감지 정확도를 평가합니다.
"""

import numpy as np
from typing import List, Dict, Tuple


class Map3DCalculator:
    """3D mAP 계산을 담당하는 클래스"""
    
    def __init__(self, iou_3d_thresholds: List[float] = [0.05, 0.1, 0.2]):
        """
        3D mAP 계산기 초기화
        
        Args:
            iou_3d_thresholds (List[float]): 3D IoU 임계값 리스트 (더 관대한 임계값 사용)
        """
        self.iou_3d_thresholds = iou_3d_thresholds
        self.default_iou_threshold = 0.05  # 기본 IoU threshold를 더 낮춤
    
    def calculate_3d_map(self, model_3d: Dict, ground_truth_3d: Dict) -> float:
        """
        3D mAP를 계산합니다.
        
        Args:
            model_3d (Dict): 변환된 3D 모델 정보
            ground_truth_3d (Dict): 참조 3D 모델 정보
            
        Returns:
            float: 3D mAP 점수
        """
        try:
            # 3D 바운딩 박스 추출
            pred_boxes_3d = self.extract_3d_bounding_boxes(model_3d)
            gt_boxes_3d = self.extract_3d_bounding_boxes(ground_truth_3d)
            
            if not pred_boxes_3d or not gt_boxes_3d:
                return 0.0
            
            # 각 IoU 임계값에 대해 mAP 계산
            map_scores = []
            
            for iou_threshold in self.iou_3d_thresholds:
                # 3D IoU 기반 매칭
                matched_pairs = self._match_3d_boxes(pred_boxes_3d, gt_boxes_3d, iou_threshold)
                
                # 정밀도-재현율 데이터 준비
                predictions, ground_truths = self._prepare_3d_pr_data(matched_pairs, pred_boxes_3d, gt_boxes_3d)
                
                # Average Precision 계산
                if predictions and ground_truths:
                    ap_score = self._calculate_3d_average_precision(predictions, ground_truths)
                    map_scores.append(ap_score)
                else:
                    map_scores.append(0.0)
            
            # 평균 mAP 계산
            mean_map = np.mean(map_scores) if map_scores else 0.0
            
            return mean_map
            
        except Exception as e:
            print(f"Error in 3D mAP calculation: {str(e)}")
            return 0.0
    
    def extract_3d_bounding_boxes(self, mesh: Dict) -> List[Dict]:
        """
        3D 메시에서 바운딩 박스를 추출합니다.
        개별 객체별로 3D 바운딩 박스를 생성하여 더 정확한 3D mAP 계산을 수행합니다.
        
        Args:
            mesh (Dict): 3D 메시 정보
            
        Returns:
            List[Dict]: 3D 바운딩 박스 리스트
        """
        print(f"3D 바운딩 박스 추출 시작")
        
        if 'vertices' not in mesh:
            print("  vertices 키가 없음, 빈 리스트 반환")
            return []
            
        vertices = np.array(mesh['vertices'])
        
        if len(vertices) == 0:
            print("  vertices가 비어있음, 빈 리스트 반환")
            return []
        
        print(f"  vertices 개수: {len(vertices)}")
        
        # 개별 객체별 바운딩 박스 생성
        bboxes_3d = []
        
        # 방법 1: 클러스터링 기반 객체 분리
        clustered_boxes = self._extract_clustered_3d_boxes(vertices)
        bboxes_3d.extend(clustered_boxes)
        print(f"  클러스터링 방법: {len(clustered_boxes)}개 박스")
        
        # 방법 2: 기하학적 특성 기반 분리
        geometric_boxes = self._extract_geometric_3d_boxes(vertices, mesh)
        bboxes_3d.extend(geometric_boxes)
        print(f"  기하학적 방법: {len(geometric_boxes)}개 박스")
        
        # 방법 3: 전체 메시를 하나의 객체로 처리 (fallback)
        if not bboxes_3d:
            print("  객체 분리 실패, 전체 메시를 하나의 객체로 처리")
            overall_box = self._extract_overall_3d_box(vertices)
            bboxes_3d.append(overall_box)
        
        # 중복 제거 및 필터링 (더 관대한 임계값 사용)
        filtered_boxes = self._filter_3d_boxes(bboxes_3d, iou_threshold=0.05)
        print(f"  필터링 후: {len(filtered_boxes)}개 박스")
        
        # 여전히 박스가 없는 경우 기본 박스 생성
        if not filtered_boxes:
            print("  모든 방법 실패, 기본 3D 박스 생성")
            filtered_boxes = self._create_default_3d_boxes(vertices)
        
        # 추가: 스케일 정규화를 통한 매칭 개선
        filtered_boxes = self._normalize_boxes_for_matching(filtered_boxes, vertices)
        
        print(f"  최종 3D 바운딩 박스: {len(filtered_boxes)}개")
        return filtered_boxes
    
    def _create_default_3d_boxes(self, vertices: np.ndarray) -> List[Dict]:
        """기본 3D 박스 생성 (모든 방법 실패 시)"""
        print("  기본 3D 박스 생성")
        
        if len(vertices) == 0:
            # vertices가 없는 경우 기본 정육면체 생성
            return [{
                'center': [0.0, 0.0, 0.0],
                'size': [1.0, 1.0, 1.0],
                'volume': 1.0,
                'confidence': 0.1,
                'method': 'default_cube'
            }]
        
        # vertices가 있는 경우 바운딩 박스 계산
        min_coords = np.min(vertices, axis=0)
        max_coords = np.max(vertices, axis=0)
        center = (min_coords + max_coords) / 2
        size = max_coords - min_coords
        
        # 최소 크기 보장
        size = np.maximum(size, [0.1, 0.1, 0.1])
        volume = np.prod(size)
        
        boxes = []
        
        # 방법 1: 전체 바운딩 박스
        boxes.append({
            'center': center.tolist(),
            'size': size.tolist(),
            'volume': float(volume),
            'confidence': 0.5,
            'method': 'overall_bbox'
        })
        
        # 방법 2: vertices를 2개 그룹으로 분할
        if len(vertices) > 10:
            mid_point = len(vertices) // 2
            group1 = vertices[:mid_point]
            group2 = vertices[mid_point:]
            
            for i, group in enumerate([group1, group2]):
                if len(group) > 0:
                    min_coords = np.min(group, axis=0)
                    max_coords = np.max(group, axis=0)
                    center = (min_coords + max_coords) / 2
                    size = max_coords - min_coords
                    size = np.maximum(size, [0.1, 0.1, 0.1])
                    volume = np.prod(size)
                    
                    boxes.append({
                        'center': center.tolist(),
                        'size': size.tolist(),
                        'volume': float(volume),
                        'confidence': 0.3,
                        'method': f'split_group_{i+1}'
                    })
        
        print(f"  기본 3D 박스 생성 완료: {len(boxes)}개")
        return boxes
    
    def _normalize_boxes_for_matching(self, boxes: List[Dict], vertices: np.ndarray) -> List[Dict]:
        """매칭을 위한 박스 정규화"""
        if not boxes or len(vertices) == 0:
            return boxes
        
        # 전체 메시의 바운딩 박스 계산
        mesh_min = np.min(vertices, axis=0)
        mesh_max = np.max(vertices, axis=0)
        mesh_size = mesh_max - mesh_min
        mesh_center = (mesh_min + mesh_max) / 2
        
        normalized_boxes = []
        for box in boxes:
            # 상대적 위치와 크기로 정규화
            normalized_box = box.copy()
            
            # 중심점을 상대 좌표로 변환
            if 'center' in box:
                relative_center = (np.array(box['center']) - mesh_center) / (mesh_size + 1e-8)
                normalized_box['normalized_center'] = relative_center.tolist()
            
            # 크기를 상대 크기로 변환
            if 'size' in box:
                relative_size = np.array(box['size']) / (mesh_size + 1e-8)
                normalized_box['normalized_size'] = relative_size.tolist()
            
            # 매칭을 위한 추가 정보
            normalized_box['mesh_center'] = mesh_center.tolist()
            normalized_box['mesh_size'] = mesh_size.tolist()
            
            normalized_boxes.append(normalized_box)
        
        return normalized_boxes
    
    def _extract_clustered_3d_boxes(self, vertices: np.ndarray) -> List[Dict]:
        """클러스터링을 사용한 3D 바운딩 박스 추출."""
        try:
            from sklearn.cluster import KMeans
            
            # 정점 수가 충분한 경우에만 클러스터링 수행
            if len(vertices) < 10:
                return []
            
            # 클러스터 개수 결정 (정점 수에 비례)
            n_clusters = min(max(2, len(vertices) // 50), 5)
            
            # K-means 클러스터링
            kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
            cluster_labels = kmeans.fit_predict(vertices)
            
            bboxes = []
            for cluster_id in range(n_clusters):
                cluster_vertices = vertices[cluster_labels == cluster_id]
                
                if len(cluster_vertices) < 3:  # 최소 정점 수 확인
                    continue
                
                # 클러스터별 바운딩 박스 계산
                min_coords = np.min(cluster_vertices, axis=0)
                max_coords = np.max(cluster_vertices, axis=0)
                center = (min_coords + max_coords) / 2
                size = max_coords - min_coords
                
                # 유효한 크기인지 확인
                if np.all(size > 0.01):  # 최소 크기 임계값
                    bbox = {
                        'center': center,
                        'size': size,
                        'min_coords': min_coords,
                        'max_coords': max_coords,
                        'volume': np.prod(size),
                        'confidence': 0.8,
                        'method': 'clustering',
                        'cluster_id': cluster_id,
                        'num_vertices': len(cluster_vertices)
                    }
                    bboxes.append(bbox)
            
            return bboxes
            
        except ImportError:
            # sklearn이 없는 경우 빈 리스트 반환
            return []
        except Exception as e:
            print(f"클러스터링 중 오류 발생: {e}")
            return []
    
    def _extract_geometric_3d_boxes(self, vertices: np.ndarray, mesh: Dict) -> List[Dict]:
        """기하학적 특성 기반 3D 바운딩 박스 추출."""
        bboxes = []
        
        # 방법 1: Z축 기반 분리 (높이별 객체 분리)
        z_based_boxes = self._extract_z_based_boxes(vertices)
        bboxes.extend(z_based_boxes)
        
        # 방법 2: 거리 기반 분리
        distance_based_boxes = self._extract_distance_based_boxes(vertices)
        bboxes.extend(distance_based_boxes)
        
        # 방법 3: 밀도 기반 분리
        density_based_boxes = self._extract_density_based_boxes(vertices)
        bboxes.extend(density_based_boxes)
        
        return bboxes
    
    def _extract_z_based_boxes(self, vertices: np.ndarray) -> List[Dict]:
        """Z축(높이) 기반으로 객체를 분리하여 바운딩 박스 생성."""
        if len(vertices) < 10:
            return []
        
        # Z축 값으로 정렬
        z_values = vertices[:, 2]
        z_sorted_indices = np.argsort(z_values)
        
        # Z축을 여러 구간으로 나누기
        n_segments = min(3, len(vertices) // 20)  # 최대 3개 구간
        if n_segments < 2:
            return []
        
        segment_size = len(vertices) // n_segments
        bboxes = []
        
        for i in range(n_segments):
            start_idx = i * segment_size
            end_idx = (i + 1) * segment_size if i < n_segments - 1 else len(vertices)
            
            segment_vertices = vertices[z_sorted_indices[start_idx:end_idx]]
            
            if len(segment_vertices) < 5:
                continue
            
            # 구간별 바운딩 박스 계산
            min_coords = np.min(segment_vertices, axis=0)
            max_coords = np.max(segment_vertices, axis=0)
            center = (min_coords + max_coords) / 2
            size = max_coords - min_coords
            
            if np.all(size > 0.01):
                bbox = {
                    'center': center,
                    'size': size,
                    'min_coords': min_coords,
                    'max_coords': max_coords,
                    'volume': np.prod(size),
                    'confidence': 0.7,
                    'method': 'z_based',
                    'segment_id': i,
                    'num_vertices': len(segment_vertices)
                }
                bboxes.append(bbox)
        
        return bboxes
    
    def _extract_distance_based_boxes(self, vertices: np.ndarray) -> List[Dict]:
        """거리 기반으로 객체를 분리하여 바운딩 박스 생성."""
        if len(vertices) < 20:
            return []
        
        # 중심점에서의 거리 계산
        center = np.mean(vertices, axis=0)
        distances = np.linalg.norm(vertices - center, axis=1)
        
        # 거리 기준으로 정점들을 그룹화
        distance_threshold = np.percentile(distances, 60)  # 60% 분위수 사용
        
        # 중심에 가까운 정점들
        close_vertices = vertices[distances <= distance_threshold]
        # 중심에서 먼 정점들
        far_vertices = vertices[distances > distance_threshold]
        
        bboxes = []
        
        # 가까운 정점들 그룹
        if len(close_vertices) >= 5:
            min_coords = np.min(close_vertices, axis=0)
            max_coords = np.max(close_vertices, axis=0)
            center = (min_coords + max_coords) / 2
            size = max_coords - min_coords
            
            if np.all(size > 0.01):
                bbox = {
                    'center': center,
                    'size': size,
                    'min_coords': min_coords,
                    'max_coords': max_coords,
                    'volume': np.prod(size),
                    'confidence': 0.6,
                    'method': 'distance_based',
                    'group': 'close',
                    'num_vertices': len(close_vertices)
                }
                bboxes.append(bbox)
        
        # 먼 정점들 그룹
        if len(far_vertices) >= 5:
            min_coords = np.min(far_vertices, axis=0)
            max_coords = np.max(far_vertices, axis=0)
            center = (min_coords + max_coords) / 2
            size = max_coords - min_coords
            
            if np.all(size > 0.01):
                bbox = {
                    'center': center,
                    'size': size,
                    'min_coords': min_coords,
                    'max_coords': max_coords,
                    'volume': np.prod(size),
                    'confidence': 0.6,
                    'method': 'distance_based',
                    'group': 'far',
                    'num_vertices': len(far_vertices)
                }
                bboxes.append(bbox)
        
        return bboxes
    
    def _extract_density_based_boxes(self, vertices: np.ndarray) -> List[Dict]:
        """밀도 기반으로 객체를 분리하여 바운딩 박스 생성."""
        if len(vertices) < 30:
            return []
        
        # 간단한 그리드 기반 밀도 계산
        min_coords = np.min(vertices, axis=0)
        max_coords = np.max(vertices, axis=0)
        
        # 그리드 크기 결정
        grid_size = (max_coords - min_coords) / 3  # 3x3x3 그리드
        
        bboxes = []
        
        for i in range(3):
            for j in range(3):
                for k in range(3):
                    # 그리드 셀 경계 계산
                    cell_min = min_coords + np.array([i, j, k]) * grid_size
                    cell_max = cell_min + grid_size
                    
                    # 해당 셀에 속하는 정점들 찾기
                    mask = np.all((vertices >= cell_min) & (vertices <= cell_max), axis=1)
                    cell_vertices = vertices[mask]
                    
                    if len(cell_vertices) >= 5:  # 충분한 정점이 있는 셀만 처리
                        center = (cell_min + cell_max) / 2
                        size = cell_max - cell_min
                        
                        bbox = {
                            'center': center,
                            'size': size,
                            'min_coords': cell_min,
                            'max_coords': cell_max,
                            'volume': np.prod(size),
                            'confidence': 0.5,
                            'method': 'density_based',
                            'grid_cell': (i, j, k),
                            'num_vertices': len(cell_vertices)
                        }
                        bboxes.append(bbox)
        
        return bboxes
    
    def _extract_overall_3d_box(self, vertices: np.ndarray) -> Dict:
        """전체 메시를 하나의 3D 바운딩 박스로 처리."""
        min_coords = np.min(vertices, axis=0)
        max_coords = np.max(vertices, axis=0)
        center = (min_coords + max_coords) / 2
        size = max_coords - min_coords
        
        return {
            'center': center,
            'size': size,
            'min_coords': min_coords,
            'max_coords': max_coords,
            'volume': np.prod(size),
            'confidence': 1.0,
            'method': 'overall',
            'num_vertices': len(vertices)
        }
    
    def _filter_3d_boxes(self, bboxes: List[Dict], iou_threshold: float = 0.1) -> List[Dict]:
        """3D 바운딩 박스 필터링 및 중복 제거."""
        if not bboxes:
            return []
        
        # 1. 최소 크기 필터링
        filtered_boxes = []
        for bbox in bboxes:
            if np.all(bbox['size'] > 0.01):  # 최소 크기 임계값
                filtered_boxes.append(bbox)
        
        if not filtered_boxes:
            return []
        
        # 2. 3D IoU 기반 중복 제거
        final_boxes = []
        used_indices = set()
        
        # 신뢰도 순으로 정렬
        sorted_boxes = sorted(filtered_boxes, key=lambda x: x['confidence'], reverse=True)
        
        for i, bbox1 in enumerate(sorted_boxes):
            if i in used_indices:
                continue
            
            is_duplicate = False
            for j, bbox2 in enumerate(sorted_boxes[i+1:], i+1):
                if j in used_indices:
                    continue
                
                iou = self.compute_3d_iou(bbox1, bbox2)
                if iou > iou_threshold:  # 더 관대한 3D IoU 임계값 사용
                    used_indices.add(j)
                    is_duplicate = True
            
            if not is_duplicate:
                final_boxes.append(bbox1)
                used_indices.add(i)
        
        # 3. 상위 결과만 반환 (최대 5개)
        final_boxes.sort(key=lambda x: x['confidence'], reverse=True)
        return final_boxes[:5]
    
    def compute_3d_iou(self, box1: Dict, box2: Dict) -> float:
        """
        두 3D 바운딩 박스 간의 IoU를 계산합니다.
        정규화된 좌표와 원본 좌표 모두 고려하여 더 정확한 매칭을 수행합니다.
        
        Args:
            box1 (Dict): 첫 번째 3D 박스
            box2 (Dict): 두 번째 3D 박스
            
        Returns:
            float: 3D IoU 값 (0-1)
        """
        # 정규화된 좌표가 있는 경우 우선 사용
        if 'normalized_center' in box1 and 'normalized_center' in box2:
            return self._compute_normalized_3d_iou(box1, box2)
        
        # 원본 좌표 사용
        if 'min_coords' not in box1 or 'min_coords' not in box2:
            # min_coords가 없는 경우 center와 size로부터 계산
            return self._compute_3d_iou_from_center_size(box1, box2)
        
        # 교집합 영역 계산
        min1, max1 = np.array(box1['min_coords']), np.array(box1['max_coords'])
        min2, max2 = np.array(box2['min_coords']), np.array(box2['max_coords'])
        
        # 교집합의 최소/최대 좌표
        intersection_min = np.maximum(min1, min2)
        intersection_max = np.minimum(max1, max2)
        
        # 교집합이 존재하는지 확인
        if np.any(intersection_min >= intersection_max):
            return 0.0
        
        # 교집합 부피 계산
        intersection_volume = np.prod(intersection_max - intersection_min)
        
        # 각 박스의 부피 계산
        volume1 = np.prod(max1 - min1)
        volume2 = np.prod(max2 - min2)
        
        # 합집합 부피 계산
        union_volume = volume1 + volume2 - intersection_volume
        
        if union_volume == 0:
            return 0.0
        
        return intersection_volume / union_volume
    
    def _compute_normalized_3d_iou(self, box1: Dict, box2: Dict) -> float:
        """정규화된 좌표를 사용한 3D IoU 계산"""
        try:
            # 정규화된 중심점과 크기 사용
            center1 = np.array(box1['normalized_center'])
            size1 = np.array(box1['normalized_size'])
            center2 = np.array(box2['normalized_center'])
            size2 = np.array(box2['normalized_size'])
            
            # 바운딩 박스 경계 계산
            min1 = center1 - size1 / 2
            max1 = center1 + size1 / 2
            min2 = center2 - size2 / 2
            max2 = center2 + size2 / 2
            
            # 교집합 계산
            intersection_min = np.maximum(min1, min2)
            intersection_max = np.minimum(max1, max2)
            
            if np.any(intersection_min >= intersection_max):
                return 0.0
            
            intersection_volume = np.prod(intersection_max - intersection_min)
            volume1 = np.prod(size1)
            volume2 = np.prod(size2)
            union_volume = volume1 + volume2 - intersection_volume
            
            return intersection_volume / union_volume if union_volume > 0 else 0.0
            
        except Exception:
            return 0.0
    
    def _compute_3d_iou_from_center_size(self, box1: Dict, box2: Dict) -> float:
        """center와 size로부터 3D IoU 계산"""
        try:
            center1 = np.array(box1['center'])
            size1 = np.array(box1['size'])
            center2 = np.array(box2['center'])
            size2 = np.array(box2['size'])
            
            # 바운딩 박스 경계 계산
            min1 = center1 - size1 / 2
            max1 = center1 + size1 / 2
            min2 = center2 - size2 / 2
            max2 = center2 + size2 / 2
            
            # 교집합 계산
            intersection_min = np.maximum(min1, min2)
            intersection_max = np.minimum(max1, max2)
            
            if np.any(intersection_min >= intersection_max):
                return 0.0
            
            intersection_volume = np.prod(intersection_max - intersection_min)
            volume1 = np.prod(size1)
            volume2 = np.prod(size2)
            union_volume = volume1 + volume2 - intersection_volume
            
            return intersection_volume / union_volume if union_volume > 0 else 0.0
            
        except Exception:
            return 0.0
    
    def _match_3d_boxes(self, pred_boxes: List[Dict], gt_boxes: List[Dict], 
                       iou_threshold: float) -> List[Tuple[int, int, float]]:
        """
        3D 예측 박스와 Ground Truth 박스를 매칭합니다.
        다중 매칭 전략을 사용하여 더 나은 매칭을 수행합니다.
        
        Args:
            pred_boxes (List[Dict]): 예측 3D 박스 리스트
            gt_boxes (List[Dict]): Ground Truth 3D 박스 리스트
            iou_threshold (float): 3D IoU 임계값
            
        Returns:
            List[Tuple[int, int, float]]: 매칭된 박스 쌍 (pred_idx, gt_idx, iou)
        """
        matches = []
        used_gt = set()
        
        # 예측 박스를 신뢰도 순으로 정렬
        sorted_pred = sorted(pred_boxes, key=lambda x: x['confidence'], reverse=True)
        
        # 1차: 정확한 IoU 매칭
        for pred_idx, pred_box in enumerate(sorted_pred):
            best_iou = 0
            best_gt_idx = -1
            
            for gt_idx, gt_box in enumerate(gt_boxes):
                if gt_idx in used_gt:
                    continue
                
                iou = self.compute_3d_iou(pred_box, gt_box)
                if iou > best_iou:
                    best_iou = iou
                    best_gt_idx = gt_idx
            
            # 매칭 임계값 확인
            if best_gt_idx != -1 and best_iou >= iou_threshold:
                matches.append((pred_idx, best_gt_idx, best_iou))
                used_gt.add(best_gt_idx)
        
        # 2차: 낮은 임계값으로 추가 매칭 시도
        if not matches and iou_threshold > 0.01:
            print(f"  정확한 매칭 실패, 낮은 임계값으로 재시도: {iou_threshold * 0.5}")
            return self._match_3d_boxes(pred_boxes, gt_boxes, iou_threshold * 0.5)
        
        # 3차: 거리 기반 매칭 (IoU가 낮은 경우)
        if not matches:
            print("  IoU 매칭 실패, 거리 기반 매칭 시도")
            matches = self._match_by_distance(pred_boxes, gt_boxes)
        
        return matches
    
    def _match_by_distance(self, pred_boxes: List[Dict], gt_boxes: List[Dict]) -> List[Tuple[int, int, float]]:
        """거리 기반 매칭 (IoU 매칭 실패 시 사용)"""
        matches = []
        used_gt = set()
        
        for pred_idx, pred_box in enumerate(pred_boxes):
            best_distance = float('inf')
            best_gt_idx = -1
            
            for gt_idx, gt_box in enumerate(gt_boxes):
                if gt_idx in used_gt:
                    continue
                
                # 중심점 간 거리 계산
                if 'center' in pred_box and 'center' in gt_box:
                    center1 = np.array(pred_box['center'])
                    center2 = np.array(gt_box['center'])
                    distance = np.linalg.norm(center1 - center2)
                    
                    if distance < best_distance:
                        best_distance = distance
                        best_gt_idx = gt_idx
            
            # 거리 임계값 확인 (상대적으로 관대한 임계값)
            if best_gt_idx != -1 and best_distance < 1.0:  # 임계값을 1.0으로 설정
                # 거리를 IoU 스타일 점수로 변환 (0-1 범위)
                similarity_score = max(0, 1 - best_distance / 2.0)
                matches.append((pred_idx, best_gt_idx, similarity_score))
                used_gt.add(best_gt_idx)
        
        return matches
    
    def _prepare_3d_pr_data(self, matches: List[Tuple[int, int, float]], 
                           pred_boxes: List[Dict], gt_boxes: List[Dict]) -> Tuple[List, List]:
        """
        3D 정밀도-재현율 계산을 위한 데이터를 준비합니다.
        
        Args:
            matches (List[Tuple[int, int, float]]): 매칭된 박스 쌍
            pred_boxes (List[Dict]): 예측 3D 박스 리스트
            gt_boxes (List[Dict]): Ground Truth 3D 박스 리스트
            
        Returns:
            Tuple[List, List]: (예측 결과, Ground Truth)
        """
        predictions = []
        ground_truths = []
        
        # 매칭된 박스들
        for pred_idx, gt_idx, iou in matches:
            predictions.append({
                'confidence': pred_boxes[pred_idx]['confidence'],
                'is_positive': True
            })
            ground_truths.append(True)
        
        # 매칭되지 않은 예측 박스들 (False Positive)
        matched_pred_indices = {match[0] for match in matches}
        for i, pred_box in enumerate(pred_boxes):
            if i not in matched_pred_indices:
                predictions.append({
                    'confidence': pred_box['confidence'],
                    'is_positive': False
                })
                ground_truths.append(False)
        
        return predictions, ground_truths
    
    def _calculate_3d_average_precision(self, predictions: List, ground_truths: List) -> float:
        """
        3D Average Precision을 계산합니다.
        
        Args:
            predictions (List): 예측 결과 리스트
            ground_truths (List): Ground Truth 리스트
            
        Returns:
            float: 3D Average Precision 점수
        """
        if not predictions or not ground_truths:
            return 0.0
        
        # 신뢰도 순으로 정렬
        sorted_data = sorted(zip(predictions, ground_truths), 
                           key=lambda x: x[0]['confidence'], reverse=True)
        
        # 정밀도-재현율 곡선 계산
        precisions = []
        recalls = []
        
        tp = 0
        fp = 0
        total_positives = sum(ground_truths)
        
        for pred, gt in sorted_data:
            if gt:
                tp += 1
            else:
                fp += 1
            
            precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
            recall = tp / total_positives if total_positives > 0 else 0.0
            
            precisions.append(precision)
            recalls.append(recall)
        
        # Average Precision 계산 (11-point interpolation)
        ap = 0.0
        for t in np.arange(0, 1.1, 0.1):
            if np.sum(np.array(recalls) >= t) == 0:
                p = 0
            else:
                p = np.max(np.array(precisions)[np.array(recalls) >= t])
            ap += p / 11.0
        
        return ap
