#!/usr/bin/env python3
"""
ESP32 CAN Logger bin 파일을 CSV 형식으로 변환
- CAN Database (DBC 파일) 사용
- Value Table (시그널 값-텍스트 매칭) 지원
- 다양한 CSV 출력 형식 지원
"""

import struct
import csv
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional
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]
    
    @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 CANBinToCSV:
    """CAN bin 파일을 CSV로 변환하는 클래스"""
    
    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()
            
        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], 
                       apply_value_table: bool = True) -> Dict[str, Dict[str, List]]:
        """
        CAN 메시지를 DBC를 사용하여 디코딩
        
        Args:
            messages: CANMessage 리스트
            apply_value_table: Value Table을 적용할지 여부
            
        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
        for msg in messages:
            try:
                msg_def = self.db.get_message_by_frame_id(msg.can_id)
                msg_name = msg_def.name
                
                decoded = msg_def.decode(msg.data)
                
                # 타임스탬프 저장 (마이크로초 -> 초)
                decoded_data[msg_name]['timestamps'].append(msg.timestamp_us / 1e6)
                
                # 각 시그널 값 저장
                for signal in msg_def.signals:
                    signal_name = signal.name
                    raw_value = decoded[signal_name]
                    
                    # Value Table 적용
                    if apply_value_table and signal.choices:
                        try:
                            int_value = int(raw_value)
                            if int_value in signal.choices:
                                value = signal.choices[int_value]
                            else:
                                value = raw_value
                        except (ValueError, TypeError):
                            value = raw_value
                    else:
                        value = raw_value
                    
                    decoded_data[msg_name][signal_name].append(value)
                
                decode_count += 1
                
            except KeyError:
                continue
            except Exception as 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 to_wide_csv(self, decoded_data: Dict[str, Dict[str, List]], 
                    output_path: str, 
                    datetime_format: bool = True):
        """
        Wide 형식 CSV 생성 (각 시그널이 별도 컬럼)
        
        모든 시그널이 컬럼으로 나열되는 형식
        Time, Message1.Signal1, Message1.Signal2, Message2.Signal1, ...
        
        Args:
            decoded_data: 디코딩된 데이터
            output_path: 출력 CSV 파일 경로
            datetime_format: True면 타임스탬프를 날짜/시간으로 변환
        """
        # 모든 고유한 타임스탬프 수집 및 정렬
        all_timestamps = set()
        for msg_data in decoded_data.values():
            all_timestamps.update(msg_data['timestamps'])
        
        all_timestamps = sorted(all_timestamps)
        
        # 각 타임스탬프에 대한 시그널 값 매핑
        signal_columns = []
        signal_data = {}
        
        for msg_name, msg_data in decoded_data.items():
            for signal_name, values in msg_data.items():
                if signal_name == 'timestamps':
                    continue
                
                col_name = f"{msg_name}.{signal_name}"
                signal_columns.append(col_name)
                
                # 타임스탬프-값 매핑 생성
                timestamp_to_value = {}
                for ts, val in zip(msg_data['timestamps'], values):
                    timestamp_to_value[ts] = val
                
                signal_data[col_name] = timestamp_to_value
        
        # CSV 작성
        with open(output_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            
            # 헤더 작성
            if datetime_format:
                header = ['Timestamp', 'DateTime'] + signal_columns
            else:
                header = ['Timestamp'] + signal_columns
            writer.writerow(header)
            
            # 데이터 작성
            for ts in all_timestamps:
                if datetime_format:
                    dt = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
                    row = [f"{ts:.6f}", dt]
                else:
                    row = [f"{ts:.6f}"]
                
                for col_name in signal_columns:
                    value = signal_data[col_name].get(ts, '')
                    row.append(value)
                
                writer.writerow(row)
        
        print(f"✓ Wide CSV 생성 완료: {output_path}")
        print(f"  - 행 수: {len(all_timestamps)}")
        print(f"  - 시그널 수: {len(signal_columns)}")
    
    def to_long_csv(self, decoded_data: Dict[str, Dict[str, List]], 
                    output_path: str,
                    datetime_format: bool = True):
        """
        Long 형식 CSV 생성 (각 시그널 값이 별도 행)
        
        Time, Message, Signal, Value
        0.001, VehicleSpeed, Speed, 50.5
        0.001, EngineData, RPM, 2000
        
        Args:
            decoded_data: 디코딩된 데이터
            output_path: 출력 CSV 파일 경로
            datetime_format: True면 타임스탬프를 날짜/시간으로 변환
        """
        with open(output_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            
            # 헤더 작성
            if datetime_format:
                header = ['Timestamp', 'DateTime', 'Message', 'Signal', 'Value']
            else:
                header = ['Timestamp', 'Message', 'Signal', 'Value']
            writer.writerow(header)
            
            # 데이터 작성
            row_count = 0
            for msg_name, msg_data in decoded_data.items():
                timestamps = msg_data['timestamps']
                
                for signal_name, values in msg_data.items():
                    if signal_name == 'timestamps':
                        continue
                    
                    for ts, val in zip(timestamps, values):
                        if datetime_format:
                            dt = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
                            row = [f"{ts:.6f}", dt, msg_name, signal_name, val]
                        else:
                            row = [f"{ts:.6f}", msg_name, signal_name, val]
                        
                        writer.writerow(row)
                        row_count += 1
        
        print(f"✓ Long CSV 생성 완료: {output_path}")
        print(f"  - 행 수: {row_count}")
    
    def to_message_csv(self, decoded_data: Dict[str, Dict[str, List]], 
                       output_dir: str,
                       datetime_format: bool = True):
        """
        메시지별 CSV 생성 (각 CAN 메시지가 별도 CSV 파일)
        
        각 메시지마다:
        Time, Signal1, Signal2, Signal3, ...
        
        Args:
            decoded_data: 디코딩된 데이터
            output_dir: 출력 디렉토리 경로
            datetime_format: True면 타임스탬프를 날짜/시간으로 변환
        """
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)
        
        file_count = 0
        for msg_name, msg_data in decoded_data.items():
            # 파일명에 사용할 수 없는 문자 제거
            safe_name = msg_name.replace('/', '_').replace('\\', '_')
            csv_path = output_path / f"{safe_name}.csv"
            
            with open(csv_path, 'w', newline='', encoding='utf-8') as f:
                writer = csv.writer(f)
                
                # 헤더 작성
                signal_names = [k for k in msg_data.keys() if k != 'timestamps']
                if datetime_format:
                    header = ['Timestamp', 'DateTime'] + signal_names
                else:
                    header = ['Timestamp'] + signal_names
                writer.writerow(header)
                
                # 데이터 작성
                timestamps = msg_data['timestamps']
                for i, ts in enumerate(timestamps):
                    if datetime_format:
                        dt = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
                        row = [f"{ts:.6f}", dt]
                    else:
                        row = [f"{ts:.6f}"]
                    
                    for signal_name in signal_names:
                        row.append(msg_data[signal_name][i])
                    
                    writer.writerow(row)
            
            file_count += 1
        
        print(f"✓ 메시지별 CSV 생성 완료: {output_dir}")
        print(f"  - 파일 수: {file_count}")
    
    def to_raw_csv(self, messages: List[CANMessage], output_path: str):
        """
        원본 CAN 메시지 CSV 생성 (디코딩 없이 raw 데이터)
        
        Timestamp, DateTime, CAN_ID, DLC, Data
        
        Args:
            messages: CANMessage 리스트
            output_path: 출력 CSV 파일 경로
        """
        with open(output_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            
            # 헤더
            writer.writerow(['Timestamp', 'DateTime', 'CAN_ID', 'DLC', 'Data'])
            
            # 데이터
            for msg in messages:
                ts = msg.timestamp_us / 1e6
                dt = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
                can_id = f"0x{msg.can_id:03X}"
                data_hex = ' '.join([f"{b:02X}" for b in msg.data])
                
                writer.writerow([f"{ts:.6f}", dt, can_id, msg.dlc, data_hex])
        
        print(f"✓ Raw CSV 생성 완료: {output_path}")
        print(f"  - 행 수: {len(messages)}")
    
    def convert(self, bin_path: str, output_path: str, 
                format: str = 'wide',
                apply_value_table: bool = True,
                datetime_format: bool = True):
        """
        bin 파일을 CSV로 변환 (전체 프로세스)
        
        Args:
            bin_path: 입력 bin 파일 경로
            output_path: 출력 경로 (파일 또는 디렉토리)
            format: CSV 형식 ('wide', 'long', 'message', 'raw')
            apply_value_table: Value Table 적용 여부
            datetime_format: 날짜/시간 포맷 포함 여부
        """
        print(f"\n{'='*60}")
        print(f"ESP32 CAN bin → CSV 변환")
        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} 초")
        
        # Raw 형식은 디코딩 없이 바로 변환
        if format == 'raw':
            self.to_raw_csv(messages, output_path)
        else:
            # 2. 메시지 디코딩
            decoded_data = self.decode_messages(messages, apply_value_table)
            
            if not decoded_data:
                print("✗ 디코딩된 데이터가 없습니다.")
                return
            
            # 3. CSV 생성
            if format == 'wide':
                self.to_wide_csv(decoded_data, output_path, datetime_format)
            elif format == 'long':
                self.to_long_csv(decoded_data, output_path, datetime_format)
            elif format == 'message':
                self.to_message_csv(decoded_data, output_path, datetime_format)
            else:
                print(f"✗ 알 수 없는 형식: {format}")
                return
        
        print(f"{'='*60}")
        print(f"✓ 변환 완료!")
        print(f"{'='*60}\n")


def main():
    """사용 예제"""
    import argparse
    
    parser = argparse.ArgumentParser(
        description='ESP32 CAN Logger bin 파일을 CSV로 변환',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
CSV 형식:
  wide    : 모든 시그널이 컬럼으로 나열 (기본값, 분석에 적합)
  long    : 각 시그널 값이 별도 행 (데이터베이스 저장에 적합)
  message : 메시지별 개별 CSV 파일 생성
  raw     : 원본 CAN 메시지 (디코딩 없음)

사용 예제:
  python can_bin_to_csv.py -d example.dbc -i data.bin -o output.csv
  python can_bin_to_csv.py -d example.dbc -i data.bin -o output.csv -f long
  python can_bin_to_csv.py -d example.dbc -i data.bin -o csv_files/ -f message
  python can_bin_to_csv.py -d example.dbc -i data.bin -o raw.csv -f raw
        """
    )
    
    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='출력 CSV 파일 경로 또는 디렉토리')
    parser.add_argument('-f', '--format', 
                       choices=['wide', 'long', 'message', 'raw'],
                       default='wide',
                       help='CSV 형식 (기본값: wide)')
    parser.add_argument('--no-value-table', action='store_true',
                       help='Value Table을 적용하지 않음 (숫자만 출력)')
    parser.add_argument('--no-datetime', action='store_true',
                       help='날짜/시간 컬럼을 포함하지 않음')
    
    args = parser.parse_args()
    
    # 변환기 초기화
    converter = CANBinToCSV(args.dbc)
    
    # 출력 경로 처리
    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 args.format == 'message':
            # message 형식은 디렉토리 필요
            if output_path.is_file():
                output_file = output_path.parent / input_path.stem
            else:
                output_file = output_path / input_path.stem
        else:
            if output_path.is_dir() or str(output_path) == '/':
                output_file = input_path.parent / f"{input_path.stem}.csv"
            else:
                output_file = output_path
        
        # 변환 실행
        try:
            converter.convert(
                str(input_path), 
                str(output_file),
                format=args.format,
                apply_value_table=not args.no_value_table,
                datetime_format=not args.no_datetime
            )
        except Exception as e:
            print(f"✗ 변환 실패 ({input_file}): {e}")
            import traceback
            traceback.print_exc()


if __name__ == "__main__":
    main()
