#!/usr/bin/env python3
"""
ESP32 CAN Logger bin 파일을 MDF 형식으로 변환
- CAN Database (DBC 파일) 사용
- Value Table (시그널 값-텍스트 매칭) 지원 (올바른 conversion으로 저장)
- asammdf 라이브러리 사용
- 같은 CAN ID의 시그널들을 그룹으로 묶어서 저장 (수정)
"""

import struct
import numpy as np
from pathlib import Path
from typing import Dict, List, Tuple, Optional
from asammdf import MDF, Signal
from cantools import database as candb
from datetime import datetime


class CANMessage:
    """ESP32 CAN Logger의 CAN 메시지 구조체"""
    STRUCT_FORMAT = '<QIB8s'  # little-endian: uint64, uint32, uint8, 8*uint8
    STRUCT_SIZE = struct.calcsize(STRUCT_FORMAT)
    
    def __init__(self, timestamp_us: int, can_id: int, dlc: int, data: bytes):
        self.timestamp_us = timestamp_us
        self.can_id = can_id
        self.dlc = dlc
        self.data = data[:dlc]  # DLC만큼만 유효
    
    @classmethod
    def from_bytes(cls, data: bytes) -> 'CANMessage':
        """바이너리 데이터에서 CANMessage 파싱"""
        timestamp_us, can_id, dlc, msg_data = struct.unpack(cls.STRUCT_FORMAT, data)
        return cls(timestamp_us, can_id, dlc, msg_data)
    
    def __repr__(self):
        return f"CANMessage(id=0x{self.can_id:X}, dlc={self.dlc}, ts={self.timestamp_us})"


class CANBinToMDF:
    """CAN bin 파일을 MDF로 변환하는 클래스"""
    
    def __init__(self, dbc_path: str):
        """
        Args:
            dbc_path: CAN Database (DBC) 파일 경로
        """
        self.db = candb.load_file(dbc_path)
        print(f"✓ DBC 파일 로드: {dbc_path}")
        print(f"  - 메시지 수: {len(self.db.messages)}")
        print(f"  - 시그널 수: {sum(len(msg.signals) for msg in self.db.messages)}")
    
    def read_bin_file(self, bin_path: str) -> List[CANMessage]:
        """
        ESP32 CAN Logger의 bin 파일 읽기
        
        Args:
            bin_path: bin 파일 경로
            
        Returns:
            CANMessage 리스트
        """
        messages = []
        
        with open(bin_path, 'rb') as f:
            data = f.read()
            
        # 전체 데이터를 CANMessage 단위로 파싱
        offset = 0
        while offset + CANMessage.STRUCT_SIZE <= len(data):
            msg_bytes = data[offset:offset + CANMessage.STRUCT_SIZE]
            try:
                msg = CANMessage.from_bytes(msg_bytes)
                messages.append(msg)
            except Exception as e:
                print(f"⚠️ 메시지 파싱 오류 at offset {offset}: {e}")
            
            offset += CANMessage.STRUCT_SIZE
        
        print(f"✓ bin 파일 읽기 완료: {bin_path}")
        print(f"  - 총 메시지: {len(messages)}")
        
        return messages
    
    def decode_messages(self, messages: List[CANMessage]) -> Dict[str, Dict[str, List]]:
        """
        CAN 메시지를 DBC를 사용하여 디코딩
        
        Args:
            messages: CANMessage 리스트
            
        Returns:
            {message_name: {signal_name: [values], 'timestamps': [us]}} 형태의 딕셔너리
        """
        decoded_data = {}
        
        # 각 메시지별로 초기화
        for msg_def in self.db.messages:
            msg_name = msg_def.name
            decoded_data[msg_name] = {'timestamps': []}
            for signal in msg_def.signals:
                decoded_data[msg_name][signal.name] = []
        
        # 메시지 디코딩
        decode_count = 0
        
        # 첫 번째 타임스탬프를 기준점으로 설정 (0초 시작)
        start_timestamp = messages[0].timestamp_us / 1e6 if messages else 0
        
        for msg in messages:
            try:
                # DBC에서 해당 CAN ID의 메시지 정의 찾기
                msg_def = self.db.get_message_by_frame_id(msg.can_id)
                msg_name = msg_def.name
                
                # 메시지 디코딩
                decoded = msg_def.decode(msg.data)
                
                # 타임스탬프 저장 (마이크로초 -> 초, 0부터 시작)
                relative_time = (msg.timestamp_us / 1e6) - start_timestamp
                decoded_data[msg_name]['timestamps'].append(relative_time)
                
                # 각 시그널 값 저장 (원본 숫자 값 유지)
                for signal in msg_def.signals:
                    signal_name = signal.name
                    raw_value = decoded[signal_name]
                    
                    # NamedSignalValue 객체인 경우 숫자 값으로 변환
                    # (value table이 있는 시그널은 NamedSignalValue로 반환됨)
                    if hasattr(raw_value, 'value'):
                        # NamedSignalValue 객체 - 숫자 값 추출
                        numeric_value = raw_value.value
                    else:
                        # 일반 숫자 값
                        numeric_value = raw_value
                    
                    # 항상 원본 숫자 값을 저장
                    decoded_data[msg_name][signal_name].append(numeric_value)
                
                decode_count += 1
                
            except KeyError:
                # DBC에 정의되지 않은 CAN ID는 무시
                continue
            except Exception as e:
                # print(f"⚠️ 디코딩 오류 (ID: 0x{msg.can_id:X}): {e}")
                continue
        
        print(f"✓ 메시지 디코딩 완료: {decode_count}/{len(messages)} 개")
        
        # 빈 메시지 제거
        decoded_data = {k: v for k, v in decoded_data.items() if len(v['timestamps']) > 0}
        
        return decoded_data
    
    def create_mdf(self, decoded_data: Dict[str, Dict[str, List]], 
                   output_path: str, 
                   metadata: Optional[Dict] = None,
                   compression: int = 2):
        """
        디코딩된 데이터로부터 MDF 파일 생성
        
        Args:
            decoded_data: decode_messages()의 반환값
            output_path: 출력 MDF 파일 경로
            metadata: MDF 파일 메타데이터 (선택)
            compression: 압축 레벨 (0=없음, 1=deflate, 2=transposed, 기본값=2)
        """
        total_signals = 0
        skipped_signals = []
        value_table_signals = []  # value table이 있는 시그널 추적
        
        # MDF 파일 생성
        if metadata is None:
            metadata = {}
        
        default_metadata = {
            'author': 'ESP32 CAN Logger',
            'department': '',
            'project': 'CAN Data Logging',
            'subject': 'CAN Bus Data',
        }
        default_metadata.update(metadata)
        
        mdf = MDF(version='4.10')
        
        # 메타데이터 설정
        for key, value in default_metadata.items():
            if hasattr(mdf.header, key):
                setattr(mdf.header, key, value)
        
        # 메시지별로 시그널 그룹을 만들어서 append
        for msg_name, msg_data in decoded_data.items():
            timestamps = np.array(msg_data['timestamps'], dtype=np.float64)
            
            # 메시지 정의 가져오기
            try:
                msg_def = self.db.get_message_by_name(msg_name)
            except:
                continue
            
            # 이 메시지의 시그널들을 담을 리스트
            msg_signals = []
            
            for signal_name, values in msg_data.items():
                if signal_name == 'timestamps':
                    continue
                
                # 시그널 정의 가져오기
                signal_def = None
                for sig in msg_def.signals:
                    if sig.name == signal_name:
                        signal_def = sig
                        break
                
                if signal_def is None:
                    continue
                
                # 값의 데이터 타입 확인
                if len(values) == 0:
                    continue
                
                # numpy array로 변환 시도
                try:
                    # 먼저 리스트로 변환하여 타입 확인
                    test_array = np.array(values)
                    
                    # object dtype이면 강제로 float로 변환 시도
                    if test_array.dtype == np.object_:
                        # 각 값을 float로 변환
                        float_values = []
                        conversion_failed = False
                        for v in values:
                            try:
                                float_values.append(float(v))
                            except (ValueError, TypeError):
                                conversion_failed = True
                                break
                        
                        if conversion_failed:
                            skipped_signals.append(f"{msg_name}.{signal_name}")
                            continue
                        
                        samples = np.array(float_values, dtype=np.float64)
                    else:
                        # 정상적인 숫자 타입이면 그대로 사용
                        samples = np.array(values, dtype=np.float64)
                    
                except Exception as e:
                    skipped_signals.append(f"{msg_name}.{signal_name}")
                    print(f"⚠️ 시그널 변환 실패 ({msg_name}.{signal_name}): {e}")
                    continue
                
                # Signal 객체 생성 시 conversion 적용
                comment = signal_def.comment or ""
                conversion = None
                
                # Value Table이 있는 경우 conversion 적용
                # asammdf 공식 예제 형식을 정확히 따름
                if signal_def.choices:
                    value_table_signals.append(f"{msg_name}.{signal_name}")
                    
                    # value-to-text conversion 생성
                    # 형식: {'val_0': 숫자, 'text_0': bytes, ..., 'default': bytes}
                    conversion = {}
                    
                    # choices를 정렬하여 순서대로 처리
                    for idx, (raw_val, text_val) in enumerate(sorted(signal_def.choices.items())):
                        conversion[f'val_{idx}'] = int(raw_val)
                        # 텍스트는 반드시 bytes로 변환
                        if isinstance(text_val, str):
                            conversion[f'text_{idx}'] = text_val.encode('utf-8')
                        elif isinstance(text_val, bytes):
                            conversion[f'text_{idx}'] = text_val
                        else:
                            conversion[f'text_{idx}'] = str(text_val).encode('utf-8')
                    
                    # 매칭되지 않는 값에 대한 기본값 (bytes)
                    conversion['default'] = b''
                    
                    # Value table 정보를 주석에도 추가
                    value_table_str = "; ".join([f"{k}={v}" for k, v in sorted(signal_def.choices.items())])
                    if comment:
                        comment += f" [Values: {value_table_str}]"
                    else:
                        comment = f"Values: {value_table_str}"
                
                # Signal 객체 생성
                signal = Signal(
                    samples=samples,
                    timestamps=timestamps,
                    name=f"{msg_name}.{signal_name}",
                    unit=signal_def.unit or "",
                    comment=comment,
                    conversion=conversion  # conversion 추가
                )
                
                msg_signals.append(signal)
            
            # 같은 CAN ID(메시지)의 시그널들을 그룹으로 묶어서 append
            if msg_signals:
                mdf.append(msg_signals)
                total_signals += len(msg_signals)
        
        # MDF 파일 저장 (압축 적용)
        if compression > 0:
            print(f"  압축 중... (레벨 {compression})")
        
        # 압축 옵션
        # 0 = 압축 없음
        # 1 = deflate (표준 압축)
        # 2 = transposed deflate (최고 압축률, 기본값)
        mdf.save(
            output_path, 
            overwrite=True,
            compression=compression,
        )
        
        # 파일 크기 확인
        import os
        compressed_size = os.path.getsize(output_path) / (1024 * 1024)  # MB
        
        compression_names = {0: "압축 없음", 1: "deflate", 2: "transposed"}
        compression_name = compression_names.get(compression, f"레벨 {compression}")
        
        print(f"✓ MDF 파일 생성 완료: {output_path}")
        print(f"  - 메시지 그룹 수: {len(decoded_data)}개")
        print(f"  - 총 시그널 수: {total_signals}개")
        print(f"  - Value Table 적용 시그널: {len(value_table_signals)}개")
        print(f"  - 파일 크기: {compressed_size:.2f} MB ({compression_name})")
        if skipped_signals:
            print(f"  ⚠️ 건너뛴 시그널: {len(skipped_signals)}개")
            if len(skipped_signals) <= 10:
                for sig in skipped_signals:
                    print(f"     - {sig}")
            else:
                for sig in skipped_signals[:5]:
                    print(f"     - {sig}")
                print(f"     ... 외 {len(skipped_signals) - 5}개")
        print(f"  💡 Value Table이 적용된 시그널은 MDF 뷰어에서 텍스트로 표시됩니다.")
    
    def convert(self, bin_path: str, output_path: str, 
                metadata: Optional[Dict] = None,
                compression: int = 2):
        """
        bin 파일을 MDF로 변환 (전체 프로세스)
        
        Args:
            bin_path: 입력 bin 파일 경로
            output_path: 출력 MDF 파일 경로
            metadata: MDF 메타데이터 (선택)
            compression: 압축 레벨 (0=없음, 1=deflate, 2=transposed, 기본값=2)
        """
        print(f"\n{'='*60}")
        print(f"ESP32 CAN bin → MDF 변환")
        print(f"{'='*60}")
        
        # 1. bin 파일 읽기
        messages = self.read_bin_file(bin_path)
        
        if not messages:
            print("✗ 메시지가 없습니다.")
            return
        
        # 시간 범위 출력
        start_time = messages[0].timestamp_us / 1e6
        end_time = messages[-1].timestamp_us / 1e6
        duration = end_time - start_time
        
        print(f"  - 시작 시간: {datetime.fromtimestamp(start_time)}")
        print(f"  - 종료 시간: {datetime.fromtimestamp(end_time)}")
        print(f"  - 기간: {duration:.2f} 초")
        
        # 2. 메시지 디코딩
        decoded_data = self.decode_messages(messages)
        
        if not decoded_data:
            print("✗ 디코딩된 데이터가 없습니다.")
            return
        
        # 3. MDF 생성
        if metadata is None:
            metadata = {
                'comment': f"Converted from {Path(bin_path).name}",
            }
        
        self.create_mdf(decoded_data, output_path, metadata, compression)
        
        print(f"{'='*60}")
        print(f"✓ 변환 완료!")
        print(f"{'='*60}\n")


def main():
    """사용 예제"""
    import argparse
    
    parser = argparse.ArgumentParser(
        description='ESP32 CAN Logger bin 파일을 MDF로 변환 (Value Table 지원)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
사용 예제:
  python can_bin_to_mdf.py -d example.dbc -i canlog_001.bin -o output.mf4
  python can_bin_to_mdf.py -d vehicle.dbc -i data/*.bin -o output/
        """
    )
    
    parser.add_argument('-d', '--dbc', required=True,
                       help='CAN Database (DBC) 파일 경로')
    parser.add_argument('-i', '--input', required=True, nargs='+',
                       help='입력 bin 파일(들) 경로')
    parser.add_argument('-o', '--output', required=True,
                       help='출력 MDF 파일 경로 또는 디렉토리')
    parser.add_argument('-m', '--metadata', 
                       help='MDF 메타데이터 (JSON 형식)')
    parser.add_argument('-c', '--compression', type=int, default=2,
                       choices=[0, 1, 2],
                       help='압축 레벨: 0=없음, 1=deflate, 2=transposed (기본값=2, 최고 압축)')
    
    args = parser.parse_args()
    
    # 변환기 초기화
    converter = CANBinToMDF(args.dbc)
    
    # 메타데이터 파싱
    metadata = None
    if args.metadata:
        import json
        metadata = json.loads(args.metadata)
    
    # 출력 경로 처리
    output_path = Path(args.output)
    
    # 여러 파일 변환
    for input_file in args.input:
        input_path = Path(input_file)
        
        if not input_path.exists():
            print(f"✗ 파일을 찾을 수 없음: {input_file}")
            continue
        
        # 출력 파일 경로 결정
        if output_path.is_dir() or str(output_path) == '/':
            output_file = input_path.parent / f"{input_path.stem}.mf4"
        else:
            output_file = output_path
        
        # 변환 실행
        try:
            converter.convert(str(input_path), str(output_file), metadata, args.compression)
        except Exception as e:
            print(f"✗ 변환 실패 ({input_file}): {e}")
            import traceback
            traceback.print_exc()


if __name__ == "__main__":
    main()
