"""
Data parser for ground truth and detection results.
Supports both TXT and JSON formats.

TXT GT format:  normalized xywh bbox + 47-dim 3D labels (47-dim label format per CLAUDE.md)
JSON GT format: absolute pixel box2d, 3d_ori=[x3d,y3d,z3d,l,h,w,rot_y,xc,yc,...], 3d_front/back/left/right=[x3d,y3d,z3d,alpha,xc,yc,score,is_visible]

TXT Det format: class_name conf x1 y1 x2 y2 coord_sys x3d y3d z3d l h w rot_y face_type
JSON Det format: type/type_name, score, box2d (absolute pixels), xyzlhwyaw=[x3d,y3d,z3d,l,h,w,rot_y], face_cls
"""
import json
import numpy as np

from ..class_config import CLASS_NAMES, CLASS_NAME_TO_ID, CLASSES_3D, FACE_3D_CLASSES


def yaw_to_radians(yaw_value, coord_system):
    """Convert parsed yaw to radians for downstream evaluation."""
    yaw = float(yaw_value)
    if coord_system == 'ego':
        return float(np.deg2rad(yaw))
    return yaw


class GroundTruthParser:
    """Parse ground truth annotation files."""
    
    # Class ID to name mapping — imported from eval_tools/class_config.py
    CLASS_NAMES = CLASS_NAMES

    # 3D classes — imported from eval_tools/class_config.py
    CLASSES_3D = CLASSES_3D
    VALID_COORD_SYSTEMS = {"camera", "ego"}
    
    def __init__(self, min_box_size=8, coord_system='camera'):
        """
        Initialize ground truth parser.
        
        Args:
            min_box_size: float, minimum bbox width or height in pixels.
                          Boxes smaller than this will be filtered out.
                          Default is 8. Should be calculated based on ROI config:
                          - ROI0 (1920->704): 8 * 1920 / 704 ≈ 21.8
                          - ROI1 (704->704): 8 * 704 / 704 = 8
        """
        self.min_box_size = min_box_size
        if coord_system not in self.VALID_COORD_SYSTEMS:
            raise ValueError(f"Unsupported coord_system: {coord_system}")
        self.coord_system = coord_system
    
    def parse_line(self, line, img_width, img_height):
        """
        Parse a single line of ground truth annotation.
        
        Args:
            line: str, annotation line
            img_width: int, image width
            img_height: int, image height
            
        Returns:
            dict with keys:
                - label: int
                - bbox_2d: [x1, y1, x2, y2] in pixel coordinates
                - has_3d: bool
                - 3d_info: dict or None
        """
        values = [float(x) for x in line.strip().split()]
        
        if len(values) < 6:
            return None
        
        label = int(values[0])
        
        # Parse 2D bbox (normalized center + width/height to pixel corners)
        x_center_norm, y_center_norm = values[1], values[2]
        w_norm, h_norm = values[3], values[4]
        
        x_center_px = x_center_norm * img_width
        y_center_px = y_center_norm * img_height
        w_px = w_norm * img_width
        h_px = h_norm * img_height
        
        x1 = x_center_px - w_px / 2
        y1 = y_center_px - h_px / 2
        x2 = x_center_px + w_px / 2
        y2 = y_center_px + h_px / 2
        
        bbox_2d = [x1, y1, x2, y2]
        
        # Filter out small objects based on configured minimum size
        bbox_width = x2 - x1
        bbox_height = y2 - y1
        if bbox_width < self.min_box_size or bbox_height < self.min_box_size:
            return None
        
        # Check if has 3D annotation
        has_3d = self.is_3d_annotated(values)
        
        result = {
            'label': label,
            'bbox_2d': bbox_2d,
            'has_3d': has_3d,
            '3d_info': None
        }
        
        if has_3d:
            result['3d_info'] = self._parse_3d_info(values, label)
        
        return result
    
    def is_3d_annotated(self, values):
        """Check if the annotation contains 3D information."""
        if len(values) == 6 and values[5] == -1:
            return False
        if len(values) >= 18:
            return True
        return False
    
    def _parse_3d_info(self, values, label):
        """Parse 3D information from annotation values."""
        info = {
            'center': [values[5], values[6], values[7]],  # x3d_ori, y3d_ori, z3d_ori
            'dimensions': [values[8], values[9], values[10]],  # l3d, h3d, w3d
            'rotation': values[11],  # rot_y
            'faces': None
        }
        
        # For face_3d_classes, parse face information
        if label in FACE_3D_CLASSES and len(values) == 50:
            info['faces'] = {
                'front': values[18:26],  # x3d, y3d, z3d, alpha, xc, yc, score, is_occ
                'back': values[26:34],
                'left': values[34:42],
                'right': values[42:50]
            }
        
        return info
    
    def get_class_name(self, label_id):
        """Get class name from label ID."""
        return self.CLASS_NAMES.get(label_id, "unknown")

    def _should_filter_negative_id_gt(self, entry, label):
        """
        Filter JSON GT objects that should not participate in 3D-class evaluation.

        Rule:
        - Only applies to 3D classes: vehicle, pedestrian, bicycle, rider
        - If GT carries an `id` field and id < 0, drop this GT entirely
        """
        if label not in self.CLASSES_3D:
            return False

        object_id = entry.get('id')
        if object_id is None:
            return False

        try:
            return int(object_id) < 0
        except (ValueError, TypeError):
            return False

    def parse_gt_json_entry(self, entry, img_width, img_height):
        """
        Parse a single entry from a GT JSON file.

        GT JSON entry format:
            {
                "type": "0",           # class id string
                "type_name": "vehicle",
                "roi_id": "1",
                "box2d": ["x1","y1","x2","y2"],   # absolute pixel coords
                "3d_ori": ["x3d","y3d","z3d","l","h","w","rot_y","xc","yc",...,"alpha","flag"],
                "3d_front": ["x3d","y3d","z3d","alpha","xc","yc","score","is_visible"],
                "3d_back":  [...],
                "3d_left":  [...],
                "3d_right": [...]
            }

        Args:
            entry: dict, single GT JSON entry
            img_width: int, image width (unused for JSON, bbox already in pixels)
            img_height: int, image height (unused for JSON, bbox already in pixels)

        Returns:
            dict or None
        """
        raw_type = entry.get('type')
        if raw_type is None or str(raw_type).strip().lower() in ('', 'none', 'null'):
            return None
        try:
            label = int(raw_type)
        except (ValueError, TypeError):
            return None

        if self._should_filter_negative_id_gt(entry, label):
            return None

        box2d = entry.get('box2d', [])
        if len(box2d) < 4:
            return None
        x1, y1, x2, y2 = float(box2d[0]), float(box2d[1]), float(box2d[2]), float(box2d[3])
        bbox_2d = [x1, y1, x2, y2]

        # Filter small objects
        if (x2 - x1) < self.min_box_size or (y2 - y1) < self.min_box_size:
            return None

        # Check whether 3D annotation is present and valid
        ori_key = '3d_ori_ego' if self.coord_system == 'ego' else '3d_ori'
        if self.coord_system == 'ego' and ori_key not in entry and '3d_ori' in entry:
            has_camera_3d = False
            try:
                has_camera_3d = len(entry['3d_ori']) >= 7 and float(entry['3d_ori'][0]) != -1
            except (ValueError, TypeError):
                has_camera_3d = False
            if has_camera_3d:
                raise ValueError(
                    "GT JSON is missing ego-coordinate fields (3d_ori_ego). "
                    "Please regenerate ground truth with ego fields before running ego-coordinate evaluation."
                )
        d3_ori = entry.get(ori_key)
        has_3d = False
        d3_info = None
        if d3_ori is not None and len(d3_ori) >= 7:
            # x3d is d3_ori[0]; -1 indicates no 3D annotation
            try:
                has_3d = float(d3_ori[0]) != -1
            except (ValueError, TypeError):
                has_3d = False

        if has_3d:
            d3_info = self._parse_3d_info_from_json(entry, label)

        return {
            'label': label,
            'bbox_2d': bbox_2d,
            'has_3d': has_3d,
            '3d_info': d3_info,
            'id': entry.get('id'),
        }

    def _parse_3d_info_from_json(self, entry, label):
        """Parse 3D information from a JSON GT entry."""
        ori_key = '3d_ori_ego' if self.coord_system == 'ego' else '3d_ori'
        d3_ori = entry[ori_key]
        info = {
            'center':     [float(d3_ori[0]), float(d3_ori[1]), float(d3_ori[2])],  # x3d, y3d, z3d
            'dimensions': [float(d3_ori[3]), float(d3_ori[4]), float(d3_ori[5])],  # l, h, w
            'rotation':    yaw_to_radians(d3_ori[6], self.coord_system),             # rot_y
            'faces': None,
            'coord_system': self.coord_system,
        }

        # Parse face information for face_3d_classes (vehicle, bus, truck, tanker, unknown)
        face_keys = {'front': '3d_front', 'back': '3d_back', 'left': '3d_left', 'right': '3d_right'}
        if self.coord_system == 'ego':
            face_keys = {name: f"{key}_ego" for name, key in face_keys.items()}
        if label in FACE_3D_CLASSES and all(k in entry for k in face_keys.values()):
            info['faces'] = {}
            for face_name, json_key in face_keys.items():
                face_data = entry[json_key]
                if len(face_data) >= 8:
                    info['faces'][face_name] = [float(v) for v in face_data[:8]]
                else:
                    info['faces'][face_name] = [float(v) for v in face_data]

        return info

    def parse_gt_json_file(self, file_path, img_width, img_height):
        """
        Parse an entire GT JSON file.

        The JSON file is a dict keyed by object index ("0", "1", ...).

        Returns:
            list of parsed annotation dicts
        """
        try:
            with open(file_path, 'r') as f:
                data = json.load(f)
        except FileNotFoundError:
            print(f"Warning: File not found: {file_path}")
            return []
        except json.JSONDecodeError as e:
            print(f"Warning: JSON decode error in {file_path}: {e}")
            return []

        if data is None:
            return []

        if isinstance(data, dict):
            items = sorted(data.items(), key=lambda item: int(item[0]) if str(item[0]).isdigit() else str(item[0]))
        elif isinstance(data, list):
            items = list(enumerate(data))
        else:
            print(f"Warning: unsupported GT JSON root type {type(data).__name__} in {file_path}")
            return []

        annotations = []
        for key, entry in items:
            if not isinstance(entry, dict):
                print(f"Warning: skipping non-dict GT entry at key={key!r} in {file_path}")
                continue
            raw_type = entry.get('type')
            if raw_type is None or str(raw_type).strip().lower() in ('', 'none', 'null'):
                print(f"Warning: skipping entry with invalid type={raw_type!r} "
                      f"(key={key!r}) in {file_path}")
                continue
            parsed = self.parse_gt_json_entry(entry, img_width, img_height)
            if parsed is not None:
                annotations.append(parsed)
        return annotations

    def parse_file(self, file_path, img_width, img_height):
        """
        Parse entire annotation file. Dispatches to JSON or TXT parser based on extension.

        Args:
            file_path: str, path to annotation file (.txt or .json)
            img_width: int, image width
            img_height: int, image height

        Returns:
            list of parsed annotations
        """
        if str(file_path).endswith('.json'):
            return self.parse_gt_json_file(file_path, img_width, img_height)

        annotations = []
        try:
            with open(file_path, 'r') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    parsed = self.parse_line(line, img_width, img_height)
                    if parsed is not None:
                        annotations.append(parsed)
        except FileNotFoundError:
            print(f"Warning: File not found: {file_path}")
            return []

        return annotations


class DetectionParser:
    """Parse detection result files."""
    
    # Class name to ID mapping — imported from eval_tools/class_config.py
    CLASS_NAME_TO_ID = CLASS_NAME_TO_ID

    # 3D classes — imported from eval_tools/class_config.py
    CLASSES_3D = CLASSES_3D
    VALID_COORD_SYSTEMS = {"camera", "ego"}
    
    def __init__(self, min_box_size=0, coord_system='camera'):
        """
        Initialize detection parser.
        
        Args:
            min_box_size: float, minimum bbox width or height in pixels.
                          Detections smaller than this will be filtered out.
                          Should match the GT min_box_size to ensure
                          symmetric filtering. Default is 0 (no filtering).
        """
        self.min_box_size = min_box_size
        if coord_system not in self.VALID_COORD_SYSTEMS:
            raise ValueError(f"Unsupported coord_system: {coord_system}")
        self.coord_system = coord_system
    
    def parse_line(self, line):
        """
        Parse a single line of detection result.
        
        Args:
            line: str, detection line
            
        Returns:
            dict with keys:
                - label: int
                - confidence: float
                - bbox_2d: [x1, y1, x2, y2] in pixel coordinates
                - 3d_info: dict or None
        """
        parts = line.strip().split()
        
        if len(parts) < 6:
            return None
        
        class_name = parts[0]
        label = self.map_class_name(class_name)
        confidence = float(parts[1])
        
        # 2D bbox
        x1, y1, x2, y2 = float(parts[2]), float(parts[3]), float(parts[4]), float(parts[5])
        bbox_2d = [x1, y1, x2, y2]
        
        # Filter small detections
        if self.min_box_size > 0 and ((x2 - x1) < self.min_box_size or (y2 - y1) < self.min_box_size):
            return None
        
        result = {
            'label': label,
            'confidence': confidence,
            'bbox_2d': bbox_2d,
            '3d_info': None
        }
        
        # Check if this is a 3D class and has 3D info
        if label in self.CLASSES_3D and len(parts) >= 15:
            result['3d_info'] = self._parse_3d_info(parts)
        
        return result
    
    def _parse_3d_info(self, parts):
        """Parse 3D information from detection parts."""
        if self.coord_system == 'ego':
            raise ValueError("TXT detection format does not support ego-coordinate 3D evaluation.")
        # Format: label conf x1 y1 x2 y2 coord_sys x3d y3d z3d l3d h3d w3d rot_y face_type
        # Index:   0     1    2  3  4  5     6       7   8   9   10  11  12   13    14
        
        # Get face_type and normalize it
        face_type = parts[14] if len(parts) > 14 else 'whole'
        # Normalize rear/tail to back for consistency
        if face_type.lower() in ['rear', 'tail']:
            face_type = 'back'
        
        info = {
            'center': [float(parts[7]), float(parts[8]), float(parts[9])],  # x3d, y3d, z3d
            'dimensions': [float(parts[10]), float(parts[11]), float(parts[12])],  # l3d, h3d, w3d
            'rotation': float(parts[13]),  # rot_y
            'face_type': face_type,
            'coord_system': 'camera',
        }
        
        return info
    
    def map_class_name(self, name_str):
        """Map class name string to class ID."""
        return self.CLASS_NAME_TO_ID.get(name_str.lower(), -1)

    def parse_det_json_entry(self, entry):
        """
        Parse a single entry from a detection JSON file.

        Det JSON entry format:
            {
                "type": "0",                # class id string
                "type_name": "vehicle",
                "score": "0.93",
                "roi_id": "0",
                "box2d": ["x1","y1","x2","y2"],   # absolute pixel coords
                "xyzlhwyaw": ["x3d","y3d","z3d","l","h","w","rot_y"],
                "face_cls": "front",        # front/tail/rear/left/right/whole/none
                "cut_cls": "0",
                "cut_cls_name": "nocut"
            }

        Returns:
            dict or None
        """
        try:
            label = int(entry['type'])
        except (KeyError, ValueError, TypeError):
            class_name = entry.get('type_name', '')
            label = self.map_class_name(class_name)

        try:
            confidence = float(entry['score'])
        except (KeyError, ValueError, TypeError):
            confidence = 0.0

        box2d = entry.get('box2d', [])
        if len(box2d) < 4:
            return None
        x1, y1, x2, y2 = float(box2d[0]), float(box2d[1]), float(box2d[2]), float(box2d[3])
        bbox_2d = [x1, y1, x2, y2]

        # Filter small detections
        if self.min_box_size > 0 and ((x2 - x1) < self.min_box_size or (y2 - y1) < self.min_box_size):
            return None

        result = {
            'label': label,
            'confidence': confidence,
            'bbox_2d': bbox_2d,
            '3d_info': None,
            'id': entry.get('track_id', entry.get('id')),
            'roi_id': self._normalize_roi_id(entry.get('roi_id')),
        }

        # Parse 3D info for 3D classes
        if label in self.CLASSES_3D:
            xyz_key = 'xyzlhwyaw_ego' if self.coord_system == 'ego' else 'xyzlhwyaw'
            xyzlhwyaw = entry.get(xyz_key, [])
            using_coord_system = self.coord_system
            if len(xyzlhwyaw) < 7 and self.coord_system == 'ego':
                has_camera_3d = False
                camera_xyz = entry.get('xyzlhwyaw', [])
                try:
                    has_camera_3d = len(camera_xyz) >= 7 and str(camera_xyz[0]) != '-1'
                except (ValueError, TypeError):
                    has_camera_3d = False
                if has_camera_3d:
                    raise ValueError(
                        "Detection JSON is missing ego-coordinate fields (xyzlhwyaw_ego). "
                        "Please export ego-coordinate detection results before running ego-coordinate evaluation."
                    )
            if len(xyzlhwyaw) >= 7 and str(xyzlhwyaw[0]) != '-1':
                face_type = entry.get('face_cls', 'whole') or 'whole'
                if face_type.lower() in ('rear', 'tail'):
                    face_type = 'back'
                result['3d_info'] = {
                    'center':     [float(xyzlhwyaw[0]), float(xyzlhwyaw[1]), float(xyzlhwyaw[2])],
                    'dimensions': [float(xyzlhwyaw[3]), float(xyzlhwyaw[4]), float(xyzlhwyaw[5])],
                    'rotation':    yaw_to_radians(xyzlhwyaw[6], using_coord_system),
                    'face_type':   face_type,
                    'coord_system': using_coord_system,
                }

        return result

    @staticmethod
    def _normalize_roi_id(roi_id):
        """Normalize ROI identifiers like 'roi0'/'0' to plain numeric strings."""
        if roi_id is None:
            return None

        roi_id_str = str(roi_id).strip().lower()
        if roi_id_str.startswith('roi'):
            roi_id_str = roi_id_str[3:]
        return roi_id_str or None

    def parse_det_json_file(self, file_path):
        """
        Parse an entire detection JSON file.

        The JSON file is a dict keyed by object index ("0", "1", ...).

        Returns:
            list of parsed detection dicts
        """
        try:
            with open(file_path, 'r') as f:
                data = json.load(f)
        except FileNotFoundError:
            print(f"Warning: File not found: {file_path}")
            return []
        except json.JSONDecodeError as e:
            print(f"Warning: JSON decode error in {file_path}: {e}")
            return []

        detections = []
        for key in sorted(data.keys(), key=lambda k: int(k) if k.isdigit() else k):
            parsed = self.parse_det_json_entry(data[key])
            if parsed is not None:
                detections.append(parsed)
        return detections

    def parse_file(self, file_path):
        """
        Parse entire detection file. Dispatches to JSON or TXT parser based on extension.

        Args:
            file_path: str, path to detection file (.txt or .json)

        Returns:
            list of parsed detections
        """
        if str(file_path).endswith('.json'):
            return self.parse_det_json_file(file_path)

        detections = []
        try:
            with open(file_path, 'r') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    parsed = self.parse_line(line)
                    if parsed is not None:
                        detections.append(parsed)
        except FileNotFoundError:
            print(f"Warning: File not found: {file_path}")
            return []

        return detections
